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

Newtonsoft: Sync TypeSafeEnum signatures #91

Merged
merged 4 commits into from
Apr 26, 2023
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ The `Unreleased` section name is replaced by the expected version of next releas

### Added
### Changed

- `NewtonsoftJson.TypeSafeEnum`: Sync with `SystemTextJson.TypeSafeEnum` [#91](https://github.com/jet/FsCodec/pull/91)

### Removed
### Fixed

Expand Down
8 changes: 4 additions & 4 deletions src/FsCodec.NewtonsoftJson/OptionConverter.fs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type OptionConverter() =
if value = null then null
else
let _, fields = FSharpValue.GetUnionFields(value, value.GetType())
fields.[0]
fields[0]

serializer.Serialize(writer, value)

Expand All @@ -26,8 +26,8 @@ type OptionConverter() =
else innerType

let cases = Union.getUnionCases t
if reader.TokenType = JsonToken.Null then FSharpValue.MakeUnion(cases.[0], Array.empty)
if reader.TokenType = JsonToken.Null then FSharpValue.MakeUnion(cases[0], Array.empty)
else
let value = serializer.Deserialize(reader, innerType)
if value = null then FSharpValue.MakeUnion(cases.[0], Array.empty)
else FSharpValue.MakeUnion(cases.[1], [|value|])
if value = null then FSharpValue.MakeUnion(cases[0], Array.empty)
else FSharpValue.MakeUnion(cases[1], [|value|])
20 changes: 10 additions & 10 deletions src/FsCodec.NewtonsoftJson/Options.fs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ type Options private () =
/// Creates a default set of serializer settings used by Json serialization. When used with no args, same as JsonSerializerSettings.CreateDefault()
static member CreateDefault
( [<Optional; ParamArray>] converters : JsonConverter[],
/// Use multi-line, indented formatting when serializing JSON; defaults to false.
// Use multi-line, indented formatting when serializing JSON; defaults to false.
[<Optional; DefaultParameterValue(null)>] ?indent : bool,
/// Render idiomatic camelCase for PascalCase items by using `CamelCasePropertyNamesContractResolver`. Defaults to false.
// Render idiomatic camelCase for PascalCase items by using `CamelCasePropertyNamesContractResolver`. Defaults to false.
[<Optional; DefaultParameterValue(null)>] ?camelCase : bool,
/// Ignore null values in input data; defaults to false.
// Ignore null values in input data; defaults to false.
[<Optional; DefaultParameterValue(null)>] ?ignoreNulls : bool,
/// Error on missing values (as opposed to letting them just be default-initialized); defaults to false.
// Error on missing values (as opposed to letting them just be default-initialized); defaults to false.
[<Optional; DefaultParameterValue(null)>] ?errorOnMissing : bool) =

let indent = defaultArg indent false
Expand All @@ -48,16 +48,16 @@ type Options private () =
/// - Always prepends an OptionConverter() to any converters supplied
/// - everything else is as per CreateDefault:- i.e. emit nulls instead of omitting fields etc
static member Create
( /// List of converters to apply. An implicit OptionConverter() will be prepended and/or be used as a default
( // List of converters to apply. An implicit OptionConverter() will be prepended and/or be used as a default
[<Optional; ParamArray>] converters : JsonConverter[],
/// Use multi-line, indented formatting when serializing JSON; defaults to false.
// Use multi-line, indented formatting when serializing JSON; defaults to false.
[<Optional; DefaultParameterValue(null)>] ?indent : bool,
/// Render idiomatic camelCase for PascalCase items by using `CamelCasePropertyNamesContractResolver`.
/// Defaults to false on basis that you'll use record and tuple field names that are camelCase (and hence not `CLSCompliant`).
// Render idiomatic camelCase for PascalCase items by using `CamelCasePropertyNamesContractResolver`.
// Defaults to false on basis that you'll use record and tuple field names that are camelCase (and hence not `CLSCompliant`).
[<Optional; DefaultParameterValue(null)>] ?camelCase : bool,
/// Ignore null values in input data; defaults to `false`.
// Ignore null values in input data; defaults to `false`.
[<Optional; DefaultParameterValue(null)>] ?ignoreNulls : bool,
/// Error on missing values (as opposed to letting them just be default-initialized); defaults to false
// Error on missing values (as opposed to letting them just be default-initialized); defaults to false
[<Optional; DefaultParameterValue(null)>] ?errorOnMissing : bool) =

Options.CreateDefault(
Expand Down
27 changes: 15 additions & 12 deletions src/FsCodec.NewtonsoftJson/TypeSafeEnumConverter.fs
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
namespace FsCodec.NewtonsoftJson

open Newtonsoft.Json
open System.Collections.Generic
open System
open System.Collections.Generic

/// Utilities for working with DUs where none of the cases have a value
module TypeSafeEnum =

let isTypeSafeEnum (t : Type) =
Union.isUnion t
&& (Union.getUnion t).cases |> Seq.forall (fun case -> case.GetFields().Length = 0)
&& Union.hasOnlyNullaryCases t

let tryParseT (t : Type) (str : string) =
let union = Union.getUnion t
union.cases
|> Array.tryFindIndex (fun case -> case.Name = str)
|> Option.map (fun tag -> (union.caseConstructor.[tag] [||]))
let u = Union.getInfo t
u.cases
|> Array.tryFindIndex (fun c -> c.Name = str)
|> Option.map (fun tag -> u.caseConstructor[tag] [||])
let tryParse<'T> (str : string) = tryParseT typeof<'T> str |> Option.map (fun e -> e :?> 'T)

let parseT (t : Type) (str : string) =
Expand All @@ -26,19 +26,22 @@ module TypeSafeEnum =
raise (KeyNotFoundException(sprintf "Could not find case '%s' for type '%s'" str t.FullName))
let parse<'T> (str : string) = parseT typeof<'T> str :?> 'T

let toString (x : obj) =
let union = Union.getUnion (x.GetType())
let tag = union.tagReader x
union.cases.[tag].Name
let toStringT (t : Type) (x : obj) =
let u = Union.getInfo t
let tag = u.tagReader x
u.cases[tag].Name
let toString<'t> (x : 't) =
toStringT typeof<'t> x

/// Maps strings to/from Union cases; refuses to convert for values not in the Union
type TypeSafeEnumConverter() =
inherit JsonConverter()

override _.CanConvert (t : Type) = TypeSafeEnum.isTypeSafeEnum t
override _.CanConvert(t : Type) =
TypeSafeEnum.isTypeSafeEnum t

override _.WriteJson(writer : JsonWriter, value : obj, _ : JsonSerializer) =
let str = TypeSafeEnum.toString value
let str = TypeSafeEnum.toStringT (value.GetType()) value
writer.WriteValue str

override _.ReadJson(reader : JsonReader, t : Type, _ : obj, _ : JsonSerializer) =
Expand Down
25 changes: 15 additions & 10 deletions src/FsCodec.NewtonsoftJson/UnionConverter.fs
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,20 @@ module private Union =
let isUnion = memoize (fun t -> FSharpType.IsUnion(t, true))
let getUnionCases = memoize (fun t -> FSharpType.GetUnionCases(t, true))

let private createUnion t =
let private createInfo t =
let cases = getUnionCases t
{
cases = cases
tagReader = FSharpValue.PreComputeUnionTagReader(t, true)
fieldReader = cases |> Array.map (fun c -> FSharpValue.PreComputeUnionReader(c, true))
caseConstructor = cases |> Array.map (fun c -> FSharpValue.PreComputeUnionConstructor(c, true))
}
let getUnion = memoize createUnion
let getInfo = memoize createInfo

/// Allows us to distinguish Unions that do not have bodies and hence should use a TypeSafeEnumConverter
let hasOnlyNullaryCases (t : Type) =
let union = getInfo t
union.cases |> Seq.forall (fun case -> case.GetFields().Length = 0)

/// Parallels F# behavior wrt how it generates a DU's underlying .NET Type
let inline isInlinedIntoUnionItem (t : Type) =
Expand All @@ -53,7 +58,7 @@ module private Union =
[| inputJObject.ToObject(singleCaseArg.PropertyType, serializer) |]
| multipleFieldsInCustomCaseType ->
[| for fi in multipleFieldsInCustomCaseType ->
match inputJObject.[fi.Name] with
match inputJObject[fi.Name] with
| null when
// Afford converters an opportunity to handle the missing field in the best way I can figure out to signal that
// The specific need being covered (see tests) is to ensure that, even with MissingMemberHandling=Ignore,
Expand All @@ -79,10 +84,10 @@ type UnionConverter private (discriminator : string, ?catchAllCase) =
override _.CanConvert (t : Type) = Union.isUnion t

override _.WriteJson(writer : JsonWriter, value : obj, serializer : JsonSerializer) =
let union = Union.getUnion (value.GetType())
let union = Union.getInfo (value.GetType())
let tag = union.tagReader value
let case = union.cases.[tag]
let fieldValues = union.fieldReader.[tag] value
let case = union.cases[tag]
let fieldValues = union.fieldReader[tag] value
let fieldInfos = case.GetFields()

writer.WriteStartObject()
Expand All @@ -92,7 +97,7 @@ type UnionConverter private (discriminator : string, ?catchAllCase) =

match fieldInfos with
| [| fi |] when not (Union.typeIsUnionWithConverterAttribute fi.PropertyType) ->
match fieldValues.[0] with
match fieldValues[0] with
| null when serializer.NullValueHandling = NullValueHandling.Ignore -> ()
| fv ->
let token = if fv = null then JToken.Parse "null" else JToken.FromObject(fv, serializer)
Expand All @@ -117,9 +122,9 @@ type UnionConverter private (discriminator : string, ?catchAllCase) =
if token.Type <> JTokenType.Object then raise (FormatException(sprintf "Expected object token, got %O" token.Type))
let inputJObject = token :?> JObject

let union = Union.getUnion t
let union = Union.getInfo t
let targetCaseIndex =
let inputCaseNameValue = inputJObject.[discriminator] |> string
let inputCaseNameValue = inputJObject[discriminator] |> string
let findCaseNamed x = union.cases |> Array.tryFindIndex (fun case -> case.Name = x)
match findCaseNamed inputCaseNameValue, catchAllCase with
| None, None ->
Expand All @@ -133,5 +138,5 @@ type UnionConverter private (discriminator : string, ?catchAllCase) =
inputCaseNameValue catchAllCaseName t.FullName |> invalidOp
| Some foundIndex -> foundIndex

let targetCaseFields, targetCaseCtor = union.cases.[targetCaseIndex].GetFields(), union.caseConstructor.[targetCaseIndex]
let targetCaseFields, targetCaseCtor = union.cases[targetCaseIndex].GetFields(), union.caseConstructor[targetCaseIndex]
targetCaseCtor (Union.mapTargetCaseArgs inputJObject serializer targetCaseFields)
26 changes: 13 additions & 13 deletions src/FsCodec.SystemTextJson/Options.fs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ type Options private () =
/// Creates a default set of serializer options used by Json serialization. When used with no args, same as `JsonSerializerOptions()`
static member CreateDefault
( [<Optional; ParamArray>] converters : JsonConverter[],
/// Use multi-line, indented formatting when serializing JSON; defaults to false.
// Use multi-line, indented formatting when serializing JSON; defaults to false.
[<Optional; DefaultParameterValue(null)>] ?indent : bool,
/// Render idiomatic camelCase for PascalCase items by using `PropertyNamingPolicy = CamelCase`. Defaults to false.
// Render idiomatic camelCase for PascalCase items by using `PropertyNamingPolicy = CamelCase`. Defaults to false.
[<Optional; DefaultParameterValue(null)>] ?camelCase : bool,
/// Ignore null values in input data, don't render fields with null values; defaults to `false`.
// Ignore null values in input data, don't render fields with null values; defaults to `false`.
[<Optional; DefaultParameterValue(null)>] ?ignoreNulls : bool,
/// Drop escaping of HTML-sensitive characters. defaults to `false`.
// Drop escaping of HTML-sensitive characters. defaults to `false`.
[<Optional; DefaultParameterValue(null)>] ?unsafeRelaxedJsonEscaping : bool) =

let indent = defaultArg indent false
Expand All @@ -43,22 +43,22 @@ type Options private () =
/// - renders values with `UnsafeRelaxedJsonEscaping` - i.e. minimal escaping as per `NewtonsoftJson`<br/>
/// Everything else is as per CreateDefault:- i.e. emit nulls instead of omitting fields, no indenting, no camelCase conversion
static member Create
( /// List of converters to apply. Implicit converters may be prepended and/or be used as a default
( // List of converters to apply. Implicit converters may be prepended and/or be used as a default
[<Optional; ParamArray>] converters : JsonConverter[],
/// Use multi-line, indented formatting when serializing JSON; defaults to false.
// Use multi-line, indented formatting when serializing JSON; defaults to false.
[<Optional; DefaultParameterValue(null)>] ?indent : bool,
/// Render idiomatic camelCase for PascalCase items by using `PropertyNamingPolicy = CamelCase`.
/// Defaults to false on basis that you'll use record and tuple field names that are camelCase (but thus not `CLSCompliant`).
// Render idiomatic camelCase for PascalCase items by using `PropertyNamingPolicy = CamelCase`.
// Defaults to false on basis that you'll use record and tuple field names that are camelCase (but thus not `CLSCompliant`).
[<Optional; DefaultParameterValue(null)>] ?camelCase : bool,
/// Ignore null values in input data, don't render fields with null values; defaults to `false`.
// Ignore null values in input data, don't render fields with null values; defaults to `false`.
[<Optional; DefaultParameterValue(null)>] ?ignoreNulls : bool,
/// Drop escaping of HTML-sensitive characters. defaults to `true`.
// Drop escaping of HTML-sensitive characters. defaults to `true`.
[<Optional; DefaultParameterValue(null)>] ?unsafeRelaxedJsonEscaping : bool,
/// <summary>Apply <c>TypeSafeEnumConverter</c> if possible. Defaults to <c>false</c>.</summary>
// <summary>Apply <c>TypeSafeEnumConverter</c> if possible. Defaults to <c>false</c>.</summary>
[<Optional; DefaultParameterValue(null)>] ?autoTypeSafeEnumToJsonString : bool,
/// <summary>Apply <c>UnionConverter</c> for all Discriminated Unions, if <c>TypeSafeEnumConverter</c> not possible. Defaults to <c>false</c>.</summary>
// <summary>Apply <c>UnionConverter</c> for all Discriminated Unions, if <c>TypeSafeEnumConverter</c> not possible. Defaults to <c>false</c>.</summary>
[<Optional; DefaultParameterValue(null)>] ?autoUnionToJsonObject : bool,
/// <summary>Apply <c>RejectNullStringConverter</c> in order to have serialization throw on <c>null</c> strings. Use <c>string option</c> to represent strings that can potentially be <c>null</c>.
// <summary>Apply <c>RejectNullStringConverter</c> in order to have serialization throw on <c>null</c> strings. Use <c>string option</c> to represent strings that can potentially be <c>null</c>.
[<Optional; DefaultParameterValue(null)>] ?rejectNullStrings: bool) =

let autoTypeSafeEnumToJsonString = defaultArg autoTypeSafeEnumToJsonString false
Expand Down
24 changes: 11 additions & 13 deletions src/FsCodec.SystemTextJson/TypeSafeEnumConverter.fs
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,19 @@ open System.Text.Json
/// Utilities for working with DUs where none of the cases have a value
module TypeSafeEnum =

let isTypeSafeEnum (typ : Type) =
Union.isUnion typ
&& Union.hasOnlyNullaryCases typ
let isTypeSafeEnum (t : Type) =
Union.isUnion t
&& Union.hasOnlyNullaryCases t

let tryParseT (t : Type) predicate =
let tryParseT (t : Type) (str : string) =
let u = Union.getInfo t
u.cases
|> Array.tryFindIndex (fun c -> predicate c.Name)
|> Option.map (fun tag -> u.caseConstructor.[tag] [||])
// TOCONSIDER memoize and/or push into `Union` https://github.com/jet/FsCodec/pull/41#discussion_r394473137
let tryParse<'T> (str : string) = tryParseT typeof<'T> ((=) str) |> Option.map (fun e -> e :?> 'T)
|> Array.tryFindIndex (fun c -> c.Name = str)
|> Option.map (fun tag -> u.caseConstructor[tag] [||])
let tryParse<'T> (str : string) = tryParseT typeof<'T> str |> Option.map (fun e -> e :?> 'T)

let parseT (t : Type) (str : string) =
match tryParseT t ((=) str) with
let parseT (t : Type) (str : string) =
match tryParseT t str with
| Some e -> e
| None ->
// Keep exception compat, but augment with a meaningful message.
Expand All @@ -29,9 +28,8 @@ module TypeSafeEnum =

let toString<'t> (x : 't) =
let u = Union.getInfo typeof<'t>
let tag = u.tagReader (box x)
// TOCONSIDER memoize and/or push into `Union` https://github.com/jet/FsCodec/pull/41#discussion_r394473137
u.cases.[tag].Name
let tag = u.tagReader x
u.cases[tag].Name

/// Maps strings to/from Union cases; refuses to convert for values not in the Union
type TypeSafeEnumConverter<'T>() =
Expand Down
8 changes: 4 additions & 4 deletions src/FsCodec.SystemTextJson/UnionConverter.fs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ module private Union =
|> Option.map (fun a -> a :?> IUnionConverterOptions) }
let getInfo : Type -> Union = memoize createInfo

/// Allows us to distinguish between Unions that have bodies and hence should UnionConverter
/// Allows us to distinguish Unions that do not have bodies and hence should use a TypeSafeEnumConverter
let hasOnlyNullaryCases (t : Type) =
let union = getInfo t
union.cases |> Seq.forall (fun case -> case.GetFields().Length = 0)
Expand All @@ -69,8 +69,8 @@ type UnionConverter<'T>() =
let union = Union.getInfo typeof<'T>
let unionOptions = getOptions union
let tag = union.tagReader value
let case = union.cases.[tag]
let fieldValues = union.fieldReader.[tag] value
let case = union.cases[tag]
let fieldValues = union.fieldReader[tag] value
let fieldInfos = case.GetFields()

writer.WriteStartObject()
Expand Down Expand Up @@ -111,7 +111,7 @@ type UnionConverter<'T>() =
inputCaseNameValue catchAllCaseName t.FullName |> invalidOp
| Some foundIndex -> foundIndex

let targetCaseFields, targetCaseCtor = union.cases.[targetCaseIndex].GetFields(), union.caseConstructor.[targetCaseIndex]
let targetCaseFields, targetCaseCtor = union.cases[targetCaseIndex].GetFields(), union.caseConstructor[targetCaseIndex]
let ctorArgs =
[| for fieldInfo in targetCaseFields ->
let t = fieldInfo.PropertyType
Expand Down