-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Port JsonIsomorphism from Newtonsoft variant
- Loading branch information
Showing
6 changed files
with
134 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
namespace FsCodec.SystemTextJson | ||
|
||
open System | ||
open System.Text.Json | ||
|
||
[<AutoOpen>] | ||
module private Prelude = | ||
/// Provides a thread-safe memoization wrapper for supplied function | ||
let memoize : ('T -> 'S) -> 'T -> 'S = | ||
fun f -> | ||
let cache = System.Collections.Concurrent.ConcurrentDictionary<'T, 'S>() | ||
fun t -> cache.GetOrAdd(t, f) | ||
|
||
[<AbstractClass>] | ||
type JsonPickler<'T>() = | ||
inherit Serialization.JsonConverter<'T>() | ||
|
||
static let isMatchingType = | ||
let rec isMatching (ts : Type list) = | ||
match ts with | ||
| [] -> false | ||
| t :: _ when t = typeof<'T> -> true | ||
| t :: tl -> | ||
let tail = | ||
[ match t.BaseType with null -> () | bt -> yield bt | ||
yield! t.GetInterfaces() | ||
yield! tl ] | ||
|
||
isMatching tail | ||
|
||
memoize (fun t -> isMatching [t]) | ||
|
||
abstract Read : reader: byref<Utf8JsonReader> * options: JsonSerializerOptions -> 'T | ||
|
||
override __.CanConvert t = isMatchingType t | ||
|
||
override __.Read(reader, _ : Type, opts) = | ||
__.Read(&reader, opts) | ||
|
||
/// Json Converter that serializes based on an isomorphic type | ||
[<AbstractClass>] | ||
type JsonIsomorphism<'T, 'U>(?targetPickler : JsonPickler<'U>) = | ||
inherit JsonPickler<'T>() | ||
|
||
abstract Pickle : 'T -> 'U | ||
abstract UnPickle : 'U -> 'T | ||
|
||
override __.Write(writer, source : 'T, options) = | ||
let target = __.Pickle source | ||
match targetPickler with | ||
| None -> JsonSerializer.Serialize(writer, target, options) | ||
| Some p -> p.Write(writer, target, options) | ||
|
||
override __.Read(reader, options) = | ||
let target = | ||
match targetPickler with | ||
| None -> JsonSerializer.Deserialize<'U>(&reader,options) | ||
| Some p -> p.Read(&reader, options) | ||
|
||
__.UnPickle target |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
module FsCodec.SystemTextJson.Tests.PicklerTests | ||
|
||
open FsCodec.SystemTextJson | ||
open FsCodec.SystemTextJson.Converters | ||
open System | ||
open System.Text.Json | ||
open Swensen.Unquote | ||
open Xunit | ||
|
||
// NB Feel free to ignore this opinion and copy the 4 lines into your own globals - the pinning test will remain here | ||
/// <summary> | ||
/// Renders Guids without dashes. | ||
/// </summary> | ||
/// <remarks> | ||
/// Can work correctly as a global converter, as some codebases do for historical reasons | ||
/// Could arguably be usable as base class for various converters, including the above. | ||
/// However, both of these usage patterns and variants thereof are not recommended for new types. | ||
/// In general, the philosophy is that, beyond the Pickler base types, an identity type should consist of explicit | ||
/// code as much as possible, and global converters really have to earn their keep - magic starts with -100 points. | ||
/// </remarks> | ||
type GuidConverter() = | ||
inherit JsonIsomorphism<Guid, string>() | ||
override __.Pickle g = g.ToString "N" | ||
override __.UnPickle g = Guid.Parse g | ||
|
||
type WithEmbeddedGuid = { a: string; [<Serialization.JsonConverter(typeof<GuidConverter>)>] b: Guid } | ||
|
||
type Configs() as this = | ||
inherit TheoryData<JsonSerializerOptions>() | ||
do this.Add(Options.CreateDefault(JsonRecordConverter())) // validate it works with minimal converters | ||
this.Add(Options.Create()) // Flush out clashes with standard converter set | ||
this.Add(Options.Create(GuidConverter())) // and a global registration does not conflict | ||
|
||
let [<Theory; ClassData(typeof<Configs>)>] ``Tagging with GuidConverter roundtrips`` (options : JsonSerializerOptions) = | ||
let value = { a = "testing"; b = Guid.Empty } | ||
|
||
let result = Serdes.Serialize(value, options) | ||
|
||
test <@ """{"a":"testing","b":"00000000000000000000000000000000"}""" = result @> | ||
|
||
let des = Serdes.Deserialize(result, options) | ||
test <@ value = des @> | ||
|
||
let [<Fact>] ``Global GuidConverter roundtrips`` () = | ||
let value = Guid.Empty | ||
|
||
let defaultHandlingHasDashes = Serdes.Serialize value | ||
|
||
let optionsWithConverter = Options.Create(GuidConverter()) | ||
let resNoDashes = Serdes.Serialize(value, optionsWithConverter) | ||
|
||
test <@ "\"00000000-0000-0000-0000-000000000000\"" = defaultHandlingHasDashes | ||
&& "\"00000000000000000000000000000000\"" = resNoDashes @> | ||
|
||
// Non-dashed is not accepted by default handling in STJ (Newtonsoft does accept it) | ||
raises<exn> <@ Serdes.Deserialize<Guid> resNoDashes @> | ||
|
||
// With the converter, things roundtrip either way | ||
for result in [defaultHandlingHasDashes; resNoDashes] do | ||
let des = Serdes.Deserialize(result, optionsWithConverter) | ||
test <@ value= des @> | ||
|