JSON enums (#175)

This commit is contained in:
Patrick Stevens
2024-06-27 21:23:06 +01:00
committed by GitHub
parent 6a81513a93
commit c590db2a65
9 changed files with 232 additions and 49 deletions

View File

@@ -93,6 +93,24 @@ open System
open System.Collections.Generic open System.Collections.Generic
open System.Text.Json.Serialization open System.Text.Json.Serialization
/// Module containing JSON serializing extension members for the SomeEnum type
[<AutoOpen>]
module SomeEnumJsonSerializeExtension =
/// Extension methods for JSON parsing
type SomeEnum with
/// Serialize to a JSON node
static member toJsonNode (input : SomeEnum) : System.Text.Json.Nodes.JsonNode =
match input with
| SomeEnum.Blah -> System.Text.Json.Nodes.JsonValue.Create 1
| SomeEnum.Thing -> System.Text.Json.Nodes.JsonValue.Create 0
| v -> failwith (sprintf "Unrecognised value for enum: %O" v)
namespace ConsumePlugin
open System
open System.Collections.Generic
open System.Text.Json.Serialization
/// Module containing JSON serializing extension members for the JsonRecordTypeWithBoth type /// Module containing JSON serializing extension members for the JsonRecordTypeWithBoth type
[<AutoOpen>] [<AutoOpen>]
module JsonRecordTypeWithBothJsonSerializeExtension = module JsonRecordTypeWithBothJsonSerializeExtension =
@@ -184,6 +202,8 @@ module JsonRecordTypeWithBothJsonSerializeExtension =
)) ))
) )
node.Add ("enum", (input.Enum |> SomeEnum.toJsonNode))
node :> _ node :> _
namespace ConsumePlugin namespace ConsumePlugin
@@ -323,6 +343,24 @@ module InnerTypeWithBothJsonParseExtension =
} }
namespace ConsumePlugin namespace ConsumePlugin
/// Module containing JSON parsing extension members for the SomeEnum type
[<AutoOpen>]
module SomeEnumJsonParseExtension =
/// Extension methods for JSON parsing
type SomeEnum with
/// Parse from a JSON node.
static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : SomeEnum =
match node.GetValueKind () with
| System.Text.Json.JsonValueKind.Number -> node.AsValue().GetValue<int> () |> enum<SomeEnum>
| System.Text.Json.JsonValueKind.String ->
match node.AsValue().GetValue<string>().ToLowerInvariant () with
| "blah" -> SomeEnum.Blah
| "thing" -> SomeEnum.Thing
| v -> failwith ("Unrecognised value for enum: %i" + v)
| _ -> failwith ("Unrecognised kind for enum of type: " + "SomeEnum")
namespace ConsumePlugin
/// Module containing JSON parsing extension members for the JsonRecordTypeWithBoth type /// Module containing JSON parsing extension members for the JsonRecordTypeWithBoth type
[<AutoOpen>] [<AutoOpen>]
module JsonRecordTypeWithBothJsonParseExtension = module JsonRecordTypeWithBothJsonParseExtension =
@@ -331,6 +369,18 @@ module JsonRecordTypeWithBothJsonParseExtension =
/// Parse from a JSON node. /// Parse from a JSON node.
static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : JsonRecordTypeWithBoth = static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : JsonRecordTypeWithBoth =
let arg_19 =
SomeEnum.jsonParse (
match node.["enum"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("enum")
)
)
| v -> v
)
let arg_18 = let arg_18 =
match node.["intMeasureNullable"] with match node.["intMeasureNullable"] with
| null -> System.Nullable () | null -> System.Nullable ()
@@ -585,6 +635,7 @@ module JsonRecordTypeWithBothJsonParseExtension =
Single = arg_16 Single = arg_16
IntMeasureOption = arg_17 IntMeasureOption = arg_17
IntMeasureNullable = arg_18 IntMeasureNullable = arg_18
Enum = arg_19
} }
namespace ConsumePlugin namespace ConsumePlugin

