First release (#10)
Some checks are pending
.NET / build (Debug) (push) Waiting to run
.NET / build (Release) (push) Waiting to run
.NET / analyzers (push) Waiting to run
.NET / check-dotnet-format (push) Waiting to run
.NET / check-nix-format (push) Waiting to run
.NET / Check links (push) Waiting to run
.NET / Check flake (push) Waiting to run
.NET / nuget-pack (push) Waiting to run
.NET / expected-pack (push) Blocked by required conditions
.NET / check-accurate-generations (push) Waiting to run
.NET / all-required-checks-complete (push) Blocked by required conditions
.NET / nuget-publish (push) Blocked by required conditions
.NET / nuget-publish-fantomas (push) Blocked by required conditions
.NET / nuget-publish-json-plugin (push) Blocked by required conditions
.NET / nuget-publish-json-attrs (push) Blocked by required conditions
.NET / nuget-publish-argparser-plugin (push) Blocked by required conditions
.NET / nuget-publish-argparser-attrs (push) Blocked by required conditions

This commit is contained in:
Patrick Stevens
2024-10-07 13:35:43 +01:00
committed by GitHub
parent dc7a0f6fc2
commit da609db2ce
60 changed files with 14225 additions and 67 deletions

View File

@@ -0,0 +1,37 @@
namespace WoofWare.Whippet.Plugin.Json
open System
/// Attribute indicating a record type to which the "Add JSON serializer" Whippet
/// generator should apply during build.
/// The purpose of this generator is to create methods (possibly extension methods) of the form
/// `{TypeName}.toJsonNode : {TypeName} -> System.Text.Json.Nodes.JsonNode`.
///
/// If you supply isExtensionMethod = false, you will get a module rather than extension methods.
/// Extension methods can only be consumed from F#, but the benefit is that they don't use up the module name.
/// (If you set this to `false`, we create a module called "{TypeName}").
type JsonSerializeAttribute (isExtensionMethod : bool) =
inherit Attribute ()
/// The default value of `isExtensionMethod`, the optional argument to the JsonSerializeAttribute constructor.
static member DefaultIsExtensionMethod = true
/// Shorthand for the "isExtensionMethod = false" constructor; see documentation there for details.
new () = JsonSerializeAttribute JsonSerializeAttribute.DefaultIsExtensionMethod
/// Attribute indicating a record type to which the "Add JSON parse" Whippet
/// generator should apply during build.
/// The purpose of this generator is to create methods (possibly extension methods) of the form
/// `{TypeName}.jsonParse : System.Text.Json.Nodes.JsonNode -> {TypeName}`.
///
/// If you supply isExtensionMethod = false, you will get extension methods.
/// Extension methods can only be consumed from F#, but the benefit is that they don't use up the module name
/// (If you set this to `false`, we create a module called "{TypeName}").
type JsonParseAttribute (isExtensionMethod : bool) =
inherit Attribute ()
/// The default value of `isExtensionMethod`, the optional argument to the JsonParseAttribute constructor.
static member DefaultIsExtensionMethod = true
/// Shorthand for the "isExtensionMethod = false" constructor; see documentation there for details.
new () = JsonParseAttribute JsonParseAttribute.DefaultIsExtensionMethod

View File

@@ -0,0 +1,6 @@
# WoofWare.Whippet.Plugin.Json.Attributes
This is a very slim runtime dependency which consumers of WoofWare.Whippet.Plugin.Json may optionally take.
This dependency contains attributes which control that source generator,
although you may instead omit this dependency and control the generator entirely through configuration in consumer's `.fsproj`.
Please see WoofWare.Whippet.Plugin.Json's README for further information.

View File

@@ -0,0 +1,10 @@
WoofWare.Whippet.Plugin.Json.JsonParseAttribute inherit System.Attribute
WoofWare.Whippet.Plugin.Json.JsonParseAttribute..ctor [constructor]: bool
WoofWare.Whippet.Plugin.Json.JsonParseAttribute..ctor [constructor]: unit
WoofWare.Whippet.Plugin.Json.JsonParseAttribute.DefaultIsExtensionMethod [static property]: [read-only] bool
WoofWare.Whippet.Plugin.Json.JsonParseAttribute.get_DefaultIsExtensionMethod [static method]: unit -> bool
WoofWare.Whippet.Plugin.Json.JsonSerializeAttribute inherit System.Attribute
WoofWare.Whippet.Plugin.Json.JsonSerializeAttribute..ctor [constructor]: bool
WoofWare.Whippet.Plugin.Json.JsonSerializeAttribute..ctor [constructor]: unit
WoofWare.Whippet.Plugin.Json.JsonSerializeAttribute.DefaultIsExtensionMethod [static property]: [read-only] bool
WoofWare.Whippet.Plugin.Json.JsonSerializeAttribute.get_DefaultIsExtensionMethod [static method]: unit -> bool

View File

@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Authors>Patrick Stevens</Authors>
<Copyright>Copyright (c) Patrick Stevens 2024</Copyright>
<Description>Attributes to accompany the WoofWare.Whippet.Plugin.Json source generator, to indicate what you want your types to be doing.</Description>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/Smaug123/WoofWare.Whippet</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageTags>fsharp;source-generator;source-gen;whippet;arguments;arg-parser</PackageTags>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarnOn>FS3559</WarnOn>
<PackageId>WoofWare.Whippet.Plugin.Json.Attributes</PackageId>
</PropertyGroup>
<ItemGroup>
<Compile Include="Attributes.fs" />
<EmbeddedResource Include="SurfaceBaseline.txt" />
<EmbeddedResource Include="version.json" />
<None Include="README.md">
<Pack>True</Pack>
<PackagePath>/</PackagePath>
<Link>README.md</Link>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Update="FSharp.Core" Version="4.3.4" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,11 @@
{
"version": "0.2",
"publicReleaseRefSpec": [
"^refs/heads/main$"
],
"pathFilters": [
"./",
":/global.json",
":/Directory.Build.props"
]
}

View File

@@ -0,0 +1,483 @@
namespace ConsumePlugin
open System.Text.Json.Serialization
open WoofWare.Whippet.Plugin.Json
/// Module containing JSON serializing extension members for the InternalTypeNotExtensionSerial type
[<AutoOpen>]
module internal InternalTypeNotExtensionSerialJsonSerializeExtension =
/// Extension methods for JSON parsing
type InternalTypeNotExtensionSerial with
/// Serialize to a JSON node
static member toJsonNode (input : InternalTypeNotExtensionSerial) : System.Text.Json.Nodes.JsonNode =
let node = System.Text.Json.Nodes.JsonObject ()
do
node.Add (
(Literals.something),
(input.InternalThing2 |> System.Text.Json.Nodes.JsonValue.Create<string>)
)
node :> _
namespace ConsumePlugin
open System.Text.Json.Serialization
open WoofWare.Whippet.Plugin.Json
/// Module containing JSON serializing extension members for the InternalTypeExtension type
[<AutoOpen>]
module internal InternalTypeExtensionJsonSerializeExtension =
/// Extension methods for JSON parsing
type InternalTypeExtension with
/// Serialize to a JSON node
static member toJsonNode (input : InternalTypeExtension) : System.Text.Json.Nodes.JsonNode =
let node = System.Text.Json.Nodes.JsonObject ()
do node.Add ((Literals.something), (input.ExternalThing |> System.Text.Json.Nodes.JsonValue.Create<string>))
node :> _
namespace ConsumePlugin
/// Module containing JSON parsing extension members for the InnerType type
[<AutoOpen>]
module InnerTypeJsonParseExtension =
/// Extension methods for JSON parsing
type InnerType with
/// Parse from a JSON node.
static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : InnerType =
let arg_0 =
(match node.[(Literals.something)] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ((Literals.something))
)
)
| v -> v)
.AsValue()
.GetValue<System.String> ()
{
Thing = arg_0
}
namespace ConsumePlugin
/// Module containing JSON parsing extension members for the JsonRecordType type
[<AutoOpen>]
module JsonRecordTypeJsonParseExtension =
/// Extension methods for JSON parsing
type JsonRecordType with
/// Parse from a JSON node.
static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : JsonRecordType =
let arg_5 =
(match node.["f"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("f")
)
)
| v -> v)
.AsArray ()
|> Seq.map (fun elt -> elt.AsValue().GetValue<System.Int32> ())
|> Array.ofSeq
let arg_4 =
(match node.["e"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("e")
)
)
| v -> v)
.AsArray ()
|> Seq.map (fun elt -> elt.AsValue().GetValue<System.String> ())
|> Array.ofSeq
let arg_3 =
InnerType.jsonParse (
match node.["d"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("d")
)
)
| v -> v
)
let arg_2 =
(match node.["hi"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("hi")
)
)
| v -> v)
.AsArray ()
|> Seq.map (fun elt -> elt.AsValue().GetValue<System.Int32> ())
|> List.ofSeq
let arg_1 =
(match node.["another-thing"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("another-thing")
)
)
| v -> v)
.AsValue()
.GetValue<System.String> ()
let arg_0 =
(match node.["a"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("a")
)
)
| v -> v)
.AsValue()
.GetValue<System.Int32> ()
{
A = arg_0
B = arg_1
C = arg_2
D = arg_3
E = arg_4
F = arg_5
}
namespace ConsumePlugin
/// Module containing JSON parsing extension members for the InternalTypeNotExtension type
[<AutoOpen>]
module internal InternalTypeNotExtensionJsonParseExtension =
/// Extension methods for JSON parsing
type InternalTypeNotExtension with
/// Parse from a JSON node.
static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : InternalTypeNotExtension =
let arg_0 =
(match node.[(Literals.something)] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ((Literals.something))
)
)
| v -> v)
.AsValue()
.GetValue<System.String> ()
{
InternalThing = arg_0
}
namespace ConsumePlugin
/// Module containing JSON parsing extension members for the InternalTypeExtension type
[<AutoOpen>]
module internal InternalTypeExtensionJsonParseExtension =
/// Extension methods for JSON parsing
type InternalTypeExtension with
/// Parse from a JSON node.
static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : InternalTypeExtension =
let arg_0 =
(match node.[(Literals.something)] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ((Literals.something))
)
)
| v -> v)
.AsValue()
.GetValue<System.String> ()
{
ExternalThing = arg_0
}
namespace ConsumePlugin
/// Module containing JSON parsing extension members for the ToGetExtensionMethod type
[<AutoOpen>]
module ToGetExtensionMethodJsonParseExtension =
/// Extension methods for JSON parsing
type ToGetExtensionMethod with
/// Parse from a JSON node.
static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : ToGetExtensionMethod =
let arg_20 = System.Numerics.BigInteger.Parse (node.["whiskey"].ToJsonString ())
let arg_19 =
(match node.["victor"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("victor")
)
)
| v -> v)
.AsValue()
.GetValue<System.Char> ()
let arg_18 =
(match node.["uniform"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("uniform")
)
)
| v -> v)
.AsValue()
.GetValue<System.Decimal> ()
let arg_17 =
(match node.["tango"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("tango")
)
)
| v -> v)
.AsValue()
.GetValue<System.SByte> ()
let arg_16 =
(match node.["quebec"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("quebec")
)
)
| v -> v)
.AsValue()
.GetValue<System.Byte> ()
let arg_15 =
(match node.["papa"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("papa")
)
)
| v -> v)
.AsValue()
.GetValue<System.Byte> ()
let arg_14 =
(match node.["oscar"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("oscar")
)
)
| v -> v)
.AsValue()
.GetValue<System.SByte> ()
let arg_13 =
(match node.["november"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("november")
)
)
| v -> v)
.AsValue()
.GetValue<System.UInt16> ()
let arg_12 =
(match node.["mike"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("mike")
)
)
| v -> v)
.AsValue()
.GetValue<System.Int16> ()
let arg_11 =
(match node.["lima"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("lima")
)
)
| v -> v)
.AsValue()
.GetValue<System.UInt32> ()
let arg_10 =
(match node.["kilo"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("kilo")
)
)
| v -> v)
.AsValue()
.GetValue<System.Int32> ()
let arg_9 =
(match node.["juliette"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("juliette")
)
)
| v -> v)
.AsValue()
.GetValue<System.UInt32> ()
let arg_8 =
(match node.["india"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("india")
)
)
| v -> v)
.AsValue()
.GetValue<System.Int32> ()
let arg_7 =
(match node.["hotel"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("hotel")
)
)
| v -> v)
.AsValue()
.GetValue<System.UInt64> ()
let arg_6 =
(match node.["golf"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("golf")
)
)
| v -> v)
.AsValue()
.GetValue<System.Int64> ()
let arg_5 =
(match node.["foxtrot"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("foxtrot")
)
)
| v -> v)
.AsValue()
.GetValue<System.Double> ()
let arg_4 =
(match node.["echo"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("echo")
)
)
| v -> v)
.AsValue()
.GetValue<System.Single> ()
let arg_3 =
(match node.["delta"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("delta")
)
)
| v -> v)
.AsValue()
.GetValue<System.Single> ()
let arg_2 =
(match node.["charlie"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("charlie")
)
)
| v -> v)
.AsValue()
.GetValue<System.Double> ()
let arg_1 =
(match node.["bravo"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("bravo")
)
)
| v -> v)
.AsValue()
.GetValue<string> ()
|> System.Uri
let arg_0 =
(match node.["alpha"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("alpha")
)
)
| v -> v)
.AsValue()
.GetValue<System.String> ()
{
Alpha = arg_0
Bravo = arg_1
Charlie = arg_2
Delta = arg_3
Echo = arg_4
Foxtrot = arg_5
Golf = arg_6
Hotel = arg_7
India = arg_8
Juliette = arg_9
Kilo = arg_10
Lima = arg_11
Mike = arg_12
November = arg_13
Oscar = arg_14
Papa = arg_15
Quebec = arg_16
Tango = arg_17
Uniform = arg_18
Victor = arg_19
Whiskey = arg_20
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,977 @@
namespace ConsumePlugin
open System
open System.Collections.Generic
open System.Text.Json.Serialization
/// Module containing JSON serializing extension members for the InnerTypeWithBoth type
[<AutoOpen>]
module InnerTypeWithBothJsonSerializeExtension =
/// Extension methods for JSON parsing
type InnerTypeWithBoth with
/// Serialize to a JSON node
static member toJsonNode (input : InnerTypeWithBoth) : System.Text.Json.Nodes.JsonNode =
let node = System.Text.Json.Nodes.JsonObject ()
do
node.Add (("it's-a-me"), (input.Thing |> System.Text.Json.Nodes.JsonValue.Create<Guid>))
node.Add (
"map",
(input.Map
|> (fun field ->
let ret = System.Text.Json.Nodes.JsonObject ()
for (KeyValue (key, value)) in field do
ret.Add (key.ToString (), System.Text.Json.Nodes.JsonValue.Create<Uri> value)
ret
))
)
node.Add (
"readOnlyDict",
(input.ReadOnlyDict
|> (fun field ->
let ret = System.Text.Json.Nodes.JsonObject ()
for (KeyValue (key, value)) in field do
ret.Add (
key.ToString (),
(fun field ->
let arr = System.Text.Json.Nodes.JsonArray ()
for mem in field do
arr.Add (System.Text.Json.Nodes.JsonValue.Create<char> mem)
arr
)
value
)
ret
))
)
node.Add (
"dict",
(input.Dict
|> (fun field ->
let ret = System.Text.Json.Nodes.JsonObject ()
for (KeyValue (key, value)) in field do
ret.Add (key.ToString (), System.Text.Json.Nodes.JsonValue.Create<bool> value)
ret
))
)
node.Add (
"concreteDict",
(input.ConcreteDict
|> (fun field ->
let ret = System.Text.Json.Nodes.JsonObject ()
for (KeyValue (key, value)) in field do
ret.Add (key.ToString (), InnerTypeWithBoth.toJsonNode value)
ret
))
)
node :> _
namespace ConsumePlugin
open System
open System.Collections.Generic
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
[<AutoOpen>]
module JsonRecordTypeWithBothJsonSerializeExtension =
/// Extension methods for JSON parsing
type JsonRecordTypeWithBoth with
/// Serialize to a JSON node
static member toJsonNode (input : JsonRecordTypeWithBoth) : System.Text.Json.Nodes.JsonNode =
let node = System.Text.Json.Nodes.JsonObject ()
do
node.Add ("a", (input.A |> System.Text.Json.Nodes.JsonValue.Create<int>))
node.Add ("b", (input.B |> System.Text.Json.Nodes.JsonValue.Create<string>))
node.Add (
"c",
(input.C
|> (fun field ->
let arr = System.Text.Json.Nodes.JsonArray ()
for mem in field do
arr.Add (System.Text.Json.Nodes.JsonValue.Create<int> mem)
arr
))
)
node.Add ("d", (input.D |> InnerTypeWithBoth.toJsonNode))
node.Add (
"e",
(input.E
|> (fun field ->
let arr = System.Text.Json.Nodes.JsonArray ()
for mem in field do
arr.Add (System.Text.Json.Nodes.JsonValue.Create<string> mem)
arr
))
)
node.Add (
"arr",
(input.Arr
|> (fun field ->
let arr = System.Text.Json.Nodes.JsonArray ()
for mem in field do
arr.Add (System.Text.Json.Nodes.JsonValue.Create<int> mem)
arr
))
)
node.Add ("byte", (input.Byte |> System.Text.Json.Nodes.JsonValue.Create<byte<measure>>))
node.Add ("sbyte", (input.Sbyte |> System.Text.Json.Nodes.JsonValue.Create<sbyte<measure>>))
node.Add ("i", (input.I |> System.Text.Json.Nodes.JsonValue.Create<int<measure>>))
node.Add ("i32", (input.I32 |> System.Text.Json.Nodes.JsonValue.Create<int32<measure>>))
node.Add ("i64", (input.I64 |> System.Text.Json.Nodes.JsonValue.Create<int64<measure>>))
node.Add ("u", (input.U |> System.Text.Json.Nodes.JsonValue.Create<uint<measure>>))
node.Add ("u32", (input.U32 |> System.Text.Json.Nodes.JsonValue.Create<uint32<measure>>))
node.Add ("u64", (input.U64 |> System.Text.Json.Nodes.JsonValue.Create<uint64<measure>>))
node.Add ("f", (input.F |> System.Text.Json.Nodes.JsonValue.Create<float<measure>>))
node.Add ("f32", (input.F32 |> System.Text.Json.Nodes.JsonValue.Create<float32<measure>>))
node.Add ("single", (input.Single |> System.Text.Json.Nodes.JsonValue.Create<single<measure>>))
node.Add (
"intMeasureOption",
(input.IntMeasureOption
|> (fun field ->
match field with
| None -> null :> System.Text.Json.Nodes.JsonNode
| Some field ->
(System.Text.Json.Nodes.JsonValue.Create<int<measure>> field)
:> System.Text.Json.Nodes.JsonNode
))
)
node.Add (
"intMeasureNullable",
(input.IntMeasureNullable
|> (fun field ->
if field.HasValue then
System.Text.Json.Nodes.JsonValue.Create<int<measure>> field.Value
:> System.Text.Json.Nodes.JsonNode
else
null :> System.Text.Json.Nodes.JsonNode
))
)
node.Add ("enum", (input.Enum |> SomeEnum.toJsonNode))
node.Add (
"timestamp",
(input.Timestamp
|> (fun field -> field.ToString "o" |> System.Text.Json.Nodes.JsonValue.Create<string>))
)
node.Add ("unit", (input.Unit |> (fun value -> System.Text.Json.Nodes.JsonObject ())))
node :> _
namespace ConsumePlugin
open System
open System.Collections.Generic
open System.Text.Json.Serialization
/// Module containing JSON serializing extension members for the FirstDu type
[<AutoOpen>]
module FirstDuJsonSerializeExtension =
/// Extension methods for JSON parsing
type FirstDu with
/// Serialize to a JSON node
static member toJsonNode (input : FirstDu) : System.Text.Json.Nodes.JsonNode =
let node = System.Text.Json.Nodes.JsonObject ()
match input with
| FirstDu.EmptyCase -> node.Add ("type", System.Text.Json.Nodes.JsonValue.Create "emptyCase")
| FirstDu.Case1 arg0 ->
node.Add ("type", System.Text.Json.Nodes.JsonValue.Create "case1")
let dataNode = System.Text.Json.Nodes.JsonObject ()
dataNode.Add ("data", System.Text.Json.Nodes.JsonValue.Create<string> arg0)
node.Add ("data", dataNode)
| FirstDu.Case2 (arg0, arg1) ->
node.Add ("type", System.Text.Json.Nodes.JsonValue.Create "case2")
let dataNode = System.Text.Json.Nodes.JsonObject ()
dataNode.Add ("record", JsonRecordTypeWithBoth.toJsonNode arg0)
dataNode.Add ("i", System.Text.Json.Nodes.JsonValue.Create<int> arg1)
node.Add ("data", dataNode)
node :> _
namespace ConsumePlugin
open System
open System.Collections.Generic
open System.Text.Json.Serialization
/// Module containing JSON serializing extension members for the HeaderAndValue type
[<AutoOpen>]
module HeaderAndValueJsonSerializeExtension =
/// Extension methods for JSON parsing
type HeaderAndValue with
/// Serialize to a JSON node
static member toJsonNode (input : HeaderAndValue) : System.Text.Json.Nodes.JsonNode =
let node = System.Text.Json.Nodes.JsonObject ()
do
node.Add ("header", (input.Header |> System.Text.Json.Nodes.JsonValue.Create<string>))
node.Add ("value", (input.Value |> System.Text.Json.Nodes.JsonValue.Create<string>))
node :> _
namespace ConsumePlugin
open System
open System.Collections.Generic
open System.Text.Json.Serialization
/// Module containing JSON serializing extension members for the Foo type
[<AutoOpen>]
module FooJsonSerializeExtension =
/// Extension methods for JSON parsing
type Foo with
/// Serialize to a JSON node
static member toJsonNode (input : Foo) : System.Text.Json.Nodes.JsonNode =
let node = System.Text.Json.Nodes.JsonObject ()
do
node.Add (
"message",
(input.Message
|> (fun field ->
match field with
| None -> null :> System.Text.Json.Nodes.JsonNode
| Some field -> HeaderAndValue.toJsonNode field
))
)
node :> _
namespace ConsumePlugin
open System
open System.Collections.Generic
open System.Text.Json.Serialization
/// Module containing JSON serializing extension members for the CollectRemaining type
[<AutoOpen>]
module CollectRemainingJsonSerializeExtension =
/// Extension methods for JSON parsing
type CollectRemaining with
/// Serialize to a JSON node
static member toJsonNode (input : CollectRemaining) : System.Text.Json.Nodes.JsonNode =
let node = System.Text.Json.Nodes.JsonObject ()
do
node.Add (
"message",
(input.Message
|> (fun field ->
match field with
| None -> null :> System.Text.Json.Nodes.JsonNode
| Some field -> HeaderAndValue.toJsonNode field
))
)
for KeyValue (key, value) in input.Rest do
node.Add (key, id value)
node :> _
namespace ConsumePlugin
open System
open System.Collections.Generic
open System.Text.Json.Serialization
/// Module containing JSON serializing extension members for the OuterCollectRemaining type
[<AutoOpen>]
module OuterCollectRemainingJsonSerializeExtension =
/// Extension methods for JSON parsing
type OuterCollectRemaining with
/// Serialize to a JSON node
static member toJsonNode (input : OuterCollectRemaining) : System.Text.Json.Nodes.JsonNode =
let node = System.Text.Json.Nodes.JsonObject ()
do
for KeyValue (key, value) in input.Others do
node.Add (key, System.Text.Json.Nodes.JsonValue.Create<int> value)
node.Add ("remaining", (input.Remaining |> CollectRemaining.toJsonNode))
node :> _
namespace ConsumePlugin
/// Module containing JSON parsing extension members for the InnerTypeWithBoth type
[<AutoOpen>]
module InnerTypeWithBothJsonParseExtension =
/// Extension methods for JSON parsing
type InnerTypeWithBoth with
/// Parse from a JSON node.
static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : InnerTypeWithBoth =
let arg_4 =
(match node.["concreteDict"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("concreteDict")
)
)
| v -> v)
.AsObject ()
|> Seq.map (fun kvp ->
let key = (kvp.Key)
let value = InnerTypeWithBoth.jsonParse (kvp.Value)
key, value
)
|> Seq.map System.Collections.Generic.KeyValuePair
|> System.Collections.Generic.Dictionary
let arg_3 =
(match node.["dict"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("dict")
)
)
| v -> v)
.AsObject ()
|> Seq.map (fun kvp ->
let key = (kvp.Key) |> System.Uri
let value = (kvp.Value).AsValue().GetValue<System.Boolean> ()
key, value
)
|> dict
let arg_2 =
(match node.["readOnlyDict"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("readOnlyDict")
)
)
| v -> v)
.AsObject ()
|> Seq.map (fun kvp ->
let key = (kvp.Key)
let value =
(kvp.Value).AsArray ()
|> Seq.map (fun elt -> elt.AsValue().GetValue<System.Char> ())
|> List.ofSeq
key, value
)
|> readOnlyDict
let arg_1 =
(match node.["map"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("map")
)
)
| v -> v)
.AsObject ()
|> Seq.map (fun kvp ->
let key = (kvp.Key)
let value = (kvp.Value).AsValue().GetValue<string> () |> System.Uri
key, value
)
|> Map.ofSeq
let arg_0 =
(match node.[("it's-a-me")] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" (("it's-a-me"))
)
)
| v -> v)
.AsValue()
.GetValue<string> ()
|> System.Guid.Parse
{
Thing = arg_0
Map = arg_1
ReadOnlyDict = arg_2
Dict = arg_3
ConcreteDict = arg_4
}
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
[<AutoOpen>]
module JsonRecordTypeWithBothJsonParseExtension =
/// Extension methods for JSON parsing
type JsonRecordTypeWithBoth with
/// Parse from a JSON node.
static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : JsonRecordTypeWithBoth =
let arg_21 = ()
let arg_20 =
(match node.["timestamp"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("timestamp")
)
)
| v -> v)
.AsValue()
.GetValue<string> ()
|> System.DateTimeOffset.Parse
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 =
match node.["intMeasureNullable"] with
| null -> System.Nullable ()
| v ->
v.AsValue().GetValue<System.Int32> ()
|> LanguagePrimitives.Int32WithMeasure
|> System.Nullable
let arg_17 =
match node.["intMeasureOption"] with
| null -> None
| v ->
v.AsValue().GetValue<System.Int32> ()
|> LanguagePrimitives.Int32WithMeasure
|> Some
let arg_16 =
(match node.["single"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("single")
)
)
| v -> v)
.AsValue()
.GetValue<System.Single> ()
|> LanguagePrimitives.Float32WithMeasure
let arg_15 =
(match node.["f32"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("f32")
)
)
| v -> v)
.AsValue()
.GetValue<System.Single> ()
|> LanguagePrimitives.Float32WithMeasure
let arg_14 =
(match node.["f"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("f")
)
)
| v -> v)
.AsValue()
.GetValue<System.Double> ()
|> LanguagePrimitives.FloatWithMeasure
let arg_13 =
(match node.["u64"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("u64")
)
)
| v -> v)
.AsValue()
.GetValue<System.UInt64> ()
|> LanguagePrimitives.UInt64WithMeasure
let arg_12 =
(match node.["u32"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("u32")
)
)
| v -> v)
.AsValue()
.GetValue<System.UInt32> ()
|> LanguagePrimitives.UInt32WithMeasure
let arg_11 =
(match node.["u"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("u")
)
)
| v -> v)
.AsValue()
.GetValue<System.UInt32> ()
|> LanguagePrimitives.UInt32WithMeasure
let arg_10 =
(match node.["i64"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("i64")
)
)
| v -> v)
.AsValue()
.GetValue<System.Int64> ()
|> LanguagePrimitives.Int64WithMeasure
let arg_9 =
(match node.["i32"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("i32")
)
)
| v -> v)
.AsValue()
.GetValue<System.Int32> ()
|> LanguagePrimitives.Int32WithMeasure
let arg_8 =
(match node.["i"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("i")
)
)
| v -> v)
.AsValue()
.GetValue<System.Int32> ()
|> LanguagePrimitives.Int32WithMeasure
let arg_7 =
(match node.["sbyte"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("sbyte")
)
)
| v -> v)
.AsValue()
.GetValue<System.SByte> ()
|> LanguagePrimitives.SByteWithMeasure
let arg_6 =
(match node.["byte"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("byte")
)
)
| v -> v)
.AsValue()
.GetValue<System.Byte> ()
|> LanguagePrimitives.ByteWithMeasure
let arg_5 =
(match node.["arr"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("arr")
)
)
| v -> v)
.AsArray ()
|> Seq.map (fun elt -> elt.AsValue().GetValue<System.Int32> ())
|> Array.ofSeq
let arg_4 =
(match node.["e"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("e")
)
)
| v -> v)
.AsArray ()
|> Seq.map (fun elt -> elt.AsValue().GetValue<System.String> ())
|> Array.ofSeq
let arg_3 =
InnerTypeWithBoth.jsonParse (
match node.["d"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("d")
)
)
| v -> v
)
let arg_2 =
(match node.["c"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("c")
)
)
| v -> v)
.AsArray ()
|> Seq.map (fun elt -> elt.AsValue().GetValue<System.Int32> ())
|> List.ofSeq
let arg_1 =
(match node.["b"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("b")
)
)
| v -> v)
.AsValue()
.GetValue<System.String> ()
let arg_0 =
(match node.["a"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("a")
)
)
| v -> v)
.AsValue()
.GetValue<System.Int32> ()
{
A = arg_0
B = arg_1
C = arg_2
D = arg_3
E = arg_4
Arr = arg_5
Byte = arg_6
Sbyte = arg_7
I = arg_8
I32 = arg_9
I64 = arg_10
U = arg_11
U32 = arg_12
U64 = arg_13
F = arg_14
F32 = arg_15
Single = arg_16
IntMeasureOption = arg_17
IntMeasureNullable = arg_18
Enum = arg_19
Timestamp = arg_20
Unit = arg_21
}
namespace ConsumePlugin
/// Module containing JSON parsing extension members for the FirstDu type
[<AutoOpen>]
module FirstDuJsonParseExtension =
/// Extension methods for JSON parsing
type FirstDu with
/// Parse from a JSON node.
static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : FirstDu =
let ty =
(match node.["type"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("type")
)
)
| v -> v)
|> (fun v -> v.GetValue<string> ())
match ty with
| "emptyCase" -> FirstDu.EmptyCase
| "case1" ->
let node =
(match node.["data"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("data")
)
)
| v -> v)
FirstDu.Case1 (
(match node.["data"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("data")
)
)
| v -> v)
.AsValue()
.GetValue<System.String> ()
)
| "case2" ->
let node =
(match node.["data"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("data")
)
)
| v -> v)
FirstDu.Case2 (
JsonRecordTypeWithBoth.jsonParse (
match node.["record"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("record")
)
)
| v -> v
),
(match node.["i"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("i")
)
)
| v -> v)
.AsValue()
.GetValue<System.Int32> ()
)
| v -> failwith ("Unrecognised 'type' field value: " + v)
namespace ConsumePlugin
/// Module containing JSON parsing extension members for the HeaderAndValue type
[<AutoOpen>]
module HeaderAndValueJsonParseExtension =
/// Extension methods for JSON parsing
type HeaderAndValue with
/// Parse from a JSON node.
static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : HeaderAndValue =
let arg_1 =
(match node.["value"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("value")
)
)
| v -> v)
.AsValue()
.GetValue<System.String> ()
let arg_0 =
(match node.["header"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("header")
)
)
| v -> v)
.AsValue()
.GetValue<System.String> ()
{
Header = arg_0
Value = arg_1
}
namespace ConsumePlugin
/// Module containing JSON parsing extension members for the Foo type
[<AutoOpen>]
module FooJsonParseExtension =
/// Extension methods for JSON parsing
type Foo with
/// Parse from a JSON node.
static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : Foo =
let arg_0 =
match node.["message"] with
| null -> None
| v -> HeaderAndValue.jsonParse v |> Some
{
Message = arg_0
}
namespace ConsumePlugin
/// Module containing JSON parsing extension members for the CollectRemaining type
[<AutoOpen>]
module CollectRemainingJsonParseExtension =
/// Extension methods for JSON parsing
type CollectRemaining with
/// Parse from a JSON node.
static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : CollectRemaining =
let arg_1 =
let result =
System.Collections.Generic.Dictionary<string, System.Text.Json.Nodes.JsonNode> ()
let node = node.AsObject ()
for KeyValue (key, value) in node do
if key = "message" then () else result.Add (key, node.[key])
result
let arg_0 =
match node.["message"] with
| null -> None
| v -> HeaderAndValue.jsonParse v |> Some
{
Message = arg_0
Rest = arg_1
}
namespace ConsumePlugin
/// Module containing JSON parsing extension members for the OuterCollectRemaining type
[<AutoOpen>]
module OuterCollectRemainingJsonParseExtension =
/// Extension methods for JSON parsing
type OuterCollectRemaining with
/// Parse from a JSON node.
static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : OuterCollectRemaining =
let arg_1 =
CollectRemaining.jsonParse (
match node.["remaining"] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" ("remaining")
)
)
| v -> v
)
let arg_0 =
let result = System.Collections.Generic.Dictionary<string, int> ()
let node = node.AsObject ()
for KeyValue (key, value) in node do
if key = "remaining" then
()
else
result.Add (
key,
(match node.[key] with
| null ->
raise (
System.Collections.Generic.KeyNotFoundException (
sprintf "Required key '%s' not found on JSON object" (key)
)
)
| v -> v)
.AsValue()
.GetValue<System.Int32> ()
)
result
{
Others = arg_0
Remaining = arg_1
}

