WoofWare.Myriad.Plugins 1.1.3

There is a newer version of this package available.
See the version list below for details.
dotnet add package WoofWare.Myriad.Plugins --version 1.1.3                
NuGet\Install-Package WoofWare.Myriad.Plugins -Version 1.1.3                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="WoofWare.Myriad.Plugins" Version="1.1.3" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add WoofWare.Myriad.Plugins --version 1.1.3                
#r "nuget: WoofWare.Myriad.Plugins, 1.1.3"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install WoofWare.Myriad.Plugins as a Cake Addin
#addin nuget:?package=WoofWare.Myriad.Plugins&version=1.1.3

// Install WoofWare.Myriad.Plugins as a Cake Tool
#tool nuget:?package=WoofWare.Myriad.Plugins&version=1.1.3                

WoofWare.Myriad.Plugins

NuGet version GitHub Actions status License file

Project logo: the face of a cartoon Shiba Inu, staring with powerful cyborg eyes directly at the viewer, with a background of stylised plugs.

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 out jsonParse : JsonNode -> 'T methods);
  • RemoveOptions (to strip option modifiers from a type).
  • HttpClient (to stamp out a RestEase-style HTTP client).

JsonParse

Takes records like this:

[<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:

/// 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.
  • Rather than just throwing NullReferenceException, print out the field name that failed.
  • 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.

  • As of this writing, [<Body>] is explicitly unsupported (it throws with a TODO).
  • 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 which JsonParse is applied to.
  • Headers are not yet supported.
  • I haven't yet worked out how to integrate this with a mocked HTTP client; you can always mock up an HttpClient, but I prefer to use a mock which defines a single member SendAsync.
  • Anonymous parameters are currently forbidden.
  • Every function must take an optional CancellationToken (which is good practice anyway); so arguments are forced to be tupled. This is a won't-fix for as long as F# requires tupled arguments if any of the args are optional.

Detailed examples

See the tests. For example, PureGymDto.fs is a real-world set of DTOs.

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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