View File

@@ -16,6 +16,12 @@ type InnerTypeWithBoth =
ConcreteDict : Dictionary<string, InnerTypeWithBoth> ConcreteDict : Dictionary<string, InnerTypeWithBoth>
} }
[<WoofWare.Myriad.Plugins.JsonParse true>]
[<WoofWare.Myriad.Plugins.JsonSerialize true>]
type SomeEnum =
| Blah = 1
| Thing = 0
[<Measure>] [<Measure>]
type measure type measure
@@ -42,6 +48,7 @@ type JsonRecordTypeWithBoth =
Single : single<measure> Single : single<measure>
IntMeasureOption : int<measure> option IntMeasureOption : int<measure> option
IntMeasureNullable : int<measure> Nullable IntMeasureNullable : int<measure> Nullable
Enum : SomeEnum
} }
[<WoofWare.Myriad.Plugins.JsonSerialize true>] [<WoofWare.Myriad.Plugins.JsonSerialize true>]

View File

@@ -49,3 +49,15 @@ module TestJsonParse =
let actual = s |> JsonNode.Parse |> InnerType.jsonParse let actual = s |> JsonNode.Parse |> InnerType.jsonParse
actual |> shouldEqual expected actual |> shouldEqual expected
[<TestCase("thing", SomeEnum.Thing)>]
[<TestCase("Thing", SomeEnum.Thing)>]
[<TestCase("THING", SomeEnum.Thing)>]
[<TestCase("blah", SomeEnum.Blah)>]
[<TestCase("Blah", SomeEnum.Blah)>]
[<TestCase("BLAH", SomeEnum.Blah)>]
let ``Can deserialise enum`` (str : string, expected : SomeEnum) =
sprintf "\"%s\"" str
|> JsonNode.Parse
|> SomeEnum.jsonParse
|> shouldEqual expected

View File

@@ -91,6 +91,7 @@ module TestJsonSerde =
let! single = Arb.generate |> Gen.filter (fun s -> Single.IsFinite (s / 1.0f<measure>)) let! single = Arb.generate |> Gen.filter (fun s -> Single.IsFinite (s / 1.0f<measure>))
let! intMeasureOption = Arb.generate let! intMeasureOption = Arb.generate
let! intMeasureNullable = Arb.generate let! intMeasureNullable = Arb.generate
let! someEnum = Gen.choose (0, 1)
return return
{ {
@@ -113,6 +114,7 @@ module TestJsonSerde =
Single = single Single = single
IntMeasureOption = intMeasureOption IntMeasureOption = intMeasureOption
IntMeasureNullable = intMeasureNullable IntMeasureNullable = intMeasureNullable
Enum = enum<SomeEnum> someEnum
} }
} }

View File

@@ -96,6 +96,11 @@ type internal AdtProduct =
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module internal AstHelper = module internal AstHelper =
let isEnum (SynTypeDefn.SynTypeDefn (_, repr, _, _, _, _)) : bool =
match repr with
| SynTypeDefnRepr.Simple (SynTypeDefnSimpleRepr.Enum _, _) -> true
| _ -> false
let instantiateRecord (fields : (RecordFieldName * SynExpr option) list) : SynExpr = let instantiateRecord (fields : (RecordFieldName * SynExpr option) list) : SynExpr =
let fields = let fields =
fields fields

View File

@@ -1070,17 +1070,11 @@ module internal CataGenerator =
body body
|> SynExpr.createLet |> SynExpr.createLet
[ [
SynExpr.TypeApp ( (SynExpr.createIdent "ResizeArray")
SynExpr.createIdent "ResizeArray", |> SynExpr.typeApp
range0,
[ [
SynType.var (SynTypar.SynTypar (unionCase.GenericName, TyparStaticReq.None, false)) SynType.var (SynTypar.SynTypar (unionCase.GenericName, TyparStaticReq.None, false))
], ]
[],
Some range0,
range0,
range0
)
|> SynExpr.applyTo (SynExpr.CreateConst ()) |> SynExpr.applyTo (SynExpr.CreateConst ())
|> SynBinding.basic [ unionCase.StackName ] [] |> SynBinding.basic [ unionCase.StackName ] []
] ]