View File

@@ -0,0 +1,83 @@
namespace ConsumePlugin
open System.Text.Json.Serialization
open WoofWare.Whippet.Plugin.Json
module Literals =
[<Literal>]
let something = "something"
[<JsonParse>]
type InnerType =
{
[<JsonPropertyName(Literals.something)>]
Thing : string
}
/// My whatnot
[<JsonParse>]
type JsonRecordType =
{
/// A thing!
A : int
/// Another thing!
[<JsonPropertyName "another-thing">]
B : string
[<System.Text.Json.Serialization.JsonPropertyName "hi">]
C : int list
D : InnerType
E : string array
F : int[]
}
[<JsonParse>]
type internal InternalTypeNotExtension =
{
[<JsonPropertyName(Literals.something)>]
InternalThing : string
}
[<JsonSerialize>]
type internal InternalTypeNotExtensionSerial =
{
[<JsonPropertyName(Literals.something)>]
InternalThing2 : string
}
[<JsonParse true>]
[<JsonSerialize true>]
type internal InternalTypeExtension =
{
[<JsonPropertyName(Literals.something)>]
ExternalThing : string
}
[<JsonParse true>]
type ToGetExtensionMethod =
{
Alpha : string
Bravo : System.Uri
Charlie : float
Delta : float32
Echo : single
Foxtrot : double
Golf : int64
Hotel : uint64
India : int
Juliette : uint
Kilo : int32
Lima : uint32
Mike : int16
November : uint16
Oscar : int8
Papa : uint8
Quebec : byte
Tango : sbyte
Uniform : decimal
Victor : char
Whiskey : bigint
}
[<RequireQualifiedAccess>]
module ToGetExtensionMethod =
let thisModuleWouldClash = 3

