Skip to content

Commit

Permalink
feat: Comparable, StringId, StringIdConverter (#119)
Browse files Browse the repository at this point in the history
  • Loading branch information
bartelink committed Mar 4, 2024
1 parent e5c4b9b commit af3864b
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 0 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ The `Unreleased` section name is replaced by the expected version of next releas
## [Unreleased]

### Added

- `StringId, Comparable`: Base types for Strongly Typed Ids with string renditions [#116](https://github.com/jet/FsCodec/pull/116)
- `NewtonsoftJson.StringIdConverter`: Converter for `StringId` [#116](https://github.com/jet/FsCodec/pull/116)
- `SystemTextJson.StringIdConverter`: Converter for `StringId` [#116](https://github.com/jet/FsCodec/pull/116)
- `SystemTextJson.StringIdOrDictionaryKeyConverter`: Converter for `StringId` that enables `Dictionary` values using a `StringId`-derived type as a key to be used as a JSON Object Key [#116](https://github.com/jet/FsCodec/pull/116)

### Changed
### Removed
### Fixed
Expand Down
1 change: 1 addition & 0 deletions src/FsCodec.NewtonsoftJson/FsCodec.NewtonsoftJson.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<Compile Include="Serdes.fs" />
<Compile Include="Codec.fs" />
<Compile Include="VerbatimUtf8Converter.fs" />
<Compile Include="StringIdConverter.fs" />
</ItemGroup>

<ItemGroup>
Expand Down
8 changes: 8 additions & 0 deletions src/FsCodec.NewtonsoftJson/StringIdConverter.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace FsCodec.NewtonsoftJson

/// <summary>Implements conversion to/from <c>string</c> for a <c>FsCodec.StringId</c>-derived type.</summary>
[<AbstractClass>]
type StringIdConverter<'T when 'T :> FsCodec.StringId<'T> >(parse: string -> 'T) =
inherit JsonIsomorphism<'T, string>()
override _.Pickle value = value.ToString()
override _.UnPickle input = parse input
1 change: 1 addition & 0 deletions src/FsCodec.SystemTextJson/FsCodec.SystemTextJson.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<Compile Include="Codec.fs" />
<Compile Include="CodecJsonElement.fs" />
<Compile Include="Interop.fs" />
<Compile Include="StringIdConverter.fs" />
</ItemGroup>

<ItemGroup>
Expand Down
18 changes: 18 additions & 0 deletions src/FsCodec.SystemTextJson/StringIdConverter.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace FsCodec.SystemTextJson

/// <summary>Implements conversion to/from <c>string</c> for a <c>FsCodec.StringId</c>-derived type.</summary>
[<AbstractClass>]
type StringIdConverter<'T when 'T :> FsCodec.StringId<'T> >(parse: string -> 'T) =
inherit System.Text.Json.Serialization.JsonConverter<'T>()
override _.Write(writer, value, _options) = value.ToString() |> writer.WriteStringValue
override _.Read(reader, _type, _options) = reader.GetString() |> parse

/// <summary>Implements conversion to/from <c>string</c> for a <c>FsCodec.StringId</c>-derived type.<br/>
/// Opts into use of the underlying token as a valid property name when tth type is used as a Key in a <c>IDictionary</c>.</summary>
[<AbstractClass>]
type StringIdOrDictionaryKeyConverter<'T when 'T :> FsCodec.StringId<'T> >(parse: string -> 'T) =
inherit System.Text.Json.Serialization.JsonConverter<'T>()
override _.Write(writer, value, _options) = value.ToString() |> writer.WriteStringValue
override _.WriteAsPropertyName(writer, value, _options) = value.ToString() |> writer.WritePropertyName
override _.Read(reader, _type, _options) = reader.GetString() |> parse
override _.ReadAsPropertyName(reader, _type, _options) = reader.GetString() |> parse
1 change: 1 addition & 0 deletions src/FsCodec/FsCodec.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<Compile Include="StreamName.fs" />
<Compile Include="Union.fs" />
<Compile Include="TypeSafeEnum.fs" />
<Compile Include="StringId.fs" />
</ItemGroup>

<ItemGroup>
Expand Down
20 changes: 20 additions & 0 deletions src/FsCodec/StringId.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace FsCodec

/// Endows any type that inherits this class with standard .NET comparison semantics using a supplied token identifier
[<AbstractClass>]
type Comparable<'TComp, 'Token when 'TComp :> Comparable<'TComp, 'Token> and 'Token: comparison>(token: 'Token) =
member private _.Token = token
override x.Equals y = match y with :? Comparable<'TComp, 'Token> as y -> x.Token = y.Token | _ -> false
override _.GetHashCode() = hash token
interface System.IComparable with
member x.CompareTo y =
match y with
| :? Comparable<'TComp, 'Token> as y -> compare x.Token y.Token
| _ -> invalidArg "y" "invalid comparand"

/// Endows any type that inherits this class with standard .NET comparison semantics using a supplied token identifier
/// + treats the token as the canonical rendition for `ToString()` purposes
[<AbstractClass>]
type StringId<'TComp when 'TComp :> Comparable<'TComp, string>>(token: string) =
inherit Comparable<'TComp, string>(token)
override _.ToString() = token
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
<Compile Include="..\FsCodec.NewtonsoftJson.Tests\SomeNullHandlingTests.fs">
<Link>SomeNullHandlingTests.fs</Link>
</Compile>
<Compile Include="StringIdTests.fs" />
</ItemGroup>

</Project>
119 changes: 119 additions & 0 deletions tests/FsCodec.SystemTextJson.Tests/StringIdTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
module FsCodec.SystemTextJson.Tests.StringIdTests

open System.Collections.Generic
open FsCodec.SystemTextJson
open Xunit
open Swensen.Unquote

(* Recommended helper aliases to put in your namespace global to avoid having to open long namespaces *)

type StjNameAttribute = System.Text.Json.Serialization.JsonPropertyNameAttribute
type StjIgnoreAttribute = System.Text.Json.Serialization.JsonIgnoreAttribute
type StjConverterAttribute = System.Text.Json.Serialization.JsonConverterAttribute

module Guid =

let inline gen () = System.Guid.NewGuid()
let inline toStringN (x: System.Guid) = x.ToString "N"
let inline parse (x: string) = System.Guid.Parse x

module Bare =

[<Sealed; AutoSerializable false; StjConverter(typeof<SkuIdConverter>)>]
type SkuId(value: System.Guid) =
// No JSON Ignore attribute required as read-only property
member val Value = value
and private SkuIdConverter() =
inherit JsonIsomorphism<SkuId, string>()
override _.Pickle(value: SkuId) = value.Value |> Guid.toStringN
override _.UnPickle input = input |> Guid.parse |> SkuId

[<Fact>]
let comparison () =
let g = Guid.gen ()
let id1, id2 = SkuId g, SkuId g
false =! id1.Equals id2
id1 <>! id2

[<Fact>]
let serdes () =
let x = Guid.gen () |> SkuId
$"\"{Guid.toStringN x.Value}\"" =! Serdes.Default.Serialize x
let ser = Serdes.Default.Serialize x
$"\"{x.Value}\"" <>! ser // Default render of Guid is not toStringN
x.Value =! Serdes.Default.Deserialize<SkuId>(ser).Value

let d = Dictionary()
d.Add(x, "value")
raises<System.NotSupportedException> <@ Serdes.Default.Serialize d @>

module StringIdIsomorphism =

[<Sealed; AutoSerializable false; StjConverter(typeof<SkuIdConverter>)>]
type SkuId(value: System.Guid) = inherit FsCodec.StringId<SkuId>(Guid.toStringN value)
and private SkuIdConverter() =
inherit JsonIsomorphism<SkuId, string>()
override _.Pickle(value: SkuId) = value |> string
override _.UnPickle input = input |> Guid.parse |> SkuId

[<Fact>]
let comparison () =
let g = Guid.gen()
let id1, id2 = SkuId g, SkuId g
true =! id1.Equals id2
id1 =! id2

[<Fact>]
let serdes () =
let x = Guid.gen () |> SkuId
let ser = Serdes.Default.Serialize x
$"\"{x}\"" =! ser
x =! Serdes.Default.Deserialize ser

let d = Dictionary()
d.Add(x, "value")
raises<System.NotSupportedException> <@ Serdes.Default.Serialize d @>

module StringIdConverter =

[<Sealed; AutoSerializable false; StjConverter(typeof<SkuIdConverter>)>]
type SkuId(value: System.Guid) = inherit FsCodec.StringId<SkuId>(Guid.toStringN value)
and private SkuIdConverter() = inherit StringIdConverter<SkuId>(Guid.parse >> SkuId)

[<Fact>]
let comparison () =
let g = Guid.gen()
let id1, id2 = SkuId g, SkuId g
true =! id1.Equals id2
id1 =! id2

[<Fact>]
let serdes () =
let x = Guid.gen () |> SkuId
$"\"{x}\"" =! Serdes.Default.Serialize x

let d = Dictionary()
d.Add(x, "value")
raises<System.NotSupportedException> <@ Serdes.Default.Serialize d @>

module StringIdOrKeyConverter =

[<Sealed; AutoSerializable false; StjConverter(typeof<SkuIdConverter>)>]
type SkuId(value: System.Guid) = inherit FsCodec.StringId<SkuId>(Guid.toStringN value)
and private SkuIdConverter() = inherit StringIdOrDictionaryKeyConverter<SkuId>(Guid.parse >> SkuId)

[<Fact>]
let comparison () =
let g = Guid.gen()
let id1, id2 = SkuId g, SkuId g
true =! id1.Equals id2
id1 =! id2

[<Fact>]
let serdes () =
let x = Guid.gen () |> SkuId
$"\"{x}\"" =! Serdes.Default.Serialize x

let d = Dictionary()
d.Add(x, "value")
$"{{\"{x}\":\"value\"}}" =! Serdes.Default.Serialize d

0 comments on commit af3864b

Please sign in to comment.