View File

@@ -24,7 +24,7 @@ module internal JsonParseGenerator =
JsonNumberHandlingArg = None JsonNumberHandlingArg = None
} }
/// (match {indexed} with | null -> raise (System.Collections.Generic.KeyNotFoundException ()) | v -> v) /// (match {indexed} with | null -> raise (System.Collections.Generic.KeyNotFoundException ({propertyName} not found)) | v -> v)
let assertNotNull (propertyName : SynExpr) (indexed : SynExpr) = let assertNotNull (propertyName : SynExpr) (indexed : SynExpr) =
let raiseExpr = let raiseExpr =
SynExpr.applyFunction SynExpr.applyFunction
@@ -488,6 +488,59 @@ module internal JsonParseGenerator =
|> SynBinding.basic [ Ident.create "ty" ] [] |> SynBinding.basic [ Ident.create "ty" ] []
] ]
let createEnumMaker
(spec : JsonParseOutputSpec)
(typeName : LongIdent)
(fields : (Ident * SynExpr) list)
: SynExpr
=
let numberKind =
[ "System" ; "Text" ; "Json" ; "JsonValueKind" ; "Number" ]
|> List.map Ident.create
let stringKind =
[ "System" ; "Text" ; "Json" ; "JsonValueKind" ; "String" ]
|> List.map Ident.create
let fail =
SynExpr.plus
(SynExpr.CreateConst "Unrecognised kind for enum of type: ")
(SynExpr.CreateConst (typeName |> List.map _.idText |> String.concat "."))
|> SynExpr.paren
|> SynExpr.applyFunction (SynExpr.createIdent "failwith")
let failString =
SynExpr.plus (SynExpr.CreateConst "Unrecognised value for enum: %i") (SynExpr.createIdent "v")
|> SynExpr.paren
|> SynExpr.applyFunction (SynExpr.createIdent "failwith")
let parseString =
fields
|> List.map (fun (ident, _) ->
SynMatchClause.create
(SynPat.createConst (
SynConst.String (ident.idText.ToLowerInvariant (), SynStringKind.Regular, range0)
))
(SynExpr.createLongIdent' (typeName @ [ ident ]))
)
|> fun l -> l @ [ SynMatchClause.create (SynPat.named "v") failString ]
|> SynExpr.createMatch (
asValueGetValue None "string" (SynExpr.createIdent "node")
|> SynExpr.callMethod "ToLowerInvariant"
)
[
SynMatchClause.create
(SynPat.identWithArgs numberKind (SynArgPats.create []))
(asValueGetValue None "int" (SynExpr.createIdent "node")
|> SynExpr.pipeThroughFunction (
SynExpr.typeApp [ SynType.createLongIdent typeName ] (SynExpr.createIdent "enum")
))
SynMatchClause.create (SynPat.identWithArgs stringKind (SynArgPats.create [])) parseString
SynMatchClause.create (SynPat.named "_") fail
]
|> SynExpr.createMatch (SynExpr.callMethod "GetValueKind" (SynExpr.createIdent "node"))
let createModule (namespaceId : LongIdent) (spec : JsonParseOutputSpec) (typeDefn : SynTypeDefn) = let createModule (namespaceId : LongIdent) (spec : JsonParseOutputSpec) (typeDefn : SynTypeDefn) =
let (SynTypeDefn (synComponentInfo, synTypeDefnRepr, _members, _implicitCtor, _, _)) = let (SynTypeDefn (synComponentInfo, synTypeDefnRepr, _members, _implicitCtor, _, _)) =
typeDefn typeDefn
@@ -548,6 +601,13 @@ module internal JsonParseGenerator =
|> List.map SynUnionCase.extract |> List.map SynUnionCase.extract
|> List.map (UnionCase.mapIdentFields optionGet) |> List.map (UnionCase.mapIdentFields optionGet)
|> createUnionMaker spec ident |> createUnionMaker spec ident
| SynTypeDefnRepr.Simple (SynTypeDefnSimpleRepr.Enum (cases, _range), _) ->
cases
|> List.map (fun c ->
match c with
| SynEnumCase.SynEnumCase (_, SynIdent.SynIdent (ident, _), value, _, _, _) -> ident, value
)
|> createEnumMaker spec ident
| _ -> failwithf "Not a record or union type" | _ -> failwithf "Not a record or union type"
[ scaffolding spec ident decl ] [ scaffolding spec ident decl ]
@@ -569,20 +629,21 @@ type JsonParseGenerator () =
let ast, _ = let ast, _ =
Ast.fromFilename context.InputFilename |> Async.RunSynchronously |> Array.head Ast.fromFilename context.InputFilename |> Async.RunSynchronously |> Array.head
let recordsAndUnions = let relevantTypes =
Ast.extractTypeDefn ast Ast.extractTypeDefn ast
|> List.map (fun (name, defns) -> |> List.map (fun (name, defns) ->
defns defns
|> List.choose (fun defn -> |> List.choose (fun defn ->
if Ast.isRecord defn then Some defn if Ast.isRecord defn then Some defn
elif Ast.isDu defn then Some defn elif Ast.isDu defn then Some defn
elif AstHelper.isEnum defn then Some defn
else None else None
) )
|> fun defns -> name, defns |> fun defns -> name, defns
) )
let namespaceAndTypes = let namespaceAndTypes =
recordsAndUnions relevantTypes
|> List.choose (fun (ns, types) -> |> List.choose (fun (ns, types) ->
types types
|> List.choose (fun typeDef -> |> List.choose (fun typeDef ->

View File

@@ -34,15 +34,8 @@ module internal JsonSerializeGenerator =
| Guid | Guid
| Uri -> | Uri ->
// JsonValue.Create<type> // JsonValue.Create<type>
SynExpr.TypeApp ( SynExpr.createLongIdent [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonValue" ; "Create" ]
SynExpr.createLongIdent [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonValue" ; "Create" ], |> SynExpr.typeApp [ fieldType ]
range0,
[ fieldType ],
[],
Some range0,
range0,
range0
)
| NullableType ty -> | NullableType ty ->
// fun field -> if field.HasValue then {serializeNode ty} field.Value else JsonValue.Create null // fun field -> if field.HasValue then {serializeNode ty} field.Value else JsonValue.Create null
SynExpr.applyFunction (serializeNode ty) (SynExpr.createLongIdent [ "field" ; "Value" ]) SynExpr.applyFunction (serializeNode ty) (SynExpr.createLongIdent [ "field" ; "Value" ])
@@ -238,8 +231,7 @@ module internal JsonSerializeGenerator =
|> SynBinding.withXmlDoc xmlDoc |> SynBinding.withXmlDoc xmlDoc
|> SynModuleDecl.createLet |> SynModuleDecl.createLet
let recordModule (spec : JsonSerializeOutputSpec) (typeName : LongIdent) (fields : SynField list) = let recordModule (spec : JsonSerializeOutputSpec) (_typeName : LongIdent) (fields : SynField list) =
let inputArg = Ident.create "input"
let fields = fields |> List.map SynField.extractWithIdent let fields = fields |> List.map SynField.extractWithIdent
fields fields
@@ -249,7 +241,6 @@ module internal JsonSerializeGenerator =
) )
|> SynExpr.sequential |> SynExpr.sequential
|> fun expr -> SynExpr.Do (expr, range0) |> fun expr -> SynExpr.Do (expr, range0)
|> scaffolding spec typeName inputArg
let unionModule (spec : JsonSerializeOutputSpec) (typeName : LongIdent) (cases : SynUnionCase list) = let unionModule (spec : JsonSerializeOutputSpec) (typeName : LongIdent) (cases : SynUnionCase list) =
let inputArg = Ident.create "input" let inputArg = Ident.create "input"
@@ -322,7 +313,68 @@ module internal JsonSerializeGenerator =
SynMatchClause.create pattern action SynMatchClause.create pattern action
) )
|> SynExpr.createMatch (SynExpr.createIdent' inputArg) |> SynExpr.createMatch (SynExpr.createIdent' inputArg)
|> scaffolding spec typeName inputArg
let enumModule
(spec : JsonSerializeOutputSpec)
(typeName : LongIdent)
(cases : (Ident * SynExpr) list)
: SynModuleDecl
=
let fail =
SynExpr.CreateConst "Unrecognised value for enum: %O"
|> SynExpr.applyFunction (SynExpr.createIdent "sprintf")
|> SynExpr.applyTo (SynExpr.createIdent "v")
|> SynExpr.paren
|> SynExpr.applyFunction (SynExpr.createIdent "failwith")
let body =
cases
|> List.map (fun (caseName, value) ->
value
|> SynExpr.applyFunction (
SynExpr.createLongIdent [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonValue" ; "Create" ]
)
|> SynMatchClause.create (SynPat.identWithArgs (typeName @ [ caseName ]) (SynArgPats.create []))
)
|> fun l -> l @ [ SynMatchClause.create (SynPat.named "v") fail ]
|> SynExpr.createMatch (SynExpr.createIdent "input")
let xmlDoc = PreXmlDoc.create "Serialize to a JSON node"
let returnInfo =
SynLongIdent.createS' [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonNode" ]
|> SynType.LongIdent
let functionName = Ident.create "toJsonNode"
let pattern =
SynPat.named "input"
|> SynPat.annotateType (SynType.LongIdent (SynLongIdent.create typeName))
if spec.ExtensionMethods then
let componentInfo =
SynComponentInfo.createLong typeName
|> SynComponentInfo.withDocString (PreXmlDoc.create "Extension methods for JSON parsing")
let memberDef =
body
|> SynBinding.basic [ functionName ] [ pattern ]
|> SynBinding.withXmlDoc xmlDoc
|> SynBinding.withReturnAnnotation returnInfo
|> SynMemberDefn.staticMember
let containingType =
SynTypeDefnRepr.augmentation ()
|> SynTypeDefn.create componentInfo
|> SynTypeDefn.withMemberDefns [ memberDef ]
SynModuleDecl.Types ([ containingType ], range0)
else
body
|> SynBinding.basic [ functionName ] [ pattern ]
|> SynBinding.withReturnAnnotation returnInfo
|> SynBinding.withXmlDoc xmlDoc
|> SynModuleDecl.createLet
let createModule let createModule
(namespaceId : LongIdent) (namespaceId : LongIdent)
@@ -378,14 +430,23 @@ module internal JsonSerializeGenerator =
let decls = let decls =
match synTypeDefnRepr with match synTypeDefnRepr with
| SynTypeDefnRepr.Simple (SynTypeDefnSimpleRepr.Record (_accessibility, recordFields, _range), _) -> | SynTypeDefnRepr.Simple (SynTypeDefnSimpleRepr.Record (_accessibility, recordFields, _range), _) ->
[ recordModule spec ident recordFields ] recordModule spec ident recordFields
|> scaffolding spec ident (Ident.create "input")
| SynTypeDefnRepr.Simple (SynTypeDefnSimpleRepr.Union (_accessibility, unionFields, _range), _) -> | SynTypeDefnRepr.Simple (SynTypeDefnSimpleRepr.Union (_accessibility, unionFields, _range), _) ->
[ unionModule spec ident unionFields ] unionModule spec ident unionFields
| _ -> failwithf "Only record types currently supported." |> scaffolding spec ident (Ident.create "input")
| SynTypeDefnRepr.Simple (SynTypeDefnSimpleRepr.Enum (cases, _range), _) ->
cases
|> List.map (fun c ->
match c with
| SynEnumCase.SynEnumCase (_, SynIdent.SynIdent (ident, _), value, _, _, _) -> ident, value
)
|> enumModule spec ident
| ty -> failwithf "Unsupported type: got %O" ty
[ [
yield! opens |> List.map SynModuleDecl.openAny yield! opens |> List.map SynModuleDecl.openAny
yield SynModuleDecl.nestedModule info decls yield decls |> List.singleton |> SynModuleDecl.nestedModule info
] ]
|> SynModuleOrNamespace.createNamespace namespaceId |> SynModuleOrNamespace.createNamespace namespaceId
@@ -403,20 +464,21 @@ type JsonSerializeGenerator () =
let ast, _ = let ast, _ =
Ast.fromFilename context.InputFilename |> Async.RunSynchronously |> Array.head Ast.fromFilename context.InputFilename |> Async.RunSynchronously |> Array.head
let recordsAndUnions = let relevantTypes =
Ast.extractTypeDefn ast Ast.extractTypeDefn ast
|> List.map (fun (name, defns) -> |> List.map (fun (name, defns) ->
defns defns
|> List.choose (fun defn -> |> List.choose (fun defn ->
if Ast.isRecord defn then Some defn if Ast.isRecord defn then Some defn
elif Ast.isDu defn then Some defn elif Ast.isDu defn then Some defn
elif AstHelper.isEnum defn then Some defn
else None else None
) )
|> fun defns -> name, defns |> fun defns -> name, defns
) )
let namespaceAndTypes = let namespaceAndTypes =
recordsAndUnions relevantTypes
|> List.choose (fun (ns, types) -> |> List.choose (fun (ns, types) ->
types types
|> List.choose (fun typeDef -> |> List.choose (fun typeDef ->

View File

@@ -121,29 +121,18 @@ module internal SynExpr =
let callMethod (meth : string) (obj : SynExpr) : SynExpr = let callMethod (meth : string) (obj : SynExpr) : SynExpr =
callMethodArg meth (SynExpr.CreateConst ()) obj callMethodArg meth (SynExpr.CreateConst ()) obj
let typeApp (types : SynType list) (operand : SynExpr) =
SynExpr.TypeApp (operand, range0, types, List.replicate (types.Length - 1) range0, Some range0, range0, range0)
let callGenericMethod (meth : string) (ty : LongIdent) (obj : SynExpr) : SynExpr = let callGenericMethod (meth : string) (ty : LongIdent) (obj : SynExpr) : SynExpr =
SynExpr.TypeApp ( SynExpr.DotGet (obj, range0, SynLongIdent.createS meth, range0)
SynExpr.DotGet (obj, range0, SynLongIdent.createS meth, range0), |> typeApp [ SynType.LongIdent (SynLongIdent.create ty) ]
range0,
[ SynType.LongIdent (SynLongIdent.create ty) ],
[],
Some range0,
range0,
range0
)
|> applyTo (SynExpr.CreateConst ()) |> applyTo (SynExpr.CreateConst ())
/// {obj}.{meth}<ty>() /// {obj}.{meth}<ty>()
let callGenericMethod' (meth : string) (ty : string) (obj : SynExpr) : SynExpr = let callGenericMethod' (meth : string) (ty : string) (obj : SynExpr) : SynExpr =
SynExpr.TypeApp ( SynExpr.DotGet (obj, range0, SynLongIdent.createS meth, range0)
SynExpr.DotGet (obj, range0, SynLongIdent.createS meth, range0), |> typeApp [ SynType.createLongIdent' [ ty ] ]
range0,
[ SynType.createLongIdent' [ ty ] ],
[],
Some range0,
range0,
range0
)
|> applyTo (SynExpr.CreateConst ()) |> applyTo (SynExpr.CreateConst ())
let inline index (property : SynExpr) (obj : SynExpr) : SynExpr = let inline index (property : SynExpr) (obj : SynExpr) : SynExpr =