View File

@@ -0,0 +1,190 @@
// Copied from https://gitea.patrickstevens.co.uk/patrick/puregym-unofficial-dotnet/src/commit/2741c5e36cf0bdb203b12b78a8062e25af9d89c7/PureGym/Api.fs
namespace PureGym
open System
open System.Text.Json.Serialization
open WoofWare.Whippet.Plugin.Json
[<JsonParse>]
type GymOpeningHours =
{
IsAlwaysOpen : bool
OpeningHours : string list
}
[<JsonParse>]
type GymAccessOptions =
{
PinAccess : bool
QrCodeAccess : bool
}
[<Measure>]
type measure
[<JsonParse>]
type GymLocation =
{
[<JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)>]
Longitude : float
[<JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)>]
Latitude : float<measure>
}
[<JsonParse>]
type GymAddress =
{
[<JsonRequired>]
AddressLine1 : string
AddressLine2 : string option
AddressLine3 : string option
[<JsonRequired>]
Town : string
County : string option
[<JsonRequired>]
Postcode : string
}
[<JsonParse>]
type Gym =
{
[<JsonRequired>]
Name : string
[<JsonRequired>]
Id : int
[<JsonRequired>]
Status : int
[<JsonRequired>]
Address : GymAddress
[<JsonRequired>]
PhoneNumber : string
[<JsonRequired>]
EmailAddress : string
[<JsonRequired>]
GymOpeningHours : GymOpeningHours
[<JsonRequired>]
AccessOptions : GymAccessOptions
[<JsonRequired>]
Location : GymLocation
[<JsonRequired>]
TimeZone : string
ReopenDate : string
}
[<JsonParse true>]
[<JsonSerialize true>]
type Member =
{
Id : int
CompoundMemberId : string
FirstName : string
LastName : string
HomeGymId : int
HomeGymName : string
EmailAddress : string
GymAccessPin : string
[<JsonPropertyName "dateofBirth">]
DateOfBirth : DateOnly
MobileNumber : string
[<JsonPropertyName "postCode">]
Postcode : string
MembershipName : string
MembershipLevel : int
SuspendedReason : int
MemberStatus : int
}
[<JsonParse>]
type GymAttendance =
{
[<JsonRequired>]
Description : string
[<JsonRequired>]
TotalPeopleInGym : int
[<JsonRequired>]
TotalPeopleInClasses : int
TotalPeopleSuffix : string option
[<JsonRequired>]
IsApproximate : bool
AttendanceTime : DateTime
LastRefreshed : DateTime
LastRefreshedPeopleInClasses : DateTime
MaximumCapacity : int
}
[<JsonParse>]
type MemberActivityDto =
{
[<JsonRequired>]
TotalDuration : int
[<JsonRequired>]
AverageDuration : int
[<JsonRequired>]
TotalVisits : int
[<JsonRequired>]
TotalClasses : int
[<JsonRequired>]
IsEstimated : bool
[<JsonRequired>]
LastRefreshed : DateTime
}
[<JsonParse>]
type SessionsAggregate =
{
[<JsonPropertyName "Activities">]
Activities : int
[<JsonPropertyName "Visits">]
Visits : int
[<JsonPropertyName "Duration">]
Duration : int
}
[<JsonParse>]
type VisitGym =
{
[<JsonPropertyName "Id">]
Id : int
[<JsonPropertyName "Name">]
Name : string
[<JsonPropertyName "Status">]
Status : string
}
[<JsonParse>]
type Visit =
{
[<JsonPropertyName "IsDurationEstimated">]
IsDurationEstimated : bool
[<JsonPropertyName "StartTime">]
StartTime : DateTime
[<JsonPropertyName "Duration">]
Duration : int
[<JsonPropertyName "Gym">]
Gym : VisitGym
}
[<JsonParse>]
type SessionsSummary =
{
[<JsonPropertyName "Total">]
Total : SessionsAggregate
[<JsonPropertyName "ThisWeek">]
ThisWeek : SessionsAggregate
}
[<JsonParse>]
type Sessions =
{
[<JsonPropertyName "Summary">]
Summary : SessionsSummary
[<JsonPropertyName "Visits">]
Visits : Visit list
}
[<JsonParse>]
type UriThing =
{
SomeUri : Uri
}

View File

@@ -0,0 +1,94 @@
namespace ConsumePlugin
open System
open System.Collections.Generic
open System.Text.Json.Serialization
[<WoofWare.Whippet.Plugin.Json.JsonParse true>]
[<WoofWare.Whippet.Plugin.Json.JsonSerialize true>]
type InnerTypeWithBoth =
{
[<JsonPropertyName("it's-a-me")>]
Thing : Guid
Map : Map<string, Uri>
ReadOnlyDict : IReadOnlyDictionary<string, char list>
Dict : IDictionary<Uri, bool>
ConcreteDict : Dictionary<string, InnerTypeWithBoth>
}
[<WoofWare.Whippet.Plugin.Json.JsonParse true>]
[<WoofWare.Whippet.Plugin.Json.JsonSerialize true>]
type SomeEnum =
| Blah = 1
| Thing = 0
[<Measure>]
type measure
[<WoofWare.Whippet.Plugin.Json.JsonParse true>]
[<WoofWare.Whippet.Plugin.Json.JsonSerialize true>]
type JsonRecordTypeWithBoth =
{
A : int
B : string
C : int list
D : InnerTypeWithBoth
E : string array
Arr : int[]
Byte : byte<measure>
Sbyte : sbyte<measure>
I : int<measure>
I32 : int32<measure>
I64 : int64<measure>
U : uint<measure>
U32 : uint32<measure>
U64 : uint64<measure>
F : float<measure>
F32 : float32<measure>
Single : single<measure>
IntMeasureOption : int<measure> option
IntMeasureNullable : int<measure> Nullable
Enum : SomeEnum
Timestamp : DateTimeOffset
Unit : unit
}
[<WoofWare.Whippet.Plugin.Json.JsonSerialize true>]
[<WoofWare.Whippet.Plugin.Json.JsonParse true>]
type FirstDu =
| EmptyCase
| Case1 of data : string
| Case2 of record : JsonRecordTypeWithBoth * i : int
[<WoofWare.Whippet.Plugin.Json.JsonParse true>]
[<WoofWare.Whippet.Plugin.Json.JsonSerialize true>]
type HeaderAndValue =
{
Header : string
Value : string
}
[<WoofWare.Whippet.Plugin.Json.JsonSerialize true>]
[<WoofWare.Whippet.Plugin.Json.JsonParse true>]
type Foo =
{
Message : HeaderAndValue option
}
[<WoofWare.Whippet.Plugin.Json.JsonSerialize true>]
[<WoofWare.Whippet.Plugin.Json.JsonParse true>]
type CollectRemaining =
{
Message : HeaderAndValue option
[<JsonExtensionData>]
Rest : Dictionary<string, System.Text.Json.Nodes.JsonNode>
}
[<WoofWare.Whippet.Plugin.Json.JsonSerialize true>]
[<WoofWare.Whippet.Plugin.Json.JsonParse true>]
type OuterCollectRemaining =
{
[<JsonExtensionData>]
Others : Dictionary<string, int>
Remaining : CollectRemaining
}

View File

@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<Compile Include="JsonRecord.fs" />
<Compile Include="GeneratedJson.fs">
<WhippetFile>JsonRecord.fs</WhippetFile>
</Compile>
<Compile Include="SerializationAndDeserialization.fs" />
<Compile Include="GeneratedSerializationAndDeserialization.fs">
<WhippetFile>SerializationAndDeserialization.fs</WhippetFile>
</Compile>
<Compile Include="PureGymDto.fs" />
<Compile Include="GeneratedPureGymDto.fs">
<WhippetFile>PureGymDto.fs</WhippetFile>
</Compile>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WoofWare.Whippet.Plugin.Json\WoofWare.Whippet.Plugin.Json.fsproj" WhippetPlugin="true" />
<!-- Dance to get a binary dependency on a locally-built Whippet -->
<!-- ProjectReference Include="..\..\..\WoofWare.Whippet\WoofWare.Whippet.fsproj" PrivateAssets="all" -->
<PackageReference Include="WoofWare.Whippet" Version="*-*" PrivateAssets="all" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,15 @@
namespace WoofWare.Whippet.Plugin.Json
type internal DesiredGenerator =
| JsonParse of extensionMethod : bool option
| JsonSerialize of extensionMethod : bool option
static member Parse (s : string) =
match s with
| "JsonParse" -> DesiredGenerator.JsonParse None
| "JsonParse(true)" -> DesiredGenerator.JsonParse (Some true)
| "JsonParse(false)" -> DesiredGenerator.JsonParse (Some false)
| "JsonSerialize" -> DesiredGenerator.JsonSerialize None
| "JsonSerialize(true)" -> DesiredGenerator.JsonSerialize (Some true)
| "JsonSerialize(false)" -> DesiredGenerator.JsonSerialize (Some false)
| _ -> failwith $"Failed to parse as a generator specification: %s{s}"

View File

