Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Comparable, StringId, StringIdConverter #119

Merged
merged 2 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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