Add JSON parse source gen (#9)

This commit is contained in:
Patrick Stevens
2023-12-27 11:46:12 +00:00
committed by GitHub
parent 055aad8c5e
commit 39d603c317
22 changed files with 882 additions and 145 deletions

View File

@@ -1,29 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<MyriadSdkGenerator Include="$(MSBuildThisFileDirectory)..\MyriadPlugin\bin\$(Configuration)\$(TargetFramework)\MyriadPlugin.dll" />
</ItemGroup>
<ItemGroup>
<Compile Include="RecordFile.fs" />
<Compile Include="Generated.fs"> <!--1-->
<MyriadFile>RecordFile.fs</MyriadFile> <!--2-->
</Compile>
<Compile Include="Program.fs" />
<None Include="..\runmyriad.sh">
<Link>runmyriad.sh</Link>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MyriadPlugin\MyriadPlugin.fsproj" />
<PackageReference Include="Myriad.Sdk" Version="0.8.3" />
<PackageReference Include="Myriad.Core" Version="0.8.3" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<MyriadSdkGenerator Include="$(MSBuildThisFileDirectory)..\MyriadPlugin\bin\$(Configuration)\net6.0\MyriadPlugin.dll" />
</ItemGroup>
<ItemGroup>
<None Include="myriad.toml" />
<Compile Include="RecordFile.fs" />
<Compile Include="GeneratedRecord.fs"> <!--1-->
<MyriadFile>RecordFile.fs</MyriadFile> <!--2-->
</Compile>
<Compile Include="JsonRecord.fs" />
<Compile Include="GeneratedJson.fs"> <!--1-->
<MyriadFile>JsonRecord.fs</MyriadFile> <!--2-->
</Compile>
<None Include="..\runmyriad.sh">
<Link>runmyriad.sh</Link>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MyriadPlugin\MyriadPlugin.fsproj" />
<PackageReference Include="Myriad.Sdk" Version="0.8.3" />
<PackageReference Include="Myriad.Core" Version="0.8.3" />
</ItemGroup>
</Project>

View File

@@ -22,7 +22,7 @@ module RecordType =
/// Remove the optional members of the input.
let shorten (input : RecordType) : Short =
{
A = input.A |> Option.defaultWith RecordType.DefaultA
A = input.A |> Option.defaultValue (RecordType.DefaultA ())
B = input.B
C = input.C
}

View File

@@ -0,0 +1,43 @@
//------------------------------------------------------------------------------
// This code was generated by myriad.
// Changes to this file will be lost when the code is regenerated.
//------------------------------------------------------------------------------
namespace ConsumePlugin
/// 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.[(Literals.something)].AsValue().GetValue<string> ()
{
Thing = Thing
}
namespace ConsumePlugin
/// Module containing JSON parsing methods for the JsonRecordType type
[<RequireQualifiedAccess>]
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module JsonRecordType =
/// 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 B2 = node.["another-thing"].AsValue ()
let B = B2.GetValue<string> ()
let A = node.["a"].AsValue().GetValue<int> ()
{
A = A
B = B
C = C
D = D
}

View File

@@ -0,0 +1,28 @@
//------------------------------------------------------------------------------
// This code was generated by myriad.
// Changes to this file will be lost when the code is regenerated.
//------------------------------------------------------------------------------
namespace ConsumePlugin
/// Module containing an option-truncated version of the RecordType type
[<RequireQualifiedAccess>]
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module RecordType =
/// My whatnot
type Short =
{
/// A thing!
A : int
/// Another thing!
B : string
/// Yet another thing!
C : float list
}
/// Remove the optional members of the input.
let shorten (input : RecordType) : Short =
{
A = input.A |> Option.defaultWith RecordType.DefaultA
B = input.B
C = input.C
}

View File

@@ -0,0 +1,28 @@
namespace ConsumePlugin
open System.Text.Json.Serialization
module Literals =
[<Literal>]
let something = "something"
[<MyriadPlugin.JsonParse>]
type InnerType =
{
[<JsonPropertyName(Literals.something)>]
Thing : string
}
/// My whatnot
[<MyriadPlugin.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
}

View File

@@ -1,6 +1,4 @@
namespace UsePlugin
open System
namespace ConsumePlugin
type ParseState =
| AwaitingKey

View File

@@ -1,6 +1,6 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "UsePlugin", "UsePlugin\UsePlugin.fsproj", "{0D174482-9CB2-448A-8BA8-846FAEC65579}"
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "ConsumePlugin", "ConsumePlugin\ConsumePlugin.fsproj", "{0D174482-9CB2-448A-8BA8-846FAEC65579}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "MyriadPlugin", "MyriadPlugin\MyriadPlugin.fsproj", "{DB86C53B-4090-4791-884B-024C5759855F}"
EndProject

View File

@@ -9,12 +9,16 @@
<ItemGroup>
<Compile Include="TestSurface.fs" />
<Compile Include="TestRemoveOptions.fs" />
<Compile Include="TestJsonParse.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ApiSurface" Version="4.0.8" />
<PackageReference Include="ApiSurface" Version="4.0.25" />
<PackageReference Include="FsCheck" Version="2.16.6" />
<PackageReference Include="FsUnit" Version="5.6.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2" />
<PackageReference Include="NUnit.Analyzers" Version="3.6.1" />
<PackageReference Include="coverlet.collector" Version="3.2.0" />
@@ -22,6 +26,7 @@
<ItemGroup>
<ProjectReference Include="..\MyriadPlugin\MyriadPlugin.fsproj" />
<ProjectReference Include="..\ConsumePlugin\ConsumePlugin.fsproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,31 @@
namespace MyriadPlugin.Test
open System.Text.Json.Nodes
open ConsumePlugin
open NUnit.Framework
open FsUnitTyped
[<TestFixture>]
module TestJsonParse =
[<Test>]
let ``Single example`` () =
let s =
"""
{
"a": 3, "another-thing": "hello", "hi": [6, 1], "d": {"something": "oh hi"}
}
"""
let expected =
{
A = 3
B = "hello"
C = [ 6 ; 1 ]
D =
{
Thing = "oh hi"
}
}
let actual = s |> JsonNode.Parse |> JsonRecordType.jsonParse
actual |> shouldEqual expected

View File

@@ -0,0 +1,24 @@
namespace MyriadPlugin.Test
open FsCheck
open ConsumePlugin
open NUnit.Framework
open FsUnitTyped
module TestRemoveOptions =
let shortenProperty (f : RecordType) =
let g = RecordType.shorten f
g.B |> shouldEqual f.B
g.C |> shouldEqual f.C
match f.A with
| None -> g.A |> shouldEqual (RecordType.DefaultA ())
| Some a -> g.A |> shouldEqual a
true
[<Test>]
let ``shorten works`` () =
Check.QuickThrowOnFailure shortenProperty

86
MyriadPlugin/AstHelper.fs Normal file
View File

@@ -0,0 +1,86 @@
namespace MyriadPlugin
open Fantomas.FCS.Syntax
open Fantomas.FCS.SyntaxTrivia
open Fantomas.FCS.Text.Range
open Fantomas.FCS.Xml
open Myriad.Core.AstExtensions
[<RequireQualifiedAccess>]
module internal AstHelper =
let constructRecord (fields : (RecordFieldName * SynExpr option) list) : SynExpr =
let fields =
fields
|> List.map (fun (rfn, synExpr) -> SynExprRecordField (rfn, Some range0, synExpr, None))
SynExpr.Record (None, None, fields, range0)
let private createRecordType
(
name : Ident,
repr : SynTypeDefnRepr,
members : SynMemberDefns,
xmldoc : PreXmlDoc
)
: SynTypeDefn
=
let name = SynComponentInfo.Create ([ name ], xmldoc = xmldoc)
let trivia : SynTypeDefnTrivia =
{
LeadingKeyword = SynTypeDefnLeadingKeyword.Type range0
EqualsRange = Some range0
WithKeyword = Some range0
}
SynTypeDefn (name, repr, members, None, range0, trivia)
let defineRecordType
(
name : Ident,
fields : SynField seq,
members : SynMemberDefns option,
xmldoc : PreXmlDoc option
)
: SynTypeDefn
=
let repr =
SynTypeDefnRepr.Simple (SynTypeDefnSimpleRepr.Record (None, Seq.toList fields, range0), range0)
createRecordType (name, repr, defaultArg members SynMemberDefns.Empty, defaultArg xmldoc PreXmlDoc.Empty)
let isOptionIdent (ident : SynLongIdent) : bool =
match ident.LongIdent with
| [ i ] when System.String.Equals (i.idText, "option", System.StringComparison.OrdinalIgnoreCase) -> true
// TODO: consider Microsoft.FSharp.Option or whatever it is
| _ -> false
let isListIdent (ident : SynLongIdent) : bool =
match ident.LongIdent with
| [ i ] when System.String.Equals (i.idText, "list", System.StringComparison.OrdinalIgnoreCase) -> true
// TODO: consider FSharpList or whatever it is
| _ -> false
[<AutoOpen>]
module internal SynTypePatterns =
let (|OptionType|_|) (fieldType : SynType) =
match fieldType with
| SynType.App (SynType.LongIdent ident, _, [ innerType ], _, _, _, _) when AstHelper.isOptionIdent ident ->
Some innerType
| _ -> None
let (|ListType|_|) (fieldType : SynType) =
match fieldType with
| SynType.App (SynType.LongIdent ident, _, [ innerType ], _, _, _, _) when AstHelper.isListIdent ident ->
Some innerType
| _ -> None
/// Returns the string name of the type.
let (|PrimitiveType|_|) (fieldType : SynType) =
match fieldType with
| SynType.LongIdent ident ->
match ident.LongIdent with
| [ i ] -> [ "string" ; "float" ; "int" ] |> List.tryFind (fun s -> s = i.idText)
| _ -> None
| _ -> None

View File

@@ -0,0 +1,385 @@
namespace MyriadPlugin
open System
open System.Text
open Fantomas.FCS.Syntax
open Fantomas.FCS.SyntaxTrivia
open Fantomas.FCS.Xml
open Myriad.Core
/// Attribute indicating a record type to which the "Add JSON parse" Myriad
/// generator should apply during build.
type JsonParseAttribute () =
inherit Attribute ()
[<RequireQualifiedAccess>]
module internal JsonParseGenerator =
open Fantomas.FCS.Text.Range
open Myriad.Core.Ast
let createParseLineValue (propertyName : SynExpr) (typeName : string) : SynExpr =
// node.["town"].AsValue().GetValue<string> ()
let indexed =
SynExpr.CreateApp (
SynExpr.DotGet (
SynExpr.DotIndexedGet (SynExpr.Ident (Ident.Create "node"), propertyName, range0, range0),
range0,
SynLongIdent.SynLongIdent (id = [ Ident.Create "AsValue" ], dotRanges = [], trivia = [ None ]),
range0
),
SynExpr.CreateConst (SynConst.Unit)
)
SynExpr.CreateApp (
SynExpr.TypeApp (
SynExpr.DotGet (
indexed,
range0,
SynLongIdent.SynLongIdent (id = [ Ident.Create "GetValue" ], dotRanges = [], trivia = [ None ]),
range0
),
range0,
[
SynType.LongIdent (
SynLongIdent.SynLongIdent (id = [ Ident.Create typeName ], dotRanges = [], trivia = [ None ])
)
],
[],
Some range0,
range0,
range0
),
SynExpr.CreateConst SynConst.Unit
)
let createParseLineCallThrough (propertyName : SynExpr) (fieldType : SynType) : SynExpr =
// Type.jsonParse node.["town"]
let typeName =
match fieldType with
| SynType.LongIdent ident -> ident.LongIdent
| _ -> failwith $"Unrecognised type: %+A{fieldType}"
SynExpr.CreateApp (
SynExpr.CreateLongIdent (SynLongIdent.CreateFromLongIdent (typeName @ [ Ident.Create "jsonParse" ])),
SynExpr.DotIndexedGet (SynExpr.CreateIdentString "node", propertyName, range0, range0)
)
/// collectionType is e.g. "List"; we'll be calling `ofSeq` on it.
let createParseLineList (collectionType : string) (propertyName : SynExpr) (elementType : string) : SynExpr =
// node.["openingHours"].AsArray()
// |> Seq.map (fun elt -> elt.AsValue().GetValue<string> ())
// |> List.ofSeq
let parsedDataPat = [ SynPat.CreateNamed (Ident.Create "elt") ]
let parsedData =
SynExpr.CreateApp (
SynExpr.TypeApp (
SynExpr.DotGet (
SynExpr.CreateLongIdent (SynLongIdent.CreateString "elt"),
range0,
SynLongIdent.Create [ "GetValue" ],
range0
),
range0,
[ SynType.CreateLongIdent elementType ],
[],
Some range0,
range0,
range0
),
SynExpr.CreateConst SynConst.Unit
)
SynExpr.CreateApp (
SynExpr.CreateAppInfix (
SynExpr.LongIdent (
false,
SynLongIdent.SynLongIdent (
[ Ident.Create "op_PipeRight" ],
[],
[ Some (IdentTrivia.OriginalNotation "|>") ]
),
None,
range0
),
SynExpr.CreateApp (
SynExpr.CreateAppInfix (
SynExpr.LongIdent (
false,
SynLongIdent.SynLongIdent (
[ Ident.Create "op_PipeRight" ],
[],
[ Some (IdentTrivia.OriginalNotation "|>") ]
),
None,
range0
),
SynExpr.CreateApp (
SynExpr.DotGet (
SynExpr.DotIndexedGet (
SynExpr.CreateIdent (Ident.Create "node"),
propertyName,
range0,
range0
),
range0,
SynLongIdent.CreateString "AsArray",
range0
),
SynExpr.CreateConst SynConst.Unit
)
),
SynExpr.CreateApp (
SynExpr.CreateLongIdent (SynLongIdent.Create [ "Seq" ; "map" ]),
SynExpr.CreateParen (
SynExpr.Lambda (
false,
false,
SynSimplePats.Create [ SynSimplePat.CreateId (Ident.Create "elt") ],
SynExpr.CreateApp (
SynExpr.TypeApp (
SynExpr.DotGet (
SynExpr.CreateApp (
SynExpr.CreateLongIdent (SynLongIdent.CreateString "elt"),
SynExpr.CreateConst SynConst.Unit
),
range0,
SynLongIdent.CreateString "GetValue",
range0
),
range0,
[ SynType.CreateLongIdent (SynLongIdent.CreateString elementType) ],
[],
None,
range0,
range0
),
SynExpr.CreateConst SynConst.Unit
),
Some (parsedDataPat, parsedData),
range0,
{
ArrowRange = Some range0
}
)
)
)
)
),
SynExpr.CreateLongIdent (SynLongIdent.Create [ collectionType ; "ofSeq" ])
)
/// propertyName is probably a string literal, but it could be a [<Literal>] variable
let createParseRhs (varName : string) (propertyName : SynExpr) (fieldType : SynType) : SynExpr =
match fieldType with
| OptionType ty -> failwith "TODO: options"
| PrimitiveType typeName -> createParseLineValue propertyName typeName
| ListType (PrimitiveType typeName) -> createParseLineList "List" propertyName typeName
// TODO: support recursive lists
| _ ->
// Let's just hope that we've also got our own type annotation!
createParseLineCallThrough propertyName fieldType
let createMaker (typeName : LongIdent) (fields : SynField list) =
let xmlDoc = PreXmlDoc.Create " Parse from a JSON node."
let returnInfo =
SynBindingReturnInfo.Create (SynType.LongIdent (SynLongIdent.CreateFromLongIdent typeName))
let inputArg = Ident.Create "node"
let functionName = Ident.Create "jsonParse"
let inputVal =
SynValData.SynValData (
None,
SynValInfo.SynValInfo ([ [ SynArgInfo.CreateId functionName ] ], SynArgInfo.Empty),
Some inputArg
)
let assignments =
fields
|> List.map (fun (SynField (attrs, _, id, fieldType, _, _, _, _, _)) ->
let id =
match id with
| None -> failwith "didn't get an ID on field"
| Some id -> id
let propertyNameAttr =
attrs
|> List.collect (fun l -> l.Attributes)
|> List.tryFind (fun attr ->
attr.TypeName.AsString.EndsWith ("JsonPropertyName", StringComparison.Ordinal)
)
let propertyName =
match propertyNameAttr with
| None ->
let sb = StringBuilder id.idText.Length
sb.Append (Char.ToLowerInvariant id.idText.[0]) |> ignore
if id.idText.Length > 1 then
sb.Append id.idText.[1..] |> ignore
sb.ToString () |> SynConst.CreateString |> SynExpr.CreateConst
| Some name -> name.ArgExpr
let pattern =
SynPat.LongIdent (
SynLongIdent.CreateFromLongIdent [ id ],
None,
None,
SynArgPats.Empty,
None,
range0
)
SynBinding.Let (
isInline = false,
isMutable = false,
expr = createParseRhs (id.ToString ()) propertyName fieldType,
valData = inputVal,
pattern = pattern
)
)
let finalConstruction =
fields
|> List.map (fun (SynField (_, _, id, _, _, _, _, _, _)) ->
let id =
match id with
| None -> failwith "Expected record field to have an identifying name"
| Some id -> id
(SynLongIdent.CreateFromLongIdent [ id ], true),
Some (SynExpr.CreateLongIdent (SynLongIdent.CreateFromLongIdent [ id ]))
)
|> AstHelper.constructRecord
let assignments =
(finalConstruction, assignments)
||> List.fold (fun final assignment ->
SynExpr.LetOrUse (
false,
false,
[ assignment ],
final,
range0,
{
InKeyword = None
}
)
)
let pattern =
SynPat.LongIdent (
SynLongIdent.CreateFromLongIdent [ functionName ],
None,
None,
SynArgPats.Pats
[
SynPat.CreateTyped (
SynPat.CreateNamed inputArg,
SynType.LongIdent (
SynLongIdent.Create [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonNode" ]
)
)
|> SynPat.CreateParen
],
None,
range0
)
let binding =
SynBinding.Let (
isInline = false,
isMutable = false,
xmldoc = xmlDoc,
returnInfo = returnInfo,
expr = assignments,
valData = inputVal,
pattern = pattern
)
SynModuleDecl.CreateLet [ binding ]
let createRecordModule (namespaceId : LongIdent) (typeDefn : SynTypeDefn) =
let (SynTypeDefn (synComponentInfo, synTypeDefnRepr, _members, _implicitCtor, _, _)) =
typeDefn
let (SynComponentInfo (_attributes, _typeParams, _constraints, recordId, _, _preferPostfix, _access, _)) =
synComponentInfo
match synTypeDefnRepr with
| SynTypeDefnRepr.Simple (SynTypeDefnSimpleRepr.Record (_accessibility, recordFields, _recordRange), _) ->
let decls = [ createMaker recordId recordFields ]
let compilationRepresentation : SynAttribute =
{
TypeName = SynLongIdent.CreateString "CompilationRepresentation"
ArgExpr =
SynExpr.CreateLongIdent (
false,
SynLongIdent.Create [ "CompilationRepresentationFlags" ; "ModuleSuffix" ],
None
)
|> SynExpr.CreateParen
Target = None
AppliesToGetterAndSetter = false
Range = range0
}
let attributes =
[
SynAttributeList.Create (SynAttribute.RequireQualifiedAccess ())
SynAttributeList.Create compilationRepresentation
]
let xmlDoc =
recordId
|> Seq.map (fun i -> i.idText)
|> String.concat "."
|> sprintf " Module containing JSON parsing methods for the %s type"
|> PreXmlDoc.Create
let info =
SynComponentInfo.Create (recordId, attributes = attributes, xmldoc = xmlDoc)
let mdl = SynModuleDecl.CreateNestedModule (info, decls)
SynModuleOrNamespace.CreateNamespace (namespaceId, decls = [ mdl ])
| _ -> failwithf "Not a record type"
/// Myriad generator that provides a JSON parse function for a record type.
[<MyriadGenerator("json-parse")>]
type JsonParseGenerator () =
interface IMyriadGenerator with
member _.ValidInputExtensions = [ ".fs" ]
member _.Generate (context : GeneratorContext) =
let ast, _ =
Ast.fromFilename context.InputFilename |> Async.RunSynchronously |> Array.head
let records = Ast.extractRecords ast
let namespaceAndRecords =
records
|> List.choose (fun (ns, types) ->
match types |> List.filter Ast.hasAttribute<JsonParseAttribute> with
| [] -> None
| types -> Some (ns, types)
)
let modules =
namespaceAndRecords
|> List.collect (fun (ns, records) ->
records
|> List.map (fun record ->
let recordModule = JsonParseGenerator.createRecordModule ns record
recordModule
)
)
Output.Ast modules

View File

@@ -1,17 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Authors>Patrick Stevens</Authors>
<Copyright>Copyright (c) Patrick Stevens 2023</Copyright>
<Description>Provides some Myriad compile-time code generation plugins.</Description>
<RepositoryType>git</RepositoryType>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageTags>myriad;fsharp;source-generator;source-gen;json</PackageTags>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarnOn>FS3559</WarnOn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Myriad.Core" Version="0.8.3" />
<PackageReference Update="FSharp.Core" Version="6.0.6" />
<!-- the lowest version allowed by Myriad.Core -->
<PackageReference Update="FSharp.Core" Version="6.0.1" />
</ItemGroup>
<ItemGroup>
<Compile Include="AstHelper.fs" />
<Compile Include="RemoveOptionsGenerator.fs" />
<Compile Include="JsonParseGenerator.fs" />
<None Include="version.json" />
<EmbeddedResource Include="SurfaceBaseline.txt" />
</ItemGroup>

View File

@@ -11,47 +11,11 @@ open Myriad.Core
type RemoveOptionsAttribute () =
inherit Attribute ()
module internal Create =
[<RequireQualifiedAccess>]
module internal RemoveOptionsGenerator =
open Fantomas.FCS.Text.Range
open Myriad.Core.Ast
let createRecordMyriad fields =
// TODO: this first equals-None requires a range
let fields =
fields
|> List.map (fun (rfn, synExpr) -> SynExprRecordField (rfn, Some range0, synExpr, None))
SynExpr.Record (None, None, fields, range0)
let createFromRepr (name : Ident, repr : SynTypeDefnRepr, members : SynMemberDefns, xmldoc : PreXmlDoc) =
let name = SynComponentInfo.Create ([ name ], xmldoc = xmldoc)
let trivia : SynTypeDefnTrivia =
{
LeadingKeyword = SynTypeDefnLeadingKeyword.Type range0
EqualsRange = Some range0
WithKeyword = Some range0
}
SynTypeDefn (name, repr, members, None, range0, trivia)
let createRecord (name : Ident, fields : SynField seq, members : SynMemberDefns option, xmldoc : PreXmlDoc option) =
let repr =
SynTypeDefnRepr.Simple (SynTypeDefnSimpleRepr.Record (None, Seq.toList fields, range0), range0)
createFromRepr (name, repr, defaultArg members SynMemberDefns.Empty, defaultArg xmldoc PreXmlDoc.Empty)
let isOptionIdent (ident : SynLongIdent) : bool =
match ident.LongIdent with
| [ i ] when String.Equals (i.idText, "option", StringComparison.OrdinalIgnoreCase) -> true
// TODO: consider Microsoft.FSharp.Option or whatever it is
| _ -> false
let (|OptionType|_|) (fieldType : SynType) =
match fieldType with
| SynType.App (SynType.LongIdent ident, _, [ innerType ], _, _, _, _) when isOptionIdent ident -> Some innerType
| _ -> None
let private removeOption (s : SynField) : SynField =
let (SynField.SynField (synAttributeLists,
isStatic,
@@ -88,8 +52,8 @@ module internal Create =
let typeDecl : SynTypeDefn =
match xmlDoc with
| None -> createRecord (name, fields, None, None)
| Some xmlDoc -> createRecord (name, fields, None, Some xmlDoc)
| None -> AstHelper.defineRecordType (name, fields, None, None)
| Some xmlDoc -> AstHelper.defineRecordType (name, fields, None, Some xmlDoc)
SynModuleDecl.Types ([ typeDecl ], range0)
@@ -150,7 +114,7 @@ module internal Create =
(SynLongIdent.CreateFromLongIdent [ id ], true), Some body
)
|> createRecordMyriad
|> AstHelper.constructRecord
let pattern =
SynPat.LongIdent (
@@ -261,7 +225,7 @@ type RemoveOptionsGenerator () =
|> List.collect (fun (ns, records) ->
records
|> List.map (fun record ->
let recordModule = Create.createRecordModule ns record
let recordModule = RemoveOptionsGenerator.createRecordModule ns record
recordModule
)
)

View File

@@ -1,3 +1,7 @@
MyriadPlugin.JsonParseAttribute inherit System.Attribute
MyriadPlugin.JsonParseAttribute..ctor [constructor]: unit
MyriadPlugin.JsonParseGenerator inherit obj, implements Myriad.Core.IMyriadGenerator
MyriadPlugin.JsonParseGenerator..ctor [constructor]: unit
MyriadPlugin.RemoveOptionsAttribute inherit System.Attribute
MyriadPlugin.RemoveOptionsAttribute..ctor [constructor]: unit
MyriadPlugin.RemoveOptionsGenerator inherit obj, implements Myriad.Core.IMyriadGenerator

View File

@@ -1,7 +1,7 @@
{
"version": "0.1",
"publicReleaseRefSpec": [
"^refs/heads/main$"
],
"pathFilters": null
}
{
"version": "0.2",
"publicReleaseRefSpec": [
"^refs/heads/main$"
],
"pathFilters": null
}

View File

@@ -29,3 +29,60 @@ module Foo =
```
(This is a proof of concept. It would be better to somehow disambiguate the module name.)
## `JsonParse`
Takes records like this:
```fsharp
[<MyriadPlugin.JsonParse>]
type InnerType =
{
[<JsonPropertyName "something">]
Thing : string
}
/// My whatnot
[<MyriadPlugin.JsonParse>]
type JsonRecordType =
{
/// A thing!
A : int
/// Another thing!
B : string
[<System.Text.Json.Serialization.JsonPropertyName "hi">]
C : int list
D : InnerType
}
```
and stamps out parsing methods like this:
```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
[<RequireQualifiedAccess>]
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module JsonRecordType =
/// 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 }
```

View File

@@ -1,26 +0,0 @@
namespace UsePlugin
module Program =
let f : RecordType =
{
A = Some 300
B = "hello"
C = [ 0.3 ]
}
let g = RecordType.shorten f
[<EntryPoint>]
let main _ =
if not (f.B = g.B && f.C = g.C) then
failwith "Non-optional fields differed"
match f.A with
| None ->
if g.A <> RecordType.DefaultA () then
failwith "Couldn't acquire default"
| Some a ->
if a <> g.A then
failwith "Didn't match existing f.A"
0

View File

@@ -48,7 +48,7 @@
src = ./nix/fetchDeps.sh;
pname = pname;
binPath = pkgs.lib.makeBinPath [pkgs.coreutils dotnet-sdk (pkgs.nuget-to-nix.override {inherit dotnet-sdk;})];
projectFiles = toString ["./MyriadPlugin/MyriadPlugin.fsproj" "./UsePlugin/UsePlugin.fsproj"];
projectFiles = toString ["./MyriadPlugin/MyriadPlugin.fsproj" "./ConsumePlugin/ConsumePlugin.fsproj"];
testProjectFiles = ["./MyriadPlugin.Test/MyriadPlugin.Test.fsproj"];
rids = pkgs.lib.concatStringsSep "\" \"" runtimeIds;
packages = dotnet-sdk.packages;

View File

@@ -8,8 +8,8 @@
})
(fetchNuGet {
pname = "ApiSurface";
version = "4.0.8";
sha256 = "0xf3kp9lzi1bgm3c1h4lclvf1nvbn3cy5zfmys3i58h9c71yfsak";
version = "4.0.25";
sha256 = "0zjq8an9cr0l7wxdmm9n9s3iyq5m0zl4x0h0wmy5cz7am8y15qc4";
})
(fetchNuGet {
pname = "coverlet.collector";
@@ -26,41 +26,81 @@
version = "6.1.1";
sha256 = "0733dm5zjdp8w5wwalqlv1q52pghfr04863i9wy807f4qfd7rrin";
})
(fetchNuGet {
pname = "FsCheck";
version = "2.16.6";
sha256 = "176rwky6b5rk8dzldiz4068p7m9c5y9ygzbhadrs14jkl94pc56n";
})
(fetchNuGet {
pname = "FSharp.Core";
version = "6.0.6";
sha256 = "1mb1rwzs48c124pqxymnjgv6g3r6zb8n1v953hflcf20nq6yxi77";
version = "6.0.1";
sha256 = "0qks2aadkhsffg9a6xq954ll9xacnph852avd7ljh9n2g6vj06qj";
})
(fetchNuGet {
pname = "FSharp.Core";
version = "8.0.100";
sha256 = "06z3vg8yj7i83x6gmnzl2lka1bp4hzc07h6mrydpilxswnmy2a0l";
})
(fetchNuGet {
pname = "FsUnit";
version = "5.6.1";
sha256 = "1zffn9dm2c44v8qjzwfg6y3psydiv2bn3n305rf7mc57cmm4ygv3";
})
(fetchNuGet {
pname = "Microsoft.AspNetCore.App.Ref";
version = "6.0.25";
sha256 = "1vrmqn5j6ibwkqasbf7x7n4w5jdclnz3giymiwvym2wa0y5zc59q";
})
(fetchNuGet {
pname = "Microsoft.AspNetCore.App.Ref";
version = "8.0.0";
sha256 = "0k304yhpm92c46a1fscbzlgvdbhrm9vlbpyfgwp3cafz4f7z7a5y";
})
(fetchNuGet {
pname = "Microsoft.AspNetCore.App.Runtime.linux-arm64";
version = "6.0.25";
sha256 = "0mgcs4si7mwd0f555s1vg17pf4nqfaijd1pci359l1pgrmv70rrg";
})
(fetchNuGet {
pname = "Microsoft.AspNetCore.App.Runtime.linux-arm64";
version = "8.0.0";
sha256 = "05y1xb5fw8lzvb4si77a5qwfwfz1855crqbphrwky6x9llivbhkx";
})
(fetchNuGet {
pname = "Microsoft.AspNetCore.App.Runtime.linux-x64";
version = "6.0.25";
sha256 = "0wvzhqhlmlbnpa18qp8m3wcrlcgj3ckvp3iv2n7g8vb60c3238aq";
})
(fetchNuGet {
pname = "Microsoft.AspNetCore.App.Runtime.linux-x64";
version = "8.0.0";
sha256 = "18zdbcb2bn7wy1dp14z5jyqiiwr9rkad1lcb158r5ikjfq1rg5iw";
})
(fetchNuGet {
pname = "Microsoft.AspNetCore.App.Runtime.osx-arm64";
version = "6.0.25";
sha256 = "1pywgvb8ck1d5aadmijd5s3z6yclchd9pa6dsahijmm55ibplx36";
})
(fetchNuGet {
pname = "Microsoft.AspNetCore.App.Runtime.osx-arm64";
version = "8.0.0";
sha256 = "1nbxzmj6cnccylxis67c54c0ik38ma4rwdvgg6sxd6r04219maqm";
})
(fetchNuGet {
pname = "Microsoft.AspNetCore.App.Runtime.osx-x64";
version = "6.0.25";
sha256 = "1zlf0w7i6r02719dv3nw4jy14sa0rs53i89an5alz5qmywdy3f1d";
})
(fetchNuGet {
pname = "Microsoft.AspNetCore.App.Runtime.osx-x64";
version = "8.0.0";
sha256 = "1wqkbjd1ywv9w397l7rsb89mijc5n0hv7jq9h09xfz6wn9qsp152";
})
(fetchNuGet {
pname = "Microsoft.AspNetCore.App.Runtime.win-x64";
version = "6.0.25";
sha256 = "1fbsnm4056cpd4avgpi5sq05m1yd9k4x229ckxpr4q7yc94sncwy";
})
(fetchNuGet {
pname = "Microsoft.AspNetCore.App.Runtime.win-x64";
version = "8.0.0";
@@ -81,56 +121,111 @@
version = "17.5.0";
sha256 = "00gz2i8kx4mlq1ywj3imvf7wc6qzh0bsnynhw06z0mgyha1a21jy";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Host.linux-arm64";
version = "6.0.25";
sha256 = "052388yjivzkfllkss0nljbzmjx787jqdjsbb6ls855sp6wh9xfd";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Host.linux-arm64";
version = "8.0.0";
sha256 = "0bpg3v9dnalz7yh7lsgriw9rnm9jx37mqhhvf7snznb3sfk7rgwb";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Host.linux-x64";
version = "6.0.25";
sha256 = "103xy6kncjwbbchfnpqvsjpjy92x3dralcg9pw939jp0dwggwarz";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Host.linux-x64";
version = "8.0.0";
sha256 = "1c7l68bm05d94x5wk1y33mnd4v8m196vyprgrzqnh94yrqy6fkf7";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Host.osx-arm64";
version = "6.0.25";
sha256 = "13m14pdx5xfxky07xgxf6hjd7g9l4k6k40wvp9znhvn27pa0wdxv";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Host.osx-arm64";
version = "8.0.0";
sha256 = "1hdv825s964vfcgnk94pzhgxnj948f1vdj423jjxpkppcy30fl0m";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Host.osx-x64";
version = "6.0.25";
sha256 = "132pgjhv42mqzx4007sd59bkds0fwsv5xaz07y2yffbn3lzr228k";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Host.osx-x64";
version = "8.0.0";
sha256 = "0jmzf58vv45j0hqlxq8yalpjwi328vp2mjr3h0pdg0qr143iivnr";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Host.win-x64";
version = "6.0.25";
sha256 = "039433rm4w37h9qri11v3lrpddpz7zcly9kq8vmk6w1ixzlqwf01";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Host.win-x64";
version = "8.0.0";
sha256 = "1n8yr13df2f6jhxpfazs6rxahfqm18fhjvfm16g5d60c3za1hwnk";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Ref";
version = "6.0.25";
sha256 = "0jfhmfxpx1h4f3axgf60gc8d4cnlvbb853400kag6nk0875hr0x1";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Ref";
version = "8.0.0";
sha256 = "0hyvbh86433764qqqhw9i7ga0ax7bbdmzh77jw58pq0ggm41cff9";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Runtime.linux-arm64";
version = "6.0.25";
sha256 = "0jpcmva1l8z36r4phz055l7fz9s6z8pv8pqc4ia69mhhgvr0ks7y";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Runtime.linux-arm64";
version = "8.0.0";
sha256 = "0gwqmkmr7jy3sjh9gha82amlry41gp8nwswy2iqfw54f28db63n7";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Runtime.linux-x64";
version = "6.0.25";
sha256 = "012jml0bqxbspahf1j4bvvd91pz85hsbcyhq00gxczcazhxpkhz4";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Runtime.linux-x64";
version = "8.0.0";
sha256 = "042cjvnwrrjs3mw5q8q5kinh0cwkks33i3n1vyifaid2jbr3wlc0";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Runtime.osx-arm64";
version = "6.0.25";
sha256 = "0wgwxpyy1n550sw7npjg69zpxknwn0ay30m2qybvqb5mj857qzxi";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Runtime.osx-arm64";
version = "8.0.0";
sha256 = "06ndp4wh1cap01dql3nixka4g56bf6ipmqys7xaxvg4xisf79x8d";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Runtime.osx-x64";
version = "6.0.25";
sha256 = "08vr7c5bg5x3w35l54z1azif7ysfc2yiyz50ip1dl0mpqywvlswr";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Runtime.osx-x64";
version = "8.0.0";
sha256 = "1kh5bnaf6h9mr4swcalrp304625frjiw6mlz1052rxwzsdq98a96";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Runtime.win-x64";
version = "6.0.25";
sha256 = "03snpmx204xvc9668riisvvdjjgdqhwj7yjp85w5lh8j8ygrqkif";
})
(fetchNuGet {
pname = "Microsoft.NETCore.App.Runtime.win-x64";
version = "8.0.0";
@@ -201,15 +296,20 @@
version = "13.0.1";
sha256 = "0fijg0w6iwap8gvzyjnndds0q4b8anwxxvik7y8vgq97dram4srb";
})
(fetchNuGet {
pname = "Newtonsoft.Json";
version = "13.0.3";
sha256 = "0xrwysmrn4midrjal8g2hr1bbg38iyisl0svamb11arqws4w2bw7";
})
(fetchNuGet {
pname = "NuGet.Common";
version = "6.5.0";
sha256 = "1k24azm8hvsm74fmy67i1a3jkzpnsa9hp6dzbam8cdz8ayy6zqc8";
version = "6.8.0";
sha256 = "0l3ij8iwy7wj6s7f93lzi9168r4wz8zyin6a08iwgk7hvq44cia1";
})
(fetchNuGet {
pname = "NuGet.Configuration";
version = "6.5.0";
sha256 = "1yakqg4x5j69p5zi5wz77ybgyrblrnszj3z1ddsi1ahkxpffx2w4";
version = "6.8.0";
sha256 = "0x03p408smkmv1gv7pmvsia4lkn0xaj4wfrkl58pjf8bbv51y0yw";
})
(fetchNuGet {
pname = "NuGet.Frameworks";
@@ -218,28 +318,28 @@
})
(fetchNuGet {
pname = "NuGet.Frameworks";
version = "6.5.0";
sha256 = "0s37d1p4md0k6d4cy6sq36f2dgkd9qfbzapxhkvi8awwh0vrynhj";
version = "6.8.0";
sha256 = "0i2xvhgkjkjr496i3pg8hamwv6505fia45qhn7jg5m01wb3cvsjl";
})
(fetchNuGet {
pname = "NuGet.Packaging";
version = "6.5.0";
sha256 = "0rkczrmw7rss91nam10rf771r31k3fwc271nvh0wn35axv3ibssl";
version = "6.8.0";
sha256 = "031z4s905bxi94h3f0qy4j1b6jxdxgqgpkzqvvpfxch07szxcbim";
})
(fetchNuGet {
pname = "NuGet.Protocol";
version = "6.5.0";
sha256 = "16df7p835aqach4qhnp9dwa335hgfjmmj520fy0h8in1zrnlfazh";
version = "6.7.0";
sha256 = "1v5ibnq2mp801vw68zyj169hkj3xm7h55824i33n1jxxj2vs3vbk";
})
(fetchNuGet {
pname = "NuGet.Versioning";
version = "6.5.0";
sha256 = "095al1ys6379gl52b6fvcn7pplc8gvbphsjz28124kcbx1a5g6vs";
version = "6.8.0";
sha256 = "1sd25h46fd12ng780r02q4ijcx1imkb53kj1y2y7cwg5myh537ks";
})
(fetchNuGet {
pname = "NUnit";
version = "3.13.3";
sha256 = "0wdzfkygqnr73s6lpxg5b1pwaqz9f414fxpvpdmf72bvh4jaqzv6";
version = "3.14.0";
sha256 = "19p8911lrfds1k9rv47jk1bbn665s0pvghkd06gzbg78j6mzzqqa";
})
(fetchNuGet {
pname = "NUnit.Analyzers";
@@ -273,8 +373,8 @@
})
(fetchNuGet {
pname = "System.Formats.Asn1";
version = "5.0.0";
sha256 = "1axc8z0839yvqi2cb63l73l6d9j6wd20lsbdymwddz9hvrsgfwpn";
version = "6.0.0";
sha256 = "1vvr7hs4qzjqb37r0w1mxq7xql2b17la63jwvmgv65s1hj00g8r9";
})
(fetchNuGet {
pname = "System.IO.Abstractions";
@@ -316,15 +416,10 @@
version = "4.5.0";
sha256 = "1wvwanz33fzzbnd2jalar0p0z3x0ba53vzx1kazlskp7pwyhlnq0";
})
(fetchNuGet {
pname = "System.Security.Cryptography.Cng";
version = "5.0.0";
sha256 = "06hkx2za8jifpslkh491dfwzm5dxrsyxzj5lsc0achb6yzg4zqlw";
})
(fetchNuGet {
pname = "System.Security.Cryptography.Pkcs";
version = "5.0.0";
sha256 = "0hb2mndac3xrw3786bsjxjfh19bwnr991qib54k6wsqjhjyyvbwj";
version = "6.0.4";
sha256 = "0hh5h38pnxmlrnvs72f2hzzpz4b2caiiv6xf8y7fzdg84r3imvfr";
})
(fetchNuGet {
pname = "System.Security.Cryptography.ProtectedData";

View File

@@ -2,8 +2,8 @@
dotnet \
"/Users/patrick/.nuget/packages/myriad.sdk/0.8.3/build/../tools/net6.0/any/Myriad.dll" \
--inputfile "/Users/patrick/Documents/GitHub/MyriadPlugin/UsePlugin/RecordFile.fs" \
--outputfile "/Users/patrick/Documents/GitHub/MyriadPlugin/UsePlugin/Generated.fs" \
--configfile "/Users/patrick/Documents/GitHub/MyriadPlugin/UsePlugin/myriad.toml" \
--contextfile "/Users/patrick/Documents/GitHub/MyriadPlugin/UsePlugin/obj/myriad.context.toml" \
--inputfile "/Users/patrick/Documents/GitHub/MyriadPlugin/ConsumePlugin/RecordFile.fs" \
--outputfile "/Users/patrick/Documents/GitHub/MyriadPlugin/ConsumePlugin/Generated.fs" \
--configfile "/Users/patrick/Documents/GitHub/MyriadPlugin/ConsumePlugin/myriad.toml" \
--contextfile "/Users/patrick/Documents/GitHub/MyriadPlugin/ConsumePlugin/obj/myriad.context.toml" \
--plugin "/Users/patrick/Documents/GitHub/MyriadPlugin/MyriadPlugin/bin/Debug/net8.0/MyriadPlugin.dll"