WoofWare.Myriad.Plugins
1.2.3
See the version list below for details.
dotnet add package WoofWare.Myriad.Plugins --version 1.2.3
NuGet\Install-Package WoofWare.Myriad.Plugins -Version 1.2.3
<PackageReference Include="WoofWare.Myriad.Plugins" Version="1.2.3" />
paket add WoofWare.Myriad.Plugins --version 1.2.3
#r "nuget: WoofWare.Myriad.Plugins, 1.2.3"
// Install WoofWare.Myriad.Plugins as a Cake Addin #addin nuget:?package=WoofWare.Myriad.Plugins&version=1.2.3 // Install WoofWare.Myriad.Plugins as a Cake Tool #tool nuget:?package=WoofWare.Myriad.Plugins&version=1.2.3
WoofWare.Myriad.Plugins
Some helpers in Myriad which might be useful.
These are currently somewhat experimental, and I personally am their primary customer.
The RemoveOptions
generator in particular is extremely half-baked.
Currently implemented:
JsonParse
(to stamp outjsonParse : JsonNode -> 'T
methods);RemoveOptions
(to stripoption
modifiers from a type).HttpClient
(to stamp out a RestEase-style HTTP client).GenerateMock
(to stamp out a record type corresponding to an interface).
JsonParse
Takes records like this:
[<WoofWare.Myriad.Plugins.JsonParse>]
type InnerType =
{
[<JsonPropertyName "something">]
Thing : string
}
/// My whatnot
[<WoofWare.Myriad.Plugins.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:
/// 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 }
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#.
This Myriad generator expects you to use System.Text.Json
to construct a JsonNode
,
and then the generator takes over to construct a strongly-typed object.
Limitations
This source generator is enough for what I first wanted to use it for. However, there is far more that could be done.
- Make it possible to give an exact format and cultural info in date and time parsing.
- Make it possible to reject parsing if extra fields are present.
- Generally support all the
System.Text.Json
attributes.
RemoveOptions
Takes a record like this:
type Foo =
{
A : int option
B : string
C : float list
}
and stamps out a record like this:
[<RequireQualifiedAccess>]
module Foo =
type Short =
{
A : int
B : string
C : float list
}
What's the point?
The motivating example is argument parsing. An argument parser naturally wants to express "the user did not supply this, so I will provide a default". But it's not a very ergonomic experience for the programmer to deal with all these options, so this Myriad generator stamps out a type without any options, and also stamps out an appropriate constructor function.
Limitations
This generator is far from where I want it, because I haven't really spent any time on it.
- It really wants to be able to recurse into the types within the record, to strip options from them.
- It needs some sort of attribute to mark a field as not receiving this treatment.
- What do we do about discriminated unions?
HttpClient
Takes a type like this:
[<WoofWare.Myriad.Plugins.HttpClient>]
type IPureGymApi =
[<Get "v1/gyms/">]
abstract GetGyms : ?ct : CancellationToken -> Task<Gym list>
[<Get "v1/gyms/{gym_id}/attendance">]
abstract GetGymAttendance : [<Path "gym_id">] gymId : int * ?ct : CancellationToken -> Task<GymAttendance>
[<Get "v1/member">]
abstract GetMember : ?ct : CancellationToken -> Task<Member>
[<Get "v1/gyms/{gym_id}">]
abstract GetGym : [<Path "gym_id">] gymId : int * ?ct : CancellationToken -> Task<Gym>
[<Get "v1/member/activity">]
abstract GetMemberActivity : ?ct : CancellationToken -> Task<MemberActivityDto>
[<Get "v2/gymSessions/member">]
abstract GetSessions :
[<Query>] fromDate : DateTime * [<Query>] toDate : DateTime * ?ct : CancellationToken -> Task<Sessions>
and stamps out a type like this:
/// Module for constructing a REST client.
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
[<RequireQualifiedAccess>]
module PureGymApi =
/// Create a REST client.
let make (client : System.Net.Http.HttpClient) : IPureGymApi =
{ new IPureGymApi with
member _.GetGyms (ct : CancellationToken option) =
async {
let! ct = Async.CancellationToken
let httpMessage =
new System.Net.Http.HttpRequestMessage (
Method = System.Net.Http.HttpMethod.Get,
RequestUri = System.Uri (client.BaseAddress.ToString () + "v1/gyms/")
)
let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask
let response = response.EnsureSuccessStatusCode ()
let! stream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask
let! node =
System.Text.Json.Nodes.JsonNode.ParseAsync (stream, cancellationToken = ct)
|> Async.AwaitTask
return node.AsArray () |> Seq.map (fun elt -> Gym.jsonParse elt) |> List.ofSeq
}
|> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct))
// (more methods here)
}
What's the point?
The motivating example is again ahead-of-time compilation: we wish to avoid the reflection which RestEase does.
Limitations
RestEase is complex, and handles a lot of different stuff.
- If you set the
BaseAddress
on your inputHttpClient
, make sure to end with a trailing slash on any trailing directories (so"blah/foo/"
rather than"blah/foo"
). We combine URIs usingUriKind.Relative
, so without a trailing slash, the last component may be chopped off. - Parameters are serialised solely with
ToString
, and there's no control over this; nor is there control over encoding in any sense. - Deserialisation follows the same logic as the
JsonParse
generator, and it generally assumes you're using types whichJsonParse
is applied to. - Headers are not yet supported.
- Anonymous parameters are currently forbidden.
There are also some design decisions:
- Every function must take an optional
CancellationToken
(which is good practice anyway); so arguments are forced to be tupled.
GenerateMock
Takes a type like this:
[<GenerateMock>]
type IPublicType =
abstract Mem1 : string * int -> string list
abstract Mem2 : string -> int
and stamps out a type like this:
/// Mock record type for an interface
type internal PublicTypeMock =
{
Mem1 : string * int -> string list
Mem2 : string -> int
}
static member Empty : PublicTypeMock =
{
Mem1 = (fun x -> raise (System.NotImplementedException "Unimplemented mock function"))
Mem2 = (fun x -> raise (System.NotImplementedException "Unimplemented mock function"))
}
interface IPublicType with
member this.Mem1 (arg0, arg1) = this.Mem1 (arg0, arg1)
member this.Mem2 (arg0) = this.Mem2 (arg0)
What's the point?
Reflective mocking libraries like Foq in my experience are a rich source of flaky tests. The Grug-brained developer would prefer to do this without reflection, and this reduces the rate of strange one-in-ten-thousand "failed to generate IL" errors. But since F# does not let you partially update an interface definition, we instead stamp out a record, thereby allowing the programmer to use F#'s record-update syntax.
Limitations
- We currently only support interfaces with tupled arguments.
- We make the resulting record type at most internal (never public), since this is intended only to be used in tests.
You will therefore need an
AssemblyInfo.fs
file like the one in WoofWare.Myriad's own tests.
Detailed examples
See the tests. For example, PureGymDto.fs is a real-world set of DTOs.
How to use
- In your
.fsproj
file, define a helper variable so that subsequent steps don't all have to be kept in sync:<PropertyGroup> <WoofWareMyriadPluginVersion>1.1.5</WoofWareMyriadPluginVersion> </PropertyGroup>
- Take a reference on
WoofWare.Myriad.Plugins
:<ItemGroup> <PackageReference Include="WoofWare.Myriad.Plugins" Version="$(WoofWareMyriadPluginVersion)" /> </ItemGroup>
- Point Myriad to the DLL within the NuGet package which is the source of the plugins:
<ItemGroup> <MyriadSdkGenerator Include="$(NuGetPackageRoot)/woofware.myriad.plugins/$(WoofWareMyriadPluginVersion)/lib/net6.0/WoofWare.Myriad.Plugins.dll" /> </ItemGroup>
Now you are ready to start using the generators.
For example, this specifies that Myriad is to use the contents of Client.fs
to generate the file GeneratedClient.fs
:
<ItemGroup>
<Compile Include="Client.fs" />
<Compile Include="GeneratedClient.fs">
<MyriadFile>Client.fs</MyriadFile>
</Compile>
</ItemGroup>
Myriad Gotchas
- MsBuild doesn't always realise that it needs to invoke Myriad during rebuild.
You can always save a whitespace change to the source file (e.g.
Client.fs
above), and MsBuild will then execute Myriad during the next build. - Fantomas, the F# source formatter which powers Myriad, is customisable with editorconfig, but it does not easily expose this customisation except through the standalone Fantomas client. So Myriad's output is formatted without respect to any conventions which may hold in the rest of your repository. You should probably add these files to your fantomasignore if you use Fantomas to format your repo; the alternative is to manually reformat every time Myriad changes the generated files.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
-
net6.0
- FSharp.Core (>= 6.0.1)
- Myriad.Core (>= 0.8.3)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
4.0.7 | 102 | 10/21/2024 |
4.0.6 | 304 | 10/14/2024 |
4.0.5 | 128 | 10/7/2024 |
4.0.4 | 78 | 10/6/2024 |
4.0.3 | 64 | 10/6/2024 |
4.0.2 | 68 | 10/6/2024 |
4.0.1 | 67 | 10/6/2024 |
3.1.4 | 77 | 10/3/2024 |
3.1.3 | 79 | 10/2/2024 |
3.1.2 | 72 | 10/2/2024 |
3.1.1 | 316 | 9/19/2024 |
3.0.2 | 82 | 9/19/2024 |
3.0.1 | 186 | 9/15/2024 |
2.3.10 | 112 | 9/15/2024 |
2.3.9 | 113 | 9/15/2024 |
2.3.8 | 114 | 9/14/2024 |
2.3.7 | 108 | 9/13/2024 |
2.3.6 | 127 | 9/12/2024 |
2.3.5 | 98 | 9/11/2024 |
2.3.4 | 121 | 9/10/2024 |
2.3.3 | 99 | 9/7/2024 |
2.3.2 | 114 | 9/5/2024 |
2.3.1 | 116 | 9/4/2024 |
2.2.7 | 108 | 9/4/2024 |
2.2.6 | 110 | 9/4/2024 |
2.2.5 | 109 | 9/4/2024 |
2.2.4 | 116 | 9/4/2024 |
2.2.3 | 116 | 9/3/2024 |
2.2.2 | 98 | 9/2/2024 |
2.2.1 | 385 | 8/26/2024 |
2.1.58 | 133 | 8/25/2024 |
2.1.57 | 228 | 8/13/2024 |
2.1.56 | 200 | 8/12/2024 |
2.1.55 | 88 | 8/4/2024 |
2.1.54 | 82 | 8/4/2024 |
2.1.53 | 323 | 7/8/2024 |
2.1.52 | 330 | 7/1/2024 |
2.1.51 | 126 | 7/1/2024 |
2.1.50 | 108 | 7/1/2024 |
2.1.49 | 121 | 6/27/2024 |
2.1.48 | 106 | 6/27/2024 |
2.1.47 | 120 | 6/24/2024 |
2.1.46 | 115 | 6/24/2024 |
2.1.45 | 496 | 6/17/2024 |
2.1.44 | 406 | 6/15/2024 |
2.1.43 | 97 | 6/10/2024 |
2.1.42 | 250 | 6/10/2024 |
2.1.41 | 96 | 6/9/2024 |
2.1.40 | 453 | 6/4/2024 |
2.1.39 | 99 | 6/3/2024 |
2.1.38 | 109 | 6/1/2024 |
2.1.37 | 85 | 5/31/2024 |
2.1.36 | 96 | 5/31/2024 |
2.1.35 | 100 | 5/31/2024 |
2.1.34 | 108 | 5/30/2024 |
2.1.33 | 104 | 5/30/2024 |
2.1.32 | 103 | 5/30/2024 |
2.1.31 | 114 | 5/30/2024 |
2.1.30 | 101 | 5/30/2024 |
2.1.29 | 104 | 5/28/2024 |
2.1.28 | 96 | 5/27/2024 |
2.1.27 | 97 | 5/24/2024 |
2.1.26 | 99 | 5/24/2024 |
2.1.25 | 96 | 5/24/2024 |
2.1.24 | 112 | 5/20/2024 |
2.1.23 | 98 | 5/20/2024 |
2.1.22 | 128 | 5/6/2024 |
2.1.21 | 103 | 4/30/2024 |
2.1.20 | 115 | 4/29/2024 |
2.1.19 | 101 | 4/29/2024 |
2.1.18 | 105 | 4/22/2024 |
2.1.17 | 102 | 4/17/2024 |
2.1.16 | 103 | 4/16/2024 |
2.1.15 | 109 | 4/16/2024 |
2.1.14 | 109 | 4/15/2024 |
2.1.13 | 125 | 3/19/2024 |
2.1.12 | 114 | 3/11/2024 |
2.1.11 | 108 | 3/4/2024 |
2.1.10 | 129 | 2/26/2024 |
2.1.9 | 123 | 2/26/2024 |
2.1.8 | 113 | 2/25/2024 |
2.1.7 | 119 | 2/25/2024 |
2.1.6 | 111 | 2/25/2024 |
2.1.5 | 105 | 2/19/2024 |
2.1.4 | 110 | 2/19/2024 |
2.1.3 | 102 | 2/18/2024 |
2.1.2 | 98 | 2/18/2024 |
2.1.1 | 107 | 2/17/2024 |
2.0.9 | 104 | 2/14/2024 |
2.0.8 | 118 | 2/13/2024 |
2.0.7 | 105 | 2/13/2024 |
2.0.6 | 105 | 2/13/2024 |
2.0.5 | 123 | 2/12/2024 |
2.0.4 | 105 | 2/7/2024 |
2.0.3 | 94 | 2/7/2024 |
2.0.2 | 97 | 2/7/2024 |
2.0.1 | 115 | 2/7/2024 |
1.4.15 | 116 | 2/6/2024 |
1.4.14 | 114 | 2/6/2024 |
1.4.13 | 96 | 2/6/2024 |
1.4.12 | 101 | 2/6/2024 |
1.4.11 | 120 | 2/6/2024 |
1.4.10 | 96 | 2/5/2024 |
1.4.9 | 106 | 1/30/2024 |
1.4.8 | 117 | 1/29/2024 |
1.4.7 | 98 | 1/29/2024 |
1.4.6 | 100 | 1/29/2024 |
1.4.5 | 100 | 1/28/2024 |
1.4.4 | 88 | 1/28/2024 |
1.4.3 | 107 | 1/26/2024 |
1.4.2 | 102 | 1/26/2024 |
1.4.1 | 86 | 1/26/2024 |
1.3.5 | 90 | 1/25/2024 |
1.3.4 | 119 | 1/15/2024 |
1.3.3 | 112 | 1/15/2024 |
1.3.2 | 125 | 1/8/2024 |
1.3.1 | 122 | 1/8/2024 |
1.2.3 | 122 | 1/3/2024 |
1.2.2 | 123 | 12/31/2023 |
1.2.1 | 128 | 12/30/2023 |
1.1.15 | 136 | 12/30/2023 |
1.1.14 | 136 | 12/30/2023 |
1.1.13 | 133 | 12/30/2023 |
1.1.12 | 130 | 12/30/2023 |
1.1.11 | 114 | 12/30/2023 |
1.1.10 | 137 | 12/29/2023 |
1.1.9 | 130 | 12/29/2023 |
1.1.8 | 129 | 12/29/2023 |
1.1.7 | 136 | 12/29/2023 |
1.1.6 | 141 | 12/29/2023 |
1.1.5 | 135 | 12/29/2023 |
1.1.4 | 129 | 12/29/2023 |
1.1.3 | 117 | 12/29/2023 |
1.1.2 | 121 | 12/28/2023 |
1.1.1 | 121 | 12/28/2023 |
1.0.6 | 123 | 12/28/2023 |
1.0.5 | 121 | 12/28/2023 |
1.0.4 | 125 | 12/27/2023 |