@@ -0,0 +1,783 @@
namespace WoofWare.Whippet.Plugin.Json
open System
open System.Text
open Fantomas.FCS.Syntax
open Fantomas.FCS.SyntaxTrivia
open WoofWare.Whippet.Core
open WoofWare.Whippet.Fantomas
type internal JsonParseOutputSpec =
{
ExtensionMethods : bool
}
[<RequireQualifiedAccess>]
module internal JsonParseGenerator =
open Fantomas.FCS.Text.Range
type JsonParseOption =
{
JsonNumberHandlingArg : SynExpr option
}
static member None =
{
JsonNumberHandlingArg = None
}
/// (match {indexed} with | null -> raise (System.Collections.Generic.KeyNotFoundException ({propertyName} not found)) | v -> v)
let assertNotNull (propertyName : SynExpr) (indexed : SynExpr) =
let raiseExpr =
SynExpr.applyFunction
(SynExpr.createIdent "sprintf")
(SynExpr.CreateConst "Required key '%s' not found on JSON object")
|> SynExpr.applyTo (SynExpr.paren propertyName)
|> SynExpr.paren
|> SynExpr.applyFunction (
SynExpr.createLongIdent [ "System" ; "Collections" ; "Generic" ; "KeyNotFoundException" ]
)
|> SynExpr.paren
|> SynExpr.applyFunction (SynExpr.createIdent "raise")
[
SynMatchClause.create SynPat.createNull raiseExpr
SynMatchClause.create (SynPat.named "v") (SynExpr.createIdent "v")
]
|> SynExpr.createMatch indexed
|> SynExpr.paren
/// {node}.AsValue().GetValue<{typeName}> ()
/// If `propertyName` is Some, uses `assertNotNull {node}` instead of `{node}`.
let asValueGetValue (propertyName : SynExpr option) (typeName : string) (node : SynExpr) : SynExpr =
match propertyName with
| None -> node
| Some propertyName -> assertNotNull propertyName node
|> SynExpr.callMethod "AsValue"
|> SynExpr.callGenericMethod' "GetValue" typeName
let asValueGetValueIdent (propertyName : SynExpr option) (typeName : LongIdent) (node : SynExpr) : SynExpr =
match propertyName with
| None -> node
| Some propertyName -> assertNotNull propertyName node
|> SynExpr.callMethod "AsValue"
|> SynExpr.callGenericMethod (SynLongIdent.createS "GetValue") [ SynType.createLongIdent typeName ]
/// {node}.AsObject()
/// If `propertyName` is Some, uses `assertNotNull {node}` instead of `{node}`.
let asObject (propertyName : SynExpr option) (node : SynExpr) : SynExpr =
match propertyName with
| None -> node
| Some propertyName -> assertNotNull propertyName node
|> SynExpr.callMethod "AsObject"
/// {type}.jsonParse {node}
let typeJsonParse (typeName : LongIdent) (node : SynExpr) : SynExpr =
node
|> SynExpr.applyFunction (SynExpr.createLongIdent' (typeName @ [ Ident.create "jsonParse" ]))
/// collectionType is e.g. "List"; we'll be calling `ofSeq` on it.
/// body is the body of a lambda which takes a parameter `elt`.
/// {assertNotNull node}.AsArray()
/// |> Seq.map (fun elt -> {body})
/// |> {collectionType}.ofSeq
let asArrayMapped
(propertyName : SynExpr option)
(collectionType : string)
(node : SynExpr)
(body : SynExpr)
: SynExpr
=
match propertyName with
| None -> node
| Some propertyName -> assertNotNull propertyName node
|> SynExpr.callMethod "AsArray"
|> SynExpr.pipeThroughFunction (
SynExpr.applyFunction (SynExpr.createLongIdent [ "Seq" ; "map" ]) (SynExpr.createLambda "elt" body)
)
|> SynExpr.pipeThroughFunction (SynExpr.createLongIdent [ collectionType ; "ofSeq" ])
let dotParse (typeName : LongIdent) : LongIdent =
List.append typeName [ Ident.create "Parse" ]
/// fun kvp -> let key = {key(kvp)} in let value = {value(kvp)} in (key, value))
/// The inputs will be fed with appropriate SynExprs to apply them to the `kvp.Key` and `kvp.Value` args.
let dictionaryMapper (key : SynExpr -> SynExpr) (value : SynExpr -> SynExpr) : SynExpr =
let keyArg = SynExpr.createLongIdent [ "kvp" ; "Key" ] |> SynExpr.paren
let valueArg = SynExpr.createLongIdent [ "kvp" ; "Value" ] |> SynExpr.paren
// No need to paren here, we're on the LHS of a `let`
SynExpr.tupleNoParen [ SynExpr.createIdent "key" ; SynExpr.createIdent "value" ]
|> SynExpr.createLet [ SynBinding.basic [ Ident.create "value" ] [] (value valueArg) ]
|> SynExpr.createLet [ SynBinding.basic [ Ident.create "key" ] [] (key keyArg) ]
|> SynExpr.createLambda "kvp"
/// A conforming JSON object has only strings as keys. But it would be reasonable to allow the user
/// to parse these as URIs, for example.
let parseKeyString (desiredType : SynType) (key : SynExpr) : SynExpr =
match desiredType with
| String -> key
| Uri ->
key
|> SynExpr.pipeThroughFunction (SynExpr.createLongIdent [ "System" ; "Uri" ])
| _ ->
failwithf
$"Unable to parse the key type %+A{desiredType} of a JSON object. Keys are strings, and this plugin does not know how to convert to that from a string."
let private parseNumberType
(options : JsonParseOption)
(propertyName : SynExpr option)
(node : SynExpr)
(typeName : LongIdent)
=
let basic = asValueGetValueIdent propertyName typeName node
match options.JsonNumberHandlingArg with
| None -> basic
| Some option ->
let cond =
SynExpr.DotGet (SynExpr.createIdent "exc", range0, SynLongIdent.createS "Message", range0)
|> SynExpr.callMethodArg "Contains" (SynExpr.CreateConst "cannot be converted to")
let handler =
asValueGetValue propertyName "string" node
|> SynExpr.pipeThroughFunction (SynExpr.createLongIdent' (typeName |> dotParse))
|> SynExpr.ifThenElse
(SynExpr.equals
option
(SynExpr.createLongIdent
[
"System"
"Text"
"Json"
"Serialization"
"JsonNumberHandling"
"AllowReadingFromString"
]))
SynExpr.reraise
|> SynExpr.ifThenElse cond SynExpr.reraise
basic
|> SynExpr.pipeThroughTryWith
(SynPat.IsInst (
SynType.LongIdent (SynLongIdent.createS' [ "System" ; "InvalidOperationException" ]),
range0
))
handler
/// Given `node.["town"]`, for example, choose how to obtain a JSON value from it.
/// The property name is used in error messages at runtime to show where a JSON
/// parse error occurred; supply `None` to indicate "don't validate".
let rec parseNode
(propertyName : SynExpr option)
(options : JsonParseOption)
(fieldType : SynType)
(node : SynExpr)
: SynExpr
=
// TODO: parsing format for DateTime etc
match fieldType with
| DateOnly ->
node
|> asValueGetValue propertyName "string"
|> SynExpr.pipeThroughFunction (SynExpr.createLongIdent [ "System" ; "DateOnly" ; "Parse" ])
| Uri ->
node
|> asValueGetValue propertyName "string"
|> SynExpr.pipeThroughFunction (SynExpr.createLongIdent [ "System" ; "Uri" ])
| Guid ->
node
|> asValueGetValue propertyName "string"
|> SynExpr.pipeThroughFunction (SynExpr.createLongIdent [ "System" ; "Guid" ; "Parse" ])
| DateTime ->
node
|> asValueGetValue propertyName "string"
|> SynExpr.pipeThroughFunction (SynExpr.createLongIdent [ "System" ; "DateTime" ; "Parse" ])
| DateTimeOffset ->
node
|> asValueGetValue propertyName "string"
|> SynExpr.pipeThroughFunction (SynExpr.createLongIdent [ "System" ; "DateTimeOffset" ; "Parse" ])
| NumberType typeName -> parseNumberType options propertyName node typeName
| PrimitiveType typeName -> asValueGetValueIdent propertyName typeName node
| OptionType ty ->
let someClause =
parseNode None options ty (SynExpr.createIdent "v")
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "Some")
|> SynMatchClause.create (SynPat.named "v")
[
SynMatchClause.create SynPat.createNull (SynExpr.createIdent "None")
someClause
]
|> SynExpr.createMatch node
| NullableType ty ->
let someClause =
parseNode None options ty (SynExpr.createIdent "v")
|> SynExpr.pipeThroughFunction (SynExpr.createLongIdent [ "System" ; "Nullable" ])
|> SynMatchClause.create (SynPat.named "v")
[
SynMatchClause.create
SynPat.createNull
(SynExpr.applyFunction (SynExpr.createLongIdent [ "System" ; "Nullable" ]) (SynExpr.CreateConst ()))
someClause
]
|> SynExpr.createMatch node
| ListType ty ->
parseNode None options ty (SynExpr.createIdent "elt")
|> asArrayMapped propertyName "List" node
| ArrayType ty ->
parseNode None options ty (SynExpr.createIdent "elt")
|> asArrayMapped propertyName "Array" node
| IDictionaryType (keyType, valueType) ->
node
|> asObject propertyName
|> SynExpr.pipeThroughFunction (
SynExpr.applyFunction
(SynExpr.createLongIdent [ "Seq" ; "map" ])
(dictionaryMapper (parseKeyString keyType) (parseNode None options valueType))
)
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "dict")
| DictionaryType (keyType, valueType) ->
node
|> asObject propertyName
|> SynExpr.pipeThroughFunction (
SynExpr.applyFunction
(SynExpr.createLongIdent [ "Seq" ; "map" ])
(dictionaryMapper (parseKeyString keyType) (parseNode None options valueType))
)
|> SynExpr.pipeThroughFunction (
SynExpr.applyFunction
(SynExpr.createLongIdent [ "Seq" ; "map" ])
(SynExpr.createLongIdent [ "System" ; "Collections" ; "Generic" ; "KeyValuePair" ])
)
|> SynExpr.pipeThroughFunction (
SynExpr.createLongIdent [ "System" ; "Collections" ; "Generic" ; "Dictionary" ]
)
| IReadOnlyDictionaryType (keyType, valueType) ->
node
|> asObject propertyName
|> SynExpr.pipeThroughFunction (
SynExpr.applyFunction
(SynExpr.createLongIdent [ "Seq" ; "map" ])
(dictionaryMapper (parseKeyString keyType) (parseNode None options valueType))
)
|> SynExpr.pipeThroughFunction (SynExpr.createIdent "readOnlyDict")
| MapType (keyType, valueType) ->
node
|> asObject propertyName
|> SynExpr.pipeThroughFunction (
SynExpr.applyFunction
(SynExpr.createLongIdent [ "Seq" ; "map" ])
(dictionaryMapper (parseKeyString keyType) (parseNode None options valueType))
)
|> SynExpr.pipeThroughFunction (SynExpr.createLongIdent [ "Map" ; "ofSeq" ])
| BigInt ->
node
|> SynExpr.callMethod "ToJsonString"
|> SynExpr.paren
|> SynExpr.applyFunction (SynExpr.createLongIdent [ "System" ; "Numerics" ; "BigInteger" ; "Parse" ])
| Measure (_measure, primType) ->
parseNumberType options propertyName node primType
|> SynExpr.pipeThroughFunction (Measure.getLanguagePrimitivesMeasure primType)
| JsonNode -> node
| UnitType -> SynExpr.CreateConst ()
| _ ->
// Let's just hope that we've also got our own type annotation!
let typeName =
match fieldType with
| SynType.LongIdent ident -> ident.LongIdent
| _ -> failwith $"Unrecognised type: %+A{fieldType}"
match propertyName with
| None -> node
| Some propertyName -> assertNotNull propertyName node
|> typeJsonParse typeName
/// propertyName is probably a string literal, but it could be a [<Literal>] variable
/// The result of this function is the body of a let-binding (not including the LHS of that let-binding).
let createParseRhs (options : JsonParseOption) (propertyName : SynExpr) (fieldType : SynType) : SynExpr =
let objectToParse = SynExpr.createIdent "node" |> SynExpr.index propertyName
parseNode (Some propertyName) options fieldType objectToParse
let isJsonNumberHandling (literal : LongIdent) : bool =
match List.rev literal |> List.map (fun ident -> ident.idText) with
| [ _ ; "JsonNumberHandling" ]
| [ _ ; "JsonNumberHandling" ; "Serialization" ]
| [ _ ; "JsonNumberHandling" ; "Serialization" ; "Json" ]
| [ _ ; "JsonNumberHandling" ; "Serialization" ; "Json" ; "Text" ]
| [ _ ; "JsonNumberHandling" ; "Serialization" ; "Json" ; "Text" ; "System" ] -> true
| _ -> false
/// `populateNode` will be inserted before we return the `node` variable.
///
/// That is, we give you access to a `JsonNode` called `node`,
/// and you must return a `typeName`.
let scaffolding (spec : JsonParseOutputSpec) (typeName : LongIdent) (functionBody : SynExpr) : SynModuleDecl =
let xmlDoc = PreXmlDoc.create "Parse from a JSON node."
let returnInfo = SynType.createLongIdent typeName
let inputArg = "node"
let functionName = Ident.create "jsonParse"
let arg =
SynPat.named inputArg
|> SynPat.annotateType (SynType.createLongIdent' [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonNode" ])
if spec.ExtensionMethods then
let binding =
SynBinding.basic [ functionName ] [ arg ] functionBody
|> SynBinding.withXmlDoc xmlDoc
|> SynBinding.withReturnAnnotation returnInfo
|> SynMemberDefn.staticMember
let componentInfo =
SynComponentInfo.createLong typeName
|> SynComponentInfo.withDocString (PreXmlDoc.create "Extension methods for JSON parsing")
let containingType =
SynTypeDefnRepr.augmentation ()
|> SynTypeDefn.create componentInfo
|> SynTypeDefn.withMemberDefns [ binding ]
SynModuleDecl.Types ([ containingType ], range0)
else
SynBinding.basic [ functionName ] [ arg ] functionBody
|> SynBinding.withXmlDoc xmlDoc
|> SynBinding.withReturnAnnotation returnInfo
|> SynModuleDecl.createLet
let getParseOptions (fieldAttrs : SynAttribute list) =
(JsonParseOption.None, fieldAttrs)
||> List.fold (fun options attr ->
if
(SynLongIdent.toString attr.TypeName)
.EndsWith ("JsonNumberHandling", StringComparison.Ordinal)
then
let qualifiedEnumValue =
match SynExpr.stripOptionalParen attr.ArgExpr with
| SynExpr.LongIdent (_, SynLongIdent (ident, _, _), _, _) when isJsonNumberHandling ident ->
// Make sure it's fully qualified
SynExpr.createLongIdent
[
"System"
"Text"
"Json"
"Serialization"
"JsonNumberHandling"
"AllowReadingFromString"
]
| _ -> attr.ArgExpr
{
JsonNumberHandlingArg = Some qualifiedEnumValue
}
else
options
)
let createRecordMaker (spec : JsonParseOutputSpec) (fields : SynFieldData<Ident> list) =
let propertyFields =
fields
|> List.map (fun fieldData ->
let propertyNameAttr =
fieldData.Attrs
|> List.tryFind (fun attr ->
(SynLongIdent.toString attr.TypeName)
.EndsWith ("JsonPropertyName", StringComparison.Ordinal)
)
let extensionDataAttr =
fieldData.Attrs
|> List.tryFind (fun attr ->
(SynLongIdent.toString attr.TypeName)
.EndsWith ("JsonExtensionData", StringComparison.Ordinal)
)
let propertyName =
match propertyNameAttr with
| None ->
let sb = StringBuilder fieldData.Ident.idText.Length
sb.Append (Char.ToLowerInvariant fieldData.Ident.idText.[0])
|> ignore<StringBuilder>
if fieldData.Ident.idText.Length > 1 then
sb.Append (fieldData.Ident.idText.Substring 1) |> ignore<StringBuilder>
sb.ToString () |> SynExpr.CreateConst
| Some name -> name.ArgExpr
propertyName, extensionDataAttr
)
let namedPropertyFields =
propertyFields
|> List.choose (fun (name, extension) ->
match extension with
| Some _ -> None
| None -> Some name
)
let isNamedPropertyField =
match namedPropertyFields with
| [] -> SynExpr.CreateConst false
| _ ->
namedPropertyFields
|> List.map (fun fieldName -> SynExpr.equals (SynExpr.createIdent "key") fieldName)
|> List.reduce SynExpr.booleanOr
let assignments =
List.zip fields propertyFields
|> List.mapi (fun i (fieldData, (propertyName, extensionDataAttr)) ->
let options = getParseOptions fieldData.Attrs
let accIdent = Ident.create $"arg_%i{i}"
match extensionDataAttr with
| Some _ ->
// Can't go through the usual parse logic here, because that will try and identify the node that's
// been labelled. The whole point of JsonExtensionData is that there is no such node!
let valType =
match fieldData.Type with
| DictionaryType (String, v) -> v
| _ -> failwith "Expected JsonExtensionData to be Dictionary<string, _>"
SynExpr.ifThenElse
isNamedPropertyField
(SynExpr.callMethodArg
"Add"
(SynExpr.tuple
[
SynExpr.createIdent "key"
createParseRhs options (SynExpr.createIdent "key") valType
])
(SynExpr.createIdent "result"))
(SynExpr.CreateConst ())
|> SynExpr.createForEach
(SynPat.nameWithArgs "KeyValue" [ SynPat.named "key" ; SynPat.named "value" ])
(SynExpr.createIdent "node")
|> fun forEach -> [ forEach ; SynExpr.createIdent "result" ]
|> SynExpr.sequential
|> SynExpr.createLet
[
SynBinding.basic
[ Ident.create "result" ]
[]
(SynExpr.typeApp
[ SynType.string ; valType ]
(SynExpr.createLongIdent [ "System" ; "Collections" ; "Generic" ; "Dictionary" ])
|> SynExpr.applyTo (SynExpr.CreateConst ()))
SynBinding.basic
[ Ident.create "node" ]
[]
(SynExpr.createIdent "node" |> SynExpr.callMethod "AsObject")
]
|> SynBinding.basic [ accIdent ] []
| None ->
createParseRhs options propertyName fieldData.Type
|> SynBinding.basic [ accIdent ] []
)
let finalConstruction =
fields
|> List.mapi (fun i fieldData -> SynLongIdent.createI fieldData.Ident, SynExpr.createIdent $"arg_%i{i}")
|> AstHelper.instantiateRecord
(finalConstruction, assignments)
||> List.fold (fun final assignment -> SynExpr.createLet [ assignment ] final)
let createUnionMaker (spec : JsonParseOutputSpec) (typeName : LongIdent) (fields : UnionCase<Ident> list) =
fields
|> List.map (fun case ->
let propertyName = JsonSerializeGenerator.getPropertyName case.Name case.Attributes
let body =
if case.Fields.IsEmpty then
SynExpr.createLongIdent' (typeName @ [ case.Name ])
else
case.Fields
|> List.map (fun field ->
let propertyName = JsonSerializeGenerator.getPropertyName field.Ident field.Attrs
let options = getParseOptions field.Attrs
createParseRhs options propertyName field.Type
)
|> SynExpr.tuple
|> SynExpr.applyFunction (SynExpr.createLongIdent' (typeName @ [ case.Name ]))
|> SynExpr.createLet
[
SynExpr.index (SynExpr.CreateConst "data") (SynExpr.createIdent "node")
|> assertNotNull (SynExpr.CreateConst "data")
|> SynBinding.basic [ Ident.create "node" ] []
]
match propertyName with
| SynExpr.Const (synConst, _) ->
SynMatchClause.SynMatchClause (
SynPat.createConst synConst,
None,
body,
range0,
DebugPointAtTarget.Yes,
{
ArrowRange = Some range0
BarRange = Some range0
}
)
| _ ->
SynMatchClause.create (SynPat.named "x") body
|> SynMatchClause.withWhere (SynExpr.equals (SynExpr.createIdent "x") propertyName)
)
|> fun l ->
l
@ [
let fail =
SynExpr.plus (SynExpr.CreateConst "Unrecognised 'type' field value: ") (SynExpr.createIdent "v")
|> SynExpr.paren
|> SynExpr.applyFunction (SynExpr.createIdent "failwith")
SynMatchClause.SynMatchClause (
SynPat.named "v",
None,
fail,
range0,
DebugPointAtTarget.Yes,
{
ArrowRange = Some range0
BarRange = Some range0
}
)
]
|> SynExpr.createMatch (SynExpr.createIdent "ty")
|> SynExpr.createLet
[
let property = SynExpr.CreateConst "type"
SynExpr.createIdent "node"
|> SynExpr.index property
|> assertNotNull property
|> SynExpr.pipeThroughFunction (
SynExpr.createLambda "v" (SynExpr.callGenericMethod' "GetValue" "string" (SynExpr.createIdent "v"))
)
|> 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 (SynTypeDefn (synComponentInfo, synTypeDefnRepr, _members, _implicitCtor, _, _)) =
typeDefn
let (SynComponentInfo (_attributes, _typeParams, _constraints, ident, _, _preferPostfix, access, _)) =
synComponentInfo
let attributes =
if spec.ExtensionMethods then
[ SynAttribute.autoOpen ]
else
[ SynAttribute.requireQualifiedAccess ; SynAttribute.compilationRepresentation ]
let xmlDoc =
let fullyQualified = ident |> Seq.map (fun i -> i.idText) |> String.concat "."
let description =
if spec.ExtensionMethods then
"extension members"
else
"methods"
$"Module containing JSON parsing %s{description} for the %s{fullyQualified} type"
|> PreXmlDoc.create
let moduleName =
if spec.ExtensionMethods then
match ident with
| [] -> failwith "unexpectedly got an empty identifier for record name"
| ident ->
let expanded =
List.last ident
|> fun i -> i.idText
|> fun s -> s + "JsonParseExtension"
|> Ident.create
List.take (List.length ident - 1) ident @ [ expanded ]
else
ident
let info =
SynComponentInfo.createLong moduleName
|> SynComponentInfo.withDocString xmlDoc
|> SynComponentInfo.setAccessibility access
|> SynComponentInfo.addAttributes attributes
let decl =
match synTypeDefnRepr with
| SynTypeDefnRepr.Simple (SynTypeDefnSimpleRepr.Record (_accessibility, fields, _range), _) ->
fields |> List.map SynField.extractWithIdent |> createRecordMaker spec
| SynTypeDefnRepr.Simple (SynTypeDefnSimpleRepr.Union (_accessibility, cases, _range), _) ->
let optionGet (i : Ident option) =
match i with
| None ->
failwith "WoofWare.Whippet.Plugin.Json requires union cases to have identifiers on each field."
| Some i -> i
cases
|> List.map UnionCase.ofSynUnionCase
|> List.map (UnionCase.mapIdentFields optionGet)
|> 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"
[ scaffolding spec ident decl ]
|> SynModuleDecl.nestedModule info
|> List.singleton
|> SynModuleOrNamespace.createNamespace namespaceId
/// Whippet generator that provides a method (possibly an extension method) for a record type,
/// containing a JSON parse function.
[<WhippetGenerator>]
type JsonParseGenerator () =
interface IGenerateRawFromRaw with
member _.GenerateRawFromRaw (context : RawSourceGenerationArgs) =
if not (context.FilePath.EndsWith (".fs", StringComparison.Ordinal)) then
null
else
let targetedTypes =
context.Parameters
|> Seq.map (fun (KeyValue (k, v)) -> k, v.Split '!' |> Array.toList |> List.map DesiredGenerator.Parse)
|> Map.ofSeq
let ast = Ast.parse (Encoding.UTF8.GetString context.FileContents)
let relevantTypes =
Ast.getTypes ast
|> List.map (fun (name, defns) ->
defns
|> List.choose (fun defn ->
if SynTypeDefn.isRecord defn then Some defn
elif SynTypeDefn.isDu defn then Some defn
elif SynTypeDefn.isEnum defn then Some defn
else None
)
|> fun defns -> name, defns
)
let namespaceAndTypes =
relevantTypes
|> List.choose (fun (ns, types) ->
types
|> List.choose (fun typeDef ->
match SynTypeDefn.getAttribute typeof<JsonParseAttribute>.Name typeDef with
| None ->
let name = SynTypeDefn.getName typeDef |> List.map _.idText |> String.concat "."
match Map.tryFind name targetedTypes with
| Some desired ->
desired
|> List.tryPick (fun generator ->
match generator with
| DesiredGenerator.JsonParse arg ->
let spec =
{
ExtensionMethods =
arg
|> Option.defaultValue
JsonParseAttribute.DefaultIsExtensionMethod
}
Some (typeDef, spec)
| _ -> None
)
| _ -> None
| Some attr ->
let arg =
match SynExpr.stripOptionalParen attr.ArgExpr with
| SynExpr.Const (SynConst.Bool value, _) -> value
| SynExpr.Const (SynConst.Unit, _) -> JsonParseAttribute.DefaultIsExtensionMethod
| arg ->
failwith
$"Unrecognised argument %+A{arg} to [<%s{nameof JsonParseAttribute}>]. Literals are not supported. Use `true` or `false` (or unit) only."
let spec =
{
ExtensionMethods = arg
}
Some (typeDef, spec)
)
|> function
| [] -> None
| ty -> Some (ns, ty)
)
let modules =
namespaceAndTypes
|> List.collect (fun (ns, types) ->
types |> List.map (fun (ty, spec) -> JsonParseGenerator.createModule ns spec ty)
)
Ast.render modules |> Option.toObj

View File

@@ -0,0 +1,602 @@
namespace WoofWare.Whippet.Plugin.Json
open System
open System.Text
open Fantomas.FCS.Syntax
open WoofWare.Whippet.Core
open WoofWare.Whippet.Fantomas
type internal JsonSerializeOutputSpec =
{
ExtensionMethods : bool
}
[<RequireQualifiedAccess>]
module internal JsonSerializeGenerator =
open Fantomas.FCS.Text.Range
// The absolutely galaxy-brained implementation of JsonValue has `JsonValue.Parse "null"`
// identically equal to null. We have to work around this later, but we might as well just
// be efficient here and whip up the null directly.
let private jsonNull () =
SynExpr.createNull ()
|> SynExpr.upcast' (SynType.createLongIdent' [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonNode" ])
/// Given `input.Ident`, for example, choose how to add it to the ambient `node`.
/// The result is a line like `(fun ident -> InnerType.toJsonNode ident)` or `(fun ident -> JsonValue.Create ident)`.
/// Returns also a bool which is true if the resulting SynExpr represents something of type JsonNode.
let rec serializeNode (fieldType : SynType) : SynExpr * bool =
// TODO: serialization format for DateTime etc
match fieldType with
| DateOnly
| DateTime
| NumberType _
| Measure _
| PrimitiveType _
| Guid
| Uri ->
// JsonValue.Create<type>
SynExpr.createLongIdent [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonValue" ; "Create" ]
|> SynExpr.typeApp [ fieldType ]
|> fun e -> e, false
| DateTimeOffset ->
// fun field -> field.ToString("o") |> JsonValue.Create<string>
let create =
SynExpr.createLongIdent [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonValue" ; "Create" ]
|> SynExpr.typeApp [ SynType.named "string" ]
SynExpr.createIdent "field"
|> SynExpr.callMethodArg "ToString" (SynExpr.CreateConst "o")
|> SynExpr.pipeThroughFunction create
|> SynExpr.createLambda "field"
|> fun e -> e, false
| NullableType ty ->
// fun field -> if field.HasValue then {serializeNode ty} field.Value else JsonValue.Create null
let inner, innerIsJsonNode = serializeNode ty
SynExpr.applyFunction inner (SynExpr.createLongIdent [ "field" ; "Value" ])
|> SynExpr.upcast' (SynType.createLongIdent' [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonNode" ])
|> SynExpr.ifThenElse (SynExpr.createLongIdent [ "field" ; "HasValue" ]) (jsonNull ())
|> SynExpr.createLambda "field"
|> fun e -> e, innerIsJsonNode
| OptionType ty ->
// fun field -> match field with | None -> JsonValue.Create null | Some v -> {serializeNode ty} field
let noneClause = jsonNull () |> SynMatchClause.create (SynPat.named "None")
let someClause =
let inner, innerIsJsonNode = serializeNode ty
let target = SynExpr.applyFunction inner (SynExpr.createIdent "field")
if innerIsJsonNode then
target
else
target
|> SynExpr.paren
|> SynExpr.upcast' (SynType.createLongIdent' [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonNode" ])
|> SynMatchClause.create (SynPat.nameWithArgs "Some" [ SynPat.named "field" ])
[ noneClause ; someClause ]
|> SynExpr.createMatch (SynExpr.createIdent "field")
|> SynExpr.createLambda "field"
|> fun e -> e, true
| ArrayType ty
| ListType ty ->
// fun field ->
// let arr = JsonArray ()
// for mem in field do arr.Add ({serializeNode} mem)
// arr
[
SynExpr.ForEach (
DebugPointAtFor.Yes range0,
DebugPointAtInOrTo.Yes range0,
SeqExprOnly.SeqExprOnly false,
true,
SynPat.named "mem",
SynExpr.createIdent "field",
SynExpr.applyFunction
(SynExpr.createLongIdent [ "arr" ; "Add" ])
(SynExpr.paren (SynExpr.applyFunction (fst (serializeNode ty)) (SynExpr.createIdent "mem"))),
range0
)
SynExpr.createIdent "arr"
]
|> SynExpr.sequential
|> SynExpr.createLet
[
SynExpr.createLongIdent [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonArray" ]
|> SynExpr.applyTo (SynExpr.CreateConst ())
|> SynBinding.basic [ Ident.create "arr" ] []
]
|> SynExpr.createLambda "field"
|> fun e -> e, false
| IDictionaryType (_keyType, valueType)
| DictionaryType (_keyType, valueType)
| IReadOnlyDictionaryType (_keyType, valueType)
| MapType (_keyType, valueType) ->
// fun field ->
// let ret = JsonObject ()
// for (KeyValue(key, value)) in field do
// ret.Add (key.ToString (), {serializeNode} value)
// ret
[
SynExpr.ForEach (
DebugPointAtFor.Yes range0,
DebugPointAtInOrTo.Yes range0,
SeqExprOnly.SeqExprOnly false,
true,
SynPat.paren (SynPat.nameWithArgs "KeyValue" [ SynPat.named "key" ; SynPat.named "value" ]),
SynExpr.createIdent "field",
SynExpr.applyFunction
(SynExpr.createLongIdent [ "ret" ; "Add" ])
(SynExpr.tuple
[
SynExpr.createLongIdent [ "key" ; "ToString" ]
|> SynExpr.applyTo (SynExpr.CreateConst ())
SynExpr.applyFunction (fst (serializeNode valueType)) (SynExpr.createIdent "value")
]),
range0
)
SynExpr.createIdent "ret"
]
|> SynExpr.sequential
|> SynExpr.createLet
[
SynExpr.createLongIdent [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonObject" ]
|> SynExpr.applyTo (SynExpr.CreateConst ())
|> SynBinding.basic [ Ident.create "ret" ] []
]
|> SynExpr.createLambda "field"
|> fun e -> e, false
| JsonNode -> SynExpr.createIdent "id", true
| UnitType ->
SynExpr.createLambda
"value"
(SynExpr.createLongIdent [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonObject" ]
|> SynExpr.applyTo (SynExpr.CreateConst ())),
false
| _ ->
// {type}.toJsonNode
let typeName =
match fieldType with
| SynType.LongIdent ident -> ident.LongIdent
| _ -> failwith $"Unrecognised type: %+A{fieldType}"
SynExpr.createLongIdent' (typeName @ [ Ident.create "toJsonNode" ]), true
/// propertyName is probably a string literal, but it could be a [<Literal>] variable
/// `node.Add ({propertyName}, {toJsonNode})`
let createSerializeRhsRecord (propertyName : SynExpr) (fieldId : Ident) (fieldType : SynType) : SynExpr =
[
propertyName
SynExpr.pipeThroughFunction
(fst (serializeNode fieldType))
(SynExpr.createLongIdent' [ Ident.create "input" ; fieldId ])
|> SynExpr.paren
]
|> SynExpr.tuple
|> SynExpr.applyFunction (SynExpr.createLongIdent [ "node" ; "Add" ])
let getPropertyName (fieldId : Ident) (attrs : SynAttribute list) : SynExpr =
let propertyNameAttr =
attrs
|> List.tryFind (fun attr ->
(SynLongIdent.toString attr.TypeName)
.EndsWith ("JsonPropertyName", StringComparison.Ordinal)
)
match propertyNameAttr with
| None ->
let sb = StringBuilder fieldId.idText.Length
sb.Append (Char.ToLowerInvariant fieldId.idText.[0]) |> ignore
if fieldId.idText.Length > 1 then
sb.Append fieldId.idText.[1..] |> ignore
sb.ToString () |> SynExpr.CreateConst
| Some name -> name.ArgExpr
let getIsJsonExtension (attrs : SynAttribute list) : bool =
attrs
|> List.tryFind (fun attr ->
(SynLongIdent.toString attr.TypeName)
.EndsWith ("JsonExtensionData", StringComparison.Ordinal)
)
|> Option.isSome
/// `populateNode` will be inserted before we return the `node` variable.
///
/// That is, we give you access to a `JsonObject` called `node`,
/// and you have access to a variable `inputArgName` which is of type `typeName`.
/// Your job is to provide a `populateNode` expression which has the side effect
/// of mutating `node` to faithfully reflect the value of `inputArgName`.
let scaffolding
(spec : JsonSerializeOutputSpec)
(typeName : LongIdent)
(inputArgName : Ident)
(populateNode : SynExpr)
: SynModuleDecl
=
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 assignments =
[
populateNode
SynExpr.Upcast (SynExpr.createIdent "node", SynType.Anon range0, range0)
]
|> SynExpr.sequential
|> SynExpr.createLet
[
SynExpr.createLongIdent [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonObject" ]
|> SynExpr.applyTo (SynExpr.CreateConst ())
|> SynBinding.basic [ Ident.create "node" ] []
]
let pattern =
SynPat.namedI inputArgName
|> 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 =
assignments
|> 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
assignments
|> SynBinding.basic [ functionName ] [ pattern ]
|> SynBinding.withReturnAnnotation returnInfo
|> SynBinding.withXmlDoc xmlDoc
|> SynModuleDecl.createLet
let recordModule (spec : JsonSerializeOutputSpec) (_typeName : LongIdent) (fields : SynField list) =
let fields = fields |> List.map SynField.extractWithIdent
fields
|> List.map (fun fieldData ->
let propertyName = getPropertyName fieldData.Ident fieldData.Attrs
let isJsonExtension = getIsJsonExtension fieldData.Attrs
if isJsonExtension then
let valType =
match fieldData.Type with
| DictionaryType (String, v) -> v
| _ -> failwith "Expected JsonExtensionData to be a Dictionary<string, something>"
let serialise = fst (serializeNode valType)
SynExpr.createIdent "node"
|> SynExpr.callMethodArg
"Add"
(SynExpr.tuple
[
SynExpr.createIdent "key"
SynExpr.applyFunction serialise (SynExpr.createIdent "value")
])
|> SynExpr.createForEach
(SynPat.identWithArgs
[ Ident.create "KeyValue" ]
(SynArgPats.create [ SynPat.named "key" ; SynPat.named "value" ]))
(SynExpr.createLongIdent' [ Ident.create "input" ; fieldData.Ident ])
else
createSerializeRhsRecord propertyName fieldData.Ident fieldData.Type
)
|> SynExpr.sequential
|> fun expr -> SynExpr.Do (expr, range0)
let unionModule (spec : JsonSerializeOutputSpec) (typeName : LongIdent) (cases : SynUnionCase list) =
let inputArg = Ident.create "input"
let fields = cases |> List.map UnionCase.ofSynUnionCase
fields
|> List.map (fun unionCase ->
let propertyName = getPropertyName unionCase.Name unionCase.Attributes
let caseNames = unionCase.Fields |> List.mapi (fun i _ -> $"arg%i{i}")
let argPats = SynArgPats.createNamed caseNames
let pattern =
SynPat.LongIdent (
SynLongIdent.create (typeName @ [ unionCase.Name ]),
None,
None,
argPats,
None,
range0
)
let typeLine =
[
SynExpr.CreateConst "type"
SynExpr.applyFunction
(SynExpr.createLongIdent [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonValue" ; "Create" ])
propertyName
]
|> SynExpr.tuple
|> SynExpr.applyFunction (SynExpr.createLongIdent [ "node" ; "Add" ])
let dataNode =
SynExpr.createLongIdent [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonObject" ]
|> SynExpr.applyTo (SynExpr.CreateConst ())
|> SynBinding.basic [ Ident.create "dataNode" ] []
let dataBindings =
(unionCase.Fields, caseNames)
||> List.zip
|> List.map (fun (fieldData, caseName) ->
let propertyName = getPropertyName (Option.get fieldData.Ident) fieldData.Attrs
let node =
SynExpr.applyFunction (fst (serializeNode fieldData.Type)) (SynExpr.createIdent caseName)
[ propertyName ; node ]
|> SynExpr.tuple
|> SynExpr.applyFunction (SynExpr.createLongIdent [ "dataNode" ; "Add" ])
)
let assignToNode =
[ SynExpr.CreateConst "data" ; SynExpr.createIdent "dataNode" ]
|> SynExpr.tuple
|> SynExpr.applyFunction (SynExpr.createLongIdent [ "node" ; "Add" ])
let dataNode =
SynExpr.sequential (dataBindings @ [ assignToNode ])
|> SynExpr.createLet [ dataNode ]
let action =
[
yield typeLine
if not dataBindings.IsEmpty then
yield dataNode
]
|> SynExpr.sequential
SynMatchClause.create pattern action
)
|> SynExpr.createMatch (SynExpr.createIdent' 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
(namespaceId : LongIdent)
(opens : SynOpenDeclTarget list)
(spec : JsonSerializeOutputSpec)
(typeDefn : SynTypeDefn)
=
let (SynTypeDefn (synComponentInfo, synTypeDefnRepr, _members, _implicitCtor, _, _)) =
typeDefn
let (SynComponentInfo (_attributes, _typeParams, _constraints, ident, _, _preferPostfix, access, _)) =
synComponentInfo
let attributes =
if spec.ExtensionMethods then
[ SynAttribute.autoOpen ]
else
[ SynAttribute.requireQualifiedAccess ; SynAttribute.compilationRepresentation ]
let xmlDoc =
let fullyQualified = ident |> Seq.map (fun i -> i.idText) |> String.concat "."
let description =
if spec.ExtensionMethods then
"extension members"
else
"methods"
$"Module containing JSON serializing %s{description} for the %s{fullyQualified} type"
|> PreXmlDoc.create
let moduleName =
if spec.ExtensionMethods then
match ident with
| [] -> failwith "unexpectedly got an empty identifier for type name"
| ident ->
let expanded =
List.last ident
|> fun i -> i.idText
|> fun s -> s + "JsonSerializeExtension"
|> Ident.create
List.take (List.length ident - 1) ident @ [ expanded ]
else
ident
let info =
SynComponentInfo.createLong moduleName
|> SynComponentInfo.addAttributes attributes
|> SynComponentInfo.setAccessibility access
|> SynComponentInfo.withDocString xmlDoc
let decls =
match synTypeDefnRepr with
| SynTypeDefnRepr.Simple (SynTypeDefnSimpleRepr.Record (_accessibility, recordFields, _range), _) ->
recordModule spec ident recordFields
|> scaffolding spec ident (Ident.create "input")
| SynTypeDefnRepr.Simple (SynTypeDefnSimpleRepr.Union (_accessibility, unionFields, _range), _) ->
unionModule spec ident unionFields
|> 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 decls |> List.singleton |> SynModuleDecl.nestedModule info
]
|> SynModuleOrNamespace.createNamespace namespaceId
/// Whippet generator that provides a method (possibly an extension method) for a record type,
/// containing a JSON serialization function.
[<WhippetGenerator>]
type JsonSerializeGenerator () =
interface IGenerateRawFromRaw with
member _.GenerateRawFromRaw (context : RawSourceGenerationArgs) =
if not (context.FilePath.EndsWith (".fs", StringComparison.Ordinal)) then
null
else
let targetedTypes =
context.Parameters
|> Seq.map (fun (KeyValue (k, v)) -> k, v.Split '!' |> Array.toList |> List.map DesiredGenerator.Parse)
|> Map.ofSeq
let ast = Ast.parse (Encoding.UTF8.GetString context.FileContents)
let relevantTypes =
Ast.getTypes ast
|> List.map (fun (name, defns) ->
defns
|> List.choose (fun defn ->
if SynTypeDefn.isRecord defn then Some defn
elif SynTypeDefn.isDu defn then Some defn
elif SynTypeDefn.isEnum defn then Some defn
else None
)
|> fun defns -> name, defns
)
let namespaceAndTypes =
relevantTypes
|> List.choose (fun (ns, types) ->
types
|> List.choose (fun typeDef ->
match SynTypeDefn.getAttribute typeof<JsonSerializeAttribute>.Name typeDef with
| None ->
let name = SynTypeDefn.getName typeDef |> List.map _.idText |> String.concat "."
match Map.tryFind name targetedTypes with
| Some desired ->
desired
|> List.tryPick (fun generator ->
match generator with
| DesiredGenerator.JsonSerialize arg ->
let spec =
{
ExtensionMethods =
arg
|> Option.defaultValue
JsonSerializeAttribute.DefaultIsExtensionMethod
}
Some (typeDef, spec)
| _ -> None
)
| _ -> None
| Some attr ->
let arg =
match SynExpr.stripOptionalParen attr.ArgExpr with
| SynExpr.Const (SynConst.Bool value, _) -> value
| SynExpr.Const (SynConst.Unit, _) -> JsonSerializeAttribute.DefaultIsExtensionMethod
| arg ->
failwith
$"Unrecognised argument %+A{arg} to [<%s{nameof JsonSerializeAttribute}>]. Literals are not supported. Use `true` or `false` (or unit) only."
let spec =
{
ExtensionMethods = arg
}
Some (typeDef, spec)
)
|> function
| [] -> None
| ty -> Some (ns, ty)
)
let opens = AstHelper.extractOpens ast
let modules =
namespaceAndTypes
|> List.collect (fun (ns, types) ->
types
|> List.map (fun (ty, spec) -> JsonSerializeGenerator.createModule ns opens spec ty)
)
Ast.render modules |> Option.toObj

View File

@@ -0,0 +1,223 @@
# WoofWare.Whippet.Plugin.Json
This is a [Whippet](https://github.com/Smaug123/WoofWare.Whippet) plugin defining JSON parse and serialise methods.
It is a copy of the corresponding [Myriad](https://github.com/MoiraeSoftware/myriad) JSON plugin in [WoofWare.Myriad](https://github.com/Smaug123/WoofWare.Myriad), taken from commit d59ebdfccb87a06579fb99008a15f58ea8be394e.
## What's the point?
`System.Text.Json`, in a `PublishAot` context, relies on C# source generators.
The default reflection-heavy implementations have the necessary code trimmed away, and result in a runtime exception.
But C# source generators [are entirely unsupported in F#](https://github.com/dotnet/fsharp/issues/14300).
These generators handle going from your strongly-typed domain objects to `System.Text.Json.Nodes.JsonNode`, and back.
## Usage: `JsonParse`
Define a `Dto.fs` file like the following:
```fsharp
namespace MyNamespace
open WoofWare.Whippet.Plugin.Json
[<JsonParse>]
type InnerType =
{
[<JsonPropertyName "something">]
Thing : string
}
/// My whatnot
[<JsonParse (* isExtensionMethod = *) false>]
type JsonRecordType =
{
/// A thing!
A : int
/// Another thing!
B : string
[<System.Text.Json.Serialization.JsonPropertyName "hi">]
C : int list
D : InnerType
}
```
In your fsproj:
```xml
<Project>
<ItemGroup>
<Compile Include="Dto.fs" />
<Compile Include="GeneratedDto.fs">
<WhippetFile>Dto.fs</WhippetFile>
</Compile>
</ItemGroup>
<ItemGroup>
<!-- Optional runtime dependency: you may use attributes to give instructions to the generator.
Specify the `Version` appropriately by getting the latest version from NuGet.org.
-->
<PackageReference Include="WoofWare.Whippet.Plugin.Json.Attributes" Version="" />
<!-- Development dependencies, hence PrivateAssets="all". Note `WhippetPlugin="true"`. -->
<PackageReference Include="WoofWare.Whippet.Plugin.Json" WhippetPlugin="true" Version="" />
<PackageReference Include="WoofWare.Whippet" Version="" PrivateAssets="all" />
</ItemGroup>
</Project>
```
The generator will produce a file somewhat like the following:
```fsharp
/// Module containing JSON parsing methods for the InnerType type
[<RequireQualifiedAccess>]
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module InnerType =
/// Parse from a JSON node.
let jsonParse (node: System.Text.Json.Nodes.JsonNode) : InnerType =
let Thing = node.["something"].AsValue().GetValue<string>()
{ Thing = Thing }
namespace UsePlugin
/// Module containing JSON parsing methods for the JsonRecordType type
[<AutoOpen>]
module JsonRecordTypeExtension =
type JsonRecordType with
/// Parse from a JSON node.
let jsonParse (node: System.Text.Json.Nodes.JsonNode) : JsonRecordType =
let D = InnerType.jsonParse node.["d"]
let C =
node.["hi"].AsArray() |> Seq.map (fun elt -> elt.GetValue<int>()) |> List.ofSeq
let B = node.["b"].AsValue().GetValue<string>()
let A = node.["a"].AsValue().GetValue<int>()
{ A = A; B = B; C = C; D = D }
```
You may instead choose to define attributes with the correct name yourself (if you don't want to take a dependency on the `WoofWare.Whippet.Plugin.Json.Attributes` package).
Alternatively, you may omit the attributes and the runtime dependency, and control the generator entirely through the fsproj file:
```xml
<Project>
<ItemGroup>
<Compile Include="Dto.fs" />
<Compile Include="GeneratedDto.fs">
<WhippetFile>Dto.fs</WhippetFile>
<WhippetParamInnerType>JsonParse</WhippetParamInnerType>
<WhippetParamJsonRecordType>JsonParse(false)</WhippetParamJsonRecordType>
</Compile>
</ItemGroup>
<ItemGroup>
<!-- Development dependencies, hence PrivateAssets="all". Note `WhippetPlugin="true"`. -->
<PackageReference Include="WoofWare.Whippet.Plugin.Json" WhippetPlugin="true" Version="" />
<PackageReference Include="WoofWare.Whippet" Version="" PrivateAssets="all" />
</ItemGroup>
</Project>
```
(This plugin follows a standard convention taken by `WoofWare.Whippet.Plugin` plugins,
where you use Whippet parameters with the same name as each input type,
whose contents are a `!`-delimited list of the generators which you wish to apply to that input type.)
## Usage: `JsonSerialize`
Define a `Dto.fs` file like the following:
```fsharp
namespace MyNamespace
open WoofWare.Whippet.Plugin.Json
[<JsonSerialize true>]
type InnerTypeWithBoth =
{
[<JsonPropertyName("it's-a-me")>]
Thing : string
ReadOnlyDict : IReadOnlyDictionary<string, Uri list>
}
```
In your fsproj:
```xml
<Project>
<ItemGroup>
<Compile Include="Dto.fs" />
<Compile Include="GeneratedDto.fs">
<WhippetFile>Dto.fs</WhippetFile>
</Compile>
</ItemGroup>
<ItemGroup>
<!-- Optional runtime dependency: you may use attributes to give instructions to the generator.
Specify the `Version` appropriately by getting the latest version from NuGet.org.
-->
<PackageReference Include="WoofWare.Whippet.Plugin.Json.Attributes" Version="" />
<!-- Development dependencies, hence PrivateAssets="all". Note `WhippetPlugin="true"`. -->
<PackageReference Include="WoofWare.Whippet.Plugin.Json" WhippetPlugin="true" Version="" />
<PackageReference Include="WoofWare.Whippet" Version="" PrivateAssets="all" />
</ItemGroup>
</Project>
```
The generator will produce a file somewhat like the following:
```fsharp
namespace UsePlugin
/// Module containing JSON parsing methods for the JsonRecordType type
[<AutoOpen>]
module JsonRecordTypeExtension =
type InnerTypeWithBoth with
let toJsonNode (input : InnerTypeWithBoth) : System.Text.Json.Nodes.JsonNode =
let node = System.Text.Json.Nodes.JsonObject ()
do
node.Add (("it's-a-me"), System.Text.Json.Nodes.JsonValue.Create<string> input.Thing)
node.Add (
"ReadOnlyDict",
(fun field ->
let ret = System.Text.Json.Nodes.JsonObject ()
for (KeyValue (key, value)) in field do
ret.Add (key.ToString (), System.Text.Json.Nodes.JsonValue.Create<Uri> value)
ret
) input.Map
)
node
```
You may instead choose to define attributes with the correct name yourself (if you don't want to take a dependency on the `WoofWare.Whippet.Plugin.Json.Attributes` package).
Alternatively, you may omit the attributes and the runtime dependency, and control the generator entirely through the fsproj file:
```xml
<Project>
<ItemGroup>
<Compile Include="Dto.fs" />
<Compile Include="GeneratedDto.fs">
<WhippetFile>Dto.fs</WhippetFile>
<WhippetParamInnerType>JsonSerialize</WhippetParamInnerType>
<WhippetParamJsonRecordType>JsonSerialize(false)</WhippetParamJsonRecordType>
</Compile>
</ItemGroup>
<ItemGroup>
<!-- Development dependencies, hence PrivateAssets="all". Note `WhippetPlugin="true"`. -->
<PackageReference Include="WoofWare.Whippet.Plugin.Json" WhippetPlugin="true" Version="" />
<PackageReference Include="WoofWare.Whippet" Version="" PrivateAssets="all" />
</ItemGroup>
</Project>
```
(This plugin follows a standard convention taken by `WoofWare.Whippet.Plugin` plugins,
where you use Whippet parameters with the same name as each input type,
whose contents are a `!`-delimited list of the generators which you wish to apply to that input type.)
## Notes
* The plugin includes an *opinionated* de/serializer for discriminated unions. (Any such serializer must be opinionated, because JSON does not natively model DUs.)
* Supply the optional boolean arg `false` to the `[<JsonParse>]`/`[<JsonSerialize>]` attributes, or pass it via `<WhippetParamMyType>JsonParse(false)</WhippetParamMyType>`, to get a genuine module that can be consumed from C#.

View File

@@ -0,0 +1,264 @@
namespace WoofWare.Whippet.Plugin.Json.Test
open PureGym
open System
[<RequireQualifiedAccess>]
module PureGymDtos =
let gymOpeningHoursCases =
[
"""{"openingHours": [], "isAlwaysOpen": false}""",
{
GymOpeningHours.OpeningHours = []
IsAlwaysOpen = false
}
"""{"openingHours": ["something"], "isAlwaysOpen": false}""",
{
GymOpeningHours.OpeningHours = [ "something" ]
IsAlwaysOpen = false
}
]
let gymAccessOptionsCases =
List.allPairs [ true ; false ] [ true ; false ]
|> List.map (fun (a, b) ->
let s = sprintf """{"pinAccess": %b, "qrCodeAccess": %b}""" a b
s,
{
GymAccessOptions.PinAccess = a
QrCodeAccess = b
}
)
let gymAddressCases =
[
"""{"addressLine1": "", "postCode": "hi", "town": ""}""",
{
GymAddress.AddressLine1 = ""
AddressLine2 = None
AddressLine3 = None
County = None
Postcode = "hi"
Town = ""
}
"""{"addressLine1": "", "addressLine2": null, "postCode": "hi", "town": ""}""",
{
GymAddress.AddressLine1 = ""
AddressLine2 = None
AddressLine3 = None
County = None
Postcode = "hi"
Town = ""
}
]
let gymLocationCases =
[
"""{"latitude": 1.0, "longitude": 3.0}""",
{
GymLocation.Latitude = 1.0<measure>
Longitude = 3.0
}
]
let gymCases =
let ovalJson =
"""{"name":"London Oval","id":19,"status":2,"address":{"addressLine1":"Canterbury Court","addressLine2":"Units 4, 4A, 5 And 5A","addressLine3":"Kennington Park","town":"LONDON","county":null,"postcode":"SW9 6DE"},"phoneNumber":"+44 3444770005","emailAddress":"info.londonoval@puregym.com","staffMembers":null,"gymOpeningHours":{"isAlwaysOpen":true,"openingHours":[]},"reasonsToJoin":null,"accessOptions":{"pinAccess":true,"qrCodeAccess":true},"virtualTourUrl":null,"personalTrainersUrl":null,"webViewUrl":null,"floorPlanUrl":null,"location":{"longitude":"-0.110252","latitude":"51.480401"},"timeZone":"Europe/London","reopenDate":"2021-04-12T00:00:00+01 Europe/London"}"""
let oval =
{
Gym.Name = "London Oval"
Id = 19
Status = 2
Address =
{
AddressLine1 = "Canterbury Court"
AddressLine2 = Some "Units 4, 4A, 5 And 5A"
AddressLine3 = Some "Kennington Park"
Town = "LONDON"
County = None
Postcode = "SW9 6DE"
}
PhoneNumber = "+44 3444770005"
EmailAddress = "info.londonoval@puregym.com"
GymOpeningHours =
{
IsAlwaysOpen = true
OpeningHours = []
}
AccessOptions =
{
PinAccess = true
QrCodeAccess = true
}
Location =
{
Longitude = -0.110252
Latitude = 51.480401<measure>
}
TimeZone = "Europe/London"
ReopenDate = "2021-04-12T00:00:00+01 Europe/London"
}
[ ovalJson, oval ]
let memberCases =
let me =
{
Id = 1234567
CompoundMemberId = "12A123456"
FirstName = "Patrick"
LastName = "Stevens"
HomeGymId = 19
HomeGymName = "London Oval"
EmailAddress = "someone@somewhere"
GymAccessPin = "00000000"
DateOfBirth = DateOnly (1994, 01, 02)
MobileNumber = "+44 1234567"
Postcode = "W1A 1AA"
MembershipName = "Corporate"
MembershipLevel = 12
SuspendedReason = 0
MemberStatus = 2
}
let meJson =
"""{
"id": 1234567,
"compoundMemberId": "12A123456",
"firstName": "Patrick",
"lastName": "Stevens",
"homeGymId": 19,
"homeGymName": "London Oval",
"emailAddress": "someone@somewhere",
"gymAccessPin": "00000000",
"dateofBirth": "1994-01-02",
"mobileNumber": "+44 1234567",
"postCode": "W1A 1AA",
"membershipName": "Corporate",
"membershipLevel": 12,
"suspendedReason": 0,
"memberStatus": 2
}"""
[ meJson, me ]
let gymAttendanceCases =
let json =
"""{
"description": "65",
"totalPeopleInGym": 65,
"totalPeopleInClasses": 2,
"totalPeopleSuffix": null,
"isApproximate": false,
"attendanceTime": "2023-12-27T18:54:09.5101697",
"lastRefreshed": "2023-12-27T18:54:09.5101697Z",
"lastRefreshedPeopleInClasses": "2023-12-27T18:50:26.0782286Z",
"maximumCapacity": 0
}"""
let expected =
{
Description = "65"
TotalPeopleInGym = 65
TotalPeopleInClasses = 2
TotalPeopleSuffix = None
IsApproximate = false
AttendanceTime =
DateTime (2023, 12, 27, 18, 54, 09, 510, 169, DateTimeKind.Utc)
+ TimeSpan.FromTicks 7L
LastRefreshed =
DateTime (2023, 12, 27, 18, 54, 09, 510, 169, DateTimeKind.Utc)
+ TimeSpan.FromTicks 7L
LastRefreshedPeopleInClasses =
DateTime (2023, 12, 27, 18, 50, 26, 078, 228, DateTimeKind.Utc)
+ TimeSpan.FromTicks 6L
MaximumCapacity = 0
}
[ json, expected ]
let memberActivityDtoCases =
let json =
"""{"totalDuration":2217,"averageDuration":48,"totalVisits":46,"totalClasses":0,"isEstimated":false,"lastRefreshed":"2023-12-27T19:00:56.0309892Z"}"""
let value =
{
TotalDuration = 2217
AverageDuration = 48
TotalVisits = 46
TotalClasses = 0
IsEstimated = false
LastRefreshed =
DateTime (2023, 12, 27, 19, 00, 56, 030, 989, DateTimeKind.Utc)
+ TimeSpan.FromTicks 2L
}
[ json, value ]
let sessionsCases =
let json =
"""{
"Summary":{"Total":{"Activities":0,"Visits":10,"Duration":445},"ThisWeek":{"Activities":0,"Visits":0,"Duration":0}},
"Visits":[
{"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-21T10:12:00","Duration":50,"Name":null},
{"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-20T12:05:00","Duration":80,"Name":null},
{"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-17T19:37:00","Duration":46,"Name":null},
{"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-16T12:19:00","Duration":37,"Name":null},
{"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-15T11:14:00","Duration":47,"Name":null},
{"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-13T10:30:00","Duration":36,"Name":null},
{"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-10T16:18:00","Duration":32,"Name":null},
{"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-05T22:36:00","Duration":40,"Name":null},
{"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-03T17:59:00","Duration":48,"Name":null},
{"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-01T21:41:00","Duration":29,"Name":null}],
"Activities":[]}
"""
let singleVisit startTime duration =
{
IsDurationEstimated = false
Gym =
{
Id = 19
Name = "London Oval"
Status = "Blocked"
}
StartTime = startTime
Duration = duration
}
let expected =
{
Summary =
{
Total =
{
Activities = 0
Visits = 10
Duration = 445
}
ThisWeek =
{
Activities = 0
Visits = 0
Duration = 0
}
}
Visits =
[
singleVisit (DateTime (2023, 12, 21, 10, 12, 00)) 50
singleVisit (DateTime (2023, 12, 20, 12, 05, 00)) 80
singleVisit (DateTime (2023, 12, 17, 19, 37, 00)) 46
singleVisit (DateTime (2023, 12, 16, 12, 19, 00)) 37
singleVisit (DateTime (2023, 12, 15, 11, 14, 00)) 47
singleVisit (DateTime (2023, 12, 13, 10, 30, 00)) 36
singleVisit (DateTime (2023, 12, 10, 16, 18, 00)) 32
singleVisit (DateTime (2023, 12, 05, 22, 36, 00)) 40
singleVisit (DateTime (2023, 12, 03, 17, 59, 00)) 48
singleVisit (DateTime (2023, 12, 01, 21, 41, 00)) 29
]
}
[ json, expected ]

View File

@@ -0,0 +1,74 @@
namespace WoofWare.Whippet.Plugin.Json.Test
open System
open System.Numerics
open System.Text.Json.Nodes
open ConsumePlugin
open NUnit.Framework
open FsUnitTyped
[<TestFixture>]
module TestExtensionMethod =
[<Test>]
let ``Parse via extension method`` () =
let json =
"""{
"alpha": "hello!",
"bravo": "https://example.com",
"charlie": 0.3341,
"delta": 110033.4,
"echo": -0.000993,
"foxtrot": -999999999999,
"golf": -123456789101112,
"hotel": 18446744073709551615,
"india": 99884,
"juliette": 12223334,
"kilo": -2147483642,
"lima": 4294967293,
"mike": -32767,
"november": 65533,
"oscar": -125,
"papa": 253,
"quebec": 254,
"tango": -3,
"uniform": 1004443.300988393349583009,
"victor": "x",
"whiskey": 123456123456123456123456123456123456123456
}"""
|> JsonNode.Parse
let expected =
{
Alpha = "hello!"
Bravo = Uri "https://example.com"
Charlie = 0.3341
Delta = 110033.4f
Echo = -0.000993f
Foxtrot = -999999999999.0
Golf = -123456789101112L
Hotel = 18446744073709551615UL
India = 99884
Juliette = 12223334u
Kilo = -2147483642
Lima = 4294967293u
Mike = -32767s
November = 65533us
Oscar = -125y
Papa = 253uy
Quebec = 254uy
Tango = -3y
Uniform = 1004443.300988393349583009m
Victor = 'x'
Whiskey =
let mutable i = BigInteger 0
for _ = 0 to 6 do
i <- i * BigInteger 1000000 + BigInteger 123456
i
}
let actual = ToGetExtensionMethod.jsonParse json
actual |> shouldEqual expected

View File

@@ -0,0 +1,63 @@
namespace WoofWare.Whippet.Plugin.Json.Test
open System.Text.Json.Nodes
open ConsumePlugin
open NUnit.Framework
open FsUnitTyped
[<TestFixture>]
module TestJsonParse =
let _canSeePastExtensionMethod = ToGetExtensionMethod.thisModuleWouldClash
[<Test>]
let ``Single example`` () =
let s =
"""
{
"a": 3, "another-thing": "hello", "hi": [6, 1], "d": {"something": "oh hi"},
"e": ["something", "else"], "f": []
}
"""
let expected =
{
A = 3
B = "hello"
C = [ 6 ; 1 ]
D =
{
Thing = "oh hi"
}
E = [| "something" ; "else" |]
F = [||]
}
let actual = s |> JsonNode.Parse |> JsonRecordType.jsonParse
actual |> shouldEqual expected
[<Test>]
let ``Inner example`` () =
let s =
"""{
"something": "oh hi"
}"""
let expected =
{
Thing = "oh hi"
}
let actual = s |> JsonNode.Parse |> InnerType.jsonParse
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

@@ -0,0 +1,474 @@
namespace WoofWare.Whippet.Plugin.Json.Test
open System
open System.Collections.Generic
open System.Text.Json.Nodes
open FsCheck.Random
open Microsoft.FSharp.Reflection
open NUnit.Framework
open FsCheck
open FsUnitTyped
open ConsumePlugin
[<TestFixture>]
module TestJsonSerde =
let uriGen : Gen<Uri> =
gen {
let! suffix = Arb.generate<int>
return Uri $"https://example.com/%i{suffix}"
}
let rec innerGen (count : int) : Gen<InnerTypeWithBoth> =
gen {
let! guid = Arb.generate<Guid>
let! mapKeys = Gen.listOf Arb.generate<NonNull<string>>
let mapKeys = mapKeys |> List.map _.Get |> List.distinct
let! mapValues = Gen.listOfLength mapKeys.Length uriGen
let map = List.zip mapKeys mapValues |> Map.ofList
let! concreteDictKeys =
if count > 0 then
Gen.listOf Arb.generate<NonNull<string>>
else
Gen.constant []
let concreteDictKeys =
concreteDictKeys
|> List.map _.Get
|> List.distinct
|> fun x -> List.take (min 3 x.Length) x
let! concreteDictValues =
if count > 0 then
Gen.listOfLength concreteDictKeys.Length (innerGen (count - 1))
else
Gen.constant []
let concreteDict =
List.zip concreteDictKeys concreteDictValues
|> List.map KeyValuePair
|> Dictionary
let! readOnlyDictKeys = Gen.listOf Arb.generate<NonNull<string>>
let readOnlyDictKeys = readOnlyDictKeys |> List.map _.Get |> List.distinct
let! readOnlyDictValues = Gen.listOfLength readOnlyDictKeys.Length (Gen.listOf Arb.generate<char>)
let readOnlyDict = List.zip readOnlyDictKeys readOnlyDictValues |> readOnlyDict
let! dictKeys = Gen.listOf uriGen
let! dictValues = Gen.listOfLength dictKeys.Length Arb.generate<bool>
let dict = List.zip dictKeys dictValues |> dict
return
{
Thing = guid
Map = map
ReadOnlyDict = readOnlyDict
Dict = dict
ConcreteDict = concreteDict
}
}
let outerGen : Gen<JsonRecordTypeWithBoth> =
gen {
let! a = Arb.generate<int>
let! b = Arb.generate<NonNull<string>>
let! c = Gen.listOf Arb.generate<int>
let! depth = Gen.choose (0, 2)
let! d = innerGen depth
let! e = Gen.arrayOf Arb.generate<NonNull<string>>
let! arr = Gen.arrayOf Arb.generate<int>
let! byte = Arb.generate
let! sbyte = Arb.generate
let! i = Arb.generate
let! i32 = Arb.generate
let! i64 = Arb.generate
let! u = Arb.generate
let! u32 = Arb.generate
let! u64 = Arb.generate
let! f = Arb.generate |> Gen.filter (fun s -> Double.IsFinite (s / 1.0<measure>))
let! f32 = 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! intMeasureNullable = Arb.generate
let! someEnum = Gen.choose (0, 1)
let! timestamp = Arb.generate
return
{
A = a
B = b.Get
C = c
D = d
E = e |> Array.map _.Get
Arr = arr
Byte = byte
Sbyte = sbyte
I = i
I32 = i32
I64 = i64
U = u
U32 = u32
U64 = u64
F = f
F32 = f32
Single = single
IntMeasureOption = intMeasureOption
IntMeasureNullable = intMeasureNullable
Enum = enum<SomeEnum> someEnum
Timestamp = timestamp
Unit = ()
}
}
[<Test>]
let ``It just works`` () =
let property (o : JsonRecordTypeWithBoth) : bool =
o
|> JsonRecordTypeWithBoth.toJsonNode
|> fun s -> s.ToJsonString ()
|> JsonNode.Parse
|> JsonRecordTypeWithBoth.jsonParse
|> shouldEqual o
true
property |> Prop.forAll (Arb.fromGen outerGen) |> Check.QuickThrowOnFailure
[<Test>]
let ``Single example of big record`` () =
let guid = Guid.Parse "dfe24db5-9f8d-447b-8463-4c0bcf1166d5"
let data =
{
A = 3
B = "hello!"
C = [ 1 ; -9 ]
D =
{
Thing = guid
Map = Map.ofList []
ReadOnlyDict = readOnlyDict []
Dict = dict []
ConcreteDict = Dictionary ()
}
E = [| "I'm-a-string" |]
Arr = [| -18883 ; 9100 |]
Byte = 87uy<measure>
Sbyte = 89y<measure>
I = 199993345<measure>
I32 = -485832<measure>
I64 = -13458625689L<measure>
U = 458582u<measure>
U32 = 857362147u<measure>
U64 = 1234567892123414596UL<measure>
F = 8833345667.1<measure>
F32 = 1000.98f<measure>
Single = 0.334f<measure>
IntMeasureOption = Some 981<measure>
IntMeasureNullable = Nullable -883<measure>
Enum = enum<SomeEnum> 1
Timestamp = DateTimeOffset (2024, 07, 01, 17, 54, 00, TimeSpan.FromHours 1.0)
Unit = ()
}
let expected =
"""{
"a": 3,
"b": "hello!",
"c": [1, -9],
"d": {
"it\u0027s-a-me": "dfe24db5-9f8d-447b-8463-4c0bcf1166d5",
"map": {},
"readOnlyDict": {},
"dict": {},
"concreteDict": {}
},
"e": ["I\u0027m-a-string"],
"arr": [-18883, 9100],
"byte": 87,
"sbyte": 89,
"i": 199993345,
"i32": -485832,
"i64": -13458625689,
"u": 458582,
"u32": 857362147,
"u64": 1234567892123414596,
"f": 8833345667.1,
"f32": 1000.98,
"single": 0.334,
"intMeasureOption": 981,
"intMeasureNullable": -883,
"enum": 1,
"timestamp": "2024-07-01T17:54:00.0000000\u002B01:00",
"unit": {}
}
"""
|> fun s -> s.ToCharArray ()
|> Array.filter (fun c -> not (Char.IsWhiteSpace c))
|> fun s -> new String (s)
JsonRecordTypeWithBoth.toJsonNode(data).ToJsonString () |> shouldEqual expected
JsonRecordTypeWithBoth.jsonParse (JsonNode.Parse expected) |> shouldEqual data
[<Test>]
let ``Guids are treated just like strings`` () =
let guidStr = "b1e7496e-6e79-4158-8579-a01de355d3b2"
let guid = Guid.Parse guidStr
let node =
{
Thing = guid
Map = Map.empty
ReadOnlyDict = readOnlyDict []
Dict = dict []
ConcreteDict = Dictionary ()
}
|> InnerTypeWithBoth.toJsonNode
node.ToJsonString ()
|> shouldEqual (
sprintf """{"it\u0027s-a-me":"%s","map":{},"readOnlyDict":{},"dict":{},"concreteDict":{}}""" guidStr
)
type Generators =
static member TestCase () =
{ new Arbitrary<InnerTypeWithBoth>() with
override x.Generator = innerGen 5
}
let sanitiseInner (r : InnerTypeWithBoth) : InnerTypeWithBoth =
{
Thing = r.Thing
Map = r.Map
ReadOnlyDict = r.ReadOnlyDict
Dict = r.Dict
ConcreteDict = r.ConcreteDict
}
let sanitiseRec (r : JsonRecordTypeWithBoth) : JsonRecordTypeWithBoth =
{ r with
B = if isNull r.B then "<null>" else r.B
C =
if Object.ReferenceEquals (r.C, (null : obj)) then
[]
else
r.C
D = sanitiseInner r.D
E = if isNull r.E then [||] else r.E
Arr =
if Object.ReferenceEquals (r.Arr, (null : obj)) then
[||]
else
r.Arr
}
let duGen =
gen {
let! case = Gen.choose (0, 2)
match case with
| 0 -> return FirstDu.EmptyCase
| 1 ->
let! s = Arb.generate<NonNull<string>>
return FirstDu.Case1 s.Get
| 2 ->
let! i = Arb.generate<int>
let! record = outerGen
return FirstDu.Case2 (record, i)
| _ -> return failwith $"unexpected: %i{case}"
}
[<Test>]
let ``Discriminated union works`` () =
let property (du : FirstDu) : unit =
du
|> FirstDu.toJsonNode
|> fun s -> s.ToJsonString ()
|> JsonNode.Parse
|> FirstDu.jsonParse
|> shouldEqual du
property |> Prop.forAll (Arb.fromGen duGen) |> Check.QuickThrowOnFailure
[<Test>]
let ``DU generator covers all cases`` () =
let rand = Random ()
let cases = FSharpType.GetUnionCases typeof<FirstDu>
let counts = Array.zeroCreate<int> cases.Length
let decompose = FSharpValue.PreComputeUnionTagReader typeof<FirstDu>
let mutable i = 0
while i < 10_000 && Array.exists (fun i -> i = 0) counts do
let du = Gen.eval 10 (StdGen.StdGen (rand.Next (), rand.Next ())) duGen
let tag = decompose du
counts.[tag] <- counts.[tag] + 1
i <- i + 1
for i in counts do
i |> shouldBeGreaterThan 0
let dict<'a, 'b when 'a : equality> (xs : ('a * 'b) seq) : Dictionary<'a, 'b> =
let result = Dictionary ()
for k, v in xs do
result.Add (k, v)
result
let inline makeJsonArr< ^t, ^u when ^u : (static member op_Implicit : ^t -> JsonNode) and ^u :> JsonNode>
(arr : ^t seq)
: JsonNode
=
let result = JsonArray ()
for a in arr do
result.Add a
result :> JsonNode
let normalise (d : Dictionary<'a, 'b>) : ('a * 'b) list =
d |> Seq.map (fun (KeyValue (a, b)) -> a, b) |> Seq.toList |> List.sortBy fst
[<Test>]
let ``Can collect extension data`` () =
let str =
"""{
"message": { "header": "hi", "value": "bye" },
"something": 3,
"arr": ["egg", "toast"],
"str": "whatnot"
}"""
|> JsonNode.Parse
let expected =
{
Rest =
[
"something", JsonNode.op_Implicit 3
"arr", makeJsonArr [| "egg" ; "toast" |]
"str", JsonNode.op_Implicit "whatnot"
]
|> dict
Message =
Some
{
Header = "hi"
Value = "bye"
}
}
let actual = CollectRemaining.jsonParse str
actual.Message |> shouldEqual expected.Message
normalise actual.Rest
|> List.map (fun (k, v) -> k, v.ToJsonString ())
|> shouldEqual (normalise expected.Rest |> List.map (fun (k, v) -> k, v.ToJsonString ()))
[<Test>]
let ``Can write out extension data`` () =
let expected =
"""{"message":{"header":"hi","value":"bye"},"something":3,"arr":["egg","toast"],"str":"whatnot"}"""
let toWrite =
{
Rest =
[
"something", JsonNode.op_Implicit 3
"arr", makeJsonArr [| "egg" ; "toast" |]
"str", JsonNode.op_Implicit "whatnot"
]
|> dict
Message =
Some
{
Header = "hi"
Value = "bye"
}
}
let actual = CollectRemaining.toJsonNode toWrite |> fun s -> s.ToJsonString ()
actual |> shouldEqual expected
[<Test>]
let ``Can collect extension data, nested`` () =
let str =
"""{
"thing": 99,
"baz": -123,
"remaining": {
"message": { "header": "hi", "value": "bye" },
"something": 3,
"arr": ["egg", "toast"],
"str": "whatnot"
}
}"""
|> JsonNode.Parse
let expected : OuterCollectRemaining =
{
Remaining =
{
Message =
Some
{
Header = "hi"
Value = "bye"
}
Rest =
[
"something", JsonNode.op_Implicit 3
"arr", makeJsonArr [| "egg" ; "toast" |]
"str", JsonNode.op_Implicit "whatnot"
]
|> dict
}
Others = [ "thing", 99 ; "baz", -123 ] |> dict
}
let actual = OuterCollectRemaining.jsonParse str
normalise actual.Others |> shouldEqual (normalise expected.Others)
let actual = actual.Remaining
let expected = expected.Remaining
actual.Message |> shouldEqual expected.Message
normalise actual.Rest
|> List.map (fun (k, v) -> k, v.ToJsonString ())
|> shouldEqual (normalise expected.Rest |> List.map (fun (k, v) -> k, v.ToJsonString ()))
[<Test>]
let ``Can write out extension data, nested`` () =
let expected =
"""{"thing":99,"baz":-123,"remaining":{"message":{"header":"hi","value":"bye"},"something":3,"arr":["egg","toast"],"str":"whatnot"}}"""
let toWrite : OuterCollectRemaining =
{
Others = [ "thing", 99 ; "baz", -123 ] |> dict
Remaining =
{
Rest =
[
"something", JsonNode.op_Implicit 3
"arr", makeJsonArr [| "egg" ; "toast" |]
"str", JsonNode.op_Implicit "whatnot"
]
|> dict
Message =
Some
{
Header = "hi"
Value = "bye"
}
}
}
let actual = OuterCollectRemaining.toJsonNode toWrite |> fun s -> s.ToJsonString ()
actual |> shouldEqual expected

View File

@@ -0,0 +1,71 @@
namespace WoofWare.Whippet.Plugin.Json.Test
open System
open System.Text.Json.Nodes
open NUnit.Framework
open FsUnitTyped
open PureGym
[<TestFixture>]
module TestPureGymJson =
let gymOpeningHoursCases = PureGymDtos.gymOpeningHoursCases |> List.map TestCaseData
[<TestCaseSource(nameof gymOpeningHoursCases)>]
let ``GymOpeningHours JSON parse`` (json : string, expected : GymOpeningHours) =
JsonNode.Parse json |> GymOpeningHours.jsonParse |> shouldEqual expected
let gymAccessOptionsCases =
PureGymDtos.gymAccessOptionsCases |> List.map TestCaseData
[<TestCaseSource(nameof gymAccessOptionsCases)>]
let ``GymAccessOptions JSON parse`` (json : string, expected : GymAccessOptions) =
JsonNode.Parse json |> GymAccessOptions.jsonParse |> shouldEqual expected
let gymLocationCases = PureGymDtos.gymLocationCases |> List.map TestCaseData
[<TestCaseSource(nameof gymLocationCases)>]
let ``GymLocation JSON parse`` (json : string, expected : GymLocation) =
JsonNode.Parse json |> GymLocation.jsonParse |> shouldEqual expected
let gymAddressCases = PureGymDtos.gymAddressCases |> List.map TestCaseData
[<TestCaseSource(nameof gymAddressCases)>]
let ``GymAddress JSON parse`` (json : string, expected : GymAddress) =
JsonNode.Parse (json, Nullable (JsonNodeOptions (PropertyNameCaseInsensitive = true)))
|> GymAddress.jsonParse
|> shouldEqual expected
let gymCases = PureGymDtos.gymCases |> List.map TestCaseData
[<TestCaseSource(nameof gymCases)>]
let ``Gym JSON parse`` (json : string, expected : Gym) =
JsonNode.Parse json |> Gym.jsonParse |> shouldEqual expected
let memberCases = PureGymDtos.memberCases |> List.map TestCaseData
[<TestCaseSource(nameof memberCases)>]
let ``Member JSON parse`` (json : string, expected : Member) =
json |> JsonNode.Parse |> Member.jsonParse |> shouldEqual expected
let gymAttendanceCases = PureGymDtos.gymAttendanceCases |> List.map TestCaseData
[<TestCaseSource(nameof gymAttendanceCases)>]
let ``GymAttendance JSON parse`` (json : string, expected : GymAttendance) =
json |> JsonNode.Parse |> GymAttendance.jsonParse |> shouldEqual expected
let memberActivityDtoCases =
PureGymDtos.memberActivityDtoCases |> List.map TestCaseData
[<TestCaseSource(nameof memberActivityDtoCases)>]
let ``MemberActivityDto JSON parse`` (json : string, expected : MemberActivityDto) =
json |> JsonNode.Parse |> MemberActivityDto.jsonParse |> shouldEqual expected
let sessionsCases = PureGymDtos.sessionsCases |> List.map TestCaseData
[<TestCaseSource(nameof sessionsCases)>]
let ``Sessions JSON parse`` (json : string, expected : Sessions) =
json
|> fun o -> JsonNode.Parse (o, Nullable (JsonNodeOptions (PropertyNameCaseInsensitive = true)))
|> Sessions.jsonParse
|> shouldEqual expected

View File

@@ -0,0 +1,26 @@
namespace WoofWare.Whippet.Plugin.Json.Test
open NUnit.Framework
open WoofWare.Whippet.Plugin.Json
open ApiSurface
[<TestFixture>]
module TestAttributeSurface =
let assembly = typeof<JsonParseAttribute>.Assembly
[<Test>]
let ``Ensure API surface has not been modified`` () = ApiSurface.assertIdentical assembly
(*
[<Test>]
let ``Check version against remote`` () =
MonotonicVersion.validate assembly "WoofWare.Whippet.Plugin.Json.Attributes"
*)
[<Test ; Explicit>]
let ``Update API surface`` () =
ApiSurface.writeAssemblyBaseline assembly
[<Test>]
let ``Ensure public API is fully documented`` () =
DocCoverage.assertFullyDocumented assembly

View File

@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<Compile Include="PureGymDtos.fs" />
<Compile Include="TestJsonSerde.fs" />
<Compile Include="TestJsonParse.fs" />
<Compile Include="TestExtensionMethod.fs" />
<Compile Include="TestPureGymJson.fs" />
<Compile Include="TestSurface.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1"/>
<PackageReference Include="NUnit" Version="4.2.2"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0"/>
<PackageReference Include="FsUnit" Version="6.0.1"/>
<PackageReference Include="FsCheck" Version="2.16.6"/>
<PackageReference Include="ApiSurface" Version="4.1.5"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\WoofWare.Whippet.Plugin.Json.Consumer\WoofWare.Whippet.Plugin.Json.Consumer.fsproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Authors>Patrick Stevens</Authors>
<Copyright>Copyright (c) Patrick Stevens 2024</Copyright>
<Description>Whippet F# source generator plugin, for generating JSON parse and serialize methods.</Description>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/Smaug123/WoofWare.Whippet</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageTags>fsharp;source-generator;source-gen;whippet;json;serialize;deserialize;serde</PackageTags>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarnOn>FS3559</WarnOn>
<PackageId>WoofWare.Whippet.Plugin.Json</PackageId>
<DevelopmentDependency>true</DevelopmentDependency>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<NoWarn>NU5118</NoWarn>
</PropertyGroup>
<ItemGroup>
<Compile Include="DesiredGenerator.fs" />
<Compile Include="JsonSerializeGenerator.fs" />
<Compile Include="JsonParseGenerator.fs" />
<None Include="README.md">
<Pack>True</Pack>
<PackagePath>/</PackagePath>
<Link>README.md</Link>
</None>
<EmbeddedResource Include="version.json" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\WoofWare.Whippet.Core\WoofWare.Whippet.Core.fsproj" />
<ProjectReference Include="..\..\..\WoofWare.Whippet.Fantomas\WoofWare.Whippet.Fantomas.fsproj" />
<ProjectReference Include="..\WoofWare.Whippet.Plugin.Json.Attributes\WoofWare.Whippet.Plugin.Json.Attributes.fsproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,14 @@
{
"version": "0.1",
"publicReleaseRefSpec": [
"^refs/heads/main$"
],
"pathFilters": [
"./",
":/WoofWare.Whippet.Core/",
":/WoofWare.Whippet.Fantomas/",
":/Plugins/Json/WoofWare.Whippet.Plugins.Json.Attributes/",
":/global.json",
":/Directory.Build.props"
]
}