Skip to content

Commit af3864b

Browse files
authored
feat: Comparable, StringId, StringIdConverter (#119)
1 parent e5c4b9b commit af3864b

9 files changed

Lines changed: 175 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ The `Unreleased` section name is replaced by the expected version of next releas
99
## [Unreleased]
1010

1111
### Added
12+
13+
- `StringId, Comparable`: Base types for Strongly Typed Ids with string renditions [#116](https://github.com/jet/FsCodec/pull/116)
14+
- `NewtonsoftJson.StringIdConverter`: Converter for `StringId` [#116](https://github.com/jet/FsCodec/pull/116)
15+
- `SystemTextJson.StringIdConverter`: Converter for `StringId` [#116](https://github.com/jet/FsCodec/pull/116)
16+
- `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)
17+
1218
### Changed
1319
### Removed
1420
### Fixed

src/FsCodec.NewtonsoftJson/FsCodec.NewtonsoftJson.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<Compile Include="Serdes.fs" />
1414
<Compile Include="Codec.fs" />
1515
<Compile Include="VerbatimUtf8Converter.fs" />
16+
<Compile Include="StringIdConverter.fs" />
1617
</ItemGroup>
1718

1819
<ItemGroup>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace FsCodec.NewtonsoftJson
2+
3+
/// <summary>Implements conversion to/from <c>string</c> for a <c>FsCodec.StringId</c>-derived type.</summary>
4+
[<AbstractClass>]
5+
type StringIdConverter<'T when 'T :> FsCodec.StringId<'T> >(parse: string -> 'T) =
6+
inherit JsonIsomorphism<'T, string>()
7+
override _.Pickle value = value.ToString()
8+
override _.UnPickle input = parse input

src/FsCodec.SystemTextJson/FsCodec.SystemTextJson.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<Compile Include="Codec.fs" />
1616
<Compile Include="CodecJsonElement.fs" />
1717
<Compile Include="Interop.fs" />
18+
<Compile Include="StringIdConverter.fs" />
1819
</ItemGroup>
1920

2021
<ItemGroup>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace FsCodec.SystemTextJson
2+
3+
/// <summary>Implements conversion to/from <c>string</c> for a <c>FsCodec.StringId</c>-derived type.</summary>
4+
[<AbstractClass>]
5+
type StringIdConverter<'T when 'T :> FsCodec.StringId<'T> >(parse: string -> 'T) =
6+
inherit System.Text.Json.Serialization.JsonConverter<'T>()
7+
override _.Write(writer, value, _options) = value.ToString() |> writer.WriteStringValue
8+
override _.Read(reader, _type, _options) = reader.GetString() |> parse
9+
10+
/// <summary>Implements conversion to/from <c>string</c> for a <c>FsCodec.StringId</c>-derived type.<br/>
11+
/// 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>
12+
[<AbstractClass>]
13+
type StringIdOrDictionaryKeyConverter<'T when 'T :> FsCodec.StringId<'T> >(parse: string -> 'T) =
14+
inherit System.Text.Json.Serialization.JsonConverter<'T>()
15+
override _.Write(writer, value, _options) = value.ToString() |> writer.WriteStringValue
16+
override _.WriteAsPropertyName(writer, value, _options) = value.ToString() |> writer.WritePropertyName
17+
override _.Read(reader, _type, _options) = reader.GetString() |> parse
18+
override _.ReadAsPropertyName(reader, _type, _options) = reader.GetString() |> parse

src/FsCodec/FsCodec.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<Compile Include="StreamName.fs" />
1212
<Compile Include="Union.fs" />
1313
<Compile Include="TypeSafeEnum.fs" />
14+
<Compile Include="StringId.fs" />
1415
</ItemGroup>
1516

1617
<ItemGroup>

src/FsCodec/StringId.fs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
namespace FsCodec
2+
3+
/// Endows any type that inherits this class with standard .NET comparison semantics using a supplied token identifier
4+
[<AbstractClass>]
5+
type Comparable<'TComp, 'Token when 'TComp :> Comparable<'TComp, 'Token> and 'Token: comparison>(token: 'Token) =
6+
member private _.Token = token
7+
override x.Equals y = match y with :? Comparable<'TComp, 'Token> as y -> x.Token = y.Token | _ -> false
8+
override _.GetHashCode() = hash token
9+
interface System.IComparable with
10+
member x.CompareTo y =
11+
match y with
12+
| :? Comparable<'TComp, 'Token> as y -> compare x.Token y.Token
13+
| _ -> invalidArg "y" "invalid comparand"
14+
15+
/// Endows any type that inherits this class with standard .NET comparison semantics using a supplied token identifier
16+
/// + treats the token as the canonical rendition for `ToString()` purposes
17+
[<AbstractClass>]
18+
type StringId<'TComp when 'TComp :> Comparable<'TComp, string>>(token: string) =
19+
inherit Comparable<'TComp, string>(token)
20+
override _.ToString() = token

tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
<Compile Include="..\FsCodec.NewtonsoftJson.Tests\SomeNullHandlingTests.fs">
4343
<Link>SomeNullHandlingTests.fs</Link>
4444
</Compile>
45+
<Compile Include="StringIdTests.fs" />
4546
</ItemGroup>
4647

4748
</Project>
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
module FsCodec.SystemTextJson.Tests.StringIdTests
2+
3+
open System.Collections.Generic
4+
open FsCodec.SystemTextJson
5+
open Xunit
6+
open Swensen.Unquote
7+
8+
(* Recommended helper aliases to put in your namespace global to avoid having to open long namespaces *)
9+
10+
type StjNameAttribute = System.Text.Json.Serialization.JsonPropertyNameAttribute
11+
type StjIgnoreAttribute = System.Text.Json.Serialization.JsonIgnoreAttribute
12+
type StjConverterAttribute = System.Text.Json.Serialization.JsonConverterAttribute
13+
14+
module Guid =
15+
16+
let inline gen () = System.Guid.NewGuid()
17+
let inline toStringN (x: System.Guid) = x.ToString "N"
18+
let inline parse (x: string) = System.Guid.Parse x
19+
20+
module Bare =
21+
22+
[<Sealed; AutoSerializable false; StjConverter(typeof<SkuIdConverter>)>]
23+
type SkuId(value: System.Guid) =
24+
// No JSON Ignore attribute required as read-only property
25+
member val Value = value
26+
and private SkuIdConverter() =
27+
inherit JsonIsomorphism<SkuId, string>()
28+
override _.Pickle(value: SkuId) = value.Value |> Guid.toStringN
29+
override _.UnPickle input = input |> Guid.parse |> SkuId
30+
31+
[<Fact>]
32+
let comparison () =
33+
let g = Guid.gen ()
34+
let id1, id2 = SkuId g, SkuId g
35+
false =! id1.Equals id2
36+
id1 <>! id2
37+
38+
[<Fact>]
39+
let serdes () =
40+
let x = Guid.gen () |> SkuId
41+
$"\"{Guid.toStringN x.Value}\"" =! Serdes.Default.Serialize x
42+
let ser = Serdes.Default.Serialize x
43+
$"\"{x.Value}\"" <>! ser // Default render of Guid is not toStringN
44+
x.Value =! Serdes.Default.Deserialize<SkuId>(ser).Value
45+
46+
let d = Dictionary()
47+
d.Add(x, "value")
48+
raises<System.NotSupportedException> <@ Serdes.Default.Serialize d @>
49+
50+
module StringIdIsomorphism =
51+
52+
[<Sealed; AutoSerializable false; StjConverter(typeof<SkuIdConverter>)>]
53+
type SkuId(value: System.Guid) = inherit FsCodec.StringId<SkuId>(Guid.toStringN value)
54+
and private SkuIdConverter() =
55+
inherit JsonIsomorphism<SkuId, string>()
56+
override _.Pickle(value: SkuId) = value |> string
57+
override _.UnPickle input = input |> Guid.parse |> SkuId
58+
59+
[<Fact>]
60+
let comparison () =
61+
let g = Guid.gen()
62+
let id1, id2 = SkuId g, SkuId g
63+
true =! id1.Equals id2
64+
id1 =! id2
65+
66+
[<Fact>]
67+
let serdes () =
68+
let x = Guid.gen () |> SkuId
69+
let ser = Serdes.Default.Serialize x
70+
$"\"{x}\"" =! ser
71+
x =! Serdes.Default.Deserialize ser
72+
73+
let d = Dictionary()
74+
d.Add(x, "value")
75+
raises<System.NotSupportedException> <@ Serdes.Default.Serialize d @>
76+
77+
module StringIdConverter =
78+
79+
[<Sealed; AutoSerializable false; StjConverter(typeof<SkuIdConverter>)>]
80+
type SkuId(value: System.Guid) = inherit FsCodec.StringId<SkuId>(Guid.toStringN value)
81+
and private SkuIdConverter() = inherit StringIdConverter<SkuId>(Guid.parse >> SkuId)
82+
83+
[<Fact>]
84+
let comparison () =
85+
let g = Guid.gen()
86+
let id1, id2 = SkuId g, SkuId g
87+
true =! id1.Equals id2
88+
id1 =! id2
89+
90+
[<Fact>]
91+
let serdes () =
92+
let x = Guid.gen () |> SkuId
93+
$"\"{x}\"" =! Serdes.Default.Serialize x
94+
95+
let d = Dictionary()
96+
d.Add(x, "value")
97+
raises<System.NotSupportedException> <@ Serdes.Default.Serialize d @>
98+
99+
module StringIdOrKeyConverter =
100+
101+
[<Sealed; AutoSerializable false; StjConverter(typeof<SkuIdConverter>)>]
102+
type SkuId(value: System.Guid) = inherit FsCodec.StringId<SkuId>(Guid.toStringN value)
103+
and private SkuIdConverter() = inherit StringIdOrDictionaryKeyConverter<SkuId>(Guid.parse >> SkuId)
104+
105+
[<Fact>]
106+
let comparison () =
107+
let g = Guid.gen()
108+
let id1, id2 = SkuId g, SkuId g
109+
true =! id1.Equals id2
110+
id1 =! id2
111+
112+
[<Fact>]
113+
let serdes () =
114+
let x = Guid.gen () |> SkuId
115+
$"\"{x}\"" =! Serdes.Default.Serialize x
116+
117+
let d = Dictionary()
118+
d.Add(x, "value")
119+
$"{{\"{x}\":\"value\"}}" =! Serdes.Default.Serialize d

0 commit comments

Comments
 (0)