From af3864bf70d6ff828fcf7c3495eac9b39243e0f2 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Mon, 4 Mar 2024 11:43:25 +0000 Subject: [PATCH] feat: Comparable, StringId, StringIdConverter (#119) --- CHANGELOG.md | 6 + .../FsCodec.NewtonsoftJson.fsproj | 1 + .../StringIdConverter.fs | 8 ++ .../FsCodec.SystemTextJson.fsproj | 1 + .../StringIdConverter.fs | 18 +++ src/FsCodec/FsCodec.fsproj | 1 + src/FsCodec/StringId.fs | 20 +++ .../FsCodec.SystemTextJson.Tests.fsproj | 1 + .../StringIdTests.fs | 119 ++++++++++++++++++ 9 files changed, 175 insertions(+) create mode 100644 src/FsCodec.NewtonsoftJson/StringIdConverter.fs create mode 100644 src/FsCodec.SystemTextJson/StringIdConverter.fs create mode 100644 src/FsCodec/StringId.fs create mode 100644 tests/FsCodec.SystemTextJson.Tests/StringIdTests.fs diff --git a/CHANGELOG.md b/CHANGELOG.md index a4fd175..03de709 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/FsCodec.NewtonsoftJson/FsCodec.NewtonsoftJson.fsproj b/src/FsCodec.NewtonsoftJson/FsCodec.NewtonsoftJson.fsproj index b8a5ee3..3521c24 100644 --- a/src/FsCodec.NewtonsoftJson/FsCodec.NewtonsoftJson.fsproj +++ b/src/FsCodec.NewtonsoftJson/FsCodec.NewtonsoftJson.fsproj @@ -13,6 +13,7 @@ + diff --git a/src/FsCodec.NewtonsoftJson/StringIdConverter.fs b/src/FsCodec.NewtonsoftJson/StringIdConverter.fs new file mode 100644 index 0000000..f2b6261 --- /dev/null +++ b/src/FsCodec.NewtonsoftJson/StringIdConverter.fs @@ -0,0 +1,8 @@ +namespace FsCodec.NewtonsoftJson + +/// Implements conversion to/from string for a FsCodec.StringId-derived type. +[] +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 diff --git a/src/FsCodec.SystemTextJson/FsCodec.SystemTextJson.fsproj b/src/FsCodec.SystemTextJson/FsCodec.SystemTextJson.fsproj index e221a36..420f367 100644 --- a/src/FsCodec.SystemTextJson/FsCodec.SystemTextJson.fsproj +++ b/src/FsCodec.SystemTextJson/FsCodec.SystemTextJson.fsproj @@ -15,6 +15,7 @@ + diff --git a/src/FsCodec.SystemTextJson/StringIdConverter.fs b/src/FsCodec.SystemTextJson/StringIdConverter.fs new file mode 100644 index 0000000..07e489e --- /dev/null +++ b/src/FsCodec.SystemTextJson/StringIdConverter.fs @@ -0,0 +1,18 @@ +namespace FsCodec.SystemTextJson + +/// Implements conversion to/from string for a FsCodec.StringId-derived type. +[] +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 + +/// Implements conversion to/from string for a FsCodec.StringId-derived type.
+/// Opts into use of the underlying token as a valid property name when tth type is used as a Key in a IDictionary.
+[] +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 diff --git a/src/FsCodec/FsCodec.fsproj b/src/FsCodec/FsCodec.fsproj index 1e96ea5..a7be5fa 100644 --- a/src/FsCodec/FsCodec.fsproj +++ b/src/FsCodec/FsCodec.fsproj @@ -11,6 +11,7 @@ +
diff --git a/src/FsCodec/StringId.fs b/src/FsCodec/StringId.fs new file mode 100644 index 0000000..330aae6 --- /dev/null +++ b/src/FsCodec/StringId.fs @@ -0,0 +1,20 @@ +namespace FsCodec + +/// Endows any type that inherits this class with standard .NET comparison semantics using a supplied token identifier +[] +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 +[] +type StringId<'TComp when 'TComp :> Comparable<'TComp, string>>(token: string) = + inherit Comparable<'TComp, string>(token) + override _.ToString() = token diff --git a/tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj b/tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj index 703244f..e6e1daa 100644 --- a/tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj +++ b/tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj @@ -42,6 +42,7 @@ SomeNullHandlingTests.fs + diff --git a/tests/FsCodec.SystemTextJson.Tests/StringIdTests.fs b/tests/FsCodec.SystemTextJson.Tests/StringIdTests.fs new file mode 100644 index 0000000..4931650 --- /dev/null +++ b/tests/FsCodec.SystemTextJson.Tests/StringIdTests.fs @@ -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 = + + [)>] + type SkuId(value: System.Guid) = + // No JSON Ignore attribute required as read-only property + member val Value = value + and private SkuIdConverter() = + inherit JsonIsomorphism() + override _.Pickle(value: SkuId) = value.Value |> Guid.toStringN + override _.UnPickle input = input |> Guid.parse |> SkuId + + [] + let comparison () = + let g = Guid.gen () + let id1, id2 = SkuId g, SkuId g + false =! id1.Equals id2 + id1 <>! id2 + + [] + 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(ser).Value + + let d = Dictionary() + d.Add(x, "value") + raises <@ Serdes.Default.Serialize d @> + +module StringIdIsomorphism = + + [)>] + type SkuId(value: System.Guid) = inherit FsCodec.StringId(Guid.toStringN value) + and private SkuIdConverter() = + inherit JsonIsomorphism() + override _.Pickle(value: SkuId) = value |> string + override _.UnPickle input = input |> Guid.parse |> SkuId + + [] + let comparison () = + let g = Guid.gen() + let id1, id2 = SkuId g, SkuId g + true =! id1.Equals id2 + id1 =! id2 + + [] + 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 <@ Serdes.Default.Serialize d @> + +module StringIdConverter = + + [)>] + type SkuId(value: System.Guid) = inherit FsCodec.StringId(Guid.toStringN value) + and private SkuIdConverter() = inherit StringIdConverter(Guid.parse >> SkuId) + + [] + let comparison () = + let g = Guid.gen() + let id1, id2 = SkuId g, SkuId g + true =! id1.Equals id2 + id1 =! id2 + + [] + let serdes () = + let x = Guid.gen () |> SkuId + $"\"{x}\"" =! Serdes.Default.Serialize x + + let d = Dictionary() + d.Add(x, "value") + raises <@ Serdes.Default.Serialize d @> + +module StringIdOrKeyConverter = + + [)>] + type SkuId(value: System.Guid) = inherit FsCodec.StringId(Guid.toStringN value) + and private SkuIdConverter() = inherit StringIdOrDictionaryKeyConverter(Guid.parse >> SkuId) + + [] + let comparison () = + let g = Guid.gen() + let id1, id2 = SkuId g, SkuId g + true =! id1.Equals id2 + id1 =! id2 + + [] + 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