{"id":29720001,"url":"https://github.com/smaug123/woofware.myriad","last_synced_at":"2025-07-24T12:25:00.676Z","repository":{"id":162565190,"uuid":"637086849","full_name":"Smaug123/WoofWare.Myriad","owner":"Smaug123","description":"Some Myriad source generators for F#","archived":false,"fork":false,"pushed_at":"2025-07-20T01:55:00.000Z","size":1480,"stargazers_count":20,"open_issues_count":16,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-07-22T09:51:15.249Z","etag":null,"topics":["fsharp","myriad"],"latest_commit_sha":null,"homepage":"","language":"F#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Smaug123.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2023-05-06T13:16:32.000Z","updated_at":"2025-07-20T01:55:04.000Z","dependencies_parsed_at":"2024-01-15T13:12:24.231Z","dependency_job_id":"7a565a38-39a3-4236-a38d-371f7df2fe5a","html_url":"https://github.com/Smaug123/WoofWare.Myriad","commit_stats":null,"previous_names":["smaug123/woofware.myriad"],"tags_count":155,"template":false,"template_full_name":null,"purl":"pkg:github/Smaug123/WoofWare.Myriad","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Smaug123%2FWoofWare.Myriad","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Smaug123%2FWoofWare.Myriad/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Smaug123%2FWoofWare.Myriad/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Smaug123%2FWoofWare.Myriad/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Smaug123","download_url":"https://codeload.github.com/Smaug123/WoofWare.Myriad/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Smaug123%2FWoofWare.Myriad/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266843568,"owners_count":23993958,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-24T02:00:09.469Z","response_time":99,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["fsharp","myriad"],"created_at":"2025-07-24T12:24:55.687Z","updated_at":"2025-07-24T12:25:00.665Z","avatar_url":"https://github.com/Smaug123.png","language":"F#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# WoofWare.Myriad.Plugins\n\n[![NuGet version](https://img.shields.io/nuget/v/WoofWare.Myriad.Plugins.svg?style=flat-square)](https://www.nuget.org/packages/WoofWare.Myriad.Plugins)\n[![GitHub Actions status](https://github.com/Smaug123/WoofWare.Myriad/actions/workflows/dotnet.yaml/badge.svg)](https://github.com/Smaug123/WoofWare.Myriad/actions?query=branch%3Amain)\n[![License file](https://img.shields.io/github/license/Smaug123/WoofWare.Myriad)](./LICENSE)\n\n![Project logo: the face of a cartoon Shiba Inu, staring with powerful cyborg eyes directly at the viewer, with a background of stylised plugs.](./WoofWare.Myriad.Plugins/logo.png)\n\nSome helpers in [Myriad](https://github.com/MoiraeSoftware/myriad/) which might be useful.\n\nCurrently implemented:\n\n* `JsonParse` (to stamp out `jsonParse : JsonNode -\u003e 'T` methods).\n* `JsonSerialize` (to stamp out `toJsonNode : 'T -\u003e JsonNode` methods).\n* `HttpClient` (to stamp out a [RestEase](https://github.com/canton7/RestEase)-style HTTP client).\n* `GenerateMock` (to stamp out a record type corresponding to an interface, like a compile-time [Foq](https://github.com/fsprojects/Foq)).\n* `ArgParser` (to stamp out a basic argument parser).\n* `SwaggerClient` (to stamp out an HTTP client for a Swagger API).\n* `CreateCatamorphism` (to stamp out a non-stack-overflowing [catamorphism](https://fsharpforfunandprofit.com/posts/recursive-types-and-folds/) for a discriminated union).\n* `RemoveOptions` (to strip `option` modifiers from a type) - this one is particularly half-baked!\n\nIf you would like to ensure that your particular use-case remains unbroken, please do contribute tests to this repository.\nThe `ConsumePlugin` assembly contains a number of invocations of these source generators,\nso you just need to add copies of your types to that assembly to ensure that I will at least notice if I break the build;\nand if you add tests to `WoofWare.Myriad.Plugins.Test` then I will also notice if I break the runtime semantics of the generated code.\n\n## `JsonParse`\n\nTakes records like this:\n\n```fsharp\n[\u003cWoofWare.Myriad.Plugins.JsonParse\u003e]\ntype InnerType =\n    {\n        [\u003cJsonPropertyName \"something\"\u003e]\n        Thing : string\n    }\n\n/// My whatnot\n[\u003cWoofWare.Myriad.Plugins.JsonParse\u003e]\ntype JsonRecordType =\n    {\n        /// A thing!\n        A : int\n        /// Another thing!\n        B : string\n        [\u003cSystem.Text.Json.Serialization.JsonPropertyName \"hi\"\u003e]\n        C : int list\n        D : InnerType\n    }\n\n```\n\nand stamps out parsing methods like this:\n\n```fsharp\n/// Module containing JSON parsing methods for the InnerType type\n[\u003cRequireQualifiedAccess\u003e]\n[\u003cCompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)\u003e]\nmodule InnerType =\n    /// Parse from a JSON node.\n    let jsonParse (node: System.Text.Json.Nodes.JsonNode) : InnerType =\n        let Thing = node.[\"something\"].AsValue().GetValue\u003cstring\u003e()\n        { Thing = Thing }\nnamespace UsePlugin\n\n/// Module containing JSON parsing methods for the JsonRecordType type\n[\u003cRequireQualifiedAccess\u003e]\n[\u003cCompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)\u003e]\nmodule JsonRecordType =\n    /// Parse from a JSON node.\n    let jsonParse (node: System.Text.Json.Nodes.JsonNode) : JsonRecordType =\n        let D = InnerType.jsonParse node.[\"d\"]\n\n        let C =\n            node.[\"hi\"].AsArray() |\u003e Seq.map (fun elt -\u003e elt.GetValue\u003cint\u003e()) |\u003e List.ofSeq\n\n        let B = node.[\"b\"].AsValue().GetValue\u003cstring\u003e()\n        let A = node.[\"a\"].AsValue().GetValue\u003cint\u003e()\n        { A = A; B = B; C = C; D = D }\n```\n\nYou can optionally supply the boolean `true` to the attribute,\nwhich will cause Myriad to stamp out an extension method rather than a module with the same name as the type.\nThis is useful if you want to reuse the type name as a module name yourself,\nor if you want to apply multiple source generators which each want to use the module name.\n\n### What's the point?\n\n`System.Text.Json`, in a `PublishAot` context, relies on C# source generators.\nThe default reflection-heavy implementations have the necessary code trimmed away, and result in a runtime exception.\nBut C# source generators [are entirely unsupported in F#](https://github.com/dotnet/fsharp/issues/14300).\n\nThis Myriad generator expects you to use `System.Text.Json` to construct a `JsonNode`,\nand then the generator takes over to construct a strongly-typed object.\n\n### Limitations\n\nThis source generator is enough for what I first wanted to use it for.\nHowever, there is *far* more that could be done.\n\n* Make it possible to give an exact format and cultural info in date and time parsing.\n* Make it possible to reject parsing if extra fields are present.\n* Generally support all the `System.Text.Json` attributes.\n\nFor an example of using both `JsonParse` and `JsonSerialize` together with complex types, see [the type definitions](./ConsumePlugin/SerializationAndDeserialization.fs) and [tests](./WoofWare.Myriad.Plugins.Test/TestJsonSerialize/TestJsonSerde.fs).\n\n## `JsonSerialize`\n\nTakes records like this:\n```fsharp\n[\u003cWoofWare.Myriad.Plugins.JsonSerialize true\u003e]\ntype InnerTypeWithBoth =\n    {\n        [\u003cJsonPropertyName(\"it's-a-me\")\u003e]\n        Thing : string\n        ReadOnlyDict : IReadOnlyDictionary\u003cstring, Uri list\u003e\n    }\n```\n\nand stamps out modules like this:\n```fsharp\nmodule InnerTypeWithBoth =\n    let toJsonNode (input : InnerTypeWithBoth) : System.Text.Json.Nodes.JsonNode =\n        let node = System.Text.Json.Nodes.JsonObject ()\n\n        do\n            node.Add ((\"it's-a-me\"), System.Text.Json.Nodes.JsonValue.Create\u003cstring\u003e input.Thing)\n\n            node.Add (\n                \"ReadOnlyDict\",\n                (fun field -\u003e\n                    let ret = System.Text.Json.Nodes.JsonObject ()\n\n                    for (KeyValue (key, value)) in field do\n                        ret.Add (key.ToString (), System.Text.Json.Nodes.JsonValue.Create\u003cUri\u003e value)\n\n                    ret\n                ) input.ReadOnlyDict\n            )\n\n        node\n```\n\nAlso includes an *opinionated* serializer for discriminated unions.\n(Any such serializer must be opinionated, because JSON does not natively model DUs.)\n\nAs in `JsonParse`, you can optionally supply the boolean `true` to the attribute,\nwhich will cause Myriad to stamp out an extension method rather than a module with the same name as the type.\n\nThe same limitations generally apply to `JsonSerialize` as do to `JsonParse`.\n\nFor an example of using both `JsonParse` and `JsonSerialize` together with complex types, see [the type definitions](./ConsumePlugin/SerializationAndDeserialization.fs) and [tests](./WoofWare.Myriad.Plugins.Test/TestJsonSerialize/TestJsonSerde.fs).\n\n## `ArgParser`\n\nTakes a record like this:\n\n```fsharp\ntype DryRunMode =\n    | [\u003cArgumentFlag true\u003e Dry\n    | [\u003cArgumentFlag false\u003e Wet\n\n[\u003cArgParser\u003e]\ntype Foo =\n    {\n        [\u003cArgumentHelpText \"Enable the frobnicator\"\u003e]\n        SomeFlag : bool\n        A : int option\n        [\u003cArgumentDefaultFunction\u003e]\n        B : Choice\u003cint, int\u003e\n        [\u003cArgumentDefaultEnvironmentVariable \"MY_ENV_VAR\"\u003e]\n        BWithEnv : Choice\u003cint, int\u003e\n        [\u003cArgumentDefaultFunction\u003e]\n        DryRun : DryRunMode\n        [\u003cArgumentLongForm \"longer-form-replaces-c\"\u003e]\n        C : float list\n        // optionally:\n        [\u003cPositionalArgs\u003e]\n        Rest : string list // or e.g. `int list` if you want them parsed into a type too\n    }\n    static member DefaultB () = 4\n    static member DefaultDryRun () = DryRunMode.Wet\n```\n\nand stamps out a basic `parse` method of this signature:\n\n```fsharp\n[\u003cRequireQualifiedAccess\u003e]\nmodule Foo =\n    // in case you want to test it\n    let parse' (getEnvVar : string -\u003e string) (args : string list) : Foo = ...\n    // the one we expect you actually want to use\n    let parse (args : string list) : Foo = ...\n```\n\nDefault arguments are handled as `Choice\u003c'a, 'a\u003e`:\nyou get a `Choice1Of2` if the user provided the input, or a `Choice2Of2` if the parser filled in your specified default value.\n\nYou can control `TimeSpan` and friends with the `[\u003cInvariantCulture\u003e]` and `[\u003cParseExact @\"hh\\:mm\\:ss\"\u003e]` attributes.\n\nYou can generate extension methods for the type, instead of a module with the type's name, using `[\u003cArgParser (* isExtensionMethod = *) true\u003e]`.\n\nIf `--help` appears in a position where the parser is expecting a key (e.g. in the first position, or after a `--foo=bar`), the parser fails with help text.\nThe parser also makes a limited effort to supply help text when encountering an invalid parse.\n\n### What's the point?\n\nI got fed up of waiting for us to find time to rewrite the in-house one at work.\nThat one has a bunch of nice compositional properties, which my version lacks:\nI can basically only deal with primitive types, and e.g. you can't stack records and discriminated unions inside each other.\n\nBut I *do* want an F#-native argument parser suitable for AOT-compilation.\n\nWhy not [Argu](https://fsprojects.github.io/Argu/)?\nAnswer: I got annoyed with having to construct my records by hand even after Argu returned and said the parsing was all \"done\".\n\n### Limitations\n\nThis is very bare-bones, but do raise GitHub issues if you like (or if you find cases where the parser does the wrong thing).\n\n* Help is signalled by throwing an exception, so you'll get an unsightly stack trace and a nonzero exit code.\n* Help doesn't take into account any arguments the user has entered. Ideally you'd get contextual information like an identification of which args the user has supplied at the point where the parse failed or help was requested.\n* I don't handle very many types, and in particular a real arg parser would handle DUs and records with nesting.\n* I don't try very hard to find a valid parse. It may well be possible to find a case where I fail to parse despite there existing a valid parse.\n* There's no subcommand support (you'll have to do that yourself).\n\nIt should work fine if you just want to compose a few primitive types, though.\n\n## `SwaggerClient`\n\nTakes a JSON-schema definition of a [Swagger API](https://swagger.io/), and stamps out a client like this:\n\n```fsharp\n/// A type which was defined in the Swagger spec\n[\u003cJsonParse true ; JsonSerialize true\u003e]\ntype SwaggerType1 =\n    {\n        [\u003cSystem.Text.Json.Serialization.JsonExtensionData\u003e]\n        AdditionalProperties : System.Collections.Generic.Dictionary\u003cstring, System.Text.Json.Nodes.JsonNode\u003e\n        Message : string\n    }\n\n/// Documentation from the Swagger spec\n[\u003cHttpClient false ; RestEase.BasePath \"/api/v1\"\u003e]\ntype IGitea =\n    /// Returns the Person actor for a user\n    [\u003cRestEase.Get \"/activitypub/user/{username}\"\u003e]\n    abstract ActivitypubPerson :\n        [\u003cRestEase.Path \"username\"\u003e] username : string * ?ct : System.Threading.CancellationToken -\u003e\n            ActivityPub System.Threading.Tasks.Task\n```\n\nNotice that we automatically decorate the type with our `[\u003cHttpClient\u003e]` attribute, so if you choose to do so, you can chain another Myriad generated file off this one and you'll get a RestEase-style client stamped out.\n(See below, searching on the string `\"Generated2SwaggerGitea.fs\"`, for an example.)\n\nYou don't need to `Content Include` or `EmbeddedResource Include` the JSON schema.\n`None Include` will do; we only need the source to be available at build time.\n\nYou *do* need to include the following configuration:\n\n```xml\n\u003cCompile Include=\"GeneratedClient.fs\"\u003e\n  \u003c!-- This bit is normal: --\u003e\n  \u003cMyriadFile\u003eswagger.json\u003c/MyriadFile\u003e\n  \u003c!-- This bit is new and required! --\u003e\n  \u003cMyriadParams\u003e\n    \u003cClassName\u003eGiteaClient\u003c/ClassName\u003e\n    \u003c!-- Optionally: --\u003e\n    \u003cGenerateMock\u003etrue\u003c/GenerateMock\u003e\n  \u003c/MyriadParams\u003e\n\u003c/Compile\u003e\n```\n\nThe `\u003cClassName /\u003e` key tells us what to name the resulting interface (it gets an `I` prepended for you).\nYou can optionally also set `\u003cGenerateMockVisibility\u003ev\u003c/GenerateMockVisibility\u003e` to add the `[\u003cGenerateMock\u003e]` attribute to the type\n(where `v` should be `internal` or `public`, indicating \"resulting mock type is internal\" vs \"is public\"),\nso that the following manoeuvre will result in a generated mock:\n\n```xml\n\u003cNone Include=\"swagger-gitea.json\" /\u003e\n\u003cCompile Include=\"GeneratedSwaggerGitea.fs\"\u003e\n  \u003cMyriadFile\u003eswagger-gitea.json\u003c/MyriadFile\u003e\n  \u003cMyriadParams\u003e\n    \u003cGenerateMockVisibility\u003epublic\u003c/GenerateMockVisibility\u003e\n    \u003cClassName\u003eGitea\u003c/ClassName\u003e\n  \u003c/MyriadParams\u003e\n\u003c/Compile\u003e\n\u003cCompile Include=\"Generated2SwaggerGitea.fs\"\u003e\n  \u003cMyriadFile\u003eGeneratedSwaggerGitea.fs\u003c/MyriadFile\u003e\n\u003c/Compile\u003e\n```\n\n(Note that you do have to create the `GeneratedSwaggerGitea.fs` file manually before code generation happens. Myriad will throw if that file isn't there, because `Generated2SwaggerGitea.fs` depends on it so Myriad wants to compute its hash. Just make an empty file.)\n\n### What's the point?\n\n[`SwaggerProvider`](https://github.com/fsprojects/SwaggerProvider) is *absolutely magical*, but it's kind of witchcraft.\nI fear no man, but that thing… it scares me.\n\nAlso, builds using `SwaggerProvider` appear to be inherently nondeterministic, even if the data source doesn't change.\n\n## Limitations\n\nSwagger API specs appear to be pretty cowboy in the wild.\nI try to cope with invalid schemas I have seen, but I can't guarantee I do so correctly.\nDefinitely do perform integration tests and let me know of weird specs you encounter, and bits of the (very extensive) Swagger spec I have omitted!\n\n## `RemoveOptions`\n\nTakes a record like this:\n\n```fsharp\ntype Foo =\n    {\n        A : int option\n        B : string\n        C : float list\n    }\n```\n\nand stamps out a record like this:\n\n```fsharp\n[\u003cRequireQualifiedAccess\u003e]\nmodule Foo =\n    type Short =\n        {\n            A : int\n            B : string\n            C : float list\n        }\n```\n\n### What's the point?\n\nThe motivating example is argument parsing.\nAn argument parser naturally wants to express \"the user did not supply this, so I will provide a default\".\nBut it's not a very ergonomic experience for the programmer to deal with all these options,\nso this Myriad generator stamps out a type *without* any options,\nand also stamps out an appropriate constructor function.\n\n### Limitations\n\nThis generator is *far* from where I want it, because I haven't really spent any time on it.\n\n* It really wants to be able to recurse into the types within the record, to strip options from them.\n* It needs some sort of attribute to mark a field as *not* receiving this treatment.\n* What do we do about discriminated unions?\n\n## `HttpClient`\n\nTakes a type like this:\n\n```fsharp\n[\u003cWoofWare.Myriad.Plugins.HttpClient\u003e]\ntype IPureGymApi =\n    [\u003cGet \"v1/gyms/\"\u003e]\n    abstract GetGyms : ?ct : CancellationToken -\u003e Task\u003cGym list\u003e\n\n    [\u003cGet \"v1/gyms/{gym_id}/attendance\"\u003e]\n    abstract GetGymAttendance : [\u003cPath \"gym_id\"\u003e] gymId : int * ?ct : CancellationToken -\u003e Task\u003cGymAttendance\u003e\n\n    [\u003cGet \"v1/member\"\u003e]\n    abstract GetMember : ?ct : CancellationToken -\u003e Task\u003cMember\u003e\n\n    [\u003cGet \"v1/gyms/{gym_id}\"\u003e]\n    abstract GetGym : [\u003cPath \"gym_id\"\u003e] gymId : int * ?ct : CancellationToken -\u003e Task\u003cGym\u003e\n\n    [\u003cGet \"v1/member/activity\"\u003e]\n    abstract GetMemberActivity : ?ct : CancellationToken -\u003e Task\u003cMemberActivityDto\u003e\n\n    [\u003cGet \"v2/gymSessions/member\"\u003e]\n    abstract GetSessions :\n        [\u003cQuery\u003e] fromDate : DateTime * [\u003cQuery\u003e] toDate : DateTime * ?ct : CancellationToken -\u003e Task\u003cSessions\u003e\n```\n\nand stamps out a type like this:\n\n```fsharp\n/// Module for constructing a REST client.\n[\u003cCompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)\u003e]\n[\u003cRequireQualifiedAccess\u003e]\nmodule PureGymApi =\n    /// Create a REST client.\n    let make (client : System.Net.Http.HttpClient) : IPureGymApi =\n        { new IPureGymApi with\n            member _.GetGyms (ct : CancellationToken option) =\n                async {\n                    let! ct = Async.CancellationToken\n\n                    let httpMessage =\n                        new System.Net.Http.HttpRequestMessage (\n                            Method = System.Net.Http.HttpMethod.Get,\n                            RequestUri = System.Uri (client.BaseAddress.ToString () + \"v1/gyms/\")\n                        )\n\n                    let! response = client.SendAsync (httpMessage, ct) |\u003e Async.AwaitTask\n                    let response = response.EnsureSuccessStatusCode ()\n                    let! stream = response.Content.ReadAsStreamAsync ct |\u003e Async.AwaitTask\n\n                    let! node =\n                        System.Text.Json.Nodes.JsonNode.ParseAsync (stream, cancellationToken = ct)\n                        |\u003e Async.AwaitTask\n\n                    return node.AsArray () |\u003e Seq.map (fun elt -\u003e Gym.jsonParse elt) |\u003e List.ofSeq\n                }\n                |\u003e (fun a -\u003e Async.StartAsTask (a, ?cancellationToken = ct))\n\n            // (more methods here)\n        }\n```\n\n### What's the point?\n\nThe motivating example is again ahead-of-time compilation: we wish to avoid the reflection which RestEase does.\n\n### Features\n\n* Variable and constant header values are supported:\n  see [the definition of `IApiWithHeaders`](./ConsumePlugin/RestApiExample.fs).\n\n### Limitations\n\nRestEase is complex, and handles a lot of different stuff.\n\n* If you set the `BaseAddress` on your input `HttpClient`, make sure to end with a trailing slash\n  on any trailing directories (so `\"blah/foo/\"` rather than `\"blah/foo\"`).\n  We combine URIs using `UriKind.Relative`, so without a trailing slash, the last component may be chopped off.\n* Parameters are serialised naively with `toJsonNode` as though the `JsonSerialize` generator were applied,\n  and you can't control the serialisation. You can't yet serialise e.g. a primitive type this way (other than `String`);\n  all body parameters must be types which have a suitable `toJsonNode : 'a -\u003e JsonNode` method.\n* Deserialisation follows the same logic as the `JsonParse` generator,\n  and it generally assumes you're using types which `JsonParse` is applied to.\n* Anonymous parameters are currently forbidden.\n\nThere are also some design decisions:\n\n* Every function must take an optional `CancellationToken` (which is good practice anyway);\n  so arguments are forced to be tupled.\n* The `[\u003cOptional\u003e]` attribute is not supported and will probably not be supported, because I consider it to be cursed.\n\n## `GenerateMock`\n\nTakes a type like this:\n\n```fsharp\n[\u003cGenerateMock\u003e]\ntype IPublicType =\n    abstract Mem1 : string * int -\u003e string list\n    abstract Mem2 : string -\u003e int\n```\n\nand stamps out a type like this:\n\n```fsharp\n/// Mock record type for an interface\ntype internal PublicTypeMock =\n    {\n        Mem1 : string * int -\u003e string list\n        Mem2 : string -\u003e int\n    }\n\n    static member Empty : PublicTypeMock =\n        {\n            Mem1 = (fun x -\u003e raise (System.NotImplementedException \"Unimplemented mock function\"))\n            Mem2 = (fun x -\u003e raise (System.NotImplementedException \"Unimplemented mock function\"))\n        }\n\n    interface IPublicType with\n        member this.Mem1 (arg0, arg1) = this.Mem1 (arg0, arg1)\n        member this.Mem2 (arg0) = this.Mem2 (arg0)\n```\n\n### What's the point?\n\nReflective mocking libraries like [Foq](https://github.com/fsprojects/Foq) in my experience are a rich source of flaky tests.\nThe [Grug-brained developer](https://grugbrain.dev/) would prefer to do this without reflection, and this reduces the rate of strange one-in-ten-thousand \"failed to generate IL\" errors.\nBut since F# does not let you partially update an interface definition, we instead stamp out a record,\nthereby allowing the programmer to use F#'s record-update syntax.\n\n### Features\n\n* You may supply an `isInternal : bool` argument to the attribute. By default, we make the resulting record type at most internal (never public), since this is intended only to be used in tests; but you can instead make it public with `[\u003cGenerateMock false\u003e]`.\n\n## `CreateCatamorphism`\n\nTakes a collection of mutually recursive discriminated unions:\n\n```fsharp\n[\u003cCreateCatamorphism \"MyCata\"\u003e]\ntype Expr =\n    | Const of Const\n    | Pair of Expr * Expr * PairOpKind\n    | Sequential of Expr list\n    | Builder of Expr * ExprBuilder\n\nand ExprBuilder =\n    | Child of ExprBuilder\n    | Parent of Expr\n```\n\nand stamps out a type like this:\n```fsharp\ntype ExprCata\u003c'Expr, 'ExprBuilder\u003e =\n    abstract Const : Const -\u003e 'Expr\n    abstract Pair : 'Expr -\u003e 'Expr -\u003e PairOpKind -\u003e 'Expr\n    abstract Sequential : 'Expr list -\u003e 'Expr\n    abstract Builder : 'Expr -\u003e 'ExprBuilder -\u003e 'Expr\n\ntype ExprBuilderCata\u003c'Expr, 'ExprBuilder\u003e =\n    abstract Child : 'ExprBuilder -\u003e 'ExprBuilder\n    abstract Parent : 'Expr -\u003e 'ExprBuilder\n\ntype MyCata\u003c'Expr, 'ExprBuilder\u003e =\n    {\n        Expr : ExprCata\u003c'Expr, 'ExprBuilder\u003e\n        ExprBuilder : ExprBuilderCata\u003c'Expr, 'ExprBuilder\u003e\n    }\n\n[\u003cRequireQualifiedAccess\u003e]\nmodule ExprCata =\n    let runExpr (cata : MyCata\u003c'ExprRet, 'ExprBuilderRet\u003e) (x : Expr) : 'ExprRet =\n        failwith \"this is implemented\"\n\n    let runExprBuilder (cata : MyCata\u003c'ExprRet, 'ExprBuilderRet\u003e) (x : ExprBuilder) : 'ExprBuilderRet =\n        failwith \"this is implemented\"\n```\n\n### What's the point?\nRecursing over a tree is not easy to get right, especially if you want to avoid stack overflows.\nInstead of writing the recursion many times, it's better to do it once,\nand then each time you only plug in what you want to do.\n\n### Features\n\n* Mutually recursive DUs are supported (as in the example above).\n  Every DU in a recursive `type Foo... and Bar...` knot will be given an appropriate cata, as long as any one of those DUs has the `[\u003cCreateCatamorphism\u003e]` attribute.\n* There is *limited* support for records and for lists.\n* There is *extremely brittle* support for generics in the DUs you are cata'ing over.\n  It is based on the names of the generic parameters, so you must ensure that generic parameters with the same name have the same meaning across the various cases in your recursive knot of DUs.\n  (If you overstep the bounds of what this generator can do, you will get compile-time errors, e.g. with generics being constrained to each other's values.)\n  See the [List tests](./WoofWare.Myriad.Plugins.Test/TestCataGenerator/TestMyList2.fs) for an example, where we re-implement `FSharpList\u003c'a\u003e`.\n\n### Limitations\n\n**I am not at all convinced of the correctness of this generator**, and I know it is very incomplete (in the sense that there are many possible DUs you could write for which the generator will bail out).\nI *strongly* recommend implementing the identity catamorphism for your type and using property-based tests ([as I do](./WoofWare.Myriad.Plugins.Test/TestCataGenerator/TestDirectory.fs)) to assert that the correct thing happens.\nFeel free to raise GitHub issues with code I can copy-paste to reproduce a case where the wrong thing happens (though I can't promise to look at them).\n\n* This is a particularly half-baked generator which has so far seen no real-world use.\n  It likely has a bunch of [80/20](https://en.wikipedia.org/wiki/Pareto_principle) low-hanging fruit remaining, but it also likely has impossible problems to solve which I don't know about yet.\n* Only a very few kinds of DU field are currently implemented.\n  For example, this generator can't see through an interface (e.g. the kind of interface one would use to implement the [crate pattern](https://www.patrickstevens.co.uk/posts/2021-10-19-crates/) to represent a [GADT](https://en.wikipedia.org/wiki/Generalized_algebraic_data_type)),\n  so the generated cata will simply grant you access to the interface (rather than attempting to descend into it to discover recursive references).\n  You can't nest lists deeply. All sorts of other cases are unaddressed.\n* This generator does not try to solve the \"exponential diamond dependency\" problem.\n  If you have a case of the form `type Expr = | Branch of Expr * Expr`, the cata will walk into both `Expr`s separately.\n  If the `Expr`s happen to be equal, the cata will nevertheless traverse them individually (that is, it will traverse the same `Expr` twice).\n  Your type may represent a [DAG](https://en.wikipedia.org/wiki/Directed_acyclic_graph), but we will always effectively expand it into a tree of paths and operate on each of the exponentially-many paths.\n\n# Detailed examples\n\nSee the tests.\nFor example, [PureGymDto.fs](./ConsumePlugin/PureGymDto.fs) is a real-world set of DTOs.\n\n## How to use\n\n* In your `.fsproj` file, define a helper variable so that subsequent steps don't all have to be kept in sync:\n    ```xml\n    \u003cPropertyGroup\u003e\n      \u003cWoofWareMyriadPluginVersion\u003e2.0.1\u003c/WoofWareMyriadPluginVersion\u003e\n    \u003c/PropertyGroup\u003e\n    ```\n* Take a reference on `WoofWare.Myriad.Plugins.Attributes` (which has no other dependencies), to obtain access to the attributes which the generator will recognise:\n    ```xml\n    \u003cItemGroup\u003e\n        \u003cPackageReference Include=\"WoofWare.Myriad.Plugins.Attributes\" Version=\"2.0.2\" /\u003e\n    \u003c/ItemGroup\u003e\n    ```\n* Take a reference (with private assets, to prevent these from propagating to your own assembly) on `WoofWare.Myriad.Plugins`, to obtain the plugins which Myriad will run, and on `Myriad.Sdk`, to obtain the Myriad binary itself:\n    ```xml\n    \u003cItemGroup\u003e\n        \u003cPackageReference Include=\"WoofWare.Myriad.Plugins\" Version=\"$(WoofWareMyriadPluginVersion)\" PrivateAssets=\"all\" /\u003e\n        \u003cPackageReference Include=\"Myriad.Sdk\" Version=\"0.8.3\" PrivateAssets=\"all\" /\u003e\n    \u003c/ItemGroup\u003e\n    ```\n* Point Myriad to the DLL within the NuGet package which is the source of the plugins:\n    ```xml\n    \u003cItemGroup\u003e\n      \u003cMyriadSdkGenerator Include=\"$(NuGetPackageRoot)/woofware.myriad.plugins/$(WoofWareMyriadPluginVersion)/lib/net6.0/WoofWare.Myriad.Plugins.dll\" /\u003e\n    \u003c/ItemGroup\u003e\n    ```\n\nNow you are ready to start using the generators.\nFor example, this specifies that Myriad is to use the contents of `Client.fs` to generate the file `GeneratedClient.fs`:\n\n```xml\n\u003cItemGroup\u003e\n    \u003cCompile Include=\"Client.fs\" /\u003e\n    \u003cCompile Include=\"GeneratedClient.fs\"\u003e\n        \u003cMyriadFile\u003eClient.fs\u003c/MyriadFile\u003e\n    \u003c/Compile\u003e\n\u003c/ItemGroup\u003e\n```\n\n## Alternative use without the attributes\n\nYou can avoid taking a reference on the `WoofWare.Myriad.Plugins.Attributes` assembly, instead putting all the configuration into the project file.\nThis is implemented for everything except the SwaggerClientGenerator.\n\n```xml\n\u003cProject\u003e\n  \u003cItemGroup\u003e\n    \u003cCompile Include=\"Client.fs\" /\u003e\n    \u003cCompile Include=\"GeneratedClient.fs\"\u003e\n        \u003cMyriadFile\u003eClient.fs\u003c/MyriadFile\u003e\n        \u003cMyriadParams\u003e\n          \u003cMyTypeName1\u003eGenerateMock(false)!JsonParse\u003c/MyTypeName1\u003e\n          \u003cSomeOtherTypeName\u003eGenerateMock\u003c/SomeOtherTypeName\u003e\n        \u003c/MyriadParams\u003e\n    \u003c/Compile\u003e\n  \u003c/ItemGroup\u003e\n  \u003cItemGroup\u003e\n    \u003cPackageReference Include=\"WoofWare.Myriad.Plugins\" Version=\"$(WoofWareMyriadPluginVersion)\" PrivateAssets=\"all\" /\u003e\n    \u003cPackageReference Include=\"Myriad.Sdk\" Version=\"0.8.3\" PrivateAssets=\"all\" /\u003e\n  \u003c/ItemGroup\u003e\n\u003c/Project\u003e\n```\n\nThat is, you specify a `!`-delimited list of the attributes you *would* apply to the type.\nSupply \"arguments\" to the attribute name in the project file as you would to the attribute itself.\n\n(Yes, this is indeed incredibly cumbersome, and you're not interested in the reasons it's all so mad!\nI'm hopefully going to get round to writing a more powerful source generation system which won't have these limitations.)\n\n### Myriad Gotchas\n\n* MsBuild doesn't always realise that it needs to invoke Myriad during rebuild.\n  You can always save a whitespace change to the source file (e.g. `Client.fs` above),\n  and MsBuild will then execute Myriad during the next build.\n* [Fantomas](https://github.com/fsprojects/fantomas), the F# source formatter which powers Myriad,\n  is customisable with [editorconfig](https://editorconfig.org/),\n  but it [does not easily expose](https://github.com/fsprojects/fantomas/issues/3031) this customisation\n  except through the standalone Fantomas client.\n  So Myriad's output is formatted without respect to any conventions which may hold in the rest of your repository.\n  You should probably add these files to your [fantomasignore](https://github.com/fsprojects/fantomas/blob/a999b77ca5a024fbc3409955faac797e29b39d27/docs/docs/end-users/IgnoreFiles.md)\n  if you use Fantomas to format your repo;\n  the alternative is to manually reformat every time Myriad changes the generated files.\n\n# Licence\n\nThe code is MIT-licenced, except for the Swagger API examples in WoofWare.Myriad.Plugins.Test, which are [CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/), copyright 2023 by the OpenAPI Initiative, and obtained from https://learn.openapis.org/examples/ with no changes made.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmaug123%2Fwoofware.myriad","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsmaug123%2Fwoofware.myriad","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmaug123%2Fwoofware.myriad/lists"}