From da609db2ce7cc01fbd6549f714ab9ca115ab6e30 Mon Sep 17 00:00:00 2001 From: Patrick Stevens <3138005+Smaug123@users.noreply.github.com> Date: Mon, 7 Oct 2024 13:35:43 +0100 Subject: [PATCH] First release (#10) --- .github/workflows/dotnet.yaml | 287 +- NuGet.config | 18 + .../Attributes.fs | 96 + .../README.md | 5 + .../SurfaceBaseline.txt | 24 + ...Whippet.Plugin.ArgParser.Attributes.fsproj | 33 + .../version.json | 11 + .../Args.fs | 237 + .../GeneratedArgs.fs | 4403 +++++++++++++++++ ...e.Whippet.Plugin.ArgParser.Consumer.fsproj | 26 + .../ArgParserGenerator.fs | 1807 +++++++ .../README.md | 97 + .../TestArgParser.fs | 706 +++ .../TestSurface.fs | 26 + ...fWare.Whippet.Plugin.ArgParser.Test.fsproj | 27 + .../WoofWare.Whippet.Plugin.ArgParser.fsproj | 40 + .../version.json | 14 + .../Attributes.fs | 37 + .../README.md | 6 + .../SurfaceBaseline.txt | 10 + ...Ware.Whippet.Plugin.Json.Attributes.fsproj | 33 + .../version.json | 11 + .../GeneratedJson.fs | 483 ++ .../GeneratedPureGymDto.fs | 1116 +++++ ...eneratedSerializationAndDeserialization.fs | 977 ++++ .../JsonRecord.fs | 83 + .../PureGymDto.fs | 190 + .../SerializationAndDeserialization.fs | 94 + ...ofWare.Whippet.Plugin.Json.Consumer.fsproj | 31 + .../DesiredGenerator.fs | 15 + .../JsonParseGenerator.fs | 783 +++ .../JsonSerializeGenerator.fs | 602 +++ .../WoofWare.Whippet.Plugin.Json/README.md | 223 + .../PureGymDtos.fs | 264 + .../TestExtensionMethod.fs | 74 + .../TestJsonParse.fs | 63 + .../TestJsonSerde.fs | 474 ++ .../TestPureGymJson.fs | 71 + .../TestSurface.fs | 26 + .../WoofWare.Whippet.Plugin.Json.Test.fsproj | 31 + .../WoofWare.Whippet.Plugin.Json.fsproj | 40 + .../WoofWare.Whippet.Plugin.Json/version.json | 14 + README.md | 79 +- WoofWare.Whippet.App/AppContext.fs | 33 + WoofWare.Whippet.App/Context.fs | 26 + .../Program.fs | 66 +- WoofWare.Whippet.App/RuntimeConfig.fs | 47 + WoofWare.Whippet.App/RuntimeConfigGen.fs | 103 + WoofWare.Whippet.App/RuntimeLocator.fs | 104 + .../WoofWare.Whippet.App.fsproj | 27 + WoofWare.Whippet.Core/Domain.fs | 4 + WoofWare.Whippet.Core/SurfaceBaseline.txt | 6 +- WoofWare.Whippet.Fantomas/Ast.fs | 3 +- WoofWare.Whippet.Fantomas/Measure.fs | 26 + WoofWare.Whippet.Fantomas/SurfaceBaseline.txt | 2 + .../WoofWare.Whippet.Fantomas.fsproj | 1 + WoofWare.Whippet.Fantomas/version.json | 2 +- WoofWare.Whippet.sln | 54 + WoofWare.Whippet/WoofWare.Whippet.fsproj | 52 +- .../build/WoofWare.Whippet.targets | 49 + 60 files changed, 14225 insertions(+), 67 deletions(-) create mode 100644 NuGet.config create mode 100644 Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Attributes/Attributes.fs create mode 100644 Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Attributes/README.md create mode 100644 Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Attributes/SurfaceBaseline.txt create mode 100644 Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Attributes/WoofWare.Whippet.Plugin.ArgParser.Attributes.fsproj create mode 100644 Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Attributes/version.json create mode 100644 Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Consumer/Args.fs create mode 100644 Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Consumer/GeneratedArgs.fs create mode 100644 Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Consumer/WoofWare.Whippet.Plugin.ArgParser.Consumer.fsproj create mode 100644 Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/ArgParserGenerator.fs create mode 100644 Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/README.md create mode 100644 Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/WoofWare.Whippet.Plugin.ArgParser.Test/TestArgParser.fs create mode 100644 Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/WoofWare.Whippet.Plugin.ArgParser.Test/TestSurface.fs create mode 100644 Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/WoofWare.Whippet.Plugin.ArgParser.Test/WoofWare.Whippet.Plugin.ArgParser.Test.fsproj create mode 100644 Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/WoofWare.Whippet.Plugin.ArgParser.fsproj create mode 100644 Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/version.json create mode 100644 Plugins/Json/WoofWare.Whippet.Plugin.Json.Attributes/Attributes.fs create mode 100644 Plugins/Json/WoofWare.Whippet.Plugin.Json.Attributes/README.md create mode 100644 Plugins/Json/WoofWare.Whippet.Plugin.Json.Attributes/SurfaceBaseline.txt create mode 100644 Plugins/Json/WoofWare.Whippet.Plugin.Json.Attributes/WoofWare.Whippet.Plugin.Json.Attributes.fsproj create mode 100644 Plugins/Json/WoofWare.Whippet.Plugin.Json.Attributes/version.json create mode 100644 Plugins/Json/WoofWare.Whippet.Plugin.Json.Consumer/GeneratedJson.fs create mode 100644 Plugins/Json/WoofWare.Whippet.Plugin.Json.Consumer/GeneratedPureGymDto.fs create mode 100644 Plugins/Json/WoofWare.Whippet.Plugin.Json.Consumer/GeneratedSerializationAndDeserialization.fs create mode 100644 Plugins/Json/WoofWare.Whippet.Plugin.Json.Consumer/JsonRecord.fs create mode 100644 Plugins/Json/WoofWare.Whippet.Plugin.Json.Consumer/PureGymDto.fs create mode 100644 Plugins/Json/WoofWare.Whippet.Plugin.Json.Consumer/SerializationAndDeserialization.fs create mode 100644 Plugins/Json/WoofWare.Whippet.Plugin.Json.Consumer/WoofWare.Whippet.Plugin.Json.Consumer.fsproj create mode 100644 Plugins/Json/WoofWare.Whippet.Plugin.Json/DesiredGenerator.fs create mode 100644 Plugins/Json/WoofWare.Whippet.Plugin.Json/JsonParseGenerator.fs create mode 100644 Plugins/Json/WoofWare.Whippet.Plugin.Json/JsonSerializeGenerator.fs create mode 100644 Plugins/Json/WoofWare.Whippet.Plugin.Json/README.md create mode 100644 Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/PureGymDtos.fs create mode 100644 Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/TestExtensionMethod.fs create mode 100644 Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/TestJsonParse.fs create mode 100644 Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/TestJsonSerde.fs create mode 100644 Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/TestPureGymJson.fs create mode 100644 Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/TestSurface.fs create mode 100644 Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/WoofWare.Whippet.Plugin.Json.Test.fsproj create mode 100644 Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.fsproj create mode 100644 Plugins/Json/WoofWare.Whippet.Plugin.Json/version.json create mode 100644 WoofWare.Whippet.App/AppContext.fs create mode 100644 WoofWare.Whippet.App/Context.fs rename {WoofWare.Whippet => WoofWare.Whippet.App}/Program.fs (71%) create mode 100644 WoofWare.Whippet.App/RuntimeConfig.fs create mode 100644 WoofWare.Whippet.App/RuntimeConfigGen.fs create mode 100644 WoofWare.Whippet.App/RuntimeLocator.fs create mode 100644 WoofWare.Whippet.App/WoofWare.Whippet.App.fsproj create mode 100644 WoofWare.Whippet.Fantomas/Measure.fs create mode 100644 WoofWare.Whippet/build/WoofWare.Whippet.targets diff --git a/.github/workflows/dotnet.yaml b/.github/workflows/dotnet.yaml index e0f444b..86ef06f 100644 --- a/.github/workflows/dotnet.yaml +++ b/.github/workflows/dotnet.yaml @@ -33,10 +33,10 @@ jobs: with: extra_nix_config: | access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} - - name: Restore dependencies - run: nix develop --command dotnet restore - - name: Build - run: nix develop --command dotnet build --no-restore --configuration ${{matrix.config}} + - name: Build source generator + run: nix develop --command dotnet build WoofWare.Whippet/ + - name: Build solution + run: nix develop --command dotnet build --configuration ${{matrix.config}} - name: Test run: nix develop --command dotnet test --no-build --verbosity normal --configuration ${{matrix.config}} @@ -61,20 +61,20 @@ jobs: - name: Run analyzers run: nix run .#fsharp-analyzers -- --project ./WoofWare.Whippet/WoofWare.Whippet.fsproj --analyzers-path ./.analyzerpackages/g-research.fsharp.analyzers/*/ --verbosity detailed --report ./analysis.sarif --treat-as-error GRA-STRING-001 GRA-STRING-002 GRA-STRING-003 GRA-UNIONCASE-001 GRA-INTERPOLATED-001 GRA-TYPE-ANNOTATE-001 GRA-VIRTUALCALL-001 GRA-IMMUTABLECOLLECTIONEQUALITY-001 GRA-JSONOPTS-001 GRA-LOGARGFUNCFULLAPP-001 GRA-DISPBEFOREASYNC-001 --exclude-analyzers PartialAppAnalyzer - build-nix: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Install Nix - uses: cachix/install-nix-action@v29 - with: - extra_nix_config: | - access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} - - name: Build - run: nix build - - name: Reproducibility check - run: nix build --rebuild +# build-nix: +# runs-on: ubuntu-latest +# steps: +# - name: Checkout +# uses: actions/checkout@v4 +# - name: Install Nix +# uses: cachix/install-nix-action@v29 +# with: +# extra_nix_config: | +# access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} +# - name: Build +# run: nix build +# - name: Reproducibility check +# run: nix build --rebuild check-dotnet-format: runs-on: ubuntu-latest @@ -139,10 +139,10 @@ jobs: with: extra_nix_config: | access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} - - name: Restore dependencies - run: nix develop --command dotnet restore - - name: Build - run: nix develop --command dotnet build --no-restore --configuration Release + - name: Build source generator + run: nix develop --command dotnet build WoofWare.Whippet/ + - name: Build solution + run: nix develop --command dotnet build --configuration Release - name: Pack run: nix develop --command dotnet pack --configuration Release - name: Upload NuGet artifact (runner) @@ -160,6 +160,26 @@ jobs: with: name: nuget-package-fantomas path: WoofWare.Whippet.Fantomas/bin/Release/WoofWare.Whippet.Fantomas.*.nupkg + - name: Upload NuGet artifact (JSON attrs) + uses: actions/upload-artifact@v4 + with: + name: nuget-package-json-attrs + path: Plugins/Json/WoofWare.Whippet.Plugin.Json.Attributes/bin/Release/WoofWare.Whippet.Plugin.Json.*.nupkg + - name: Upload NuGet artifact (JSON plugin) + uses: actions/upload-artifact@v4 + with: + name: nuget-package-json + path: Plugins/Json/WoofWare.Whippet.Plugin.Json/bin/Release/WoofWare.Whippet.Plugin.Json.*.nupkg + - name: Upload NuGet artifact (argparser attrs) + uses: actions/upload-artifact@v4 + with: + name: nuget-package-argparser-attrs + path: Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Attributes/bin/Release/WoofWare.Whippet.Plugin.ArgParser.*.nupkg + - name: Upload NuGet artifact (argparser plugin) + uses: actions/upload-artifact@v4 + with: + name: nuget-package-argparser + path: Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/bin/Release/WoofWare.Whippet.Plugin.ArgParser.*.nupkg expected-pack: needs: [nuget-pack] @@ -189,9 +209,66 @@ jobs: - name: Check NuGet contents # Verify that there is exactly one nupkg in the artifact that would be NuGet published run: if [[ $(find packed-fantomas -maxdepth 1 -name 'WoofWare.Whippet.Fantomas.*.nupkg' -printf c | wc -c) -ne "1" ]]; then exit 1; fi + - name: Download NuGet artifact (JSON attrs) + uses: actions/download-artifact@v4 + with: + name: nuget-package-json-attrs + path: packed-json-attrs + - name: Check NuGet contents + # Verify that there is exactly one nupkg in the artifact that would be NuGet published + run: if [[ $(find packed-json-attrs -maxdepth 1 -name 'WoofWare.Whippet.Plugin.Json.Attributes.*.nupkg' -printf c | wc -c) -ne "1" ]]; then exit 1; fi + - name: Download NuGet artifact (JSON plugin) + uses: actions/download-artifact@v4 + with: + name: nuget-package-json + path: packed-json + - name: Check NuGet contents + # Verify that there is exactly one nupkg in the artifact that would be NuGet published + run: if [[ $(find packed-json -maxdepth 1 -name 'WoofWare.Whippet.Plugin.Json.*.nupkg' -printf c | wc -c) -ne "1" ]]; then exit 1; fi + - name: Download NuGet artifact (argparser attrs) + uses: actions/download-artifact@v4 + with: + name: nuget-package-argparser-attrs + path: packed-argparser-attrs + - name: Check NuGet contents + # Verify that there is exactly one nupkg in the artifact that would be NuGet published + run: if [[ $(find packed-argparser-attrs -maxdepth 1 -name 'WoofWare.Whippet.Plugin.ArgParser.Attributes.*.nupkg' -printf c | wc -c) -ne "1" ]]; then exit 1; fi + - name: Download NuGet artifact (argparser plugin) + uses: actions/download-artifact@v4 + with: + name: nuget-package-argparser + path: packed-argparser + - name: Check NuGet contents + # Verify that there is exactly one nupkg in the artifact that would be NuGet published + run: if [[ $(find packed-argparser -maxdepth 1 -name 'WoofWare.Whippet.Plugin.ArgParser.*.nupkg' -printf c | wc -c) -ne "1" ]]; then exit 1; fi + + check-accurate-generations: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # so that NerdBank.GitVersioning has access to history + - name: Install Nix + uses: cachix/install-nix-action@v29 + with: + extra_nix_config: | + access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} + - name: Whitespace change to invalidate all caches + run: | + find . -type f -name '*.fs' -exec bash -c "echo ' ' >> '{}'" \; + find . -type f -name 'Generated*.fs' -exec bash -c "rm '{}'" \; + - name: Build Whippet + run: nix develop --command dotnet build WoofWare.Whippet + - name: Generate code + run: nix develop --command dotnet build + - name: Run Fantomas + run: nix run .#fantomas -- . + - name: Verify there is no diff + run: git diff --name-only --no-color --exit-code all-required-checks-complete: - needs: [check-dotnet-format, check-nix-format, build, build-nix, linkcheck, flake-check, nuget-pack, expected-pack, analyzers] + needs: [check-dotnet-format, check-nix-format, build, linkcheck, flake-check, nuget-pack, expected-pack, analyzers, check-accurate-generations] if: ${{ always() }} runs-on: ubuntu-latest steps: @@ -199,6 +276,39 @@ jobs: with: needs-context: ${{ toJSON(needs) }} + nuget-publish: + runs-on: ubuntu-latest + if: ${{ !github.event.repository.fork && github.ref == 'refs/heads/main' }} + needs: [all-required-checks-complete] + environment: main-deploy + permissions: + id-token: write + attestations: write + contents: read + steps: + - uses: actions/checkout@v4 + - name: Install Nix + uses: cachix/install-nix-action@v29 + with: + extra_nix_config: | + access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} + - name: Download NuGet artifact + uses: actions/download-artifact@v4 + with: + name: nuget-package-runner + path: packed + - name: Identify `dotnet` + id: dotnet-identify + run: nix develop --command bash -c 'echo "dotnet=$(which dotnet)" >> $GITHUB_OUTPUT' + - name: Publish to NuGet + id: publish-success + uses: G-Research/common-actions/publish-nuget@2b7dc49cb14f3344fbe6019c14a31165e258c059 + with: + package-name: WoofWare.Whippet + nuget-key: ${{ secrets.NUGET_API_KEY }} + nupkg-dir: packed/ + dotnet: ${{ steps.dotnet-identify.outputs.dotnet }} + nuget-publish-fantomas: runs-on: ubuntu-latest if: ${{ !github.event.repository.fork && github.ref == 'refs/heads/main' }} @@ -232,3 +342,134 @@ jobs: nupkg-dir: packed/ dotnet: ${{ steps.dotnet-identify.outputs.dotnet }} + nuget-publish-json-plugin: + runs-on: ubuntu-latest + if: ${{ !github.event.repository.fork && github.ref == 'refs/heads/main' }} + needs: [all-required-checks-complete] + environment: main-deploy + permissions: + id-token: write + attestations: write + contents: read + steps: + - uses: actions/checkout@v4 + - name: Install Nix + uses: cachix/install-nix-action@v29 + with: + extra_nix_config: | + access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} + - name: Download NuGet artifact + uses: actions/download-artifact@v4 + with: + name: nuget-package-json + path: packed + - name: Identify `dotnet` + id: dotnet-identify + run: nix develop --command bash -c 'echo "dotnet=$(which dotnet)" >> $GITHUB_OUTPUT' + - name: Publish to NuGet + id: publish-success + uses: G-Research/common-actions/publish-nuget@2b7dc49cb14f3344fbe6019c14a31165e258c059 + with: + package-name: WoofWare.Whippet.Plugin.Json + nuget-key: ${{ secrets.NUGET_API_KEY }} + nupkg-dir: packed/ + dotnet: ${{ steps.dotnet-identify.outputs.dotnet }} + + nuget-publish-json-attrs: + runs-on: ubuntu-latest + if: ${{ !github.event.repository.fork && github.ref == 'refs/heads/main' }} + needs: [all-required-checks-complete] + environment: main-deploy + permissions: + id-token: write + attestations: write + contents: read + steps: + - uses: actions/checkout@v4 + - name: Install Nix + uses: cachix/install-nix-action@v29 + with: + extra_nix_config: | + access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} + - name: Download NuGet artifact + uses: actions/download-artifact@v4 + with: + name: nuget-package-json-attrs + path: packed + - name: Identify `dotnet` + id: dotnet-identify + run: nix develop --command bash -c 'echo "dotnet=$(which dotnet)" >> $GITHUB_OUTPUT' + - name: Publish to NuGet + id: publish-success + uses: G-Research/common-actions/publish-nuget@2b7dc49cb14f3344fbe6019c14a31165e258c059 + with: + package-name: WoofWare.Whippet.Plugin.Json.Attributes + nuget-key: ${{ secrets.NUGET_API_KEY }} + nupkg-dir: packed/ + dotnet: ${{ steps.dotnet-identify.outputs.dotnet }} + + nuget-publish-argparser-plugin: + runs-on: ubuntu-latest + if: ${{ !github.event.repository.fork && github.ref == 'refs/heads/main' }} + needs: [all-required-checks-complete] + environment: main-deploy + permissions: + id-token: write + attestations: write + contents: read + steps: + - uses: actions/checkout@v4 + - name: Install Nix + uses: cachix/install-nix-action@v29 + with: + extra_nix_config: | + access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} + - name: Download NuGet artifact + uses: actions/download-artifact@v4 + with: + name: nuget-package-argparser + path: packed + - name: Identify `dotnet` + id: dotnet-identify + run: nix develop --command bash -c 'echo "dotnet=$(which dotnet)" >> $GITHUB_OUTPUT' + - name: Publish to NuGet + id: publish-success + uses: G-Research/common-actions/publish-nuget@2b7dc49cb14f3344fbe6019c14a31165e258c059 + with: + package-name: WoofWare.Whippet.Plugin.ArgParser + nuget-key: ${{ secrets.NUGET_API_KEY }} + nupkg-dir: packed/ + dotnet: ${{ steps.dotnet-identify.outputs.dotnet }} + + nuget-publish-argparser-attrs: + runs-on: ubuntu-latest + if: ${{ !github.event.repository.fork && github.ref == 'refs/heads/main' }} + needs: [all-required-checks-complete] + environment: main-deploy + permissions: + id-token: write + attestations: write + contents: read + steps: + - uses: actions/checkout@v4 + - name: Install Nix + uses: cachix/install-nix-action@v29 + with: + extra_nix_config: | + access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} + - name: Download NuGet artifact + uses: actions/download-artifact@v4 + with: + name: nuget-package-argparser-attrs + path: packed + - name: Identify `dotnet` + id: dotnet-identify + run: nix develop --command bash -c 'echo "dotnet=$(which dotnet)" >> $GITHUB_OUTPUT' + - name: Publish to NuGet + id: publish-success + uses: G-Research/common-actions/publish-nuget@2b7dc49cb14f3344fbe6019c14a31165e258c059 + with: + package-name: WoofWare.Whippet.Plugin.ArgParser.Attributes + nuget-key: ${{ secrets.NUGET_API_KEY }} + nupkg-dir: packed/ + dotnet: ${{ steps.dotnet-identify.outputs.dotnet }} diff --git a/NuGet.config b/NuGet.config new file mode 100644 index 0000000..7f5f908 --- /dev/null +++ b/NuGet.config @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + diff --git a/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Attributes/Attributes.fs b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Attributes/Attributes.fs new file mode 100644 index 0000000..c35fa1d --- /dev/null +++ b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Attributes/Attributes.fs @@ -0,0 +1,96 @@ +namespace WoofWare.Whippet.Plugin.ArgParser + +open System + +/// Attribute indicating a record type to which the "build arg parser" Whippet generator should apply during build. +/// +/// If you supply isExtensionMethod = false, you will get a module rather than extension methods. +/// These can only be consumed from F#, but the benefit is that they don't use up the module name +/// (since by default we create a module called "{TypeName}"). +type ArgParserAttribute (isExtensionMethod : bool) = + inherit Attribute () + + /// The default value of `isExtensionMethod`, the optional argument to the ArgParserAttribute constructor. + static member DefaultIsExtensionMethod = true + + /// Shorthand for the "isExtensionMethod = false" constructor; see documentation there for details. + new () = ArgParserAttribute ArgParserAttribute.DefaultIsExtensionMethod + +/// Attribute indicating that this field shall accumulate all unmatched args, +/// as well as any that appear after a bare `--`. +/// +/// Set `includeFlagLike = true` to include args that begin `--` in the +/// positional args. +/// (By default, `includeFlagLike = false` and we throw when encountering +/// an argument which looks like a flag but which we don't recognise.) +/// We will still interpret `--help` as requesting help, unless it comes after +/// a standalone `--` separator. +type PositionalArgsAttribute (includeFlagLike : bool) = + inherit Attribute () + + /// The default value of `isExtensionMethod`, the optional argument to the ArgParserAttribute constructor. + static member DefaultIncludeFlagLike = false + + /// Shorthand for the "includeFlagLike = false" constructor; see documentation there for details. + new () = PositionalArgsAttribute PositionalArgsAttribute.DefaultIncludeFlagLike + +/// Attribute indicating that this field shall have a default value derived +/// from calling an appropriately named static method on the type. +/// +/// This attribute can only be placed on fields of type `Choice<_, _>` where both type parameters +/// are the same. +/// After a successful parse, the value is Choice1Of2 if the user supplied an input, +/// or Choice2Of2 if the input was obtained by calling the default function. +/// +/// The static method we call for field `FieldName : 'a` is `DefaultFieldName : unit -> 'a`. +type ArgumentDefaultFunctionAttribute () = + inherit Attribute () + +/// Attribute indicating that this field shall have a default value derived +/// from an environment variable (whose name you give in the attribute constructor). +/// +/// This attribute can only be placed on fields of type `Choice<_, _>` where both type parameters +/// are the same. +/// After a successful parse, the value is Choice1Of2 if the user supplied an input, +/// or Choice2Of2 if the input was obtained by pulling a value from `Environment.GetEnvironmentVariable`. +type ArgumentDefaultEnvironmentVariableAttribute (envVar : string) = + inherit Attribute () + +/// Attribute indicating that this field shall have the given help text, when `--help` is invoked +/// or when a parse error causes us to print help text. +type ArgumentHelpTextAttribute (helpText : string) = + inherit Attribute () + +/// Attribute indicating that this field should be parsed with a ParseExact method on its type. +/// For example, on a TimeSpan field, with [], we will call +/// `TimeSpan.ParseExact (s, @"hh\:mm\:ss", CultureInfo.CurrentCulture). +type ParseExactAttribute (format : string) = + inherit Attribute () + +/// Attribute indicating that this field should be parsed in the invariant culture, rather than the +/// default current culture. +/// For example, on a TimeSpan field, with [] and [], we will call +/// `TimeSpan.ParseExact (s, @"hh\:mm\:ss", CultureInfo.InvariantCulture). +type InvariantCultureAttribute () = + inherit Attribute () + +/// Attribute placed on a field of a two-case no-data discriminated union, indicating that this is "basically a bool". +/// For example: `type DryRun = | [] Dry | [] Wet` +/// A record with `{ DryRun : DryRun }` will then be parsed like `{ DryRun : bool }` (so the user supplies `--dry-run`), +/// but that you get this strongly-typed value directly in the code (so you `match args.DryRun with | DryRun.Dry ...`). +/// +/// You must put this attribute on both cases of the discriminated union, with opposite values in each case. +type ArgumentFlagAttribute (flagValue : bool) = + inherit Attribute () + +/// Attribute placed on a field of a record to specify a different long form from the default. If you place this +/// attribute, you won't get the default: ArgFoo would normally be expressed as `--arg-foo`, but if you instead +/// say `[]` or `[]`, you instead use `--thingy-blah` +/// or `--thingy` respectively. +/// +/// You can place this argument multiple times. +/// +/// Omit the initial `--` that you expect the user to type. +[] +type ArgumentLongForm (s : string) = + inherit Attribute () diff --git a/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Attributes/README.md b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Attributes/README.md new file mode 100644 index 0000000..d0c5620 --- /dev/null +++ b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Attributes/README.md @@ -0,0 +1,5 @@ +# WoofWare.Whippet.Plugin.ArgParser.Attributes + +This is a very slim runtime dependency we expect consumers of WoofWare.Whippet.Plugin.ArgParser to take. +This dependency contains attributes which control that source generator. +Please see WoofWare.Whippet.Plugin.ArgParser's README for further information. diff --git a/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Attributes/SurfaceBaseline.txt b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Attributes/SurfaceBaseline.txt new file mode 100644 index 0000000..d6fefe6 --- /dev/null +++ b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Attributes/SurfaceBaseline.txt @@ -0,0 +1,24 @@ +WoofWare.Whippet.Plugin.ArgParser.ArgParserAttribute inherit System.Attribute +WoofWare.Whippet.Plugin.ArgParser.ArgParserAttribute..ctor [constructor]: bool +WoofWare.Whippet.Plugin.ArgParser.ArgParserAttribute..ctor [constructor]: unit +WoofWare.Whippet.Plugin.ArgParser.ArgParserAttribute.DefaultIsExtensionMethod [static property]: [read-only] bool +WoofWare.Whippet.Plugin.ArgParser.ArgParserAttribute.get_DefaultIsExtensionMethod [static method]: unit -> bool +WoofWare.Whippet.Plugin.ArgParser.ArgumentDefaultEnvironmentVariableAttribute inherit System.Attribute +WoofWare.Whippet.Plugin.ArgParser.ArgumentDefaultEnvironmentVariableAttribute..ctor [constructor]: string +WoofWare.Whippet.Plugin.ArgParser.ArgumentDefaultFunctionAttribute inherit System.Attribute +WoofWare.Whippet.Plugin.ArgParser.ArgumentDefaultFunctionAttribute..ctor [constructor]: unit +WoofWare.Whippet.Plugin.ArgParser.ArgumentFlagAttribute inherit System.Attribute +WoofWare.Whippet.Plugin.ArgParser.ArgumentFlagAttribute..ctor [constructor]: bool +WoofWare.Whippet.Plugin.ArgParser.ArgumentHelpTextAttribute inherit System.Attribute +WoofWare.Whippet.Plugin.ArgParser.ArgumentHelpTextAttribute..ctor [constructor]: string +WoofWare.Whippet.Plugin.ArgParser.ArgumentLongForm inherit System.Attribute +WoofWare.Whippet.Plugin.ArgParser.ArgumentLongForm..ctor [constructor]: string +WoofWare.Whippet.Plugin.ArgParser.InvariantCultureAttribute inherit System.Attribute +WoofWare.Whippet.Plugin.ArgParser.InvariantCultureAttribute..ctor [constructor]: unit +WoofWare.Whippet.Plugin.ArgParser.ParseExactAttribute inherit System.Attribute +WoofWare.Whippet.Plugin.ArgParser.ParseExactAttribute..ctor [constructor]: string +WoofWare.Whippet.Plugin.ArgParser.PositionalArgsAttribute inherit System.Attribute +WoofWare.Whippet.Plugin.ArgParser.PositionalArgsAttribute..ctor [constructor]: bool +WoofWare.Whippet.Plugin.ArgParser.PositionalArgsAttribute..ctor [constructor]: unit +WoofWare.Whippet.Plugin.ArgParser.PositionalArgsAttribute.DefaultIncludeFlagLike [static property]: [read-only] bool +WoofWare.Whippet.Plugin.ArgParser.PositionalArgsAttribute.get_DefaultIncludeFlagLike [static method]: unit -> bool \ No newline at end of file diff --git a/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Attributes/WoofWare.Whippet.Plugin.ArgParser.Attributes.fsproj b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Attributes/WoofWare.Whippet.Plugin.ArgParser.Attributes.fsproj new file mode 100644 index 0000000..159e473 --- /dev/null +++ b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Attributes/WoofWare.Whippet.Plugin.ArgParser.Attributes.fsproj @@ -0,0 +1,33 @@ + + + + netstandard2.0 + true + Patrick Stevens + Copyright (c) Patrick Stevens 2024 + Attributes to accompany the WoofWare.Whippet.Plugin.ArgParser source generator, to indicate what you want your types to be doing. + git + https://github.com/Smaug123/WoofWare.Whippet + MIT + README.md + fsharp;source-generator;source-gen;whippet;arguments;arg-parser + true + FS3559 + WoofWare.Whippet.Plugin.ArgParser.Attributes + + + + + + + + True + / + README.md + + + + + + + diff --git a/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Attributes/version.json b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Attributes/version.json new file mode 100644 index 0000000..47ae36c --- /dev/null +++ b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Attributes/version.json @@ -0,0 +1,11 @@ +{ + "version": "0.2", + "publicReleaseRefSpec": [ + "^refs/heads/main$" + ], + "pathFilters": [ + "./", + ":/global.json", + ":/Directory.Build.props" + ] +} \ No newline at end of file diff --git a/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Consumer/Args.fs b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Consumer/Args.fs new file mode 100644 index 0000000..a64e62f --- /dev/null +++ b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Consumer/Args.fs @@ -0,0 +1,237 @@ +namespace Consumer + +open System +open System.IO +open WoofWare.Whippet.Plugin.ArgParser + +[] +type BasicNoPositionals = + { + Foo : int + Bar : string + Baz : bool + Rest : int list + } + +[] +type Basic = + { + [] + Foo : int + Bar : string + Baz : bool + [] + [] + Rest : string list + } + +[] +type BasicWithIntPositionals = + { + Foo : int + Bar : string + Baz : bool + [] + Rest : int list + } + +[] +type LoadsOfTypes = + { + Foo : int + Bar : string + Baz : bool + SomeFile : FileInfo + SomeDirectory : DirectoryInfo + SomeList : DirectoryInfo list + OptionalThingWithNoDefault : int option + [] + Positionals : int list + [] + OptionalThing : Choice + [] + AnotherOptionalThing : Choice + [] + YetAnotherOptionalThing : Choice + } + + static member DefaultOptionalThing () = true + + static member DefaultAnotherOptionalThing () = 3 + +[] +type LoadsOfTypesNoPositionals = + { + Foo : int + Bar : string + Baz : bool + SomeFile : FileInfo + SomeDirectory : DirectoryInfo + SomeList : DirectoryInfo list + OptionalThingWithNoDefault : int option + [] + OptionalThing : Choice + [] + AnotherOptionalThing : Choice + [] + YetAnotherOptionalThing : Choice + } + + static member DefaultOptionalThing () = false + + static member DefaultAnotherOptionalThing () = 3 + +[] +type DatesAndTimes = + { + Plain : TimeSpan + [] + Invariant : TimeSpan + [] + [] + Exact : TimeSpan + [] + InvariantExact : TimeSpan + } + +type ChildRecord = + { + Thing1 : int + Thing2 : string + } + +[] +type ParentRecord = + { + Child : ChildRecord + AndAnother : bool + } + +type ChildRecordWithPositional = + { + Thing1 : int + [] + Thing2 : Uri list + } + +[] +type ParentRecordChildPos = + { + Child : ChildRecordWithPositional + AndAnother : bool + } + +[] +type ParentRecordSelfPos = + { + Child : ChildRecord + [] + AndAnother : bool list + } + +[] +type ChoicePositionals = + { + [] + Args : Choice list + } + +[] +type ContainsBoolEnvVar = + { + [] + BoolVar : Choice + } + +[] +module Consts = + [] + let FALSE = false + + [] + let TRUE = true + +type DryRunMode = + | [] Wet + | [] Dry + +[] +type WithFlagDu = + { + DryRun : DryRunMode + } + +[] +type ContainsFlagEnvVar = + { + // This phrasing is odd, but it's for a test. Nobody's really going to have `--dry-run` + // controlled by an env var! + [] + DryRun : Choice + } + +[] +type ContainsFlagDefaultValue = + { + [] + DryRun : Choice + } + + static member DefaultDryRun () = DryRunMode.Wet + +[] +type ManyLongForms = + { + [] + [] + DoTheThing : string + + [] + [] + SomeFlag : bool + } + +[] +type private IrrelevantDu = + | Foo + | Bar + +[] +type FlagsIntoPositionalArgs = + { + A : string + [] + GrabEverything : string list + } + +[] +type FlagsIntoPositionalArgsChoice = + { + A : string + [] + GrabEverything : Choice list + } + +[] +type FlagsIntoPositionalArgsInt = + { + A : string + [] + GrabEverything : int list + } + +[] +type FlagsIntoPositionalArgsIntChoice = + { + A : string + [] + GrabEverything : Choice list + } + +[] +type FlagsIntoPositionalArgs' = + { + A : string + [] + DontGrabEverything : string list + } diff --git a/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Consumer/GeneratedArgs.fs b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Consumer/GeneratedArgs.fs new file mode 100644 index 0000000..44b4a7a --- /dev/null +++ b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Consumer/GeneratedArgs.fs @@ -0,0 +1,4403 @@ +namespace Consumer + +open System +open System.IO +open WoofWare.Whippet.Plugin.ArgParser + +/// Methods to parse arguments for the type BasicNoPositionals +[] +module BasicNoPositionalsArgParse = + type private ParseState_BasicNoPositionals = + /// Ready to consume a key or positional arg + | AwaitingKey + /// Waiting to receive a value for the key we've already consumed + | AwaitingValue of key : string + + /// Extension methods for argument parsing + type BasicNoPositionals with + + static member parse' (getEnvironmentVariable : string -> string) (args : string list) : BasicNoPositionals = + let ArgParser_errors = ResizeArray () + + let helpText () = + [ + (sprintf "%s int32%s%s" (sprintf "--%s" "foo") "" "") + (sprintf "%s string%s%s" (sprintf "--%s" "bar") "" "") + (sprintf "%s bool%s%s" (sprintf "--%s" "baz") "" "") + (sprintf "%s int32%s%s" (sprintf "--%s" "rest") " (can be repeated)" "") + ] + |> String.concat "\n" + + let parser_LeftoverArgs : string ResizeArray = ResizeArray () + let mutable arg_0 : int option = None + let mutable arg_1 : string option = None + let mutable arg_2 : bool option = None + let arg_3 : int ResizeArray = ResizeArray () + + /// Processes the key-value pair, returning Error if no key was matched. + /// If the key is an arg which can have arity 1, but throws when consuming that arg, we return Error(). + /// This can nevertheless be a successful parse, e.g. when the key may have arity 0. + let processKeyValue (key : string) (value : string) : Result = + if System.String.Equals (key, sprintf "--%s" "rest", System.StringComparison.OrdinalIgnoreCase) then + value |> (fun x -> System.Int32.Parse x) |> arg_3.Add + () |> Ok + else if System.String.Equals (key, sprintf "--%s" "baz", System.StringComparison.OrdinalIgnoreCase) then + match arg_2 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "baz") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_2 <- value |> (fun x -> System.Boolean.Parse x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if System.String.Equals (key, sprintf "--%s" "bar", System.StringComparison.OrdinalIgnoreCase) then + match arg_1 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "bar") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_1 <- value |> (fun x -> x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if System.String.Equals (key, sprintf "--%s" "foo", System.StringComparison.OrdinalIgnoreCase) then + match arg_0 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "foo") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_0 <- value |> (fun x -> System.Int32.Parse x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else + Error None + + /// Returns false if we didn't set a value. + let setFlagValue (key : string) : bool = + if System.String.Equals (key, sprintf "--%s" "baz", System.StringComparison.OrdinalIgnoreCase) then + match arg_2 with + | Some x -> + sprintf "Flag '%s' was supplied multiple times" (sprintf "--%s" "baz") + |> ArgParser_errors.Add + + true + | None -> + arg_2 <- true |> Some + true + else + false + + let rec go (state : ParseState_BasicNoPositionals) (args : string list) = + match args with + | [] -> + match state with + | ParseState_BasicNoPositionals.AwaitingKey -> () + | ParseState_BasicNoPositionals.AwaitingValue key -> + if setFlagValue key then + () + else + sprintf + "Trailing argument %s had no value. Use a double-dash to separate positional args from key-value args." + key + |> ArgParser_errors.Add + | "--" :: rest -> parser_LeftoverArgs.AddRange (rest |> Seq.map (fun x -> x)) + | arg :: args -> + match state with + | ParseState_BasicNoPositionals.AwaitingKey -> + if arg.StartsWith ("--", System.StringComparison.Ordinal) then + if arg = "--help" then + helpText () |> failwithf "Help text requested.\n%s" + else + let equals = arg.IndexOf (char 61) + + if equals < 0 then + args |> go (ParseState_BasicNoPositionals.AwaitingValue arg) + else + let key = arg.[0 .. equals - 1] + let value = arg.[equals + 1 ..] + + match processKeyValue key value with + | Ok () -> go ParseState_BasicNoPositionals.AwaitingKey args + | Error x -> + match x with + | None -> + failwithf + "Unable to process argument %s as key %s and value %s" + arg + key + value + | Some msg -> + sprintf "%s (at arg %s)" msg arg |> ArgParser_errors.Add + go ParseState_BasicNoPositionals.AwaitingKey args + else + arg |> (fun x -> x) |> parser_LeftoverArgs.Add + go ParseState_BasicNoPositionals.AwaitingKey args + | ParseState_BasicNoPositionals.AwaitingValue key -> + match processKeyValue key arg with + | Ok () -> go ParseState_BasicNoPositionals.AwaitingKey args + | Error exc -> + if setFlagValue key then + go ParseState_BasicNoPositionals.AwaitingKey (arg :: args) + else + match exc with + | None -> + failwithf + "Unable to process supplied arg %s. Help text follows.\n%s" + key + (helpText ()) + | Some msg -> msg |> ArgParser_errors.Add + + go ParseState_BasicNoPositionals.AwaitingKey args + + let parser_LeftoverArgs = + if 0 = parser_LeftoverArgs.Count then + () + else + parser_LeftoverArgs + |> String.concat " " + |> sprintf "There were leftover args: %s" + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + + let arg_0 = + match arg_0 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "foo") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + let arg_1 = + match arg_1 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "bar") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + let arg_2 = + match arg_2 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "baz") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + let arg_3 = arg_3 |> Seq.toList + + if 0 = ArgParser_errors.Count then + { + Bar = arg_1 + Baz = arg_2 + Foo = arg_0 + Rest = arg_3 + } + else + ArgParser_errors |> String.concat "\n" |> failwithf "Errors during parse!\n%s" + + static member parse (args : string list) : BasicNoPositionals = + BasicNoPositionals.parse' System.Environment.GetEnvironmentVariable args +namespace Consumer + +open System +open System.IO +open WoofWare.Whippet.Plugin.ArgParser + +/// Methods to parse arguments for the type Basic +[] +module BasicArgParse = + type private ParseState_Basic = + /// Ready to consume a key or positional arg + | AwaitingKey + /// Waiting to receive a value for the key we've already consumed + | AwaitingValue of key : string + + /// Extension methods for argument parsing + type Basic with + + static member parse' (getEnvironmentVariable : string -> string) (args : string list) : Basic = + let ArgParser_errors = ResizeArray () + + let helpText () = + [ + (sprintf "%s int32%s%s" (sprintf "--%s" "foo") "" (sprintf " : %s" ("This is a foo!"))) + (sprintf "%s string%s%s" (sprintf "--%s" "bar") "" "") + (sprintf "%s bool%s%s" (sprintf "--%s" "baz") "" "") + (sprintf + "%s string%s%s" + (sprintf "--%s" "rest") + " (positional args) (can be repeated)" + (sprintf " : %s" ("Here's where the rest of the args go"))) + ] + |> String.concat "\n" + + let arg_3 : string ResizeArray = ResizeArray () + let mutable arg_0 : int option = None + let mutable arg_1 : string option = None + let mutable arg_2 : bool option = None + + /// Processes the key-value pair, returning Error if no key was matched. + /// If the key is an arg which can have arity 1, but throws when consuming that arg, we return Error(). + /// This can nevertheless be a successful parse, e.g. when the key may have arity 0. + let processKeyValue (key : string) (value : string) : Result = + if System.String.Equals (key, sprintf "--%s" "baz", System.StringComparison.OrdinalIgnoreCase) then + match arg_2 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "baz") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_2 <- value |> (fun x -> System.Boolean.Parse x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if System.String.Equals (key, sprintf "--%s" "bar", System.StringComparison.OrdinalIgnoreCase) then + match arg_1 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "bar") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_1 <- value |> (fun x -> x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if System.String.Equals (key, sprintf "--%s" "foo", System.StringComparison.OrdinalIgnoreCase) then + match arg_0 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "foo") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_0 <- value |> (fun x -> System.Int32.Parse x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals (key, sprintf "--%s" "rest", System.StringComparison.OrdinalIgnoreCase) + then + value |> (fun x -> x) |> arg_3.Add + () |> Ok + else + Error None + + /// Returns false if we didn't set a value. + let setFlagValue (key : string) : bool = + if System.String.Equals (key, sprintf "--%s" "baz", System.StringComparison.OrdinalIgnoreCase) then + match arg_2 with + | Some x -> + sprintf "Flag '%s' was supplied multiple times" (sprintf "--%s" "baz") + |> ArgParser_errors.Add + + true + | None -> + arg_2 <- true |> Some + true + else + false + + let rec go (state : ParseState_Basic) (args : string list) = + match args with + | [] -> + match state with + | ParseState_Basic.AwaitingKey -> () + | ParseState_Basic.AwaitingValue key -> + if setFlagValue key then + () + else + sprintf + "Trailing argument %s had no value. Use a double-dash to separate positional args from key-value args." + key + |> ArgParser_errors.Add + | "--" :: rest -> arg_3.AddRange (rest |> Seq.map (fun x -> x)) + | arg :: args -> + match state with + | ParseState_Basic.AwaitingKey -> + if arg.StartsWith ("--", System.StringComparison.Ordinal) then + if arg = "--help" then + helpText () |> failwithf "Help text requested.\n%s" + else + let equals = arg.IndexOf (char 61) + + if equals < 0 then + args |> go (ParseState_Basic.AwaitingValue arg) + else + let key = arg.[0 .. equals - 1] + let value = arg.[equals + 1 ..] + + match processKeyValue key value with + | Ok () -> go ParseState_Basic.AwaitingKey args + | Error x -> + match x with + | None -> + failwithf + "Unable to process argument %s as key %s and value %s" + arg + key + value + | Some msg -> + sprintf "%s (at arg %s)" msg arg |> ArgParser_errors.Add + go ParseState_Basic.AwaitingKey args + else + arg |> (fun x -> x) |> arg_3.Add + go ParseState_Basic.AwaitingKey args + | ParseState_Basic.AwaitingValue key -> + match processKeyValue key arg with + | Ok () -> go ParseState_Basic.AwaitingKey args + | Error exc -> + if setFlagValue key then + go ParseState_Basic.AwaitingKey (arg :: args) + else + match exc with + | None -> + failwithf + "Unable to process supplied arg %s. Help text follows.\n%s" + key + (helpText ()) + | Some msg -> msg |> ArgParser_errors.Add + + go ParseState_Basic.AwaitingKey args + let arg_3 = arg_3 |> Seq.toList + + let arg_0 = + match arg_0 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "foo") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + let arg_1 = + match arg_1 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "bar") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + let arg_2 = + match arg_2 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "baz") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + if 0 = ArgParser_errors.Count then + { + Bar = arg_1 + Baz = arg_2 + Foo = arg_0 + Rest = arg_3 + } + else + ArgParser_errors |> String.concat "\n" |> failwithf "Errors during parse!\n%s" + + static member parse (args : string list) : Basic = + Basic.parse' System.Environment.GetEnvironmentVariable args +namespace Consumer + +open System +open System.IO +open WoofWare.Whippet.Plugin.ArgParser + +/// Methods to parse arguments for the type BasicWithIntPositionals +[] +module BasicWithIntPositionalsArgParse = + type private ParseState_BasicWithIntPositionals = + /// Ready to consume a key or positional arg + | AwaitingKey + /// Waiting to receive a value for the key we've already consumed + | AwaitingValue of key : string + + /// Extension methods for argument parsing + type BasicWithIntPositionals with + + static member parse' + (getEnvironmentVariable : string -> string) + (args : string list) + : BasicWithIntPositionals + = + let ArgParser_errors = ResizeArray () + + let helpText () = + [ + (sprintf "%s int32%s%s" (sprintf "--%s" "foo") "" "") + (sprintf "%s string%s%s" (sprintf "--%s" "bar") "" "") + (sprintf "%s bool%s%s" (sprintf "--%s" "baz") "" "") + (sprintf "%s int32%s%s" (sprintf "--%s" "rest") " (positional args) (can be repeated)" "") + ] + |> String.concat "\n" + + let arg_3 : int ResizeArray = ResizeArray () + let mutable arg_0 : int option = None + let mutable arg_1 : string option = None + let mutable arg_2 : bool option = None + + /// Processes the key-value pair, returning Error if no key was matched. + /// If the key is an arg which can have arity 1, but throws when consuming that arg, we return Error(). + /// This can nevertheless be a successful parse, e.g. when the key may have arity 0. + let processKeyValue (key : string) (value : string) : Result = + if System.String.Equals (key, sprintf "--%s" "baz", System.StringComparison.OrdinalIgnoreCase) then + match arg_2 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "baz") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_2 <- value |> (fun x -> System.Boolean.Parse x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if System.String.Equals (key, sprintf "--%s" "bar", System.StringComparison.OrdinalIgnoreCase) then + match arg_1 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "bar") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_1 <- value |> (fun x -> x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if System.String.Equals (key, sprintf "--%s" "foo", System.StringComparison.OrdinalIgnoreCase) then + match arg_0 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "foo") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_0 <- value |> (fun x -> System.Int32.Parse x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals (key, sprintf "--%s" "rest", System.StringComparison.OrdinalIgnoreCase) + then + value |> (fun x -> System.Int32.Parse x) |> arg_3.Add + () |> Ok + else + Error None + + /// Returns false if we didn't set a value. + let setFlagValue (key : string) : bool = + if System.String.Equals (key, sprintf "--%s" "baz", System.StringComparison.OrdinalIgnoreCase) then + match arg_2 with + | Some x -> + sprintf "Flag '%s' was supplied multiple times" (sprintf "--%s" "baz") + |> ArgParser_errors.Add + + true + | None -> + arg_2 <- true |> Some + true + else + false + + let rec go (state : ParseState_BasicWithIntPositionals) (args : string list) = + match args with + | [] -> + match state with + | ParseState_BasicWithIntPositionals.AwaitingKey -> () + | ParseState_BasicWithIntPositionals.AwaitingValue key -> + if setFlagValue key then + () + else + sprintf + "Trailing argument %s had no value. Use a double-dash to separate positional args from key-value args." + key + |> ArgParser_errors.Add + | "--" :: rest -> arg_3.AddRange (rest |> Seq.map (fun x -> System.Int32.Parse x)) + | arg :: args -> + match state with + | ParseState_BasicWithIntPositionals.AwaitingKey -> + if arg.StartsWith ("--", System.StringComparison.Ordinal) then + if arg = "--help" then + helpText () |> failwithf "Help text requested.\n%s" + else + let equals = arg.IndexOf (char 61) + + if equals < 0 then + args |> go (ParseState_BasicWithIntPositionals.AwaitingValue arg) + else + let key = arg.[0 .. equals - 1] + let value = arg.[equals + 1 ..] + + match processKeyValue key value with + | Ok () -> go ParseState_BasicWithIntPositionals.AwaitingKey args + | Error x -> + match x with + | None -> + failwithf + "Unable to process argument %s as key %s and value %s" + arg + key + value + | Some msg -> + sprintf "%s (at arg %s)" msg arg |> ArgParser_errors.Add + go ParseState_BasicWithIntPositionals.AwaitingKey args + else + arg |> (fun x -> System.Int32.Parse x) |> arg_3.Add + go ParseState_BasicWithIntPositionals.AwaitingKey args + | ParseState_BasicWithIntPositionals.AwaitingValue key -> + match processKeyValue key arg with + | Ok () -> go ParseState_BasicWithIntPositionals.AwaitingKey args + | Error exc -> + if setFlagValue key then + go ParseState_BasicWithIntPositionals.AwaitingKey (arg :: args) + else + match exc with + | None -> + failwithf + "Unable to process supplied arg %s. Help text follows.\n%s" + key + (helpText ()) + | Some msg -> msg |> ArgParser_errors.Add + + go ParseState_BasicWithIntPositionals.AwaitingKey args + let arg_3 = arg_3 |> Seq.toList + + let arg_0 = + match arg_0 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "foo") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + let arg_1 = + match arg_1 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "bar") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + let arg_2 = + match arg_2 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "baz") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + if 0 = ArgParser_errors.Count then + { + Bar = arg_1 + Baz = arg_2 + Foo = arg_0 + Rest = arg_3 + } + else + ArgParser_errors |> String.concat "\n" |> failwithf "Errors during parse!\n%s" + + static member parse (args : string list) : BasicWithIntPositionals = + BasicWithIntPositionals.parse' System.Environment.GetEnvironmentVariable args +namespace Consumer + +open System +open System.IO +open WoofWare.Whippet.Plugin.ArgParser + +/// Methods to parse arguments for the type LoadsOfTypes +[] +module LoadsOfTypesArgParse = + type private ParseState_LoadsOfTypes = + /// Ready to consume a key or positional arg + | AwaitingKey + /// Waiting to receive a value for the key we've already consumed + | AwaitingValue of key : string + + /// Extension methods for argument parsing + type LoadsOfTypes with + + static member parse' (getEnvironmentVariable : string -> string) (args : string list) : LoadsOfTypes = + let ArgParser_errors = ResizeArray () + + let helpText () = + [ + (sprintf "%s int32%s%s" (sprintf "--%s" "foo") "" "") + (sprintf "%s string%s%s" (sprintf "--%s" "bar") "" "") + (sprintf "%s bool%s%s" (sprintf "--%s" "baz") "" "") + (sprintf "%s FileInfo%s%s" (sprintf "--%s" "some-file") "" "") + (sprintf "%s DirectoryInfo%s%s" (sprintf "--%s" "some-directory") "" "") + (sprintf "%s DirectoryInfo%s%s" (sprintf "--%s" "some-list") " (can be repeated)" "") + (sprintf "%s int32%s%s" (sprintf "--%s" "optional-thing-with-no-default") " (optional)" "") + + (sprintf + "%s bool%s%s" + (sprintf "--%s" "optional-thing") + (LoadsOfTypes.DefaultOptionalThing () + |> (fun x -> x.ToString ()) + |> sprintf " (default value: %s)") + "") + + (sprintf + "%s int32%s%s" + (sprintf "--%s" "another-optional-thing") + (LoadsOfTypes.DefaultAnotherOptionalThing () + |> (fun x -> x.ToString ()) + |> sprintf " (default value: %s)") + "") + + (sprintf + "%s string%s%s" + (sprintf "--%s" "yet-another-optional-thing") + ("CONSUMEPLUGIN_THINGS" |> sprintf " (default value populated from env var %s)") + "") + (sprintf "%s int32%s%s" (sprintf "--%s" "positionals") " (positional args) (can be repeated)" "") + ] + |> String.concat "\n" + + let arg_7 : int ResizeArray = ResizeArray () + let mutable arg_0 : int option = None + let mutable arg_1 : string option = None + let mutable arg_2 : bool option = None + let mutable arg_3 : FileInfo option = None + let mutable arg_4 : DirectoryInfo option = None + let arg_5 : DirectoryInfo ResizeArray = ResizeArray () + let mutable arg_6 : int option = None + let mutable arg_8 : bool option = None + let mutable arg_9 : int option = None + let mutable arg_10 : string option = None + + /// Processes the key-value pair, returning Error if no key was matched. + /// If the key is an arg which can have arity 1, but throws when consuming that arg, we return Error(). + /// This can nevertheless be a successful parse, e.g. when the key may have arity 0. + let processKeyValue (key : string) (value : string) : Result = + if + System.String.Equals ( + key, + sprintf "--%s" "yet-another-optional-thing", + System.StringComparison.OrdinalIgnoreCase + ) + then + match arg_10 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "yet-another-optional-thing") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_10 <- value |> (fun x -> x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals ( + key, + sprintf "--%s" "another-optional-thing", + System.StringComparison.OrdinalIgnoreCase + ) + then + match arg_9 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "another-optional-thing") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_9 <- value |> (fun x -> System.Int32.Parse x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals ( + key, + sprintf "--%s" "optional-thing", + System.StringComparison.OrdinalIgnoreCase + ) + then + match arg_8 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "optional-thing") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_8 <- value |> (fun x -> System.Boolean.Parse x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals ( + key, + sprintf "--%s" "optional-thing-with-no-default", + System.StringComparison.OrdinalIgnoreCase + ) + then + match arg_6 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "optional-thing-with-no-default") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_6 <- value |> (fun x -> System.Int32.Parse x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals (key, sprintf "--%s" "some-list", System.StringComparison.OrdinalIgnoreCase) + then + value |> (fun x -> System.IO.DirectoryInfo x) |> arg_5.Add + () |> Ok + else if + System.String.Equals ( + key, + sprintf "--%s" "some-directory", + System.StringComparison.OrdinalIgnoreCase + ) + then + match arg_4 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "some-directory") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_4 <- value |> (fun x -> System.IO.DirectoryInfo x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals (key, sprintf "--%s" "some-file", System.StringComparison.OrdinalIgnoreCase) + then + match arg_3 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "some-file") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_3 <- value |> (fun x -> System.IO.FileInfo x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if System.String.Equals (key, sprintf "--%s" "baz", System.StringComparison.OrdinalIgnoreCase) then + match arg_2 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "baz") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_2 <- value |> (fun x -> System.Boolean.Parse x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if System.String.Equals (key, sprintf "--%s" "bar", System.StringComparison.OrdinalIgnoreCase) then + match arg_1 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "bar") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_1 <- value |> (fun x -> x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if System.String.Equals (key, sprintf "--%s" "foo", System.StringComparison.OrdinalIgnoreCase) then + match arg_0 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "foo") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_0 <- value |> (fun x -> System.Int32.Parse x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals (key, sprintf "--%s" "positionals", System.StringComparison.OrdinalIgnoreCase) + then + value |> (fun x -> System.Int32.Parse x) |> arg_7.Add + () |> Ok + else + Error None + + /// Returns false if we didn't set a value. + let setFlagValue (key : string) : bool = + if + System.String.Equals ( + key, + sprintf "--%s" "optional-thing", + System.StringComparison.OrdinalIgnoreCase + ) + then + match arg_8 with + | Some x -> + sprintf "Flag '%s' was supplied multiple times" (sprintf "--%s" "optional-thing") + |> ArgParser_errors.Add + + true + | None -> + arg_8 <- true |> Some + true + else if System.String.Equals (key, sprintf "--%s" "baz", System.StringComparison.OrdinalIgnoreCase) then + match arg_2 with + | Some x -> + sprintf "Flag '%s' was supplied multiple times" (sprintf "--%s" "baz") + |> ArgParser_errors.Add + + true + | None -> + arg_2 <- true |> Some + true + else + false + + let rec go (state : ParseState_LoadsOfTypes) (args : string list) = + match args with + | [] -> + match state with + | ParseState_LoadsOfTypes.AwaitingKey -> () + | ParseState_LoadsOfTypes.AwaitingValue key -> + if setFlagValue key then + () + else + sprintf + "Trailing argument %s had no value. Use a double-dash to separate positional args from key-value args." + key + |> ArgParser_errors.Add + | "--" :: rest -> arg_7.AddRange (rest |> Seq.map (fun x -> System.Int32.Parse x)) + | arg :: args -> + match state with + | ParseState_LoadsOfTypes.AwaitingKey -> + if arg.StartsWith ("--", System.StringComparison.Ordinal) then + if arg = "--help" then + helpText () |> failwithf "Help text requested.\n%s" + else + let equals = arg.IndexOf (char 61) + + if equals < 0 then + args |> go (ParseState_LoadsOfTypes.AwaitingValue arg) + else + let key = arg.[0 .. equals - 1] + let value = arg.[equals + 1 ..] + + match processKeyValue key value with + | Ok () -> go ParseState_LoadsOfTypes.AwaitingKey args + | Error x -> + match x with + | None -> + failwithf + "Unable to process argument %s as key %s and value %s" + arg + key + value + | Some msg -> + sprintf "%s (at arg %s)" msg arg |> ArgParser_errors.Add + go ParseState_LoadsOfTypes.AwaitingKey args + else + arg |> (fun x -> System.Int32.Parse x) |> arg_7.Add + go ParseState_LoadsOfTypes.AwaitingKey args + | ParseState_LoadsOfTypes.AwaitingValue key -> + match processKeyValue key arg with + | Ok () -> go ParseState_LoadsOfTypes.AwaitingKey args + | Error exc -> + if setFlagValue key then + go ParseState_LoadsOfTypes.AwaitingKey (arg :: args) + else + match exc with + | None -> + failwithf + "Unable to process supplied arg %s. Help text follows.\n%s" + key + (helpText ()) + | Some msg -> msg |> ArgParser_errors.Add + + go ParseState_LoadsOfTypes.AwaitingKey args + let arg_7 = arg_7 |> Seq.toList + + let arg_0 = + match arg_0 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "foo") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + let arg_1 = + match arg_1 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "bar") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + let arg_2 = + match arg_2 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "baz") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + let arg_3 = + match arg_3 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "some-file") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + let arg_4 = + match arg_4 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "some-directory") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + let arg_5 = arg_5 |> Seq.toList + let arg_6 = arg_6 + + let arg_8 = + match arg_8 with + | None -> LoadsOfTypes.DefaultOptionalThing () |> Choice2Of2 + | Some x -> Choice1Of2 x + + let arg_9 = + match arg_9 with + | None -> LoadsOfTypes.DefaultAnotherOptionalThing () |> Choice2Of2 + | Some x -> Choice1Of2 x + + let arg_10 = + match arg_10 with + | None -> + match "CONSUMEPLUGIN_THINGS" |> getEnvironmentVariable with + | null -> + sprintf + "No value was supplied for %s, nor was environment variable %s set" + (sprintf "--%s" "yet-another-optional-thing") + "CONSUMEPLUGIN_THINGS" + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | x -> x |> (fun x -> x) + |> Choice2Of2 + | Some x -> Choice1Of2 x + + if 0 = ArgParser_errors.Count then + { + AnotherOptionalThing = arg_9 + Bar = arg_1 + Baz = arg_2 + Foo = arg_0 + OptionalThing = arg_8 + OptionalThingWithNoDefault = arg_6 + Positionals = arg_7 + SomeDirectory = arg_4 + SomeFile = arg_3 + SomeList = arg_5 + YetAnotherOptionalThing = arg_10 + } + else + ArgParser_errors |> String.concat "\n" |> failwithf "Errors during parse!\n%s" + + static member parse (args : string list) : LoadsOfTypes = + LoadsOfTypes.parse' System.Environment.GetEnvironmentVariable args +namespace Consumer + +open System +open System.IO +open WoofWare.Whippet.Plugin.ArgParser + +/// Methods to parse arguments for the type LoadsOfTypesNoPositionals +[] +module LoadsOfTypesNoPositionalsArgParse = + type private ParseState_LoadsOfTypesNoPositionals = + /// Ready to consume a key or positional arg + | AwaitingKey + /// Waiting to receive a value for the key we've already consumed + | AwaitingValue of key : string + + /// Extension methods for argument parsing + type LoadsOfTypesNoPositionals with + + static member parse' + (getEnvironmentVariable : string -> string) + (args : string list) + : LoadsOfTypesNoPositionals + = + let ArgParser_errors = ResizeArray () + + let helpText () = + [ + (sprintf "%s int32%s%s" (sprintf "--%s" "foo") "" "") + (sprintf "%s string%s%s" (sprintf "--%s" "bar") "" "") + (sprintf "%s bool%s%s" (sprintf "--%s" "baz") "" "") + (sprintf "%s FileInfo%s%s" (sprintf "--%s" "some-file") "" "") + (sprintf "%s DirectoryInfo%s%s" (sprintf "--%s" "some-directory") "" "") + (sprintf "%s DirectoryInfo%s%s" (sprintf "--%s" "some-list") " (can be repeated)" "") + (sprintf "%s int32%s%s" (sprintf "--%s" "optional-thing-with-no-default") " (optional)" "") + + (sprintf + "%s bool%s%s" + (sprintf "--%s" "optional-thing") + (LoadsOfTypesNoPositionals.DefaultOptionalThing () + |> (fun x -> x.ToString ()) + |> sprintf " (default value: %s)") + "") + + (sprintf + "%s int32%s%s" + (sprintf "--%s" "another-optional-thing") + (LoadsOfTypesNoPositionals.DefaultAnotherOptionalThing () + |> (fun x -> x.ToString ()) + |> sprintf " (default value: %s)") + "") + (sprintf + "%s string%s%s" + (sprintf "--%s" "yet-another-optional-thing") + ("CONSUMEPLUGIN_THINGS" |> sprintf " (default value populated from env var %s)") + "") + ] + |> String.concat "\n" + + let parser_LeftoverArgs : string ResizeArray = ResizeArray () + let mutable arg_0 : int option = None + let mutable arg_1 : string option = None + let mutable arg_2 : bool option = None + let mutable arg_3 : FileInfo option = None + let mutable arg_4 : DirectoryInfo option = None + let arg_5 : DirectoryInfo ResizeArray = ResizeArray () + let mutable arg_6 : int option = None + let mutable arg_7 : bool option = None + let mutable arg_8 : int option = None + let mutable arg_9 : string option = None + + /// Processes the key-value pair, returning Error if no key was matched. + /// If the key is an arg which can have arity 1, but throws when consuming that arg, we return Error(). + /// This can nevertheless be a successful parse, e.g. when the key may have arity 0. + let processKeyValue (key : string) (value : string) : Result = + if + System.String.Equals ( + key, + sprintf "--%s" "yet-another-optional-thing", + System.StringComparison.OrdinalIgnoreCase + ) + then + match arg_9 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "yet-another-optional-thing") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_9 <- value |> (fun x -> x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals ( + key, + sprintf "--%s" "another-optional-thing", + System.StringComparison.OrdinalIgnoreCase + ) + then + match arg_8 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "another-optional-thing") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_8 <- value |> (fun x -> System.Int32.Parse x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals ( + key, + sprintf "--%s" "optional-thing", + System.StringComparison.OrdinalIgnoreCase + ) + then + match arg_7 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "optional-thing") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_7 <- value |> (fun x -> System.Boolean.Parse x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals ( + key, + sprintf "--%s" "optional-thing-with-no-default", + System.StringComparison.OrdinalIgnoreCase + ) + then + match arg_6 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "optional-thing-with-no-default") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_6 <- value |> (fun x -> System.Int32.Parse x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals (key, sprintf "--%s" "some-list", System.StringComparison.OrdinalIgnoreCase) + then + value |> (fun x -> System.IO.DirectoryInfo x) |> arg_5.Add + () |> Ok + else if + System.String.Equals ( + key, + sprintf "--%s" "some-directory", + System.StringComparison.OrdinalIgnoreCase + ) + then + match arg_4 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "some-directory") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_4 <- value |> (fun x -> System.IO.DirectoryInfo x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals (key, sprintf "--%s" "some-file", System.StringComparison.OrdinalIgnoreCase) + then + match arg_3 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "some-file") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_3 <- value |> (fun x -> System.IO.FileInfo x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if System.String.Equals (key, sprintf "--%s" "baz", System.StringComparison.OrdinalIgnoreCase) then + match arg_2 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "baz") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_2 <- value |> (fun x -> System.Boolean.Parse x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if System.String.Equals (key, sprintf "--%s" "bar", System.StringComparison.OrdinalIgnoreCase) then + match arg_1 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "bar") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_1 <- value |> (fun x -> x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if System.String.Equals (key, sprintf "--%s" "foo", System.StringComparison.OrdinalIgnoreCase) then + match arg_0 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "foo") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_0 <- value |> (fun x -> System.Int32.Parse x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else + Error None + + /// Returns false if we didn't set a value. + let setFlagValue (key : string) : bool = + if + System.String.Equals ( + key, + sprintf "--%s" "optional-thing", + System.StringComparison.OrdinalIgnoreCase + ) + then + match arg_7 with + | Some x -> + sprintf "Flag '%s' was supplied multiple times" (sprintf "--%s" "optional-thing") + |> ArgParser_errors.Add + + true + | None -> + arg_7 <- true |> Some + true + else if System.String.Equals (key, sprintf "--%s" "baz", System.StringComparison.OrdinalIgnoreCase) then + match arg_2 with + | Some x -> + sprintf "Flag '%s' was supplied multiple times" (sprintf "--%s" "baz") + |> ArgParser_errors.Add + + true + | None -> + arg_2 <- true |> Some + true + else + false + + let rec go (state : ParseState_LoadsOfTypesNoPositionals) (args : string list) = + match args with + | [] -> + match state with + | ParseState_LoadsOfTypesNoPositionals.AwaitingKey -> () + | ParseState_LoadsOfTypesNoPositionals.AwaitingValue key -> + if setFlagValue key then + () + else + sprintf + "Trailing argument %s had no value. Use a double-dash to separate positional args from key-value args." + key + |> ArgParser_errors.Add + | "--" :: rest -> parser_LeftoverArgs.AddRange (rest |> Seq.map (fun x -> x)) + | arg :: args -> + match state with + | ParseState_LoadsOfTypesNoPositionals.AwaitingKey -> + if arg.StartsWith ("--", System.StringComparison.Ordinal) then + if arg = "--help" then + helpText () |> failwithf "Help text requested.\n%s" + else + let equals = arg.IndexOf (char 61) + + if equals < 0 then + args |> go (ParseState_LoadsOfTypesNoPositionals.AwaitingValue arg) + else + let key = arg.[0 .. equals - 1] + let value = arg.[equals + 1 ..] + + match processKeyValue key value with + | Ok () -> go ParseState_LoadsOfTypesNoPositionals.AwaitingKey args + | Error x -> + match x with + | None -> + failwithf + "Unable to process argument %s as key %s and value %s" + arg + key + value + | Some msg -> + sprintf "%s (at arg %s)" msg arg |> ArgParser_errors.Add + go ParseState_LoadsOfTypesNoPositionals.AwaitingKey args + else + arg |> (fun x -> x) |> parser_LeftoverArgs.Add + go ParseState_LoadsOfTypesNoPositionals.AwaitingKey args + | ParseState_LoadsOfTypesNoPositionals.AwaitingValue key -> + match processKeyValue key arg with + | Ok () -> go ParseState_LoadsOfTypesNoPositionals.AwaitingKey args + | Error exc -> + if setFlagValue key then + go ParseState_LoadsOfTypesNoPositionals.AwaitingKey (arg :: args) + else + match exc with + | None -> + failwithf + "Unable to process supplied arg %s. Help text follows.\n%s" + key + (helpText ()) + | Some msg -> msg |> ArgParser_errors.Add + + go ParseState_LoadsOfTypesNoPositionals.AwaitingKey args + + let parser_LeftoverArgs = + if 0 = parser_LeftoverArgs.Count then + () + else + parser_LeftoverArgs + |> String.concat " " + |> sprintf "There were leftover args: %s" + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + + let arg_0 = + match arg_0 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "foo") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + let arg_1 = + match arg_1 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "bar") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + let arg_2 = + match arg_2 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "baz") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + let arg_3 = + match arg_3 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "some-file") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + let arg_4 = + match arg_4 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "some-directory") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + let arg_5 = arg_5 |> Seq.toList + let arg_6 = arg_6 + + let arg_7 = + match arg_7 with + | None -> LoadsOfTypesNoPositionals.DefaultOptionalThing () |> Choice2Of2 + | Some x -> Choice1Of2 x + + let arg_8 = + match arg_8 with + | None -> LoadsOfTypesNoPositionals.DefaultAnotherOptionalThing () |> Choice2Of2 + | Some x -> Choice1Of2 x + + let arg_9 = + match arg_9 with + | None -> + match "CONSUMEPLUGIN_THINGS" |> getEnvironmentVariable with + | null -> + sprintf + "No value was supplied for %s, nor was environment variable %s set" + (sprintf "--%s" "yet-another-optional-thing") + "CONSUMEPLUGIN_THINGS" + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | x -> x |> (fun x -> x) + |> Choice2Of2 + | Some x -> Choice1Of2 x + + if 0 = ArgParser_errors.Count then + { + AnotherOptionalThing = arg_8 + Bar = arg_1 + Baz = arg_2 + Foo = arg_0 + OptionalThing = arg_7 + OptionalThingWithNoDefault = arg_6 + SomeDirectory = arg_4 + SomeFile = arg_3 + SomeList = arg_5 + YetAnotherOptionalThing = arg_9 + } + else + ArgParser_errors |> String.concat "\n" |> failwithf "Errors during parse!\n%s" + + static member parse (args : string list) : LoadsOfTypesNoPositionals = + LoadsOfTypesNoPositionals.parse' System.Environment.GetEnvironmentVariable args +namespace Consumer + +open System +open System.IO +open WoofWare.Whippet.Plugin.ArgParser + +/// Methods to parse arguments for the type DatesAndTimes +[] +module DatesAndTimesArgParse = + type private ParseState_DatesAndTimes = + /// Ready to consume a key or positional arg + | AwaitingKey + /// Waiting to receive a value for the key we've already consumed + | AwaitingValue of key : string + + /// Extension methods for argument parsing + type DatesAndTimes with + + static member parse' (getEnvironmentVariable : string -> string) (args : string list) : DatesAndTimes = + let ArgParser_errors = ResizeArray () + + let helpText () = + [ + (sprintf "%s TimeSpan%s%s" (sprintf "--%s" "plain") "" "") + (sprintf "%s TimeSpan%s%s" (sprintf "--%s" "invariant") "" "") + + (sprintf + "%s TimeSpan%s%s" + (sprintf "--%s" "exact") + "" + (sprintf " : %s" (sprintf "%s [Parse format (.NET): %s]" "An exact time please" @"hh\:mm\:ss"))) + (sprintf + "%s TimeSpan%s%s" + (sprintf "--%s" "invariant-exact") + "" + (sprintf " : %s" (sprintf "[Parse format (.NET): %s]" @"hh\:mm\:ss"))) + ] + |> String.concat "\n" + + let parser_LeftoverArgs : string ResizeArray = ResizeArray () + let mutable arg_0 : TimeSpan option = None + let mutable arg_1 : TimeSpan option = None + let mutable arg_2 : TimeSpan option = None + let mutable arg_3 : TimeSpan option = None + + /// Processes the key-value pair, returning Error if no key was matched. + /// If the key is an arg which can have arity 1, but throws when consuming that arg, we return Error(). + /// This can nevertheless be a successful parse, e.g. when the key may have arity 0. + let processKeyValue (key : string) (value : string) : Result = + if + System.String.Equals ( + key, + sprintf "--%s" "invariant-exact", + System.StringComparison.OrdinalIgnoreCase + ) + then + match arg_3 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "invariant-exact") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_3 <- + value + |> (fun x -> + System.TimeSpan.ParseExact ( + x, + @"hh\:mm\:ss", + System.Globalization.CultureInfo.InvariantCulture + ) + ) + |> Some + + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals (key, sprintf "--%s" "exact", System.StringComparison.OrdinalIgnoreCase) + then + match arg_2 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "exact") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_2 <- + value + |> (fun x -> + System.TimeSpan.ParseExact ( + x, + @"hh\:mm\:ss", + System.Globalization.CultureInfo.CurrentCulture + ) + ) + |> Some + + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals (key, sprintf "--%s" "invariant", System.StringComparison.OrdinalIgnoreCase) + then + match arg_1 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "invariant") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_1 <- + value + |> (fun x -> + System.TimeSpan.Parse (x, System.Globalization.CultureInfo.InvariantCulture) + ) + |> Some + + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals (key, sprintf "--%s" "plain", System.StringComparison.OrdinalIgnoreCase) + then + match arg_0 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "plain") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_0 <- value |> (fun x -> System.TimeSpan.Parse x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else + Error None + + /// Returns false if we didn't set a value. + let setFlagValue (key : string) : bool = false + + let rec go (state : ParseState_DatesAndTimes) (args : string list) = + match args with + | [] -> + match state with + | ParseState_DatesAndTimes.AwaitingKey -> () + | ParseState_DatesAndTimes.AwaitingValue key -> + if setFlagValue key then + () + else + sprintf + "Trailing argument %s had no value. Use a double-dash to separate positional args from key-value args." + key + |> ArgParser_errors.Add + | "--" :: rest -> parser_LeftoverArgs.AddRange (rest |> Seq.map (fun x -> x)) + | arg :: args -> + match state with + | ParseState_DatesAndTimes.AwaitingKey -> + if arg.StartsWith ("--", System.StringComparison.Ordinal) then + if arg = "--help" then + helpText () |> failwithf "Help text requested.\n%s" + else + let equals = arg.IndexOf (char 61) + + if equals < 0 then + args |> go (ParseState_DatesAndTimes.AwaitingValue arg) + else + let key = arg.[0 .. equals - 1] + let value = arg.[equals + 1 ..] + + match processKeyValue key value with + | Ok () -> go ParseState_DatesAndTimes.AwaitingKey args + | Error x -> + match x with + | None -> + failwithf + "Unable to process argument %s as key %s and value %s" + arg + key + value + | Some msg -> + sprintf "%s (at arg %s)" msg arg |> ArgParser_errors.Add + go ParseState_DatesAndTimes.AwaitingKey args + else + arg |> (fun x -> x) |> parser_LeftoverArgs.Add + go ParseState_DatesAndTimes.AwaitingKey args + | ParseState_DatesAndTimes.AwaitingValue key -> + match processKeyValue key arg with + | Ok () -> go ParseState_DatesAndTimes.AwaitingKey args + | Error exc -> + if setFlagValue key then + go ParseState_DatesAndTimes.AwaitingKey (arg :: args) + else + match exc with + | None -> + failwithf + "Unable to process supplied arg %s. Help text follows.\n%s" + key + (helpText ()) + | Some msg -> msg |> ArgParser_errors.Add + + go ParseState_DatesAndTimes.AwaitingKey args + + let parser_LeftoverArgs = + if 0 = parser_LeftoverArgs.Count then + () + else + parser_LeftoverArgs + |> String.concat " " + |> sprintf "There were leftover args: %s" + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + + let arg_0 = + match arg_0 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "plain") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + let arg_1 = + match arg_1 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "invariant") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + let arg_2 = + match arg_2 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "exact") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + let arg_3 = + match arg_3 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "invariant-exact") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + if 0 = ArgParser_errors.Count then + { + Exact = arg_2 + Invariant = arg_1 + InvariantExact = arg_3 + Plain = arg_0 + } + else + ArgParser_errors |> String.concat "\n" |> failwithf "Errors during parse!\n%s" + + static member parse (args : string list) : DatesAndTimes = + DatesAndTimes.parse' System.Environment.GetEnvironmentVariable args +namespace Consumer + +open System +open System.IO +open WoofWare.Whippet.Plugin.ArgParser + +/// Methods to parse arguments for the type ParentRecord +[] +module ParentRecordArgParse = + type private ParseState_ParentRecord = + /// Ready to consume a key or positional arg + | AwaitingKey + /// Waiting to receive a value for the key we've already consumed + | AwaitingValue of key : string + + /// Extension methods for argument parsing + type ParentRecord with + + static member parse' (getEnvironmentVariable : string -> string) (args : string list) : ParentRecord = + let ArgParser_errors = ResizeArray () + + let helpText () = + [ + (sprintf "%s int32%s%s" (sprintf "--%s" "thing1") "" "") + (sprintf "%s string%s%s" (sprintf "--%s" "thing2") "" "") + (sprintf "%s bool%s%s" (sprintf "--%s" "and-another") "" "") + ] + |> String.concat "\n" + + let parser_LeftoverArgs : string ResizeArray = ResizeArray () + let mutable arg_0 : int option = None + let mutable arg_1 : string option = None + let mutable arg_2 : bool option = None + + /// Processes the key-value pair, returning Error if no key was matched. + /// If the key is an arg which can have arity 1, but throws when consuming that arg, we return Error(). + /// This can nevertheless be a successful parse, e.g. when the key may have arity 0. + let processKeyValue (key : string) (value : string) : Result = + if + System.String.Equals (key, sprintf "--%s" "and-another", System.StringComparison.OrdinalIgnoreCase) + then + match arg_2 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "and-another") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_2 <- value |> (fun x -> System.Boolean.Parse x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals (key, sprintf "--%s" "thing2", System.StringComparison.OrdinalIgnoreCase) + then + match arg_1 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "thing2") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_1 <- value |> (fun x -> x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals (key, sprintf "--%s" "thing1", System.StringComparison.OrdinalIgnoreCase) + then + match arg_0 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "thing1") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_0 <- value |> (fun x -> System.Int32.Parse x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else + Error None + + /// Returns false if we didn't set a value. + let setFlagValue (key : string) : bool = + if + System.String.Equals (key, sprintf "--%s" "and-another", System.StringComparison.OrdinalIgnoreCase) + then + match arg_2 with + | Some x -> + sprintf "Flag '%s' was supplied multiple times" (sprintf "--%s" "and-another") + |> ArgParser_errors.Add + + true + | None -> + arg_2 <- true |> Some + true + else + false + + let rec go (state : ParseState_ParentRecord) (args : string list) = + match args with + | [] -> + match state with + | ParseState_ParentRecord.AwaitingKey -> () + | ParseState_ParentRecord.AwaitingValue key -> + if setFlagValue key then + () + else + sprintf + "Trailing argument %s had no value. Use a double-dash to separate positional args from key-value args." + key + |> ArgParser_errors.Add + | "--" :: rest -> parser_LeftoverArgs.AddRange (rest |> Seq.map (fun x -> x)) + | arg :: args -> + match state with + | ParseState_ParentRecord.AwaitingKey -> + if arg.StartsWith ("--", System.StringComparison.Ordinal) then + if arg = "--help" then + helpText () |> failwithf "Help text requested.\n%s" + else + let equals = arg.IndexOf (char 61) + + if equals < 0 then + args |> go (ParseState_ParentRecord.AwaitingValue arg) + else + let key = arg.[0 .. equals - 1] + let value = arg.[equals + 1 ..] + + match processKeyValue key value with + | Ok () -> go ParseState_ParentRecord.AwaitingKey args + | Error x -> + match x with + | None -> + failwithf + "Unable to process argument %s as key %s and value %s" + arg + key + value + | Some msg -> + sprintf "%s (at arg %s)" msg arg |> ArgParser_errors.Add + go ParseState_ParentRecord.AwaitingKey args + else + arg |> (fun x -> x) |> parser_LeftoverArgs.Add + go ParseState_ParentRecord.AwaitingKey args + | ParseState_ParentRecord.AwaitingValue key -> + match processKeyValue key arg with + | Ok () -> go ParseState_ParentRecord.AwaitingKey args + | Error exc -> + if setFlagValue key then + go ParseState_ParentRecord.AwaitingKey (arg :: args) + else + match exc with + | None -> + failwithf + "Unable to process supplied arg %s. Help text follows.\n%s" + key + (helpText ()) + | Some msg -> msg |> ArgParser_errors.Add + + go ParseState_ParentRecord.AwaitingKey args + + let parser_LeftoverArgs = + if 0 = parser_LeftoverArgs.Count then + () + else + parser_LeftoverArgs + |> String.concat " " + |> sprintf "There were leftover args: %s" + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + + let arg_0 = + match arg_0 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "thing1") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + let arg_1 = + match arg_1 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "thing2") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + let arg_2 = + match arg_2 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "and-another") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + if 0 = ArgParser_errors.Count then + { + AndAnother = arg_2 + Child = + { + Thing1 = arg_0 + Thing2 = arg_1 + } + } + else + ArgParser_errors |> String.concat "\n" |> failwithf "Errors during parse!\n%s" + + static member parse (args : string list) : ParentRecord = + ParentRecord.parse' System.Environment.GetEnvironmentVariable args +namespace Consumer + +open System +open System.IO +open WoofWare.Whippet.Plugin.ArgParser + +/// Methods to parse arguments for the type ParentRecordChildPos +[] +module ParentRecordChildPosArgParse = + type private ParseState_ParentRecordChildPos = + /// Ready to consume a key or positional arg + | AwaitingKey + /// Waiting to receive a value for the key we've already consumed + | AwaitingValue of key : string + + /// Extension methods for argument parsing + type ParentRecordChildPos with + + static member parse' (getEnvironmentVariable : string -> string) (args : string list) : ParentRecordChildPos = + let ArgParser_errors = ResizeArray () + + let helpText () = + [ + (sprintf "%s bool%s%s" (sprintf "--%s" "and-another") "" "") + (sprintf "%s int32%s%s" (sprintf "--%s" "thing1") "" "") + (sprintf "%s URI%s%s" (sprintf "--%s" "thing2") " (positional args) (can be repeated)" "") + ] + |> String.concat "\n" + + let arg_1 : Uri ResizeArray = ResizeArray () + let mutable arg_2 : bool option = None + let mutable arg_0 : int option = None + + /// Processes the key-value pair, returning Error if no key was matched. + /// If the key is an arg which can have arity 1, but throws when consuming that arg, we return Error(). + /// This can nevertheless be a successful parse, e.g. when the key may have arity 0. + let processKeyValue (key : string) (value : string) : Result = + if System.String.Equals (key, sprintf "--%s" "thing1", System.StringComparison.OrdinalIgnoreCase) then + match arg_0 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "thing1") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_0 <- value |> (fun x -> System.Int32.Parse x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals (key, sprintf "--%s" "and-another", System.StringComparison.OrdinalIgnoreCase) + then + match arg_2 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "and-another") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_2 <- value |> (fun x -> System.Boolean.Parse x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals (key, sprintf "--%s" "thing2", System.StringComparison.OrdinalIgnoreCase) + then + value |> (fun x -> System.Uri x) |> arg_1.Add + () |> Ok + else + Error None + + /// Returns false if we didn't set a value. + let setFlagValue (key : string) : bool = + if + System.String.Equals (key, sprintf "--%s" "and-another", System.StringComparison.OrdinalIgnoreCase) + then + match arg_2 with + | Some x -> + sprintf "Flag '%s' was supplied multiple times" (sprintf "--%s" "and-another") + |> ArgParser_errors.Add + + true + | None -> + arg_2 <- true |> Some + true + else + false + + let rec go (state : ParseState_ParentRecordChildPos) (args : string list) = + match args with + | [] -> + match state with + | ParseState_ParentRecordChildPos.AwaitingKey -> () + | ParseState_ParentRecordChildPos.AwaitingValue key -> + if setFlagValue key then + () + else + sprintf + "Trailing argument %s had no value. Use a double-dash to separate positional args from key-value args." + key + |> ArgParser_errors.Add + | "--" :: rest -> arg_1.AddRange (rest |> Seq.map (fun x -> System.Uri x)) + | arg :: args -> + match state with + | ParseState_ParentRecordChildPos.AwaitingKey -> + if arg.StartsWith ("--", System.StringComparison.Ordinal) then + if arg = "--help" then + helpText () |> failwithf "Help text requested.\n%s" + else + let equals = arg.IndexOf (char 61) + + if equals < 0 then + args |> go (ParseState_ParentRecordChildPos.AwaitingValue arg) + else + let key = arg.[0 .. equals - 1] + let value = arg.[equals + 1 ..] + + match processKeyValue key value with + | Ok () -> go ParseState_ParentRecordChildPos.AwaitingKey args + | Error x -> + match x with + | None -> + failwithf + "Unable to process argument %s as key %s and value %s" + arg + key + value + | Some msg -> + sprintf "%s (at arg %s)" msg arg |> ArgParser_errors.Add + go ParseState_ParentRecordChildPos.AwaitingKey args + else + arg |> (fun x -> System.Uri x) |> arg_1.Add + go ParseState_ParentRecordChildPos.AwaitingKey args + | ParseState_ParentRecordChildPos.AwaitingValue key -> + match processKeyValue key arg with + | Ok () -> go ParseState_ParentRecordChildPos.AwaitingKey args + | Error exc -> + if setFlagValue key then + go ParseState_ParentRecordChildPos.AwaitingKey (arg :: args) + else + match exc with + | None -> + failwithf + "Unable to process supplied arg %s. Help text follows.\n%s" + key + (helpText ()) + | Some msg -> msg |> ArgParser_errors.Add + + go ParseState_ParentRecordChildPos.AwaitingKey args + let arg_1 = arg_1 |> Seq.toList + + let arg_2 = + match arg_2 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "and-another") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + let arg_0 = + match arg_0 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "thing1") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + if 0 = ArgParser_errors.Count then + { + AndAnother = arg_2 + Child = + { + Thing1 = arg_0 + Thing2 = arg_1 + } + } + else + ArgParser_errors |> String.concat "\n" |> failwithf "Errors during parse!\n%s" + + static member parse (args : string list) : ParentRecordChildPos = + ParentRecordChildPos.parse' System.Environment.GetEnvironmentVariable args +namespace Consumer + +open System +open System.IO +open WoofWare.Whippet.Plugin.ArgParser + +/// Methods to parse arguments for the type ParentRecordSelfPos +[] +module ParentRecordSelfPosArgParse = + type private ParseState_ParentRecordSelfPos = + /// Ready to consume a key or positional arg + | AwaitingKey + /// Waiting to receive a value for the key we've already consumed + | AwaitingValue of key : string + + /// Extension methods for argument parsing + type ParentRecordSelfPos with + + static member parse' (getEnvironmentVariable : string -> string) (args : string list) : ParentRecordSelfPos = + let ArgParser_errors = ResizeArray () + + let helpText () = + [ + (sprintf "%s int32%s%s" (sprintf "--%s" "thing1") "" "") + (sprintf "%s string%s%s" (sprintf "--%s" "thing2") "" "") + (sprintf "%s bool%s%s" (sprintf "--%s" "and-another") " (positional args) (can be repeated)" "") + ] + |> String.concat "\n" + + let arg_2 : bool ResizeArray = ResizeArray () + let mutable arg_0 : int option = None + let mutable arg_1 : string option = None + + /// Processes the key-value pair, returning Error if no key was matched. + /// If the key is an arg which can have arity 1, but throws when consuming that arg, we return Error(). + /// This can nevertheless be a successful parse, e.g. when the key may have arity 0. + let processKeyValue (key : string) (value : string) : Result = + if System.String.Equals (key, sprintf "--%s" "thing2", System.StringComparison.OrdinalIgnoreCase) then + match arg_1 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "thing2") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_1 <- value |> (fun x -> x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals (key, sprintf "--%s" "thing1", System.StringComparison.OrdinalIgnoreCase) + then + match arg_0 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "thing1") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_0 <- value |> (fun x -> System.Int32.Parse x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals (key, sprintf "--%s" "and-another", System.StringComparison.OrdinalIgnoreCase) + then + value |> (fun x -> System.Boolean.Parse x) |> arg_2.Add + () |> Ok + else + Error None + + /// Returns false if we didn't set a value. + let setFlagValue (key : string) : bool = false + + let rec go (state : ParseState_ParentRecordSelfPos) (args : string list) = + match args with + | [] -> + match state with + | ParseState_ParentRecordSelfPos.AwaitingKey -> () + | ParseState_ParentRecordSelfPos.AwaitingValue key -> + if setFlagValue key then + () + else + sprintf + "Trailing argument %s had no value. Use a double-dash to separate positional args from key-value args." + key + |> ArgParser_errors.Add + | "--" :: rest -> arg_2.AddRange (rest |> Seq.map (fun x -> System.Boolean.Parse x)) + | arg :: args -> + match state with + | ParseState_ParentRecordSelfPos.AwaitingKey -> + if arg.StartsWith ("--", System.StringComparison.Ordinal) then + if arg = "--help" then + helpText () |> failwithf "Help text requested.\n%s" + else + let equals = arg.IndexOf (char 61) + + if equals < 0 then + args |> go (ParseState_ParentRecordSelfPos.AwaitingValue arg) + else + let key = arg.[0 .. equals - 1] + let value = arg.[equals + 1 ..] + + match processKeyValue key value with + | Ok () -> go ParseState_ParentRecordSelfPos.AwaitingKey args + | Error x -> + match x with + | None -> + failwithf + "Unable to process argument %s as key %s and value %s" + arg + key + value + | Some msg -> + sprintf "%s (at arg %s)" msg arg |> ArgParser_errors.Add + go ParseState_ParentRecordSelfPos.AwaitingKey args + else + arg |> (fun x -> System.Boolean.Parse x) |> arg_2.Add + go ParseState_ParentRecordSelfPos.AwaitingKey args + | ParseState_ParentRecordSelfPos.AwaitingValue key -> + match processKeyValue key arg with + | Ok () -> go ParseState_ParentRecordSelfPos.AwaitingKey args + | Error exc -> + if setFlagValue key then + go ParseState_ParentRecordSelfPos.AwaitingKey (arg :: args) + else + match exc with + | None -> + failwithf + "Unable to process supplied arg %s. Help text follows.\n%s" + key + (helpText ()) + | Some msg -> msg |> ArgParser_errors.Add + + go ParseState_ParentRecordSelfPos.AwaitingKey args + let arg_2 = arg_2 |> Seq.toList + + let arg_0 = + match arg_0 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "thing1") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + let arg_1 = + match arg_1 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "thing2") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + if 0 = ArgParser_errors.Count then + { + AndAnother = arg_2 + Child = + { + Thing1 = arg_0 + Thing2 = arg_1 + } + } + else + ArgParser_errors |> String.concat "\n" |> failwithf "Errors during parse!\n%s" + + static member parse (args : string list) : ParentRecordSelfPos = + ParentRecordSelfPos.parse' System.Environment.GetEnvironmentVariable args +namespace Consumer + +open System +open System.IO +open WoofWare.Whippet.Plugin.ArgParser + +/// Methods to parse arguments for the type ChoicePositionals +[] +module ChoicePositionalsArgParse = + type private ParseState_ChoicePositionals = + /// Ready to consume a key or positional arg + | AwaitingKey + /// Waiting to receive a value for the key we've already consumed + | AwaitingValue of key : string + + /// Extension methods for argument parsing + type ChoicePositionals with + + static member parse' (getEnvironmentVariable : string -> string) (args : string list) : ChoicePositionals = + let ArgParser_errors = ResizeArray () + + let helpText () = + [ + (sprintf "%s string%s%s" (sprintf "--%s" "args") " (positional args) (can be repeated)" "") + ] + |> String.concat "\n" + + let arg_0 : Choice ResizeArray = ResizeArray () + + /// Processes the key-value pair, returning Error if no key was matched. + /// If the key is an arg which can have arity 1, but throws when consuming that arg, we return Error(). + /// This can nevertheless be a successful parse, e.g. when the key may have arity 0. + let processKeyValue (key : string) (value : string) : Result = + if System.String.Equals (key, sprintf "--%s" "args", System.StringComparison.OrdinalIgnoreCase) then + value |> (fun x -> x) |> Choice1Of2 |> arg_0.Add + () |> Ok + else + Error None + + /// Returns false if we didn't set a value. + let setFlagValue (key : string) : bool = false + + let rec go (state : ParseState_ChoicePositionals) (args : string list) = + match args with + | [] -> + match state with + | ParseState_ChoicePositionals.AwaitingKey -> () + | ParseState_ChoicePositionals.AwaitingValue key -> + if setFlagValue key then + () + else + sprintf + "Trailing argument %s had no value. Use a double-dash to separate positional args from key-value args." + key + |> ArgParser_errors.Add + | "--" :: rest -> arg_0.AddRange (rest |> Seq.map (fun x -> x) |> Seq.map Choice2Of2) + | arg :: args -> + match state with + | ParseState_ChoicePositionals.AwaitingKey -> + if arg.StartsWith ("--", System.StringComparison.Ordinal) then + if arg = "--help" then + helpText () |> failwithf "Help text requested.\n%s" + else + let equals = arg.IndexOf (char 61) + + if equals < 0 then + args |> go (ParseState_ChoicePositionals.AwaitingValue arg) + else + let key = arg.[0 .. equals - 1] + let value = arg.[equals + 1 ..] + + match processKeyValue key value with + | Ok () -> go ParseState_ChoicePositionals.AwaitingKey args + | Error x -> + match x with + | None -> + failwithf + "Unable to process argument %s as key %s and value %s" + arg + key + value + | Some msg -> + sprintf "%s (at arg %s)" msg arg |> ArgParser_errors.Add + go ParseState_ChoicePositionals.AwaitingKey args + else + arg |> (fun x -> x) |> Choice1Of2 |> arg_0.Add + go ParseState_ChoicePositionals.AwaitingKey args + | ParseState_ChoicePositionals.AwaitingValue key -> + match processKeyValue key arg with + | Ok () -> go ParseState_ChoicePositionals.AwaitingKey args + | Error exc -> + if setFlagValue key then + go ParseState_ChoicePositionals.AwaitingKey (arg :: args) + else + match exc with + | None -> + failwithf + "Unable to process supplied arg %s. Help text follows.\n%s" + key + (helpText ()) + | Some msg -> msg |> ArgParser_errors.Add + + go ParseState_ChoicePositionals.AwaitingKey args + let arg_0 = arg_0 |> Seq.toList + + if 0 = ArgParser_errors.Count then + { + Args = arg_0 + } + else + ArgParser_errors |> String.concat "\n" |> failwithf "Errors during parse!\n%s" + + static member parse (args : string list) : ChoicePositionals = + ChoicePositionals.parse' System.Environment.GetEnvironmentVariable args +namespace Consumer + +open System +open System.IO +open WoofWare.Whippet.Plugin.ArgParser + +/// Methods to parse arguments for the type ContainsBoolEnvVar +[] +module ContainsBoolEnvVarArgParse = + type private ParseState_ContainsBoolEnvVar = + /// Ready to consume a key or positional arg + | AwaitingKey + /// Waiting to receive a value for the key we've already consumed + | AwaitingValue of key : string + + /// Extension methods for argument parsing + type ContainsBoolEnvVar with + + static member parse' (getEnvironmentVariable : string -> string) (args : string list) : ContainsBoolEnvVar = + let ArgParser_errors = ResizeArray () + + let helpText () = + [ + (sprintf + "%s bool%s%s" + (sprintf "--%s" "bool-var") + ("CONSUMEPLUGIN_THINGS" |> sprintf " (default value populated from env var %s)") + "") + ] + |> String.concat "\n" + + let parser_LeftoverArgs : string ResizeArray = ResizeArray () + let mutable arg_0 : bool option = None + + /// Processes the key-value pair, returning Error if no key was matched. + /// If the key is an arg which can have arity 1, but throws when consuming that arg, we return Error(). + /// This can nevertheless be a successful parse, e.g. when the key may have arity 0. + let processKeyValue (key : string) (value : string) : Result = + if System.String.Equals (key, sprintf "--%s" "bool-var", System.StringComparison.OrdinalIgnoreCase) then + match arg_0 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "bool-var") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_0 <- value |> (fun x -> System.Boolean.Parse x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else + Error None + + /// Returns false if we didn't set a value. + let setFlagValue (key : string) : bool = + if System.String.Equals (key, sprintf "--%s" "bool-var", System.StringComparison.OrdinalIgnoreCase) then + match arg_0 with + | Some x -> + sprintf "Flag '%s' was supplied multiple times" (sprintf "--%s" "bool-var") + |> ArgParser_errors.Add + + true + | None -> + arg_0 <- true |> Some + true + else + false + + let rec go (state : ParseState_ContainsBoolEnvVar) (args : string list) = + match args with + | [] -> + match state with + | ParseState_ContainsBoolEnvVar.AwaitingKey -> () + | ParseState_ContainsBoolEnvVar.AwaitingValue key -> + if setFlagValue key then + () + else + sprintf + "Trailing argument %s had no value. Use a double-dash to separate positional args from key-value args." + key + |> ArgParser_errors.Add + | "--" :: rest -> parser_LeftoverArgs.AddRange (rest |> Seq.map (fun x -> x)) + | arg :: args -> + match state with + | ParseState_ContainsBoolEnvVar.AwaitingKey -> + if arg.StartsWith ("--", System.StringComparison.Ordinal) then + if arg = "--help" then + helpText () |> failwithf "Help text requested.\n%s" + else + let equals = arg.IndexOf (char 61) + + if equals < 0 then + args |> go (ParseState_ContainsBoolEnvVar.AwaitingValue arg) + else + let key = arg.[0 .. equals - 1] + let value = arg.[equals + 1 ..] + + match processKeyValue key value with + | Ok () -> go ParseState_ContainsBoolEnvVar.AwaitingKey args + | Error x -> + match x with + | None -> + failwithf + "Unable to process argument %s as key %s and value %s" + arg + key + value + | Some msg -> + sprintf "%s (at arg %s)" msg arg |> ArgParser_errors.Add + go ParseState_ContainsBoolEnvVar.AwaitingKey args + else + arg |> (fun x -> x) |> parser_LeftoverArgs.Add + go ParseState_ContainsBoolEnvVar.AwaitingKey args + | ParseState_ContainsBoolEnvVar.AwaitingValue key -> + match processKeyValue key arg with + | Ok () -> go ParseState_ContainsBoolEnvVar.AwaitingKey args + | Error exc -> + if setFlagValue key then + go ParseState_ContainsBoolEnvVar.AwaitingKey (arg :: args) + else + match exc with + | None -> + failwithf + "Unable to process supplied arg %s. Help text follows.\n%s" + key + (helpText ()) + | Some msg -> msg |> ArgParser_errors.Add + + go ParseState_ContainsBoolEnvVar.AwaitingKey args + + let parser_LeftoverArgs = + if 0 = parser_LeftoverArgs.Count then + () + else + parser_LeftoverArgs + |> String.concat " " + |> sprintf "There were leftover args: %s" + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + + let arg_0 = + match arg_0 with + | None -> + match "CONSUMEPLUGIN_THINGS" |> getEnvironmentVariable with + | null -> + sprintf + "No value was supplied for %s, nor was environment variable %s set" + (sprintf "--%s" "bool-var") + "CONSUMEPLUGIN_THINGS" + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | x -> + if System.String.Equals (x, "1", System.StringComparison.OrdinalIgnoreCase) then + true + else if System.String.Equals (x, "0", System.StringComparison.OrdinalIgnoreCase) then + false + else + x |> (fun x -> System.Boolean.Parse x) + |> Choice2Of2 + | Some x -> Choice1Of2 x + + if 0 = ArgParser_errors.Count then + { + BoolVar = arg_0 + } + else + ArgParser_errors |> String.concat "\n" |> failwithf "Errors during parse!\n%s" + + static member parse (args : string list) : ContainsBoolEnvVar = + ContainsBoolEnvVar.parse' System.Environment.GetEnvironmentVariable args +namespace Consumer + +open System +open System.IO +open WoofWare.Whippet.Plugin.ArgParser + +/// Methods to parse arguments for the type WithFlagDu +[] +module WithFlagDuArgParse = + type private ParseState_WithFlagDu = + /// Ready to consume a key or positional arg + | AwaitingKey + /// Waiting to receive a value for the key we've already consumed + | AwaitingValue of key : string + + /// Extension methods for argument parsing + type WithFlagDu with + + static member parse' (getEnvironmentVariable : string -> string) (args : string list) : WithFlagDu = + let ArgParser_errors = ResizeArray () + + let helpText () = + [ (sprintf "%s bool%s%s" (sprintf "--%s" "dry-run") "" "") ] + |> String.concat "\n" + + let parser_LeftoverArgs : string ResizeArray = ResizeArray () + let mutable arg_0 : DryRunMode option = None + + /// Processes the key-value pair, returning Error if no key was matched. + /// If the key is an arg which can have arity 1, but throws when consuming that arg, we return Error(). + /// This can nevertheless be a successful parse, e.g. when the key may have arity 0. + let processKeyValue (key : string) (value : string) : Result = + if System.String.Equals (key, sprintf "--%s" "dry-run", System.StringComparison.OrdinalIgnoreCase) then + match arg_0 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "dry-run") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_0 <- + value + |> (fun x -> + if System.Boolean.Parse x = Consts.FALSE then + DryRunMode.Wet + else + DryRunMode.Dry + ) + |> Some + + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else + Error None + + /// Returns false if we didn't set a value. + let setFlagValue (key : string) : bool = + if System.String.Equals (key, sprintf "--%s" "dry-run", System.StringComparison.OrdinalIgnoreCase) then + match arg_0 with + | Some x -> + sprintf "Flag '%s' was supplied multiple times" (sprintf "--%s" "dry-run") + |> ArgParser_errors.Add + + true + | None -> + arg_0 <- + if true = Consts.FALSE then + DryRunMode.Wet + else + DryRunMode.Dry + |> Some + + true + else + false + + let rec go (state : ParseState_WithFlagDu) (args : string list) = + match args with + | [] -> + match state with + | ParseState_WithFlagDu.AwaitingKey -> () + | ParseState_WithFlagDu.AwaitingValue key -> + if setFlagValue key then + () + else + sprintf + "Trailing argument %s had no value. Use a double-dash to separate positional args from key-value args." + key + |> ArgParser_errors.Add + | "--" :: rest -> parser_LeftoverArgs.AddRange (rest |> Seq.map (fun x -> x)) + | arg :: args -> + match state with + | ParseState_WithFlagDu.AwaitingKey -> + if arg.StartsWith ("--", System.StringComparison.Ordinal) then + if arg = "--help" then + helpText () |> failwithf "Help text requested.\n%s" + else + let equals = arg.IndexOf (char 61) + + if equals < 0 then + args |> go (ParseState_WithFlagDu.AwaitingValue arg) + else + let key = arg.[0 .. equals - 1] + let value = arg.[equals + 1 ..] + + match processKeyValue key value with + | Ok () -> go ParseState_WithFlagDu.AwaitingKey args + | Error x -> + match x with + | None -> + failwithf + "Unable to process argument %s as key %s and value %s" + arg + key + value + | Some msg -> + sprintf "%s (at arg %s)" msg arg |> ArgParser_errors.Add + go ParseState_WithFlagDu.AwaitingKey args + else + arg |> (fun x -> x) |> parser_LeftoverArgs.Add + go ParseState_WithFlagDu.AwaitingKey args + | ParseState_WithFlagDu.AwaitingValue key -> + match processKeyValue key arg with + | Ok () -> go ParseState_WithFlagDu.AwaitingKey args + | Error exc -> + if setFlagValue key then + go ParseState_WithFlagDu.AwaitingKey (arg :: args) + else + match exc with + | None -> + failwithf + "Unable to process supplied arg %s. Help text follows.\n%s" + key + (helpText ()) + | Some msg -> msg |> ArgParser_errors.Add + + go ParseState_WithFlagDu.AwaitingKey args + + let parser_LeftoverArgs = + if 0 = parser_LeftoverArgs.Count then + () + else + parser_LeftoverArgs + |> String.concat " " + |> sprintf "There were leftover args: %s" + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + + let arg_0 = + match arg_0 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "dry-run") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + if 0 = ArgParser_errors.Count then + { + DryRun = arg_0 + } + else + ArgParser_errors |> String.concat "\n" |> failwithf "Errors during parse!\n%s" + + static member parse (args : string list) : WithFlagDu = + WithFlagDu.parse' System.Environment.GetEnvironmentVariable args +namespace Consumer + +open System +open System.IO +open WoofWare.Whippet.Plugin.ArgParser + +/// Methods to parse arguments for the type ContainsFlagEnvVar +[] +module ContainsFlagEnvVarArgParse = + type private ParseState_ContainsFlagEnvVar = + /// Ready to consume a key or positional arg + | AwaitingKey + /// Waiting to receive a value for the key we've already consumed + | AwaitingValue of key : string + + /// Extension methods for argument parsing + type ContainsFlagEnvVar with + + static member parse' (getEnvironmentVariable : string -> string) (args : string list) : ContainsFlagEnvVar = + let ArgParser_errors = ResizeArray () + + let helpText () = + [ + (sprintf + "%s bool%s%s" + (sprintf "--%s" "dry-run") + ("CONSUMEPLUGIN_THINGS" |> sprintf " (default value populated from env var %s)") + "") + ] + |> String.concat "\n" + + let parser_LeftoverArgs : string ResizeArray = ResizeArray () + let mutable arg_0 : DryRunMode option = None + + /// Processes the key-value pair, returning Error if no key was matched. + /// If the key is an arg which can have arity 1, but throws when consuming that arg, we return Error(). + /// This can nevertheless be a successful parse, e.g. when the key may have arity 0. + let processKeyValue (key : string) (value : string) : Result = + if System.String.Equals (key, sprintf "--%s" "dry-run", System.StringComparison.OrdinalIgnoreCase) then + match arg_0 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "dry-run") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_0 <- + value + |> (fun x -> + if System.Boolean.Parse x = Consts.FALSE then + DryRunMode.Wet + else + DryRunMode.Dry + ) + |> Some + + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else + Error None + + /// Returns false if we didn't set a value. + let setFlagValue (key : string) : bool = + if System.String.Equals (key, sprintf "--%s" "dry-run", System.StringComparison.OrdinalIgnoreCase) then + match arg_0 with + | Some x -> + sprintf "Flag '%s' was supplied multiple times" (sprintf "--%s" "dry-run") + |> ArgParser_errors.Add + + true + | None -> + arg_0 <- + if true = Consts.FALSE then + DryRunMode.Wet + else + DryRunMode.Dry + |> Some + + true + else + false + + let rec go (state : ParseState_ContainsFlagEnvVar) (args : string list) = + match args with + | [] -> + match state with + | ParseState_ContainsFlagEnvVar.AwaitingKey -> () + | ParseState_ContainsFlagEnvVar.AwaitingValue key -> + if setFlagValue key then + () + else + sprintf + "Trailing argument %s had no value. Use a double-dash to separate positional args from key-value args." + key + |> ArgParser_errors.Add + | "--" :: rest -> parser_LeftoverArgs.AddRange (rest |> Seq.map (fun x -> x)) + | arg :: args -> + match state with + | ParseState_ContainsFlagEnvVar.AwaitingKey -> + if arg.StartsWith ("--", System.StringComparison.Ordinal) then + if arg = "--help" then + helpText () |> failwithf "Help text requested.\n%s" + else + let equals = arg.IndexOf (char 61) + + if equals < 0 then + args |> go (ParseState_ContainsFlagEnvVar.AwaitingValue arg) + else + let key = arg.[0 .. equals - 1] + let value = arg.[equals + 1 ..] + + match processKeyValue key value with + | Ok () -> go ParseState_ContainsFlagEnvVar.AwaitingKey args + | Error x -> + match x with + | None -> + failwithf + "Unable to process argument %s as key %s and value %s" + arg + key + value + | Some msg -> + sprintf "%s (at arg %s)" msg arg |> ArgParser_errors.Add + go ParseState_ContainsFlagEnvVar.AwaitingKey args + else + arg |> (fun x -> x) |> parser_LeftoverArgs.Add + go ParseState_ContainsFlagEnvVar.AwaitingKey args + | ParseState_ContainsFlagEnvVar.AwaitingValue key -> + match processKeyValue key arg with + | Ok () -> go ParseState_ContainsFlagEnvVar.AwaitingKey args + | Error exc -> + if setFlagValue key then + go ParseState_ContainsFlagEnvVar.AwaitingKey (arg :: args) + else + match exc with + | None -> + failwithf + "Unable to process supplied arg %s. Help text follows.\n%s" + key + (helpText ()) + | Some msg -> msg |> ArgParser_errors.Add + + go ParseState_ContainsFlagEnvVar.AwaitingKey args + + let parser_LeftoverArgs = + if 0 = parser_LeftoverArgs.Count then + () + else + parser_LeftoverArgs + |> String.concat " " + |> sprintf "There were leftover args: %s" + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + + let arg_0 = + match arg_0 with + | None -> + match "CONSUMEPLUGIN_THINGS" |> getEnvironmentVariable with + | null -> + sprintf + "No value was supplied for %s, nor was environment variable %s set" + (sprintf "--%s" "dry-run") + "CONSUMEPLUGIN_THINGS" + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | x -> + if System.String.Equals (x, "1", System.StringComparison.OrdinalIgnoreCase) then + if true = Consts.FALSE then + DryRunMode.Wet + else + DryRunMode.Dry + else if System.String.Equals (x, "0", System.StringComparison.OrdinalIgnoreCase) then + if false = Consts.FALSE then + DryRunMode.Wet + else + DryRunMode.Dry + else + x + |> (fun x -> + if System.Boolean.Parse x = Consts.FALSE then + DryRunMode.Wet + else + DryRunMode.Dry + ) + |> Choice2Of2 + | Some x -> Choice1Of2 x + + if 0 = ArgParser_errors.Count then + { + DryRun = arg_0 + } + else + ArgParser_errors |> String.concat "\n" |> failwithf "Errors during parse!\n%s" + + static member parse (args : string list) : ContainsFlagEnvVar = + ContainsFlagEnvVar.parse' System.Environment.GetEnvironmentVariable args +namespace Consumer + +open System +open System.IO +open WoofWare.Whippet.Plugin.ArgParser + +/// Methods to parse arguments for the type ContainsFlagDefaultValue +[] +module ContainsFlagDefaultValueArgParse = + type private ParseState_ContainsFlagDefaultValue = + /// Ready to consume a key or positional arg + | AwaitingKey + /// Waiting to receive a value for the key we've already consumed + | AwaitingValue of key : string + + /// Extension methods for argument parsing + type ContainsFlagDefaultValue with + + static member parse' + (getEnvironmentVariable : string -> string) + (args : string list) + : ContainsFlagDefaultValue + = + let ArgParser_errors = ResizeArray () + + let helpText () = + [ + (sprintf + "%s bool%s%s" + (sprintf "--%s" "dry-run") + (match ContainsFlagDefaultValue.DefaultDryRun () with + | DryRunMode.Wet -> if Consts.FALSE = true then "true" else "false" + | DryRunMode.Dry -> if true = true then "true" else "false" + |> (fun x -> x.ToString ()) + |> sprintf " (default value: %s)") + "") + ] + |> String.concat "\n" + + let parser_LeftoverArgs : string ResizeArray = ResizeArray () + let mutable arg_0 : DryRunMode option = None + + /// Processes the key-value pair, returning Error if no key was matched. + /// If the key is an arg which can have arity 1, but throws when consuming that arg, we return Error(). + /// This can nevertheless be a successful parse, e.g. when the key may have arity 0. + let processKeyValue (key : string) (value : string) : Result = + if System.String.Equals (key, sprintf "--%s" "dry-run", System.StringComparison.OrdinalIgnoreCase) then + match arg_0 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "dry-run") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_0 <- + value + |> (fun x -> + if System.Boolean.Parse x = Consts.FALSE then + DryRunMode.Wet + else + DryRunMode.Dry + ) + |> Some + + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else + Error None + + /// Returns false if we didn't set a value. + let setFlagValue (key : string) : bool = + if System.String.Equals (key, sprintf "--%s" "dry-run", System.StringComparison.OrdinalIgnoreCase) then + match arg_0 with + | Some x -> + sprintf "Flag '%s' was supplied multiple times" (sprintf "--%s" "dry-run") + |> ArgParser_errors.Add + + true + | None -> + arg_0 <- + if true = Consts.FALSE then + DryRunMode.Wet + else + DryRunMode.Dry + |> Some + + true + else + false + + let rec go (state : ParseState_ContainsFlagDefaultValue) (args : string list) = + match args with + | [] -> + match state with + | ParseState_ContainsFlagDefaultValue.AwaitingKey -> () + | ParseState_ContainsFlagDefaultValue.AwaitingValue key -> + if setFlagValue key then + () + else + sprintf + "Trailing argument %s had no value. Use a double-dash to separate positional args from key-value args." + key + |> ArgParser_errors.Add + | "--" :: rest -> parser_LeftoverArgs.AddRange (rest |> Seq.map (fun x -> x)) + | arg :: args -> + match state with + | ParseState_ContainsFlagDefaultValue.AwaitingKey -> + if arg.StartsWith ("--", System.StringComparison.Ordinal) then + if arg = "--help" then + helpText () |> failwithf "Help text requested.\n%s" + else + let equals = arg.IndexOf (char 61) + + if equals < 0 then + args |> go (ParseState_ContainsFlagDefaultValue.AwaitingValue arg) + else + let key = arg.[0 .. equals - 1] + let value = arg.[equals + 1 ..] + + match processKeyValue key value with + | Ok () -> go ParseState_ContainsFlagDefaultValue.AwaitingKey args + | Error x -> + match x with + | None -> + failwithf + "Unable to process argument %s as key %s and value %s" + arg + key + value + | Some msg -> + sprintf "%s (at arg %s)" msg arg |> ArgParser_errors.Add + go ParseState_ContainsFlagDefaultValue.AwaitingKey args + else + arg |> (fun x -> x) |> parser_LeftoverArgs.Add + go ParseState_ContainsFlagDefaultValue.AwaitingKey args + | ParseState_ContainsFlagDefaultValue.AwaitingValue key -> + match processKeyValue key arg with + | Ok () -> go ParseState_ContainsFlagDefaultValue.AwaitingKey args + | Error exc -> + if setFlagValue key then + go ParseState_ContainsFlagDefaultValue.AwaitingKey (arg :: args) + else + match exc with + | None -> + failwithf + "Unable to process supplied arg %s. Help text follows.\n%s" + key + (helpText ()) + | Some msg -> msg |> ArgParser_errors.Add + + go ParseState_ContainsFlagDefaultValue.AwaitingKey args + + let parser_LeftoverArgs = + if 0 = parser_LeftoverArgs.Count then + () + else + parser_LeftoverArgs + |> String.concat " " + |> sprintf "There were leftover args: %s" + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + + let arg_0 = + match arg_0 with + | None -> ContainsFlagDefaultValue.DefaultDryRun () |> Choice2Of2 + | Some x -> Choice1Of2 x + + if 0 = ArgParser_errors.Count then + { + DryRun = arg_0 + } + else + ArgParser_errors |> String.concat "\n" |> failwithf "Errors during parse!\n%s" + + static member parse (args : string list) : ContainsFlagDefaultValue = + ContainsFlagDefaultValue.parse' System.Environment.GetEnvironmentVariable args +namespace Consumer + +open System +open System.IO +open WoofWare.Whippet.Plugin.ArgParser + +/// Methods to parse arguments for the type ManyLongForms +[] +module ManyLongFormsArgParse = + type private ParseState_ManyLongForms = + /// Ready to consume a key or positional arg + | AwaitingKey + /// Waiting to receive a value for the key we've already consumed + | AwaitingValue of key : string + + /// Extension methods for argument parsing + type ManyLongForms with + + static member parse' (getEnvironmentVariable : string -> string) (args : string list) : ManyLongForms = + let ArgParser_errors = ResizeArray () + + let helpText () = + [ + (sprintf "%s string%s%s" (sprintf "--%s / --%s" "do-something-else" "anotherarg") "" "") + (sprintf "%s bool%s%s" (sprintf "--%s / --%s" "turn-it-on" "dont-turn-it-off") "" "") + ] + |> String.concat "\n" + + let parser_LeftoverArgs : string ResizeArray = ResizeArray () + let mutable arg_0 : string option = None + let mutable arg_1 : bool option = None + + /// Processes the key-value pair, returning Error if no key was matched. + /// If the key is an arg which can have arity 1, but throws when consuming that arg, we return Error(). + /// This can nevertheless be a successful parse, e.g. when the key may have arity 0. + let processKeyValue (key : string) (value : string) : Result = + if + System.String.Equals ( + key, + sprintf "--%s" "dont-turn-it-off", + System.StringComparison.OrdinalIgnoreCase + ) + then + match arg_1 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s / --%s" "turn-it-on" "dont-turn-it-off") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_1 <- value |> (fun x -> System.Boolean.Parse x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals (key, sprintf "--%s" "turn-it-on", System.StringComparison.OrdinalIgnoreCase) + then + match arg_1 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s / --%s" "turn-it-on" "dont-turn-it-off") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_1 <- value |> (fun x -> System.Boolean.Parse x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals (key, sprintf "--%s" "anotherarg", System.StringComparison.OrdinalIgnoreCase) + then + match arg_0 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s / --%s" "do-something-else" "anotherarg") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_0 <- value |> (fun x -> x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals ( + key, + sprintf "--%s" "do-something-else", + System.StringComparison.OrdinalIgnoreCase + ) + then + match arg_0 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s / --%s" "do-something-else" "anotherarg") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_0 <- value |> (fun x -> x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else + Error None + + /// Returns false if we didn't set a value. + let setFlagValue (key : string) : bool = + if + System.String.Equals ( + key, + sprintf "--%s" "dont-turn-it-off", + System.StringComparison.OrdinalIgnoreCase + ) + then + match arg_1 with + | Some x -> + sprintf + "Flag '%s' was supplied multiple times" + (sprintf "--%s / --%s" "turn-it-on" "dont-turn-it-off") + |> ArgParser_errors.Add + + true + | None -> + arg_1 <- true |> Some + true + else if + System.String.Equals (key, sprintf "--%s" "turn-it-on", System.StringComparison.OrdinalIgnoreCase) + then + match arg_1 with + | Some x -> + sprintf + "Flag '%s' was supplied multiple times" + (sprintf "--%s / --%s" "turn-it-on" "dont-turn-it-off") + |> ArgParser_errors.Add + + true + | None -> + arg_1 <- true |> Some + true + else + false + + let rec go (state : ParseState_ManyLongForms) (args : string list) = + match args with + | [] -> + match state with + | ParseState_ManyLongForms.AwaitingKey -> () + | ParseState_ManyLongForms.AwaitingValue key -> + if setFlagValue key then + () + else + sprintf + "Trailing argument %s had no value. Use a double-dash to separate positional args from key-value args." + key + |> ArgParser_errors.Add + | "--" :: rest -> parser_LeftoverArgs.AddRange (rest |> Seq.map (fun x -> x)) + | arg :: args -> + match state with + | ParseState_ManyLongForms.AwaitingKey -> + if arg.StartsWith ("--", System.StringComparison.Ordinal) then + if arg = "--help" then + helpText () |> failwithf "Help text requested.\n%s" + else + let equals = arg.IndexOf (char 61) + + if equals < 0 then + args |> go (ParseState_ManyLongForms.AwaitingValue arg) + else + let key = arg.[0 .. equals - 1] + let value = arg.[equals + 1 ..] + + match processKeyValue key value with + | Ok () -> go ParseState_ManyLongForms.AwaitingKey args + | Error x -> + match x with + | None -> + failwithf + "Unable to process argument %s as key %s and value %s" + arg + key + value + | Some msg -> + sprintf "%s (at arg %s)" msg arg |> ArgParser_errors.Add + go ParseState_ManyLongForms.AwaitingKey args + else + arg |> (fun x -> x) |> parser_LeftoverArgs.Add + go ParseState_ManyLongForms.AwaitingKey args + | ParseState_ManyLongForms.AwaitingValue key -> + match processKeyValue key arg with + | Ok () -> go ParseState_ManyLongForms.AwaitingKey args + | Error exc -> + if setFlagValue key then + go ParseState_ManyLongForms.AwaitingKey (arg :: args) + else + match exc with + | None -> + failwithf + "Unable to process supplied arg %s. Help text follows.\n%s" + key + (helpText ()) + | Some msg -> msg |> ArgParser_errors.Add + + go ParseState_ManyLongForms.AwaitingKey args + + let parser_LeftoverArgs = + if 0 = parser_LeftoverArgs.Count then + () + else + parser_LeftoverArgs + |> String.concat " " + |> sprintf "There were leftover args: %s" + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + + let arg_0 = + match arg_0 with + | None -> + sprintf + "Required argument '%s' received no value" + (sprintf "--%s / --%s" "do-something-else" "anotherarg") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + let arg_1 = + match arg_1 with + | None -> + sprintf + "Required argument '%s' received no value" + (sprintf "--%s / --%s" "turn-it-on" "dont-turn-it-off") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + if 0 = ArgParser_errors.Count then + { + DoTheThing = arg_0 + SomeFlag = arg_1 + } + else + ArgParser_errors |> String.concat "\n" |> failwithf "Errors during parse!\n%s" + + static member parse (args : string list) : ManyLongForms = + ManyLongForms.parse' System.Environment.GetEnvironmentVariable args +namespace Consumer + +open System +open System.IO +open WoofWare.Whippet.Plugin.ArgParser + +/// Methods to parse arguments for the type FlagsIntoPositionalArgs +[] +module FlagsIntoPositionalArgsArgParse = + type private ParseState_FlagsIntoPositionalArgs = + /// Ready to consume a key or positional arg + | AwaitingKey + /// Waiting to receive a value for the key we've already consumed + | AwaitingValue of key : string + + /// Extension methods for argument parsing + type FlagsIntoPositionalArgs with + + static member parse' + (getEnvironmentVariable : string -> string) + (args : string list) + : FlagsIntoPositionalArgs + = + let ArgParser_errors = ResizeArray () + + let helpText () = + [ + (sprintf "%s string%s%s" (sprintf "--%s" "a") "" "") + (sprintf + "%s string%s%s" + (sprintf "--%s" "grab-everything") + " (positional args) (can be repeated)" + "") + ] + |> String.concat "\n" + + let arg_1 : string ResizeArray = ResizeArray () + let mutable arg_0 : string option = None + + /// Processes the key-value pair, returning Error if no key was matched. + /// If the key is an arg which can have arity 1, but throws when consuming that arg, we return Error(). + /// This can nevertheless be a successful parse, e.g. when the key may have arity 0. + let processKeyValue (key : string) (value : string) : Result = + if System.String.Equals (key, sprintf "--%s" "a", System.StringComparison.OrdinalIgnoreCase) then + match arg_0 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "a") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_0 <- value |> (fun x -> x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals ( + key, + sprintf "--%s" "grab-everything", + System.StringComparison.OrdinalIgnoreCase + ) + then + value |> (fun x -> x) |> arg_1.Add + () |> Ok + else + Error None + + /// Returns false if we didn't set a value. + let setFlagValue (key : string) : bool = false + + let rec go (state : ParseState_FlagsIntoPositionalArgs) (args : string list) = + match args with + | [] -> + match state with + | ParseState_FlagsIntoPositionalArgs.AwaitingKey -> () + | ParseState_FlagsIntoPositionalArgs.AwaitingValue key -> + if setFlagValue key then + () + else + sprintf + "Trailing argument %s had no value. Use a double-dash to separate positional args from key-value args." + key + |> ArgParser_errors.Add + | "--" :: rest -> arg_1.AddRange (rest |> Seq.map (fun x -> x)) + | arg :: args -> + match state with + | ParseState_FlagsIntoPositionalArgs.AwaitingKey -> + if arg.StartsWith ("--", System.StringComparison.Ordinal) then + if arg = "--help" then + helpText () |> failwithf "Help text requested.\n%s" + else + let equals = arg.IndexOf (char 61) + + if equals < 0 then + args |> go (ParseState_FlagsIntoPositionalArgs.AwaitingValue arg) + else + let key = arg.[0 .. equals - 1] + let value = arg.[equals + 1 ..] + + match processKeyValue key value with + | Ok () -> go ParseState_FlagsIntoPositionalArgs.AwaitingKey args + | Error x -> + if true then + arg |> (fun x -> x) |> arg_1.Add + go ParseState_FlagsIntoPositionalArgs.AwaitingKey args + else + match x with + | None -> + failwithf + "Unable to process argument %s as key %s and value %s" + arg + key + value + | Some msg -> + sprintf "%s (at arg %s)" msg arg |> ArgParser_errors.Add + go ParseState_FlagsIntoPositionalArgs.AwaitingKey args + else + arg |> (fun x -> x) |> arg_1.Add + go ParseState_FlagsIntoPositionalArgs.AwaitingKey args + | ParseState_FlagsIntoPositionalArgs.AwaitingValue key -> + match processKeyValue key arg with + | Ok () -> go ParseState_FlagsIntoPositionalArgs.AwaitingKey args + | Error exc -> + if setFlagValue key then + go ParseState_FlagsIntoPositionalArgs.AwaitingKey (arg :: args) + else if true then + key |> (fun x -> x) |> arg_1.Add + go ParseState_FlagsIntoPositionalArgs.AwaitingKey (arg :: args) + else + match exc with + | None -> + failwithf + "Unable to process supplied arg %s. Help text follows.\n%s" + key + (helpText ()) + | Some msg -> msg |> ArgParser_errors.Add + + go ParseState_FlagsIntoPositionalArgs.AwaitingKey args + let arg_1 = arg_1 |> Seq.toList + + let arg_0 = + match arg_0 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "a") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + if 0 = ArgParser_errors.Count then + { + A = arg_0 + GrabEverything = arg_1 + } + else + ArgParser_errors |> String.concat "\n" |> failwithf "Errors during parse!\n%s" + + static member parse (args : string list) : FlagsIntoPositionalArgs = + FlagsIntoPositionalArgs.parse' System.Environment.GetEnvironmentVariable args +namespace Consumer + +open System +open System.IO +open WoofWare.Whippet.Plugin.ArgParser + +/// Methods to parse arguments for the type FlagsIntoPositionalArgsChoice +[] +module FlagsIntoPositionalArgsChoiceArgParse = + type private ParseState_FlagsIntoPositionalArgsChoice = + /// Ready to consume a key or positional arg + | AwaitingKey + /// Waiting to receive a value for the key we've already consumed + | AwaitingValue of key : string + + /// Extension methods for argument parsing + type FlagsIntoPositionalArgsChoice with + + static member parse' + (getEnvironmentVariable : string -> string) + (args : string list) + : FlagsIntoPositionalArgsChoice + = + let ArgParser_errors = ResizeArray () + + let helpText () = + [ + (sprintf "%s string%s%s" (sprintf "--%s" "a") "" "") + (sprintf + "%s string%s%s" + (sprintf "--%s" "grab-everything") + " (positional args) (can be repeated)" + "") + ] + |> String.concat "\n" + + let arg_1 : Choice ResizeArray = ResizeArray () + let mutable arg_0 : string option = None + + /// Processes the key-value pair, returning Error if no key was matched. + /// If the key is an arg which can have arity 1, but throws when consuming that arg, we return Error(). + /// This can nevertheless be a successful parse, e.g. when the key may have arity 0. + let processKeyValue (key : string) (value : string) : Result = + if System.String.Equals (key, sprintf "--%s" "a", System.StringComparison.OrdinalIgnoreCase) then + match arg_0 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "a") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_0 <- value |> (fun x -> x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals ( + key, + sprintf "--%s" "grab-everything", + System.StringComparison.OrdinalIgnoreCase + ) + then + value |> (fun x -> x) |> Choice1Of2 |> arg_1.Add + () |> Ok + else + Error None + + /// Returns false if we didn't set a value. + let setFlagValue (key : string) : bool = false + + let rec go (state : ParseState_FlagsIntoPositionalArgsChoice) (args : string list) = + match args with + | [] -> + match state with + | ParseState_FlagsIntoPositionalArgsChoice.AwaitingKey -> () + | ParseState_FlagsIntoPositionalArgsChoice.AwaitingValue key -> + if setFlagValue key then + () + else + sprintf + "Trailing argument %s had no value. Use a double-dash to separate positional args from key-value args." + key + |> ArgParser_errors.Add + | "--" :: rest -> arg_1.AddRange (rest |> Seq.map (fun x -> x) |> Seq.map Choice2Of2) + | arg :: args -> + match state with + | ParseState_FlagsIntoPositionalArgsChoice.AwaitingKey -> + if arg.StartsWith ("--", System.StringComparison.Ordinal) then + if arg = "--help" then + helpText () |> failwithf "Help text requested.\n%s" + else + let equals = arg.IndexOf (char 61) + + if equals < 0 then + args |> go (ParseState_FlagsIntoPositionalArgsChoice.AwaitingValue arg) + else + let key = arg.[0 .. equals - 1] + let value = arg.[equals + 1 ..] + + match processKeyValue key value with + | Ok () -> go ParseState_FlagsIntoPositionalArgsChoice.AwaitingKey args + | Error x -> + if true then + arg |> (fun x -> x) |> Choice1Of2 |> arg_1.Add + go ParseState_FlagsIntoPositionalArgsChoice.AwaitingKey args + else + match x with + | None -> + failwithf + "Unable to process argument %s as key %s and value %s" + arg + key + value + | Some msg -> + sprintf "%s (at arg %s)" msg arg |> ArgParser_errors.Add + go ParseState_FlagsIntoPositionalArgsChoice.AwaitingKey args + else + arg |> (fun x -> x) |> Choice1Of2 |> arg_1.Add + go ParseState_FlagsIntoPositionalArgsChoice.AwaitingKey args + | ParseState_FlagsIntoPositionalArgsChoice.AwaitingValue key -> + match processKeyValue key arg with + | Ok () -> go ParseState_FlagsIntoPositionalArgsChoice.AwaitingKey args + | Error exc -> + if setFlagValue key then + go ParseState_FlagsIntoPositionalArgsChoice.AwaitingKey (arg :: args) + else if true then + key |> (fun x -> x) |> Choice1Of2 |> arg_1.Add + go ParseState_FlagsIntoPositionalArgsChoice.AwaitingKey (arg :: args) + else + match exc with + | None -> + failwithf + "Unable to process supplied arg %s. Help text follows.\n%s" + key + (helpText ()) + | Some msg -> msg |> ArgParser_errors.Add + + go ParseState_FlagsIntoPositionalArgsChoice.AwaitingKey args + let arg_1 = arg_1 |> Seq.toList + + let arg_0 = + match arg_0 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "a") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + if 0 = ArgParser_errors.Count then + { + A = arg_0 + GrabEverything = arg_1 + } + else + ArgParser_errors |> String.concat "\n" |> failwithf "Errors during parse!\n%s" + + static member parse (args : string list) : FlagsIntoPositionalArgsChoice = + FlagsIntoPositionalArgsChoice.parse' System.Environment.GetEnvironmentVariable args +namespace Consumer + +open System +open System.IO +open WoofWare.Whippet.Plugin.ArgParser + +/// Methods to parse arguments for the type FlagsIntoPositionalArgsInt +[] +module FlagsIntoPositionalArgsIntArgParse = + type private ParseState_FlagsIntoPositionalArgsInt = + /// Ready to consume a key or positional arg + | AwaitingKey + /// Waiting to receive a value for the key we've already consumed + | AwaitingValue of key : string + + /// Extension methods for argument parsing + type FlagsIntoPositionalArgsInt with + + static member parse' + (getEnvironmentVariable : string -> string) + (args : string list) + : FlagsIntoPositionalArgsInt + = + let ArgParser_errors = ResizeArray () + + let helpText () = + [ + (sprintf "%s string%s%s" (sprintf "--%s" "a") "" "") + (sprintf + "%s int32%s%s" + (sprintf "--%s" "grab-everything") + " (positional args) (can be repeated)" + "") + ] + |> String.concat "\n" + + let arg_1 : int ResizeArray = ResizeArray () + let mutable arg_0 : string option = None + + /// Processes the key-value pair, returning Error if no key was matched. + /// If the key is an arg which can have arity 1, but throws when consuming that arg, we return Error(). + /// This can nevertheless be a successful parse, e.g. when the key may have arity 0. + let processKeyValue (key : string) (value : string) : Result = + if System.String.Equals (key, sprintf "--%s" "a", System.StringComparison.OrdinalIgnoreCase) then + match arg_0 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "a") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_0 <- value |> (fun x -> x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals ( + key, + sprintf "--%s" "grab-everything", + System.StringComparison.OrdinalIgnoreCase + ) + then + value |> (fun x -> System.Int32.Parse x) |> arg_1.Add + () |> Ok + else + Error None + + /// Returns false if we didn't set a value. + let setFlagValue (key : string) : bool = false + + let rec go (state : ParseState_FlagsIntoPositionalArgsInt) (args : string list) = + match args with + | [] -> + match state with + | ParseState_FlagsIntoPositionalArgsInt.AwaitingKey -> () + | ParseState_FlagsIntoPositionalArgsInt.AwaitingValue key -> + if setFlagValue key then + () + else + sprintf + "Trailing argument %s had no value. Use a double-dash to separate positional args from key-value args." + key + |> ArgParser_errors.Add + | "--" :: rest -> arg_1.AddRange (rest |> Seq.map (fun x -> System.Int32.Parse x)) + | arg :: args -> + match state with + | ParseState_FlagsIntoPositionalArgsInt.AwaitingKey -> + if arg.StartsWith ("--", System.StringComparison.Ordinal) then + if arg = "--help" then + helpText () |> failwithf "Help text requested.\n%s" + else + let equals = arg.IndexOf (char 61) + + if equals < 0 then + args |> go (ParseState_FlagsIntoPositionalArgsInt.AwaitingValue arg) + else + let key = arg.[0 .. equals - 1] + let value = arg.[equals + 1 ..] + + match processKeyValue key value with + | Ok () -> go ParseState_FlagsIntoPositionalArgsInt.AwaitingKey args + | Error x -> + if true then + arg |> (fun x -> System.Int32.Parse x) |> arg_1.Add + go ParseState_FlagsIntoPositionalArgsInt.AwaitingKey args + else + match x with + | None -> + failwithf + "Unable to process argument %s as key %s and value %s" + arg + key + value + | Some msg -> + sprintf "%s (at arg %s)" msg arg |> ArgParser_errors.Add + go ParseState_FlagsIntoPositionalArgsInt.AwaitingKey args + else + arg |> (fun x -> System.Int32.Parse x) |> arg_1.Add + go ParseState_FlagsIntoPositionalArgsInt.AwaitingKey args + | ParseState_FlagsIntoPositionalArgsInt.AwaitingValue key -> + match processKeyValue key arg with + | Ok () -> go ParseState_FlagsIntoPositionalArgsInt.AwaitingKey args + | Error exc -> + if setFlagValue key then + go ParseState_FlagsIntoPositionalArgsInt.AwaitingKey (arg :: args) + else if true then + key |> (fun x -> System.Int32.Parse x) |> arg_1.Add + go ParseState_FlagsIntoPositionalArgsInt.AwaitingKey (arg :: args) + else + match exc with + | None -> + failwithf + "Unable to process supplied arg %s. Help text follows.\n%s" + key + (helpText ()) + | Some msg -> msg |> ArgParser_errors.Add + + go ParseState_FlagsIntoPositionalArgsInt.AwaitingKey args + let arg_1 = arg_1 |> Seq.toList + + let arg_0 = + match arg_0 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "a") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + if 0 = ArgParser_errors.Count then + { + A = arg_0 + GrabEverything = arg_1 + } + else + ArgParser_errors |> String.concat "\n" |> failwithf "Errors during parse!\n%s" + + static member parse (args : string list) : FlagsIntoPositionalArgsInt = + FlagsIntoPositionalArgsInt.parse' System.Environment.GetEnvironmentVariable args +namespace Consumer + +open System +open System.IO +open WoofWare.Whippet.Plugin.ArgParser + +/// Methods to parse arguments for the type FlagsIntoPositionalArgsIntChoice +[] +module FlagsIntoPositionalArgsIntChoiceArgParse = + type private ParseState_FlagsIntoPositionalArgsIntChoice = + /// Ready to consume a key or positional arg + | AwaitingKey + /// Waiting to receive a value for the key we've already consumed + | AwaitingValue of key : string + + /// Extension methods for argument parsing + type FlagsIntoPositionalArgsIntChoice with + + static member parse' + (getEnvironmentVariable : string -> string) + (args : string list) + : FlagsIntoPositionalArgsIntChoice + = + let ArgParser_errors = ResizeArray () + + let helpText () = + [ + (sprintf "%s string%s%s" (sprintf "--%s" "a") "" "") + (sprintf + "%s int32%s%s" + (sprintf "--%s" "grab-everything") + " (positional args) (can be repeated)" + "") + ] + |> String.concat "\n" + + let arg_1 : Choice ResizeArray = ResizeArray () + let mutable arg_0 : string option = None + + /// Processes the key-value pair, returning Error if no key was matched. + /// If the key is an arg which can have arity 1, but throws when consuming that arg, we return Error(). + /// This can nevertheless be a successful parse, e.g. when the key may have arity 0. + let processKeyValue (key : string) (value : string) : Result = + if System.String.Equals (key, sprintf "--%s" "a", System.StringComparison.OrdinalIgnoreCase) then + match arg_0 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "a") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_0 <- value |> (fun x -> x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals ( + key, + sprintf "--%s" "grab-everything", + System.StringComparison.OrdinalIgnoreCase + ) + then + value |> (fun x -> System.Int32.Parse x) |> Choice1Of2 |> arg_1.Add + () |> Ok + else + Error None + + /// Returns false if we didn't set a value. + let setFlagValue (key : string) : bool = false + + let rec go (state : ParseState_FlagsIntoPositionalArgsIntChoice) (args : string list) = + match args with + | [] -> + match state with + | ParseState_FlagsIntoPositionalArgsIntChoice.AwaitingKey -> () + | ParseState_FlagsIntoPositionalArgsIntChoice.AwaitingValue key -> + if setFlagValue key then + () + else + sprintf + "Trailing argument %s had no value. Use a double-dash to separate positional args from key-value args." + key + |> ArgParser_errors.Add + | "--" :: rest -> arg_1.AddRange (rest |> Seq.map (fun x -> System.Int32.Parse x) |> Seq.map Choice2Of2) + | arg :: args -> + match state with + | ParseState_FlagsIntoPositionalArgsIntChoice.AwaitingKey -> + if arg.StartsWith ("--", System.StringComparison.Ordinal) then + if arg = "--help" then + helpText () |> failwithf "Help text requested.\n%s" + else + let equals = arg.IndexOf (char 61) + + if equals < 0 then + args |> go (ParseState_FlagsIntoPositionalArgsIntChoice.AwaitingValue arg) + else + let key = arg.[0 .. equals - 1] + let value = arg.[equals + 1 ..] + + match processKeyValue key value with + | Ok () -> go ParseState_FlagsIntoPositionalArgsIntChoice.AwaitingKey args + | Error x -> + if true then + arg |> (fun x -> System.Int32.Parse x) |> Choice1Of2 |> arg_1.Add + go ParseState_FlagsIntoPositionalArgsIntChoice.AwaitingKey args + else + match x with + | None -> + failwithf + "Unable to process argument %s as key %s and value %s" + arg + key + value + | Some msg -> + sprintf "%s (at arg %s)" msg arg |> ArgParser_errors.Add + go ParseState_FlagsIntoPositionalArgsIntChoice.AwaitingKey args + else + arg |> (fun x -> System.Int32.Parse x) |> Choice1Of2 |> arg_1.Add + go ParseState_FlagsIntoPositionalArgsIntChoice.AwaitingKey args + | ParseState_FlagsIntoPositionalArgsIntChoice.AwaitingValue key -> + match processKeyValue key arg with + | Ok () -> go ParseState_FlagsIntoPositionalArgsIntChoice.AwaitingKey args + | Error exc -> + if setFlagValue key then + go ParseState_FlagsIntoPositionalArgsIntChoice.AwaitingKey (arg :: args) + else if true then + key |> (fun x -> System.Int32.Parse x) |> Choice1Of2 |> arg_1.Add + go ParseState_FlagsIntoPositionalArgsIntChoice.AwaitingKey (arg :: args) + else + match exc with + | None -> + failwithf + "Unable to process supplied arg %s. Help text follows.\n%s" + key + (helpText ()) + | Some msg -> msg |> ArgParser_errors.Add + + go ParseState_FlagsIntoPositionalArgsIntChoice.AwaitingKey args + let arg_1 = arg_1 |> Seq.toList + + let arg_0 = + match arg_0 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "a") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + if 0 = ArgParser_errors.Count then + { + A = arg_0 + GrabEverything = arg_1 + } + else + ArgParser_errors |> String.concat "\n" |> failwithf "Errors during parse!\n%s" + + static member parse (args : string list) : FlagsIntoPositionalArgsIntChoice = + FlagsIntoPositionalArgsIntChoice.parse' System.Environment.GetEnvironmentVariable args +namespace Consumer + +open System +open System.IO +open WoofWare.Whippet.Plugin.ArgParser + +/// Methods to parse arguments for the type FlagsIntoPositionalArgs' +[] +module FlagsIntoPositionalArgs'ArgParse = + type private ParseState_FlagsIntoPositionalArgs' = + /// Ready to consume a key or positional arg + | AwaitingKey + /// Waiting to receive a value for the key we've already consumed + | AwaitingValue of key : string + + /// Extension methods for argument parsing + type FlagsIntoPositionalArgs' with + + static member parse' + (getEnvironmentVariable : string -> string) + (args : string list) + : FlagsIntoPositionalArgs' + = + let ArgParser_errors = ResizeArray () + + let helpText () = + [ + (sprintf "%s string%s%s" (sprintf "--%s" "a") "" "") + (sprintf + "%s string%s%s" + (sprintf "--%s" "dont-grab-everything") + " (positional args) (can be repeated)" + "") + ] + |> String.concat "\n" + + let arg_1 : string ResizeArray = ResizeArray () + let mutable arg_0 : string option = None + + /// Processes the key-value pair, returning Error if no key was matched. + /// If the key is an arg which can have arity 1, but throws when consuming that arg, we return Error(). + /// This can nevertheless be a successful parse, e.g. when the key may have arity 0. + let processKeyValue (key : string) (value : string) : Result = + if System.String.Equals (key, sprintf "--%s" "a", System.StringComparison.OrdinalIgnoreCase) then + match arg_0 with + | Some x -> + sprintf + "Argument '%s' was supplied multiple times: %s and %s" + (sprintf "--%s" "a") + (x.ToString ()) + (value.ToString ()) + |> ArgParser_errors.Add + + Ok () + | None -> + try + arg_0 <- value |> (fun x -> x) |> Some + Ok () + with _ as exc -> + exc.Message |> Some |> Error + else if + System.String.Equals ( + key, + sprintf "--%s" "dont-grab-everything", + System.StringComparison.OrdinalIgnoreCase + ) + then + value |> (fun x -> x) |> arg_1.Add + () |> Ok + else + Error None + + /// Returns false if we didn't set a value. + let setFlagValue (key : string) : bool = false + + let rec go (state : ParseState_FlagsIntoPositionalArgs') (args : string list) = + match args with + | [] -> + match state with + | ParseState_FlagsIntoPositionalArgs'.AwaitingKey -> () + | ParseState_FlagsIntoPositionalArgs'.AwaitingValue key -> + if setFlagValue key then + () + else + sprintf + "Trailing argument %s had no value. Use a double-dash to separate positional args from key-value args." + key + |> ArgParser_errors.Add + | "--" :: rest -> arg_1.AddRange (rest |> Seq.map (fun x -> x)) + | arg :: args -> + match state with + | ParseState_FlagsIntoPositionalArgs'.AwaitingKey -> + if arg.StartsWith ("--", System.StringComparison.Ordinal) then + if arg = "--help" then + helpText () |> failwithf "Help text requested.\n%s" + else + let equals = arg.IndexOf (char 61) + + if equals < 0 then + args |> go (ParseState_FlagsIntoPositionalArgs'.AwaitingValue arg) + else + let key = arg.[0 .. equals - 1] + let value = arg.[equals + 1 ..] + + match processKeyValue key value with + | Ok () -> go ParseState_FlagsIntoPositionalArgs'.AwaitingKey args + | Error x -> + if false then + arg |> (fun x -> x) |> arg_1.Add + go ParseState_FlagsIntoPositionalArgs'.AwaitingKey args + else + match x with + | None -> + failwithf + "Unable to process argument %s as key %s and value %s" + arg + key + value + | Some msg -> + sprintf "%s (at arg %s)" msg arg |> ArgParser_errors.Add + go ParseState_FlagsIntoPositionalArgs'.AwaitingKey args + else + arg |> (fun x -> x) |> arg_1.Add + go ParseState_FlagsIntoPositionalArgs'.AwaitingKey args + | ParseState_FlagsIntoPositionalArgs'.AwaitingValue key -> + match processKeyValue key arg with + | Ok () -> go ParseState_FlagsIntoPositionalArgs'.AwaitingKey args + | Error exc -> + if setFlagValue key then + go ParseState_FlagsIntoPositionalArgs'.AwaitingKey (arg :: args) + else if false then + key |> (fun x -> x) |> arg_1.Add + go ParseState_FlagsIntoPositionalArgs'.AwaitingKey (arg :: args) + else + match exc with + | None -> + failwithf + "Unable to process supplied arg %s. Help text follows.\n%s" + key + (helpText ()) + | Some msg -> msg |> ArgParser_errors.Add + + go ParseState_FlagsIntoPositionalArgs'.AwaitingKey args + let arg_1 = arg_1 |> Seq.toList + + let arg_0 = + match arg_0 with + | None -> + sprintf "Required argument '%s' received no value" (sprintf "--%s" "a") + |> ArgParser_errors.Add + + Unchecked.defaultof<_> + | Some x -> x + + if 0 = ArgParser_errors.Count then + { + A = arg_0 + DontGrabEverything = arg_1 + } + else + ArgParser_errors |> String.concat "\n" |> failwithf "Errors during parse!\n%s" + + static member parse (args : string list) : FlagsIntoPositionalArgs' = + FlagsIntoPositionalArgs'.parse' System.Environment.GetEnvironmentVariable args diff --git a/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Consumer/WoofWare.Whippet.Plugin.ArgParser.Consumer.fsproj b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Consumer/WoofWare.Whippet.Plugin.ArgParser.Consumer.fsproj new file mode 100644 index 0000000..430484e --- /dev/null +++ b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser.Consumer/WoofWare.Whippet.Plugin.ArgParser.Consumer.fsproj @@ -0,0 +1,26 @@ + + + + net8.0 + true + false + $(MSBuildThisFileDirectory)/../../../WoofWare.Whippet/bin/$(Configuration) + + + + + + Args.fs + + + + + + + + + + + + + diff --git a/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/ArgParserGenerator.fs b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/ArgParserGenerator.fs new file mode 100644 index 0000000..88e5609 --- /dev/null +++ b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/ArgParserGenerator.fs @@ -0,0 +1,1807 @@ +namespace WoofWare.Whippet.Plugin.ArgParser + +open System +open System.Text +open Fantomas.FCS.Syntax +open WoofWare.Whippet.Core +open WoofWare.Whippet.Fantomas +open Fantomas.FCS.Text.Range +open TypeEquality + +type internal ArgParserOutputSpec = + { + ExtensionMethods : bool + } + +type internal FlagDu = + { + Name : Ident + Case1Name : Ident + Case2Name : Ident + /// Hopefully this is simply the const bool True or False, but it might e.g. be a literal + Case1Arg : SynExpr + /// Hopefully this is simply the const bool True or False, but it might e.g. be a literal + Case2Arg : SynExpr + } + + static member FromBoolean (flagDu : FlagDu) (value : SynExpr) = + SynExpr.ifThenElse + (SynExpr.equals value flagDu.Case1Arg) + (SynExpr.createLongIdent' [ flagDu.Name ; flagDu.Case2Name ]) + (SynExpr.createLongIdent' [ flagDu.Name ; flagDu.Case1Name ]) + +/// The default value of an argument which admits default values can be pulled from different sources. +/// This defines which source a particular default value comes from. +type private ArgumentDefaultSpec = + /// From parsing the environment variable with the given name (e.g. "WOOFWARE_DISABLE_FOO" or whatever). + | EnvironmentVariable of name : SynExpr + /// From calling the static member `{typeWeParseInto}.Default{name}()` + /// For example, if `type MyArgs = { Thing : Choice }`, then + /// we would use `MyArgs.DefaultThing () : int`. + /// + | FunctionCall of name : Ident + +type private Accumulation<'choice> = + | Required + | Optional + | Choice of 'choice + | List of Accumulation<'choice> + +type private ParseFunction<'acc> = + { + FieldName : Ident + TargetVariable : Ident + /// Any of the forms in this set are acceptable, but make sure they all start with a dash, or we might + /// get confused with positional args or something! I haven't thought that hard about this. + /// In the default case, this is `Const("arg-name")` for the `ArgName : blah` field; note that we have + /// omitted the initial `--` that will be required at runtime. + ArgForm : SynExpr list + /// If this is a boolean-like field (e.g. a bool or a flag DU), the help text should look a bit different: + /// we should lie to the user about the value of the cases there. + /// Similarly, if we're reading from an environment variable with the laxer parsing rules of accepting e.g. + /// "0" instead of "false", we need to know if we're reading a bool. + /// In that case, `boolCases` is Some, and contains the construction of the flag (or boolean, in which case + /// you get no data). + BoolCases : Choice option + Help : SynExpr option + /// A function string -> %TargetType%, where TargetVariable is probably a `%TargetType% option`. + /// (Depending on `Accumulation`, we'll remove the `option` at the end of the parse, asserting that the + /// argument was supplied.) + /// This is allowed to throw if it fails to parse. + Parser : SynExpr + /// If `Accumulation` is `List`, then this is the type of the list *element*; analogously for optionals + /// and choices and so on. + TargetType : SynType + Accumulation : 'acc + } + + /// A SynExpr of type `string` which we can display to the user at generated-program runtime to display all + /// the ways they can refer to this arg. + member arg.HumanReadableArgForm : SynExpr = + let formatString = List.replicate arg.ArgForm.Length "--%s" |> String.concat " / " + + (SynExpr.applyFunction (SynExpr.createIdent "sprintf") (SynExpr.CreateConst formatString), arg.ArgForm) + ||> List.fold SynExpr.applyFunction + |> SynExpr.paren + +[] +type private ChoicePositional = + | Normal of includeFlagLike : SynExpr option + | Choice of includeFlagLike : SynExpr option + +type private ParseFunctionPositional = ParseFunction +type private ParseFunctionNonPositional = ParseFunction> + +type private ParserSpec = + { + NonPositionals : ParseFunctionNonPositional list + /// The variable into which positional arguments will be accumulated. + /// In this case, the TargetVariable is a `ResizeArray` rather than the usual `option`. + Positionals : ParseFunctionPositional option + } + +type private HasPositional = HasPositional +type private HasNoPositional = HasNoPositional + +[] +module private TeqUtils = + let exFalso<'a> (_ : Teq) : 'a = failwith "LOGIC ERROR!" + let exFalso'<'a> (_ : Teq) : 'a = failwith "LOGIC ERROR!" + +[] +type private ParseTree<'hasPositional> = + | NonPositionalLeaf of ParseFunctionNonPositional * Teq<'hasPositional, HasNoPositional> + | PositionalLeaf of ParseFunctionPositional * Teq<'hasPositional, HasPositional> + /// `assemble` takes the SynExpr's (e.g. each record field contents) corresponding to each `Ident` in + /// the branch (e.g. each record field name), + /// and composes them into a `SynExpr` (e.g. the record-typed object). + | Branch of + fields : (Ident * ParseTree) list * + assemble : (Map -> SynExpr) * + Teq<'hasPositional, HasNoPositional> + /// `assemble` takes the SynExpr's (e.g. each record field contents) corresponding to each `Ident` in + /// the branch (e.g. each record field name), + /// and composes them into a `SynExpr` (e.g. the record-typed object). + | BranchPos of + posField : Ident * + fields : ParseTree * + (Ident * ParseTree) list * + assemble : (Map -> SynExpr) * + Teq<'hasPositional, HasPositional> + +type private ParseTreeEval<'ret> = + abstract Eval<'a> : ParseTree<'a> -> 'ret + +type private ParseTreeCrate = + abstract Apply<'ret> : ParseTreeEval<'ret> -> 'ret + +[] +module private ParseTreeCrate = + let make<'a> (p : ParseTree<'a>) = + { new ParseTreeCrate with + member _.Apply a = a.Eval p + } + +[] +module private ParseTree = + [] + type State = + | Positional of ParseTree * ParseTree list + | NoPositional of ParseTree list + + let private cast (t : Teq<'a, 'b>) : Teq, ParseTree<'b>> = Teq.Cong.believeMe t + + /// The `Ident` here is the field name. + let branch (assemble : Map -> SynExpr) (subs : (Ident * ParseTreeCrate) list) : ParseTreeCrate = + let rec go + (selfIdent : Ident option) + (acc : (Ident * ParseTree) list, pos : (Ident * ParseTree) option) + (subs : (Ident * ParseTreeCrate) list) + : ParseTreeCrate + = + match subs with + | [] -> + match pos with + | None -> ParseTree.Branch (List.rev acc, assemble, Teq.refl) |> ParseTreeCrate.make + | Some (posField, pos) -> + ParseTree.BranchPos (posField, pos, List.rev acc, assemble, Teq.refl) + |> ParseTreeCrate.make + | (fieldName, sub) :: subs -> + { new ParseTreeEval<_> with + member _.Eval (t : ParseTree<'a>) = + match t with + | ParseTree.NonPositionalLeaf (_, teq) + | ParseTree.Branch (_, _, teq) -> + go selfIdent (((fieldName, Teq.cast (cast teq) t) :: acc), pos) subs + | ParseTree.PositionalLeaf (_, teq) + | ParseTree.BranchPos (_, _, _, _, teq) -> + match pos with + | None -> go selfIdent (acc, Some (fieldName, Teq.cast (cast teq) t)) subs + | Some (ident, _) -> + failwith + $"Multiple entries tried to claim positional args! %s{ident.idText} and %s{fieldName.idText}" + } + |> sub.Apply + + go None ([], None) subs + + let rec accumulatorsNonPos (tree : ParseTree) : ParseFunctionNonPositional list = + match tree with + | ParseTree.PositionalLeaf (_, teq) -> exFalso teq + | ParseTree.BranchPos (_, _, _, _, teq) -> exFalso teq + | ParseTree.NonPositionalLeaf (pf, _) -> [ pf ] + | ParseTree.Branch (trees, _, _) -> trees |> List.collect (snd >> accumulatorsNonPos) + + /// Returns the positional arg separately. + let rec accumulatorsPos + (tree : ParseTree) + : ParseFunctionNonPositional list * ParseFunctionPositional + = + match tree with + | ParseTree.PositionalLeaf (pf, _) -> [], pf + | ParseTree.NonPositionalLeaf (_, teq) -> exFalso' teq + | ParseTree.Branch (_, _, teq) -> exFalso' teq + | ParseTree.BranchPos (_, tree, trees, _, _) -> + let nonPos = trees |> List.collect (snd >> accumulatorsNonPos) + + let nonPos2, pos = accumulatorsPos tree + nonPos @ nonPos2, pos + + /// Collect all the ParseFunctions which are necessary to define variables, throwing away + /// all information relevant to composing the resulting variables into records. + /// Returns the list of non-positional parsers, and any positional parser that exists. + let accumulators<'a> (tree : ParseTree<'a>) : ParseFunctionNonPositional list * ParseFunctionPositional option = + // Sad duplication of some code here, but it was the easiest way to make it type-safe :( + match tree with + | ParseTree.PositionalLeaf (pf, _) -> [], Some pf + | ParseTree.NonPositionalLeaf (pf, _) -> [ pf ], None + | ParseTree.Branch (trees, _, _) -> trees |> List.collect (snd >> accumulatorsNonPos) |> (fun i -> i, None) + | ParseTree.BranchPos (_, tree, trees, _, _) -> + let nonPos = trees |> List.collect (snd >> accumulatorsNonPos) + + let nonPos2, pos = accumulatorsPos tree + nonPos @ nonPos2, Some pos + + |> fun (nonPos, pos) -> + let duplicateArgs = + // This is best-effort. We can't necessarily detect all SynExprs here, but usually it'll be strings. + Option.toList (pos |> Option.map _.ArgForm) @ (nonPos |> List.map _.ArgForm) + |> Seq.concat + |> Seq.choose (fun expr -> + match expr |> SynExpr.stripOptionalParen with + | SynExpr.Const (SynConst.String (s, _, _), _) -> Some s + | _ -> None + ) + |> List.ofSeq + |> List.groupBy id + |> List.choose (fun (key, v) -> if v.Length > 1 then Some key else None) + + match duplicateArgs with + | [] -> nonPos, pos + | dups -> + let dups = dups |> String.concat " " + failwith $"Duplicate args detected! %s{dups}" + + /// Build the return value. + let rec instantiate<'a> (tree : ParseTree<'a>) : SynExpr = + match tree with + | ParseTree.NonPositionalLeaf (pf, _) -> SynExpr.createIdent' pf.TargetVariable + | ParseTree.PositionalLeaf (pf, _) -> SynExpr.createIdent' pf.TargetVariable + | ParseTree.Branch (trees, assemble, _) -> + trees + |> List.map (fun (fieldName, contents) -> + let instantiated = instantiate contents + fieldName.idText, instantiated + ) + |> Map.ofList + |> assemble + | ParseTree.BranchPos (posField, tree, trees, assemble, _) -> + let withPos = instantiate tree + + trees + |> List.map (fun (fieldName, contents) -> + let instantiated = instantiate contents + fieldName.idText, instantiated + ) + |> Map.ofList + |> Map.add posField.idText withPos + |> assemble + +[] +module internal ArgParserGenerator = + + /// Convert e.g. "Foo" into "--foo". + let argify (ident : Ident) : string = + let result = StringBuilder () + + for c in ident.idText do + if Char.IsUpper c then + result.Append('-').Append (Char.ToLowerInvariant c) |> ignore + else + result.Append c |> ignore + + result.ToString().TrimStart '-' + + let private identifyAsFlag (flagDus : FlagDu list) (ty : SynType) : FlagDu option = + match ty with + | SynType.LongIdent (SynLongIdent.SynLongIdent (ident, _, _)) -> + flagDus + |> List.tryPick (fun du -> + let duName = du.Name.idText + let ident = List.last(ident).idText + if duName = ident then Some du else None + ) + | _ -> None + + /// Builds a function or lambda of one string argument, which returns a `ty` (as modified by the `Accumulation`; + /// for example, maybe it returns a `ty option` or a `ty list`). + /// The resulting SynType is the type of the *element* being parsed; so if the Accumulation is List, the SynType + /// is the list element. + let rec private createParseFunction<'choice> + (choice : ArgumentDefaultSpec option -> 'choice) + (flagDus : FlagDu list) + (fieldName : Ident) + (attrs : SynAttribute list) + (ty : SynType) + : SynExpr * Accumulation<'choice> * SynType + = + match ty with + | String -> SynExpr.createLambda "x" (SynExpr.createIdent "x"), Accumulation.Required, SynType.string + | PrimitiveType pt -> + SynExpr.createLambda + "x" + (SynExpr.applyFunction + (SynExpr.createLongIdent' (pt @ [ Ident.create "Parse" ])) + (SynExpr.createIdent "x")), + Accumulation.Required, + ty + | Uri -> + SynExpr.createLambda + "x" + (SynExpr.applyFunction (SynExpr.createLongIdent [ "System" ; "Uri" ]) (SynExpr.createIdent "x")), + Accumulation.Required, + ty + | TimeSpan -> + let parseExact = + attrs + |> List.tryPick (fun attr -> + match attr.TypeName with + | SynLongIdent.SynLongIdent (idents, _, _) -> + match idents |> List.map (fun i -> i.idText) |> List.tryLast with + | Some "ParseExactAttribute" + | Some "ParseExact" -> Some attr.ArgExpr + | _ -> None + ) + + let culture = + attrs + |> List.tryPick (fun attr -> + match attr.TypeName with + | SynLongIdent.SynLongIdent (idents, _, _) -> + match idents |> List.map (fun i -> i.idText) |> List.tryLast with + | Some "InvariantCultureAttribute" + | Some "InvariantCulture" -> Some () + | _ -> None + ) + + let parser = + match parseExact, culture with + | None, None -> + SynExpr.createIdent "x" + |> SynExpr.applyFunction (SynExpr.createLongIdent [ "System" ; "TimeSpan" ; "Parse" ]) + | Some format, None -> + [ + SynExpr.createIdent "x" + format + SynExpr.createLongIdent [ "System" ; "Globalization" ; "CultureInfo" ; "CurrentCulture" ] + ] + |> SynExpr.tuple + |> SynExpr.applyFunction (SynExpr.createLongIdent [ "System" ; "TimeSpan" ; "ParseExact" ]) + | None, Some () -> + [ + SynExpr.createIdent "x" + SynExpr.createLongIdent [ "System" ; "Globalization" ; "CultureInfo" ; "InvariantCulture" ] + ] + |> SynExpr.tuple + |> SynExpr.applyFunction (SynExpr.createLongIdent [ "System" ; "TimeSpan" ; "Parse" ]) + | Some format, Some () -> + [ + SynExpr.createIdent "x" + format + SynExpr.createLongIdent [ "System" ; "Globalization" ; "CultureInfo" ; "InvariantCulture" ] + ] + |> SynExpr.tuple + |> SynExpr.applyFunction (SynExpr.createLongIdent [ "System" ; "TimeSpan" ; "ParseExact" ]) + |> SynExpr.createLambda "x" + + parser, Accumulation.Required, ty + | FileInfo -> + SynExpr.createLambda + "x" + (SynExpr.applyFunction + (SynExpr.createLongIdent [ "System" ; "IO" ; "FileInfo" ]) + (SynExpr.createIdent "x")), + Accumulation.Required, + ty + | DirectoryInfo -> + SynExpr.createLambda + "x" + (SynExpr.applyFunction + (SynExpr.createLongIdent [ "System" ; "IO" ; "DirectoryInfo" ]) + (SynExpr.createIdent "x")), + Accumulation.Required, + ty + | OptionType eltTy -> + let parseElt, acc, childTy = + createParseFunction choice flagDus fieldName attrs eltTy + + match acc with + | Accumulation.Optional -> + failwith + $"ArgParser does not support optionals containing options at field %s{fieldName.idText}: %O{ty}" + | Accumulation.Choice _ -> + failwith + $"ArgParser does not support optionals containing choices at field %s{fieldName.idText}: %O{ty}" + | Accumulation.List _ -> + failwith $"ArgParser does not support optional lists at field %s{fieldName.idText}: %O{ty}" + | Accumulation.Required -> parseElt, Accumulation.Optional, childTy + | ChoiceType elts -> + match elts with + | [ elt1 ; elt2 ] -> + if not (SynType.provablyEqual elt1 elt2) then + failwith + $"ArgParser was unable to prove types %O{elt1} and %O{elt2} to be equal in a Choice. We require them to be equal." + + let parseElt, acc, childTy = createParseFunction choice flagDus fieldName attrs elt1 + + match acc with + | Accumulation.Optional -> + failwith + $"ArgParser does not support choices containing options at field %s{fieldName.idText}: %O{ty}" + | Accumulation.List _ -> + failwith + $"ArgParser does not support choices containing lists at field %s{fieldName.idText}: %O{ty}" + | Accumulation.Choice _ -> + failwith + $"ArgParser does not support choices containing choices at field %s{fieldName.idText}: %O{ty}" + | Accumulation.Required -> + + let relevantAttrs = + attrs + |> List.choose (fun attr -> + let (SynLongIdent.SynLongIdent (name, _, _)) = attr.TypeName + + match name |> List.map _.idText |> List.last with + | "ArgumentDefaultFunction" + | "ArgumentDefaultFunctionAttribute" -> + ArgumentDefaultSpec.FunctionCall (Ident.create ("Default" + fieldName.idText)) + |> Some + | "ArgumentDefaultEnvironmentVariable" + | "ArgumentDefaultEnvironmentVariableAttribute" -> + ArgumentDefaultSpec.EnvironmentVariable attr.ArgExpr |> Some + | _ -> None + ) + + let relevantAttr = + match relevantAttrs with + | [] -> None + | [ x ] -> Some x + | _ -> + failwith + $"Expected Choice to be annotated with at most one ArgumentDefaultFunction or similar, but it was annotated with multiple. Field: %s{fieldName.idText}" + + parseElt, Accumulation.Choice (choice relevantAttr), childTy + | elts -> + let elts = elts |> List.map string |> String.concat ", " + + failwith + $"ArgParser requires Choice to be of the form Choice<'a, 'a>; that is, two arguments, both the same. For field %s{fieldName.idText}, got: %s{elts}" + | ListType eltTy -> + let parseElt, acc, childTy = + createParseFunction choice flagDus fieldName attrs eltTy + + parseElt, Accumulation.List acc, childTy + | ty -> + match identifyAsFlag flagDus ty with + | None -> failwith $"Could not decide how to parse arguments for field %s{fieldName.idText} of type %O{ty}" + | Some flagDu -> + // Parse as a bool, and then do the `if-then` dance. + let parser = + SynExpr.createIdent "x" + |> SynExpr.applyFunction (SynExpr.createLongIdent [ "System" ; "Boolean" ; "Parse" ]) + |> FlagDu.FromBoolean flagDu + |> SynExpr.createLambda "x" + + parser, Accumulation.Required, ty + + let rec private toParseSpec + (counter : int) + (flagDus : FlagDu list) + (ambientRecords : RecordType list) + (finalRecord : RecordType) + : ParseTreeCrate * int + = + finalRecord.Fields + |> List.iter (fun (SynField.SynField (isStatic = isStatic)) -> + if isStatic then + failwith "No static record fields allowed in ArgParserGenerator" + ) + + let counter, contents = + ((counter, []), finalRecord.Fields) + ||> List.fold (fun (counter, acc) (SynField.SynField (attrs, _, identOption, fieldType, _, _, _, _, _)) -> + let attrs = attrs |> List.collect (fun a -> a.Attributes) + + let positionalArgAttr = + attrs + |> List.tryPick (fun a -> + match (List.last a.TypeName.LongIdent).idText with + | "PositionalArgsAttribute" + | "PositionalArgs" -> + match a.ArgExpr with + | SynExpr.Const (SynConst.Unit, _) -> Some None + | a -> Some (Some a) + | _ -> None + ) + + let parseExactModifier = + attrs + |> List.tryPick (fun a -> + match (List.last a.TypeName.LongIdent).idText with + | "ParseExactAttribute" + | "ParseExact" -> Some a.ArgExpr + | _ -> None + ) + + let helpText = + attrs + |> List.tryPick (fun a -> + match (List.last a.TypeName.LongIdent).idText with + | "ArgumentHelpTextAttribute" + | "ArgumentHelpText" -> Some a.ArgExpr + | _ -> None + ) + + let helpText = + match parseExactModifier, helpText with + | None, ht -> ht + | Some pe, None -> + SynExpr.createIdent "sprintf" + |> SynExpr.applyTo (SynExpr.CreateConst "[Parse format (.NET): %s]") + |> SynExpr.applyTo pe + |> Some + | Some pe, Some ht -> + SynExpr.createIdent "sprintf" + |> SynExpr.applyTo (SynExpr.CreateConst "%s [Parse format (.NET): %s]") + |> SynExpr.applyTo ht + |> SynExpr.applyTo pe + |> Some + + let ident = + match identOption with + | None -> failwith "expected args field to have a name, but it did not" + | Some i -> i + + let longForms = + attrs + |> List.choose (fun attr -> + match attr.TypeName with + | SynLongIdent.SynLongIdent (ident, _, _) -> + if (List.last ident).idText = "ArgumentLongForm" then + Some attr.ArgExpr + else + None + ) + |> function + | [] -> List.singleton (SynExpr.CreateConst (argify ident)) + | l -> List.ofSeq l + + let ambientRecordMatch = + match fieldType with + | SynType.LongIdent (SynLongIdent.SynLongIdent (id, _, _)) -> + let target = List.last(id).idText + ambientRecords |> List.tryFind (fun r -> r.Name.idText = target) + | _ -> None + + match ambientRecordMatch with + | Some ambient -> + // This field has a type we need to obtain from parsing another record. + let spec, counter = toParseSpec counter flagDus ambientRecords ambient + counter, (ident, spec) :: acc + | None -> + + match positionalArgAttr with + | Some includeFlagLike -> + let getChoice (spec : ArgumentDefaultSpec option) : unit = + match spec with + | Some _ -> + failwith + "Positional Choice args cannot have default values. Remove [] from the positional arg." + | None -> () + + let parser, accumulation, parseTy = + createParseFunction getChoice flagDus ident attrs fieldType + + let isBoolLike = + match parseTy with + | PrimitiveType ident when ident |> List.map _.idText = [ "System" ; "Boolean" ] -> + Some (Choice2Of2 ()) + | parseTy -> identifyAsFlag flagDus parseTy |> Option.map Choice1Of2 + + match accumulation with + | Accumulation.List (Accumulation.List _) -> + failwith "A list of positional args cannot contain lists." + | Accumulation.List Accumulation.Optional -> + failwith "A list of positional args cannot contain optionals. What would that even mean?" + | Accumulation.List (Accumulation.Choice ()) -> + { + FieldName = ident + Parser = parser + TargetVariable = Ident.create $"arg_%i{counter}" + Accumulation = ChoicePositional.Choice includeFlagLike + TargetType = parseTy + ArgForm = longForms + Help = helpText + BoolCases = isBoolLike + } + |> fun t -> ParseTree.PositionalLeaf (t, Teq.refl) + | Accumulation.List Accumulation.Required -> + { + FieldName = ident + Parser = parser + TargetVariable = Ident.create $"arg_%i{counter}" + Accumulation = ChoicePositional.Normal includeFlagLike + TargetType = parseTy + ArgForm = longForms + Help = helpText + BoolCases = isBoolLike + } + |> fun t -> ParseTree.PositionalLeaf (t, Teq.refl) + | Accumulation.Choice _ + | Accumulation.Optional + | Accumulation.Required -> + failwith $"Expected positional arg accumulation type to be List, but it was %O{fieldType}" + |> ParseTreeCrate.make + | None -> + let getChoice (spec : ArgumentDefaultSpec option) : ArgumentDefaultSpec = + match spec with + | None -> + failwith + "Non-positional Choice args must have an `[]` attribute on them." + | Some spec -> spec + + let parser, accumulation, parseTy = + createParseFunction getChoice flagDus ident attrs fieldType + + let isBoolLike = + match parseTy with + | PrimitiveType ident when ident |> List.map _.idText = [ "System" ; "Boolean" ] -> + Some (Choice2Of2 ()) + | parseTy -> identifyAsFlag flagDus parseTy |> Option.map Choice1Of2 + + { + FieldName = ident + Parser = parser + TargetVariable = Ident.create $"arg_%i{counter}" + Accumulation = accumulation + TargetType = parseTy + ArgForm = longForms + Help = helpText + BoolCases = isBoolLike + } + |> fun t -> ParseTree.NonPositionalLeaf (t, Teq.refl) + |> ParseTreeCrate.make + |> fun tree -> counter + 1, (ident, tree) :: acc + ) + + let tree = + contents + |> List.rev + |> ParseTree.branch (fun args -> + args + |> Map.toList + |> List.map (fun (ident, expr) -> SynLongIdent.create [ Ident.create ident ], expr) + |> AstHelper.instantiateRecord + ) + + tree, counter + + /// let helpText : string = ... + let private helpText + (typeName : Ident) + (positional : ParseFunctionPositional option) + (args : ParseFunctionNonPositional list) + : SynBinding + = + let describeNonPositional + (acc : Accumulation) + (flagCases : Choice option) + : SynExpr + = + match acc with + | Accumulation.Required -> SynExpr.CreateConst "" + | Accumulation.Optional -> SynExpr.CreateConst " (optional)" + | Accumulation.Choice (ArgumentDefaultSpec.EnvironmentVariable var) -> + // We don't print out the default value in case it's a secret. People often pass secrets + // through env vars! + var + |> SynExpr.pipeThroughFunction ( + SynExpr.applyFunction + (SynExpr.createIdent "sprintf") + (SynExpr.CreateConst " (default value populated from env var %s)") + ) + |> SynExpr.paren + | Accumulation.Choice (ArgumentDefaultSpec.FunctionCall var) -> + match flagCases with + | None -> SynExpr.callMethod var.idText (SynExpr.createIdent' typeName) + | Some (Choice2Of2 ()) -> SynExpr.callMethod var.idText (SynExpr.createIdent' typeName) + | Some (Choice1Of2 flagDu) -> + // Care required here. The return value from the Default call is not a bool, + // but we should display it as such to the user! + [ + SynMatchClause.create + (SynPat.identWithArgs [ flagDu.Name ; flagDu.Case1Name ] (SynArgPats.create [])) + (SynExpr.ifThenElse + (SynExpr.equals flagDu.Case1Arg (SynExpr.CreateConst true)) + (SynExpr.CreateConst "false") + (SynExpr.CreateConst "true")) + SynMatchClause.create + (SynPat.identWithArgs [ flagDu.Name ; flagDu.Case2Name ] (SynArgPats.create [])) + (SynExpr.ifThenElse + (SynExpr.equals flagDu.Case2Arg (SynExpr.CreateConst true)) + (SynExpr.CreateConst "false") + (SynExpr.CreateConst "true")) + ] + |> SynExpr.createMatch (SynExpr.callMethod var.idText (SynExpr.createIdent' typeName)) + |> SynExpr.pipeThroughFunction ( + SynExpr.createLambda "x" (SynExpr.callMethod "ToString" (SynExpr.createIdent "x")) + ) + |> SynExpr.pipeThroughFunction ( + SynExpr.applyFunction (SynExpr.createIdent "sprintf") (SynExpr.CreateConst " (default value: %s)") + ) + |> SynExpr.paren + | Accumulation.List _ -> SynExpr.CreateConst " (can be repeated)" + + let describePositional _ _ = + SynExpr.CreateConst " (positional args) (can be repeated)" + + /// We may sometimes lie about the type name, if e.g. this is a flag DU which we're pretending is a boolean. + /// So the `renderTypeName` takes the Accumulation which tells us whether we're lying. + let toPrintable (describe : 'a -> Choice option -> SynExpr) (arg : ParseFunction<'a>) : SynExpr = + let ty = + match arg.BoolCases with + | None -> SynType.toHumanReadableString arg.TargetType + | Some _ -> "bool" + + let helpText = + match arg.Help with + | None -> SynExpr.CreateConst "" + | Some helpText -> + SynExpr.applyFunction (SynExpr.createIdent "sprintf") (SynExpr.CreateConst " : %s") + |> SynExpr.applyTo (SynExpr.paren helpText) + |> SynExpr.paren + + let descriptor = describe arg.Accumulation arg.BoolCases + + SynExpr.applyFunction (SynExpr.createIdent "sprintf") (SynExpr.CreateConst $"%%s %s{ty}%%s%%s") + |> SynExpr.applyTo arg.HumanReadableArgForm + |> SynExpr.applyTo descriptor + |> SynExpr.applyTo helpText + |> SynExpr.paren + + args + |> List.map (toPrintable describeNonPositional) + |> fun l -> + match positional with + | None -> l + | Some pos -> l @ [ toPrintable describePositional pos ] + |> SynExpr.listLiteral + |> SynExpr.pipeThroughFunction ( + SynExpr.applyFunction (SynExpr.createLongIdent [ "String" ; "concat" ]) (SynExpr.CreateConst @"\n") + ) + |> SynBinding.basic [ Ident.create "helpText" ] [ SynPat.unit ] + + /// `let processKeyValue (key : string) (value : string) : Result = ...` + /// Returns a possible error. + /// A parse failure might not be fatal (e.g. maybe the input was optionally of arity 0, and we failed to do + /// the parse because in fact the key decided not to take this argument); in that case we return Error None. + let private processKeyValue + (argParseErrors : Ident) + (pos : ParseFunctionPositional option) + (args : ParseFunctionNonPositional list) + : SynBinding + = + let args = + args + |> List.map (fun arg -> + match arg.Accumulation with + | Accumulation.Required + | Accumulation.Choice _ + | Accumulation.Optional -> + let multipleErrorMessage = + SynExpr.createIdent "sprintf" + |> SynExpr.applyTo (SynExpr.CreateConst "Argument '%s' was supplied multiple times: %s and %s") + |> SynExpr.applyTo arg.HumanReadableArgForm + |> SynExpr.applyTo (SynExpr.createIdent "x" |> SynExpr.callMethod "ToString" |> SynExpr.paren) + |> SynExpr.applyTo ( + SynExpr.createIdent "value" |> SynExpr.callMethod "ToString" |> SynExpr.paren + ) + + let performAssignment = + [ + SynExpr.createIdent "value" + |> SynExpr.pipeThroughFunction arg.Parser + |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Some") + |> SynExpr.assign (SynLongIdent.createI arg.TargetVariable) + + SynExpr.applyFunction (SynExpr.createIdent "Ok") (SynExpr.CreateConst ()) + ] + |> SynExpr.sequential + + [ + SynMatchClause.create + (SynPat.nameWithArgs "Some" [ SynPat.named "x" ]) + (SynExpr.sequential + [ + multipleErrorMessage + |> SynExpr.pipeThroughFunction ( + SynExpr.dotGet "Add" (SynExpr.createIdent' argParseErrors) + ) + SynExpr.applyFunction (SynExpr.createIdent "Ok") (SynExpr.CreateConst ()) + ]) + SynMatchClause.create + (SynPat.named "None") + (SynExpr.pipeThroughTryWith + SynPat.anon + (SynExpr.createLongIdent [ "exc" ; "Message" ] + |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Some") + |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Error")) + performAssignment) + ] + |> SynExpr.createMatch (SynExpr.createIdent' arg.TargetVariable) + | Accumulation.List (Accumulation.List _) + | Accumulation.List Accumulation.Optional + | Accumulation.List (Accumulation.Choice _) -> + failwith + "WoofWare.Whippet.Plugin.ArgParser invariant violated: expected a list to contain only a Required accumulation. Non-positional lists cannot be optional or Choice, nor can they themselves contain lists." + | Accumulation.List Accumulation.Required -> + [ + SynExpr.createIdent "value" + |> SynExpr.pipeThroughFunction arg.Parser + |> SynExpr.pipeThroughFunction ( + SynExpr.createLongIdent' [ arg.TargetVariable ; Ident.create "Add" ] + ) + SynExpr.CreateConst () |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Ok") + ] + |> SynExpr.sequential + |> fun expr -> arg.ArgForm, expr + ) + + let posArg = + match pos with + | None -> [] + | Some pos -> + [ + SynExpr.createIdent "value" + |> SynExpr.pipeThroughFunction pos.Parser + |> fun p -> + match pos.Accumulation with + | ChoicePositional.Choice _ -> + p |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Choice1Of2") + | ChoicePositional.Normal _ -> p + |> SynExpr.pipeThroughFunction ( + SynExpr.createLongIdent' [ pos.TargetVariable ; Ident.create "Add" ] + ) + SynExpr.CreateConst () |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Ok") + ] + |> SynExpr.sequential + |> fun expr -> pos.ArgForm, expr + |> List.singleton + + (SynExpr.applyFunction (SynExpr.createIdent "Error") (SynExpr.createIdent "None"), posArg @ args) + ||> List.fold (fun finalBranch (argForm, arg) -> + (finalBranch, argForm) + ||> List.fold (fun finalBranch argForm -> + arg + |> SynExpr.ifThenElse + (SynExpr.applyFunction + (SynExpr.createLongIdent [ "System" ; "String" ; "Equals" ]) + (SynExpr.tuple + [ + SynExpr.createIdent "key" + SynExpr.applyFunction + (SynExpr.applyFunction + (SynExpr.createIdent "sprintf") + (SynExpr.CreateConst "--%s")) + argForm + SynExpr.createLongIdent [ "System" ; "StringComparison" ; "OrdinalIgnoreCase" ] + ])) + finalBranch + ) + ) + |> SynBinding.basic + [ Ident.create "processKeyValue" ] + [ + SynPat.annotateType SynType.string (SynPat.named "key") + SynPat.annotateType SynType.string (SynPat.named "value") + ] + |> SynBinding.withReturnAnnotation ( + SynType.app "Result" [ SynType.unit ; SynType.appPostfix "option" SynType.string ] + ) + |> SynBinding.withXmlDoc ( + [ + " Processes the key-value pair, returning Error if no key was matched." + " If the key is an arg which can have arity 1, but throws when consuming that arg, we return Error()." + " This can nevertheless be a successful parse, e.g. when the key may have arity 0." + ] + |> PreXmlDoc.create' + ) + + /// `let setFlagValue (key : string) : bool = ...` + /// The second member of the `flags` list tuple is the constant "true" with which we will interpret the + /// arity-0 `--foo`. So in the case of a boolean-typed field, this is `true`; in the case of a Flag-typed field, + /// this is `FlagType.WhicheverCaseHadTrue`. + let private setFlagValue (argParseErrors : Ident) (flags : (ParseFunction<'a> * SynExpr) list) : SynBinding = + (SynExpr.CreateConst false, flags) + ||> List.fold (fun finalExpr (flag, trueCase) -> + let multipleErrorMessage = + SynExpr.createIdent "sprintf" + |> SynExpr.applyTo (SynExpr.CreateConst "Flag '%s' was supplied multiple times") + |> SynExpr.applyTo flag.HumanReadableArgForm + + let matchFlag = + [ + SynMatchClause.create + (SynPat.nameWithArgs "Some" [ SynPat.named "x" ]) + // This is an error, but it's one we can gracefully report at the end. + (SynExpr.sequential + [ + multipleErrorMessage + |> SynExpr.pipeThroughFunction ( + SynExpr.dotGet "Add" (SynExpr.createIdent' argParseErrors) + ) + SynExpr.CreateConst true + ]) + + SynMatchClause.create + (SynPat.named "None") + ([ + SynExpr.assign + (SynLongIdent.createI flag.TargetVariable) + (SynExpr.pipeThroughFunction (SynExpr.createIdent "Some") trueCase) + SynExpr.CreateConst true + ] + |> SynExpr.sequential) + ] + |> SynExpr.createMatch (SynExpr.createIdent' flag.TargetVariable) + + (finalExpr, flag.ArgForm) + ||> List.fold (fun finalExpr argForm -> + SynExpr.ifThenElse + (SynExpr.applyFunction + (SynExpr.createLongIdent [ "System" ; "String" ; "Equals" ]) + (SynExpr.tuple + [ + SynExpr.createIdent "key" + SynExpr.applyFunction + (SynExpr.applyFunction + (SynExpr.createIdent "sprintf") + (SynExpr.CreateConst "--%s")) + argForm + SynExpr.createLongIdent [ "System" ; "StringComparison" ; "OrdinalIgnoreCase" ] + ])) + finalExpr + matchFlag + ) + ) + |> SynBinding.basic [ Ident.create "setFlagValue" ] [ SynPat.annotateType SynType.string (SynPat.named "key") ] + |> SynBinding.withReturnAnnotation (SynType.named "bool") + |> SynBinding.withXmlDoc (PreXmlDoc.create "Returns false if we didn't set a value.") + + /// `let rec go (state : %ParseState%) (args : string list) : unit = ...` + let private mainLoop + (parseState : Ident) + (errorAcc : Ident) + (leftoverArgAcc : ChoicePositional) + (leftoverArgs : Ident) + (leftoverArgParser : SynExpr) + : SynBinding + = + /// `go (AwaitingValue arg) args` + let recurseValue = + SynExpr.createIdent "go" + |> SynExpr.applyTo ( + SynExpr.paren ( + SynExpr.applyFunction + (SynExpr.createLongIdent' [ parseState ; Ident.create "AwaitingValue" ]) + (SynExpr.createIdent "arg") + ) + ) + + /// `go AwaitingKey args` + let recurseKey = + (SynExpr.createIdent "go") + |> SynExpr.applyTo (SynExpr.createLongIdent' [ parseState ; Ident.create "AwaitingKey" ]) + |> SynExpr.applyTo (SynExpr.createIdent "args") + + /// `failwithf "Unable to process argument ..."` + let fail = + SynExpr.createIdent "failwithf" + |> SynExpr.applyTo (SynExpr.CreateConst "Unable to process argument %s as key %s and value %s") + |> SynExpr.applyTo (SynExpr.createIdent "arg") + |> SynExpr.applyTo (SynExpr.createIdent "key") + |> SynExpr.applyTo (SynExpr.createIdent "value") + + let processAsPositional = + SynExpr.sequential + [ + SynExpr.createIdent "arg" + |> SynExpr.pipeThroughFunction leftoverArgParser + |> fun p -> + match leftoverArgAcc with + | ChoicePositional.Normal _ -> p + | ChoicePositional.Choice _ -> + p |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Choice1Of2") + |> SynExpr.pipeThroughFunction (SynExpr.createLongIdent' [ leftoverArgs ; Ident.create "Add" ]) + + recurseKey + ] + + let posAttr = + match leftoverArgAcc with + | ChoicePositional.Choice a + | ChoicePositional.Normal a -> a + + let notMatched = + let handleFailure = + [ + SynMatchClause.create (SynPat.named "None") fail + + SynMatchClause.create + (SynPat.nameWithArgs "Some" [ SynPat.named "msg" ]) + (SynExpr.sequential + [ + SynExpr.createIdent "sprintf" + |> SynExpr.applyTo (SynExpr.CreateConst "%s (at arg %s)") + |> SynExpr.applyTo (SynExpr.createIdent "msg") + |> SynExpr.applyTo (SynExpr.createIdent "arg") + |> SynExpr.pipeThroughFunction (SynExpr.dotGet "Add" (SynExpr.createIdent' errorAcc)) + + recurseKey + ]) + ] + |> SynExpr.createMatch (SynExpr.createIdent "x") + + match posAttr with + | None -> handleFailure + | Some posAttr -> SynExpr.ifThenElse posAttr handleFailure processAsPositional + + let argStartsWithDashes = + SynExpr.createIdent "arg" + |> SynExpr.callMethodArg + "StartsWith" + (SynExpr.tuple + [ + SynExpr.CreateConst "--" + SynExpr.createLongIdent [ "System" ; "StringComparison" ; "Ordinal" ] + ]) + + let processKey = + SynExpr.ifThenElse + argStartsWithDashes + processAsPositional + (SynExpr.ifThenElse + (SynExpr.equals (SynExpr.createIdent "arg") (SynExpr.CreateConst "--help")) + (SynExpr.createLet + [ + SynBinding.basic + [ Ident.create "equals" ] + [] + (SynExpr.callMethodArg "IndexOf" (SynExpr.CreateConst '=') (SynExpr.createIdent "arg")) + ] + (SynExpr.ifThenElse + (SynExpr.lessThan (SynExpr.CreateConst 0) (SynExpr.createIdent "equals")) + (SynExpr.createLet + [ + SynBinding.basic + [ Ident.create "key" ] + [] + (SynExpr.arrayIndexRange + (Some (SynExpr.CreateConst 0)) + (Some (SynExpr.minusN (SynLongIdent.createS "equals") 1)) + (SynExpr.createIdent "arg")) + SynBinding.basic + [ Ident.create "value" ] + [] + (SynExpr.arrayIndexRange + (Some (SynExpr.plus (SynExpr.createIdent "equals") (SynExpr.CreateConst 1))) + None + (SynExpr.createIdent "arg")) + ] + (SynExpr.createMatch + (SynExpr.createIdent "processKeyValue" + |> SynExpr.applyTo (SynExpr.createIdent "key") + |> SynExpr.applyTo (SynExpr.createIdent "value")) + [ + SynMatchClause.create (SynPat.nameWithArgs "Ok" [ SynPat.unit ]) recurseKey + + SynMatchClause.create + (SynPat.nameWithArgs "Error" [ SynPat.named "x" ]) + notMatched + ])) + (SynExpr.createIdent "args" |> SynExpr.pipeThroughFunction recurseValue))) + (SynExpr.createIdent "helpText" + |> SynExpr.applyTo (SynExpr.CreateConst ()) + |> SynExpr.pipeThroughFunction ( + SynExpr.applyFunction + (SynExpr.createIdent "failwithf") + (SynExpr.CreateConst @"Help text requested.\n%s") + ))) + + let processValue = + // During failure, we've received an optional exception message that happened when we tried to parse + // the value; it's in the variable `exc`. + // `fail` is for the case where we're genuinely emitting an error. + // If we're in `PositionalArgs true` mode, though, we won't call `fail`. + let fail = + [ + SynExpr.createIdent "failwithf" + |> SynExpr.applyTo ( + SynExpr.CreateConst @"Unable to process supplied arg %s. Help text follows.\n%s" + ) + |> SynExpr.applyTo (SynExpr.createIdent "key") + |> SynExpr.applyTo ( + SynExpr.applyFunction (SynExpr.createIdent "helpText") (SynExpr.CreateConst ()) + |> SynExpr.paren + ) + |> SynMatchClause.create (SynPat.named "None") + + SynExpr.createIdent "msg" + |> SynExpr.pipeThroughFunction (SynExpr.dotGet "Add" (SynExpr.createIdent' errorAcc)) + |> SynMatchClause.create (SynPat.nameWithArgs "Some" [ SynPat.named "msg" ]) + ] + |> SynExpr.createMatch (SynExpr.createIdent "exc") + + let onFailure = + match posAttr with + | None -> fail + | Some includeFlagLike -> + [ + SynExpr.createIdent "key" + |> SynExpr.pipeThroughFunction leftoverArgParser + |> fun i -> + match leftoverArgAcc with + | ChoicePositional.Choice _ -> + i |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Choice1Of2") + | ChoicePositional.Normal _ -> i + |> SynExpr.pipeThroughFunction (SynExpr.createLongIdent' [ leftoverArgs ; Ident.create "Add" ]) + + SynExpr.createIdent "go" + |> SynExpr.applyTo (SynExpr.createLongIdent' [ parseState ; Ident.create "AwaitingKey" ]) + |> SynExpr.applyTo (SynExpr.listCons (SynExpr.createIdent "arg") (SynExpr.createIdent "args")) + ] + |> SynExpr.sequential + |> SynExpr.ifThenElse includeFlagLike fail + + [ + SynMatchClause.create + (SynPat.nameWithArgs "Ok" [ SynPat.unit ]) + (SynExpr.applyFunction + (SynExpr.applyFunction + (SynExpr.createIdent "go") + (SynExpr.createLongIdent' [ parseState ; Ident.create "AwaitingKey" ])) + (SynExpr.createIdent "args")) + SynMatchClause.create + (SynPat.nameWithArgs "Error" [ SynPat.named "exc" ]) + (SynExpr.ifThenElse + (SynExpr.applyFunction (SynExpr.createIdent "setFlagValue") (SynExpr.createIdent "key")) + onFailure + (SynExpr.createIdent "go" + |> SynExpr.applyTo (SynExpr.createLongIdent' [ parseState ; Ident.create "AwaitingKey" ]) + |> SynExpr.applyTo (SynExpr.listCons (SynExpr.createIdent "arg") (SynExpr.createIdent "args")))) + ] + |> SynExpr.createMatch ( + SynExpr.applyFunction + (SynExpr.applyFunction (SynExpr.createIdent "processKeyValue") (SynExpr.createIdent "key")) + (SynExpr.createIdent "arg") + ) + + let argBody = + [ + SynMatchClause.create + (SynPat.identWithArgs [ parseState ; Ident.create "AwaitingKey" ] (SynArgPats.create [])) + processKey + SynMatchClause.create + (SynPat.identWithArgs + [ parseState ; Ident.create "AwaitingValue" ] + (SynArgPats.createNamed [ "key" ])) + processValue + ] + |> SynExpr.createMatch (SynExpr.createIdent "state") + + let body = + let trailingArgMessage = + SynExpr.createIdent "sprintf" + |> SynExpr.applyTo ( + SynExpr.CreateConst + "Trailing argument %s had no value. Use a double-dash to separate positional args from key-value args." + ) + |> SynExpr.applyTo (SynExpr.createIdent "key") + + [ + SynMatchClause.create + SynPat.emptyList + (SynExpr.createMatch + (SynExpr.createIdent "state") + [ + SynMatchClause.create + (SynPat.identWithArgs [ parseState ; Ident.create "AwaitingKey" ] (SynArgPats.create [])) + (SynExpr.CreateConst ()) + SynMatchClause.create + (SynPat.identWithArgs + [ parseState ; Ident.create "AwaitingValue" ] + (SynArgPats.createNamed [ "key" ])) + (SynExpr.ifThenElse + (SynExpr.applyFunction + (SynExpr.createIdent "setFlagValue") + (SynExpr.createIdent "key")) + (trailingArgMessage + |> SynExpr.pipeThroughFunction ( + SynExpr.dotGet "Add" (SynExpr.createIdent' errorAcc) + )) + (SynExpr.CreateConst ())) + ]) + SynMatchClause.create + (SynPat.listCons (SynPat.createConst (SynConst.Create "--")) (SynPat.named "rest")) + (SynExpr.callMethodArg + "AddRange" + (SynExpr.paren ( + SynExpr.createIdent "rest" + |> SynExpr.pipeThroughFunction ( + SynExpr.applyFunction (SynExpr.createLongIdent [ "Seq" ; "map" ]) leftoverArgParser + ) + |> fun p -> + match leftoverArgAcc with + | ChoicePositional.Normal _ -> p + | ChoicePositional.Choice _ -> + p + |> SynExpr.pipeThroughFunction ( + SynExpr.applyFunction + (SynExpr.createLongIdent [ "Seq" ; "map" ]) + (SynExpr.createIdent "Choice2Of2") + ) + )) + (SynExpr.createIdent' leftoverArgs)) + SynMatchClause.create (SynPat.listCons (SynPat.named "arg") (SynPat.named "args")) argBody + ] + |> SynExpr.createMatch (SynExpr.createIdent "args") + + let args = + [ + SynPat.named "state" + |> SynPat.annotateType (SynType.createLongIdent [ parseState ]) + SynPat.named "args" + |> SynPat.annotateType (SynType.appPostfix "list" SynType.string) + ] + + SynBinding.basic [ Ident.create "go" ] args body + |> SynBinding.withRecursion true + + /// Takes a single argument, `args : string list`, and returns something of the type indicated by `recordType`. + let createRecordParse + (parseState : Ident) + (flagDus : FlagDu list) + (ambientRecords : RecordType list) + (recordType : RecordType) + : SynExpr + = + let spec, _ = toParseSpec 0 flagDus ambientRecords recordType + // For each argument (positional and non-positional), create an accumulator for it. + let nonPos, pos = + { new ParseTreeEval<_> with + member _.Eval tree = ParseTree.accumulators tree + } + |> spec.Apply + + let bindings = + nonPos + |> List.map (fun pf -> + match pf.Accumulation with + | Accumulation.Required + | Accumulation.Choice _ + | Accumulation.Optional -> + SynExpr.createIdent "None" + |> SynBinding.basic [ pf.TargetVariable ] [] + |> SynBinding.withMutability true + |> SynBinding.withReturnAnnotation (SynType.appPostfix "option" pf.TargetType) + | Accumulation.List (Accumulation.List _) + | Accumulation.List Accumulation.Optional + | Accumulation.List (Accumulation.Choice _) -> + failwith + "WoofWare.Whippet.Plugin.ArgParser invariant violated: expected a list to contain only a Required accumulation. Non-positional lists cannot be optional or Choice, nor can they themselves contain lists." + | Accumulation.List Accumulation.Required -> + SynExpr.createIdent "ResizeArray" + |> SynExpr.applyTo (SynExpr.CreateConst ()) + |> SynBinding.basic [ pf.TargetVariable ] [] + |> SynBinding.withReturnAnnotation (SynType.appPostfix "ResizeArray" pf.TargetType) + ) + + let bindings, leftoverArgsName, leftoverArgsParser = + let bindingName, leftoverArgsParser, leftoverArgsType = + match pos with + | None -> + Ident.create "parser_LeftoverArgs", + (SynExpr.createLambda "x" (SynExpr.createIdent "x")), + SynType.string + | Some pf -> + match pf.Accumulation with + | ChoicePositional.Choice _ -> + pf.TargetVariable, pf.Parser, SynType.app "Choice" [ pf.TargetType ; pf.TargetType ] + | ChoicePositional.Normal _ -> pf.TargetVariable, pf.Parser, pf.TargetType + + let bindings = + SynExpr.createIdent "ResizeArray" + |> SynExpr.applyTo (SynExpr.CreateConst ()) + |> SynBinding.basic [ bindingName ] [] + |> SynBinding.withReturnAnnotation (SynType.appPostfix "ResizeArray" leftoverArgsType) + |> fun b -> b :: bindings + + bindings, bindingName, leftoverArgsParser + + let argParseErrors = Ident.create "ArgParser_errors" + + let errorCollection : SynBinding = + SynExpr.createIdent "ResizeArray" + |> SynExpr.applyTo (SynExpr.CreateConst ()) + |> SynBinding.basic [ argParseErrors ] [] + + let helpText = helpText recordType.Name pos nonPos + + let bindings = errorCollection :: helpText :: bindings + + let unchecked = + SynExpr.createLongIdent [ "Unchecked" ; "defaultof" ] + |> SynExpr.typeApp [ SynType.anon ] + + // Determine whether any required arg is missing, and freeze args into immutable form. + let freezeNonPositionalArgs = + nonPos + |> List.map (fun pf -> + match pf.Accumulation with + | Accumulation.Choice spec -> + let getDefaultValue = + match spec with + | ArgumentDefaultSpec.EnvironmentVariable name -> + let result = + name + |> SynExpr.pipeThroughFunction (SynExpr.createIdent "getEnvironmentVariable") + + /// Assumes access to a non-null variable `x` containing the string value. + let parser = + match pf.BoolCases with + | Some boolLike -> + let trueCase, falseCase = + match boolLike with + | Choice2Of2 () -> SynExpr.CreateConst true, SynExpr.CreateConst false + | Choice1Of2 flag -> + FlagDu.FromBoolean flag (SynExpr.CreateConst true), + FlagDu.FromBoolean flag (SynExpr.CreateConst false) + + // We permit environment variables to be populated with 0 and 1 as well. + SynExpr.ifThenElse + (SynExpr.applyFunction + (SynExpr.createLongIdent [ "System" ; "String" ; "Equals" ]) + (SynExpr.tuple + [ + SynExpr.createIdent "x" + SynExpr.CreateConst "1" + SynExpr.createLongIdent + [ "System" ; "StringComparison" ; "OrdinalIgnoreCase" ] + ])) + (SynExpr.ifThenElse + (SynExpr.applyFunction + (SynExpr.createLongIdent [ "System" ; "String" ; "Equals" ]) + (SynExpr.tuple + [ + SynExpr.createIdent "x" + SynExpr.CreateConst "0" + SynExpr.createLongIdent + [ "System" ; "StringComparison" ; "OrdinalIgnoreCase" ] + ])) + (SynExpr.createIdent "x" |> SynExpr.pipeThroughFunction pf.Parser) + falseCase) + trueCase + | None -> (SynExpr.createIdent "x" |> SynExpr.pipeThroughFunction pf.Parser) + + let errorMessage = + SynExpr.createIdent "sprintf" + |> SynExpr.applyTo ( + SynExpr.CreateConst + "No value was supplied for %s, nor was environment variable %s set" + ) + |> SynExpr.applyTo pf.HumanReadableArgForm + |> SynExpr.applyTo name + + [ + SynMatchClause.create + SynPat.createNull + (SynExpr.sequential + [ + errorMessage + |> SynExpr.pipeThroughFunction ( + SynExpr.dotGet "Add" (SynExpr.createIdent' argParseErrors) + ) + unchecked + ]) + + SynMatchClause.create (SynPat.named "x") parser + ] + |> SynExpr.createMatch result + | ArgumentDefaultSpec.FunctionCall name -> + SynExpr.callMethod name.idText (SynExpr.createIdent' recordType.Name) + + [ + SynMatchClause.create + (SynPat.named "None") + (getDefaultValue + |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Choice2Of2")) + SynMatchClause.create + (SynPat.nameWithArgs "Some" [ SynPat.named "x" ]) + (SynExpr.applyFunction (SynExpr.createIdent "Choice1Of2") (SynExpr.createIdent "x")) + ] + |> SynExpr.createMatch (SynExpr.createIdent' pf.TargetVariable) + |> SynBinding.basic [ pf.TargetVariable ] [] + | Accumulation.Optional -> + SynBinding.basic [ pf.TargetVariable ] [] (SynExpr.createIdent' pf.TargetVariable) + | Accumulation.List (Accumulation.List _) + | Accumulation.List Accumulation.Optional + | Accumulation.List (Accumulation.Choice _) -> + failwith + "WoofWare.Whippet.Plugin.ArgParser invariant violated: expected a list to contain only a Required accumulation. Non-positional lists cannot be optional or Choice, nor can they themselves contain lists." + | Accumulation.List Accumulation.Required -> + SynBinding.basic + [ pf.TargetVariable ] + [] + (SynExpr.createIdent' pf.TargetVariable + |> SynExpr.pipeThroughFunction (SynExpr.createLongIdent [ "Seq" ; "toList" ])) + | Accumulation.Required -> + let errorMessage = + SynExpr.createIdent "sprintf" + |> SynExpr.applyTo (SynExpr.CreateConst "Required argument '%s' received no value") + |> SynExpr.applyTo pf.HumanReadableArgForm + + [ + SynMatchClause.create + (SynPat.named "None") + (SynExpr.sequential + [ + errorMessage + |> SynExpr.pipeThroughFunction ( + SynExpr.dotGet "Add" (SynExpr.createIdent' argParseErrors) + ) + unchecked + ]) + + SynMatchClause.create + (SynPat.nameWithArgs "Some" [ SynPat.named "x" ]) + (SynExpr.createIdent "x") + ] + |> SynExpr.createMatch (SynExpr.createIdent' pf.TargetVariable) + |> SynBinding.basic [ pf.TargetVariable ] [] + ) + + let freezePositional = + match pos with + | None -> + // Check if there are leftover args. If there are, throw. + let errorMessage = + SynExpr.createIdent' leftoverArgsName + |> SynExpr.pipeThroughFunction ( + SynExpr.applyFunction + (SynExpr.createLongIdent [ "String" ; "concat" ]) + (SynExpr.CreateConst " ") + ) + |> SynExpr.pipeThroughFunction ( + SynExpr.applyFunction + (SynExpr.createIdent "sprintf") + (SynExpr.CreateConst "There were leftover args: %s") + ) + + SynExpr.ifThenElse + (SynExpr.dotGet "Count" (SynExpr.createIdent' leftoverArgsName) + |> SynExpr.equals (SynExpr.CreateConst 0)) + (SynExpr.sequential + [ + errorMessage + |> SynExpr.pipeThroughFunction (SynExpr.dotGet "Add" (SynExpr.createIdent' argParseErrors)) + unchecked + ]) + (SynExpr.CreateConst ()) + | Some _ -> + SynExpr.createIdent' leftoverArgsName + |> SynExpr.pipeThroughFunction (SynExpr.createLongIdent [ "Seq" ; "toList" ]) + |> SynBinding.basic [ leftoverArgsName ] [] + |> List.singleton + + let freezeArgs = freezePositional @ freezeNonPositionalArgs + + let retValue = + let happyPath = + { new ParseTreeEval<_> with + member _.Eval tree = ParseTree.instantiate tree + } + |> spec.Apply + + let sadPath = + SynExpr.createIdent' argParseErrors + |> SynExpr.pipeThroughFunction ( + SynExpr.applyFunction (SynExpr.createLongIdent [ "String" ; "concat" ]) (SynExpr.CreateConst @"\n") + ) + |> SynExpr.pipeThroughFunction ( + SynExpr.createIdent "failwithf" + |> SynExpr.applyTo (SynExpr.CreateConst @"Errors during parse!\n%s") + ) + + let areErrors = + SynExpr.dotGet "Count" (SynExpr.createIdent' argParseErrors) + |> SynExpr.equals (SynExpr.CreateConst 0) + + SynExpr.ifThenElse areErrors sadPath happyPath + + let flags = + nonPos + |> List.choose (fun pf -> + match pf.TargetType with + | PrimitiveType pt -> + if (pt |> List.map _.idText) = [ "System" ; "Boolean" ] then + Some (pf, SynExpr.CreateConst true) + else + None + | ty -> + match identifyAsFlag flagDus ty with + | Some flag -> (pf, FlagDu.FromBoolean flag (SynExpr.CreateConst true)) |> Some + | _ -> None + ) + + let leftoverArgAcc = + match pos with + | None -> ChoicePositional.Normal None + | Some pos -> pos.Accumulation + + [ + SynExpr.createIdent "go" + |> SynExpr.applyTo (SynExpr.createLongIdent' [ parseState ; Ident.create "AwaitingKey" ]) + |> SynExpr.applyTo (SynExpr.createIdent "args") + + SynExpr.createLet freezeArgs retValue + ] + |> SynExpr.sequential + |> SynExpr.createLet ( + bindings + @ [ + processKeyValue argParseErrors pos nonPos + setFlagValue argParseErrors flags + mainLoop parseState argParseErrors leftoverArgAcc leftoverArgsName leftoverArgsParser + ] + ) + + // The type for which we're generating args may refer to any of the supplied records/unions. + let createModule + (opens : SynOpenDeclTarget list) + (ns : LongIdent) + ((taggedType : SynTypeDefn, spec : ArgParserOutputSpec)) + (allUnionTypes : UnionType list) + (allRecordTypes : RecordType list) + : SynModuleOrNamespace + = + let flagDus = + allUnionTypes + |> List.choose (fun ty -> + match ty.Cases with + | [ c1 ; c2 ] -> + let c1Attr = + c1.Attributes + |> List.tryPick (fun attr -> + match attr.TypeName with + | SynLongIdent.SynLongIdent (id, _, _) -> + match id |> List.last |> _.idText with + | "ArgumentFlagAttribute" + | "ArgumentFlag" -> Some (SynExpr.stripOptionalParen attr.ArgExpr) + | _ -> None + ) + + let c2Attr = + c2.Attributes + |> List.tryPick (fun attr -> + match attr.TypeName with + | SynLongIdent.SynLongIdent (id, _, _) -> + match id |> List.last |> _.idText with + | "ArgumentFlagAttribute" + | "ArgumentFlag" -> Some (SynExpr.stripOptionalParen attr.ArgExpr) + | _ -> None + ) + + match c1Attr, c2Attr with + | Some _, None + | None, Some _ -> + failwith + "[] must be placed on both cases of a two-case discriminated union, with opposite argument values on each case." + | None, None -> None + | Some c1Attr, Some c2Attr -> + + // Sanity check where possible + match c1Attr, c2Attr with + | SynExpr.Const (SynConst.Bool b1, _), SynExpr.Const (SynConst.Bool b2, _) -> + if b1 = b2 then + failwith + "[] must have opposite argument values on each case in a two-case discriminated union." + | _, _ -> () + + match c1.Fields, c2.Fields with + | [], [] -> + { + Name = ty.Name + Case1Name = c1.Name + Case1Arg = c1Attr + Case2Name = c2.Name + Case2Arg = c2Attr + } + |> Some + | _, _ -> + failwith "[] may only be placed on discriminated union members with no data." + | _ -> None + ) + + let taggedType = + match taggedType with + | SynTypeDefn.SynTypeDefn (sci, + SynTypeDefnRepr.Simple (SynTypeDefnSimpleRepr.Record (access, fields, _), _), + smd, + _, + _, + _) -> RecordType.OfRecord sci smd access fields + | _ -> failwith "[] currently only supports being placed on records." + + let modAttrs, modName = + if spec.ExtensionMethods then + [ SynAttribute.autoOpen ], Ident.create (taggedType.Name.idText + "ArgParse") + else + [ SynAttribute.requireQualifiedAccess ; SynAttribute.compilationRepresentation ], taggedType.Name + + let modInfo = + SynComponentInfo.create modName + |> SynComponentInfo.withDocString ( + PreXmlDoc.create $"Methods to parse arguments for the type %s{taggedType.Name.idText}" + ) + |> SynComponentInfo.addAttributes modAttrs + + let parseStateIdent = Ident.create $"ParseState_%s{taggedType.Name.idText}" + + let parseStateType = + [ + SynUnionCase.create + { + Attributes = [] + Fields = [] + Name = Ident.create "AwaitingKey" + XmlDoc = Some (PreXmlDoc.create "Ready to consume a key or positional arg") + Access = None + } + SynUnionCase.create + { + Attributes = [] + Fields = + [ + { + Attrs = [] + Ident = Some (Ident.create "key") + Type = SynType.string + } + ] + Name = Ident.create "AwaitingValue" + XmlDoc = Some (PreXmlDoc.create "Waiting to receive a value for the key we've already consumed") + Access = None + } + ] + |> SynTypeDefnRepr.union + |> SynTypeDefn.create ( + SynComponentInfo.create parseStateIdent + |> SynComponentInfo.setAccessibility (Some (SynAccess.Private range0)) + ) + |> List.singleton + |> SynModuleDecl.createTypes + + let taggedMod = + let argsParam = + SynPat.named "args" + |> SynPat.annotateType (SynType.appPostfix "list" SynType.string) + + let parsePrime = + createRecordParse parseStateIdent flagDus allRecordTypes taggedType + |> SynBinding.basic + [ Ident.create "parse'" ] + [ + SynPat.named "getEnvironmentVariable" + |> SynPat.annotateType (SynType.funFromDomain SynType.string SynType.string) + argsParam + ] + |> SynBinding.withReturnAnnotation (SynType.createLongIdent [ taggedType.Name ]) + + let parsePrimeCall = + if spec.ExtensionMethods then + // need to fully qualify + [ taggedType.Name ; Ident.create "parse'" ] + else + [ Ident.create "parse'" ] + + let parse = + SynExpr.createLongIdent' parsePrimeCall + |> SynExpr.applyTo (SynExpr.createLongIdent [ "System" ; "Environment" ; "GetEnvironmentVariable" ]) + |> SynExpr.applyTo (SynExpr.createIdent "args") + |> SynBinding.basic [ Ident.create "parse" ] [ argsParam ] + |> SynBinding.withReturnAnnotation (SynType.createLongIdent [ taggedType.Name ]) + + [ + yield parseStateType + + if spec.ExtensionMethods then + let bindingPrime = parsePrime |> SynMemberDefn.staticMember + + let binding = parse |> SynMemberDefn.staticMember + + let componentInfo = + SynComponentInfo.create taggedType.Name + |> SynComponentInfo.withDocString (PreXmlDoc.create "Extension methods for argument parsing") + + let containingType = + SynTypeDefnRepr.augmentation () + |> SynTypeDefn.create componentInfo + |> SynTypeDefn.withMemberDefns [ bindingPrime ; binding ] + + yield SynModuleDecl.createTypes [ containingType ] + else + yield SynModuleDecl.createLet parsePrime + + yield SynModuleDecl.createLet parse + ] + |> SynModuleDecl.nestedModule modInfo + + [ + for openStatement in opens do + yield SynModuleDecl.openAny openStatement + yield taggedMod + ] + |> SynModuleOrNamespace.createNamespace ns + +/// Whippet generator that provides an argument parser, taking a list of command line args and parsing them into a +/// structured record. +[] +type ArgParserGenerator () = + + interface IGenerateRawFromRaw with + member _.GenerateRawFromRaw (context : RawSourceGenerationArgs) = + if not (context.FilePath.EndsWith (".fs", StringComparison.Ordinal)) then + null + else + + let ast = Ast.parse (Encoding.UTF8.GetString context.FileContents) + + let types = Ast.getTypes ast + + let opens = AstHelper.extractOpens ast + + let namespaceAndTypes = + types + |> List.collect (fun (ns, types) -> + let typeWithAttr = + types + |> List.choose (fun ty -> + match SynTypeDefn.getAttribute "ArgParserAttribute" ty with + | None -> None + | Some attr -> + let arg = + match SynExpr.stripOptionalParen attr.ArgExpr with + | SynExpr.Const (SynConst.Bool value, _) -> value + | SynExpr.Const (SynConst.Unit, _) -> true + | arg -> + failwith + $"Unrecognised argument %+A{arg} to []. Literals are not supported. Use `true` or `false` (or unit) only." + + let spec = + { + ExtensionMethods = arg + } + + Some (ty, spec) + ) + + typeWithAttr + |> List.map (fun taggedType -> + let unions, records, others = + (([], [], []), types) + ||> List.fold (fun + (unions, records, others) + (SynTypeDefn.SynTypeDefn (sci, repr, smd, _, _, _) as ty) -> + match repr with + | SynTypeDefnRepr.Simple (SynTypeDefnSimpleRepr.Union (access, cases, _), _) -> + UnionType.OfUnion sci smd access cases :: unions, records, others + | SynTypeDefnRepr.Simple (SynTypeDefnSimpleRepr.Record (access, fields, _), _) -> + unions, RecordType.OfRecord sci smd access fields :: records, others + | _ -> unions, records, ty :: others + ) + + if not others.IsEmpty then + failwith + $"Error: all types recursively defined together with an ArgParserGenerator type must be discriminated unions or records. %+A{others}" + + (ns, taggedType, unions, records) + ) + ) + + let modules = + namespaceAndTypes + |> List.map (fun (ns, taggedType, unions, records) -> + ArgParserGenerator.createModule opens ns taggedType unions records + ) + + Ast.render modules |> Option.toObj diff --git a/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/README.md b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/README.md new file mode 100644 index 0000000..c552a7d --- /dev/null +++ b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/README.md @@ -0,0 +1,97 @@ +# WoofWare.Whippet.Plugin.ArgParser + +This is a [Whippet](https://github.com/Smaug123/WoofWare.Whippet) plugin defining an argument parser. + +It is a copy of the corresponding [Myriad](https://github.com/MoiraeSoftware/myriad) arg parser in [WoofWare.Myriad](https://github.com/Smaug123/WoofWare.Myriad), taken from commit d59ebdfccb87a06579fb99008a15f58ea8be394e. + +## Usage + +Define an `Args.fs` file like the following: + +```fsharp +namespace MyNamespace + +[] +type LoadsOfTypes = + { + Foo : int + Bar : string + Baz : bool + SomeFile : FileInfo + SomeDirectory : DirectoryInfo + SomeList : DirectoryInfo list + OptionalThingWithNoDefault : int option + [] + Positionals : int list + [] + OptionalThing : Choice + [] + AnotherOptionalThing : Choice + [] + YetAnotherOptionalThing : Choice + } + + static member DefaultOptionalThing () = true + + static member DefaultAnotherOptionalThing () = 3 +``` + +In your fsproj: + +```xml + + + + + Args.fs + + + + + + + + + + + +``` + +The generator will produce a file like the following: + +```fsharp +[] +module LoadsOfTypes = + // in case you want to test it, you get one with dependencies injected + let parse' (getEnvVar : string -> string) (args : string list) : LoadsOfTypes = ... + // this is the one we expect you actually want to use, if you don't want to test the arg parser + let parse (args : string list) : LoadsOfTypes = ... +``` + +## Features + +* Default arguments are handled as `Choice<'a, 'a>`: you get a `Choice1Of2` if the user provided the input, or a `Choice2Of2` if the parser filled in your specified default value. +* Default arguments from the environment, specified with `[]`. If such an arg is not supplied on the command line, its value is parsed from the value of that env var. +* Default arguments, specified with `[]`. If an arg `[] Foo : Choice<'a, 'a>` is not supplied on the command line, the parser calls `DefaultFoo : unit -> 'a` to obtain its value. +* Positional arguments: a list with attribute `[]` will accumulate all args which didn't match anything else. By default, the parser will fail if any of these arguments looks like an arg itself (i.e. it starts with `--`) but comes *before* a positional arg separator `--`; you can optionally give this attribute the argument `(* includeFlagLike = *) true` to instead just put such flag-like args into the accumulator. +* Positional args can also be of type `Choice<'a, 'a> list`, in which case we tell you whether the arg came before (`Choice1Of2`) or after (`Choice2Of2`) any `--` positional args separator. +* You can control TimeSpan and friends with the `[]` and `[]` attributes. +* By default, we generate F# extension methods for the type; you can instead create a module with the type's name, using `[]`. +* If `--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. The parser also makes a limited effort to supply help text when encountering an invalid parse. +* "Flag DUs": if a two-case DU appears in the generator input file, you can tag its cases as in `type DryRun = | [] Wet | [] Dry`. Then you can consume the flag like a bool: `[] type Args = { DryRun : DryRun }`, so `--dry-run` is parsed into `DryRun.Dry`. +* Control long forms of arguments with `[] Foo : int`, so that instead of accepting the default `--foo=3`, we accept `--alternative-name=3`. +* Custom help text for individual args is supplied with `[]`, and similarly help text for the entire args object is supplied with `[] [] type Args = ...`. + +## Limitations + +This is very bare-bones, but do raise GitHub issues if you like (or if you find cases where the parser does the wrong thing). + +* Help is signalled by throwing an exception, so you'll get an unsightly stack trace and a nonzero exit code. +* 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. +* I don't handle very many types, and in particular a real arg parser would handle DUs and records with nesting. +* 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. +* There's no subcommand support (you'll have to do that yourself). + +It should work fine if you just want to compose a few primitive types, though. diff --git a/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/WoofWare.Whippet.Plugin.ArgParser.Test/TestArgParser.fs b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/WoofWare.Whippet.Plugin.ArgParser.Test/TestArgParser.fs new file mode 100644 index 0000000..afba69c --- /dev/null +++ b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/WoofWare.Whippet.Plugin.ArgParser.Test/TestArgParser.fs @@ -0,0 +1,706 @@ +namespace WoofWare.Whippet.Plugin.ArgParser.Test + +open System +open System.Threading +open NUnit.Framework +open FsUnitTyped +open Consumer +open FsCheck + +[] +module TestArgParser = + + [] + [] + let ``Positionals get parsed: they don't have to be strings`` (sep : bool) = + let getEnvVar (_ : string) = failwith "should not call" + + let property + (fooSep : bool) + (barSep : bool) + (bazSep : bool) + (pos0 : int list) + (pos1 : int list) + (pos2 : int list) + (pos3 : int list) + (pos4 : int list) + = + let args = + [ + yield! pos0 |> List.map string + if fooSep then + yield "--foo=3" + else + yield "--foo" + yield "3" + yield! pos1 |> List.map string + if barSep then + yield "--bar=4" + else + yield "--bar" + yield "4" + yield! pos2 |> List.map string + if bazSep then + yield "--baz=true" + else + yield "--baz" + yield "true" + yield! pos3 |> List.map string + if sep then + yield "--" + yield! pos4 |> List.map string + ] + + BasicWithIntPositionals.parse' getEnvVar args + |> shouldEqual + { + Foo = 3 + Bar = "4" + Baz = true + Rest = pos0 @ pos1 @ pos2 @ pos3 @ pos4 + } + + Check.QuickThrowOnFailure property + + [] + let ``Arg-like thing appearing before double dash`` () = + let envCalls = ref 0 + + let getEnvVar (_ : string) = + Interlocked.Increment envCalls |> ignore + "" + + let args = [ "--foo=3" ; "--non-existent" ; "--bar=4" ; "--baz=true" ] + + let exc = + Assert.Throws (fun () -> Basic.parse' getEnvVar args |> ignore) + + envCalls.Value |> shouldEqual 0 + + exc.Message + |> shouldEqual + """Unable to process supplied arg --non-existent. Help text follows. +--foo int32 : This is a foo! +--bar string +--baz bool +--rest string (positional args) (can be repeated) : Here's where the rest of the args go""" + + [] + let ``Can supply positional args with key`` () = + let envCalls = ref 0 + + let getEnvVar (_ : string) = + Interlocked.Increment envCalls |> ignore + "" + + let property (args : (int * bool) list) (afterDoubleDash : int list option) = + let flatArgs = + args + |> List.collect (fun (value, sep) -> + if sep then + [ $"--rest=%i{value}" ] + else + [ "--rest" ; string value ] + ) + |> fun l -> l @ [ "--foo=3" ; "--bar=4" ; "--baz=true" ] + + let flatArgs, expected = + match afterDoubleDash with + | None -> flatArgs, List.map fst args + | Some rest -> flatArgs @ [ "--" ] @ (List.map string rest), List.map fst args @ rest + + BasicWithIntPositionals.parse' getEnvVar flatArgs + |> shouldEqual + { + Foo = 3 + Bar = "4" + Baz = true + Rest = expected + } + + Check.QuickThrowOnFailure property + envCalls.Value |> shouldEqual 0 + + [] + let ``Consume multiple occurrences of required arg`` () = + let envCalls = ref 0 + + let getEnvVar (_ : string) = + Interlocked.Increment envCalls |> ignore + "" + + let args = [ "--foo=3" ; "--rest" ; "7" ; "--bar=4" ; "--baz=true" ; "--rest=8" ] + + let result = BasicNoPositionals.parse' getEnvVar args + + envCalls.Value |> shouldEqual 0 + + result + |> shouldEqual + { + Foo = 3 + Bar = "4" + Baz = true + Rest = [ 7 ; 8 ] + } + + [] + let ``Gracefully handle invalid multiple occurrences of required arg`` () = + let envCalls = ref 0 + + let getEnvVar (_ : string) = + Interlocked.Increment envCalls |> ignore + "" + + let args = [ "--foo=3" ; "--foo" ; "9" ; "--bar=4" ; "--baz=true" ; "--baz=false" ] + + let exc = + Assert.Throws (fun () -> Basic.parse' getEnvVar args |> ignore) + + envCalls.Value |> shouldEqual 0 + + exc.Message + |> shouldEqual + """Errors during parse! +Argument '--foo' was supplied multiple times: 3 and 9 +Argument '--baz' was supplied multiple times: True and false""" + + [] + let ``Args appearing after double dash are positional`` () = + let envCalls = ref 0 + + let getEnvVar (_ : string) = + Interlocked.Increment envCalls |> ignore + "" + + let args = [ "--" ; "--foo=3" ; "--bar=4" ; "--baz=true" ] + + let exc = + Assert.Throws (fun () -> Basic.parse' getEnvVar args |> ignore) + + exc.Message + |> shouldEqual + """Errors during parse! +Required argument '--foo' received no value +Required argument '--bar' received no value +Required argument '--baz' received no value""" + + envCalls.Value |> shouldEqual 0 + + [] + let ``Help text`` () = + let getEnvVar (s : string) = + s |> shouldEqual "CONSUMEPLUGIN_THINGS" + "hi!" + + let exc = + Assert.Throws (fun () -> Basic.parse' getEnvVar [ "--help" ] |> ignore) + + exc.Message + |> shouldEqual + """Help text requested. +--foo int32 : This is a foo! +--bar string +--baz bool +--rest string (positional args) (can be repeated) : Here's where the rest of the args go""" + + [] + let ``Help text, with default values`` () = + let envVars = ref 0 + + let getEnvVar (_ : string) = + Interlocked.Increment envVars |> ignore + "" + + let exc = + Assert.Throws (fun () -> LoadsOfTypes.parse' getEnvVar [ "--help" ] |> ignore) + + exc.Message + |> shouldEqual + """Help text requested. +--foo int32 +--bar string +--baz bool +--some-file FileInfo +--some-directory DirectoryInfo +--some-list DirectoryInfo (can be repeated) +--optional-thing-with-no-default int32 (optional) +--optional-thing bool (default value: True) +--another-optional-thing int32 (default value: 3) +--yet-another-optional-thing string (default value populated from env var CONSUMEPLUGIN_THINGS) +--positionals int32 (positional args) (can be repeated)""" + + envVars.Value |> shouldEqual 0 + + [] + let ``Default values`` () = + let getEnvVar (s : string) = + s |> shouldEqual "CONSUMEPLUGIN_THINGS" + "hi!" + + let args = + [ + "--foo" + "3" + "--bar=some string" + "--baz" + "--some-file=/path/to/file" + "--some-directory" + "/a/dir" + "--another-optional-thing" + "3000" + ] + + let result = LoadsOfTypes.parse' getEnvVar args + + result.OptionalThing |> shouldEqual (Choice2Of2 true) + result.OptionalThingWithNoDefault |> shouldEqual None + result.AnotherOptionalThing |> shouldEqual (Choice1Of2 3000) + result.YetAnotherOptionalThing |> shouldEqual (Choice2Of2 "hi!") + + [] + let ``ParseExact and help`` () = + let count = ref 0 + + let getEnvVar (_ : string) = + Interlocked.Increment count |> ignore + "" + + let exc = + Assert.Throws (fun () -> DatesAndTimes.parse' getEnvVar [ "--help" ] |> ignore) + + exc.Message + |> shouldEqual + @"Help text requested. +--plain TimeSpan +--invariant TimeSpan +--exact TimeSpan : An exact time please [Parse format (.NET): hh\:mm\:ss] +--invariant-exact TimeSpan : [Parse format (.NET): hh\:mm\:ss]" + + count.Value |> shouldEqual 0 + + [] + let rec ``TimeSpans and their attributes`` () = + let count = ref 0 + + let getEnvVar (_ : string) = + Interlocked.Increment count |> ignore + "" + + let parsed = + DatesAndTimes.parse' + getEnvVar + [ + "--exact=11:34:00" + "--plain=1" + "--invariant=23:59" + "--invariant-exact=23:59:00" + ] + + parsed.Plain |> shouldEqual (TimeSpan (1, 0, 0, 0)) + parsed.Invariant |> shouldEqual (TimeSpan (23, 59, 00)) + parsed.Exact |> shouldEqual (TimeSpan (11, 34, 00)) + parsed.InvariantExact |> shouldEqual (TimeSpan (23, 59, 00)) + + let exc = + Assert.Throws (fun () -> + DatesAndTimes.parse' + getEnvVar + [ + "--exact=11:34:00" + "--plain=1" + "--invariant=23:59" + "--invariant-exact=23:59" + ] + |> ignore + ) + + exc.Message + |> shouldEqual + """Errors during parse! +Input string was not in a correct format. (at arg --invariant-exact=23:59) +Required argument '--invariant-exact' received no value""" + + let exc = + Assert.Throws (fun () -> + DatesAndTimes.parse' + getEnvVar + [ + "--exact=11:34" + "--plain=1" + "--invariant=23:59" + "--invariant-exact=23:59:00" + ] + |> ignore + ) + + exc.Message + |> shouldEqual + """Errors during parse! +Input string was not in a correct format. (at arg --exact=11:34) +Required argument '--exact' received no value""" + + count.Value |> shouldEqual 0 + + [] + let ``Can consume stacked record without positionals`` () = + let getEnvVar (_ : string) = failwith "should not call" + + let parsed = + ParentRecord.parse' getEnvVar [ "--and-another=true" ; "--thing1=9" ; "--thing2=a thing!" ] + + parsed + |> shouldEqual + { + Child = + { + Thing1 = 9 + Thing2 = "a thing!" + } + AndAnother = true + } + + [] + let ``Can consume stacked record, child has positionals`` () = + let getEnvVar (_ : string) = failwith "should not call" + + let parsed = + ParentRecordChildPos.parse' + getEnvVar + [ + "--and-another=true" + "--thing1=9" + "--thing2=https://example.com" + "--thing2=http://example.com" + ] + + parsed.AndAnother |> shouldEqual true + parsed.Child.Thing1 |> shouldEqual 9 + + parsed.Child.Thing2 + |> List.map (fun (x : Uri) -> x.ToString ()) + |> shouldEqual [ "https://example.com/" ; "http://example.com/" ] + + [] + let ``Can consume stacked record, child has no positionals, parent has positionals`` () = + let getEnvVar (_ : string) = failwith "should not call" + + let parsed = + ParentRecordSelfPos.parse' + getEnvVar + [ + "--and-another=true" + "--and-another=false" + "--and-another=true" + "--thing1=9" + "--thing2=some" + ] + + parsed + |> shouldEqual + { + Child = + { + Thing1 = 9 + Thing2 = "some" + } + AndAnother = [ true ; false ; true ] + } + + [] + let ``Help text for stacked records`` () = + let getEnvVar (_ : string) = failwith "should not call" + + let exc = + Assert.Throws (fun () -> + ParentRecordSelfPos.parse' getEnvVar [ "--help" ] |> ignore + ) + + exc.Message + |> shouldEqual + """Help text requested. +--thing1 int32 +--thing2 string +--and-another bool (positional args) (can be repeated)""" + + [] + let ``Positionals are tagged with Choice`` () = + let getEnvVar (_ : string) = failwith "should not call" + + ChoicePositionals.parse' getEnvVar [ "a" ; "b" ; "--" ; "--c" ; "--help" ] + |> shouldEqual + { + Args = [ Choice1Of2 "a" ; Choice1Of2 "b" ; Choice2Of2 "--c" ; Choice2Of2 "--help" ] + } + + let boolCases = + [ + "1", true + "0", false + "true", true + "false", false + "TRUE", true + "FALSE", false + ] + |> List.map TestCaseData + + [] + let ``Bool env vars can be populated`` (envValue : string, boolValue : bool) = + let getEnvVar (s : string) = + s |> shouldEqual "CONSUMEPLUGIN_THINGS" + envValue + + ContainsBoolEnvVar.parse' getEnvVar [] + |> shouldEqual + { + BoolVar = Choice2Of2 boolValue + } + + [] + let ``Bools can be treated with arity 0`` () = + let getEnvVar (_ : string) = failwith "do not call" + + ContainsBoolEnvVar.parse' getEnvVar [ "--bool-var" ] + |> shouldEqual + { + BoolVar = Choice1Of2 true + } + + [] + let ``Flag DUs can be parsed from env var`` (envValue : string, boolValue : bool) = + let getEnvVar (s : string) = + s |> shouldEqual "CONSUMEPLUGIN_THINGS" + envValue + + let boolValue = if boolValue then DryRunMode.Dry else DryRunMode.Wet + + ContainsFlagEnvVar.parse' getEnvVar [] + |> shouldEqual + { + DryRun = Choice2Of2 boolValue + } + + let dryRunData = + [ + [ "--dry-run" ], DryRunMode.Dry + [ "--dry-run" ; "true" ], DryRunMode.Dry + [ "--dry-run=true" ], DryRunMode.Dry + [ "--dry-run" ; "True" ], DryRunMode.Dry + [ "--dry-run=True" ], DryRunMode.Dry + [ "--dry-run" ; "false" ], DryRunMode.Wet + [ "--dry-run=false" ], DryRunMode.Wet + [ "--dry-run" ; "False" ], DryRunMode.Wet + [ "--dry-run=False" ], DryRunMode.Wet + ] + |> List.map TestCaseData + + [] + let ``Flag DUs can be parsed`` (args : string list, expected : DryRunMode) = + let getEnvVar (_ : string) = failwith "do not call" + + ContainsFlagEnvVar.parse' getEnvVar args + |> shouldEqual + { + DryRun = Choice1Of2 expected + } + + [] + let ``Flag DUs can be parsed, ArgumentDefaultFunction`` (args : string list, expected : DryRunMode) = + let getEnvVar (_ : string) = failwith "do not call" + + ContainsFlagDefaultValue.parse' getEnvVar args + |> shouldEqual + { + DryRun = Choice1Of2 expected + } + + [] + let ``Flag DUs can be given a default value`` () = + let getEnvVar (_ : string) = failwith "do not call" + + ContainsFlagDefaultValue.parse' getEnvVar [] + |> shouldEqual + { + DryRun = Choice2Of2 DryRunMode.Wet + } + + [] + let ``Help text for flag DU`` () = + let getEnvVar (_ : string) = failwith "do not call" + + let exc = + Assert.Throws (fun () -> + ContainsFlagDefaultValue.parse' getEnvVar [ "--help" ] + |> ignore + ) + + exc.Message + |> shouldEqual + """Help text requested. +--dry-run bool (default value: false)""" + + [] + let ``Help text for flag DU, non default`` () = + let getEnvVar (_ : string) = failwith "do not call" + + let exc = + Assert.Throws (fun () -> WithFlagDu.parse' getEnvVar [ "--help" ] |> ignore) + + exc.Message + |> shouldEqual + """Help text requested. +--dry-run bool""" + + let longFormCases = + let doTheThing = + [ + [ "--do-something-else=foo" ] + [ "--anotherarg=foo" ] + [ "--do-something-else" ; "foo" ] + [ "--anotherarg" ; "foo" ] + ] + + let someFlag = + [ + [ "--turn-it-on" ], true + [ "--dont-turn-it-off" ], true + [ "--turn-it-on=true" ], true + [ "--dont-turn-it-off=true" ], true + [ "--turn-it-on=false" ], false + [ "--dont-turn-it-off=false" ], false + [ "--turn-it-on" ; "true" ], true + [ "--dont-turn-it-off" ; "true" ], true + [ "--turn-it-on" ; "false" ], false + [ "--dont-turn-it-off" ; "false" ], false + ] + + List.allPairs doTheThing someFlag + |> List.map (fun (doTheThing, (someFlag, someFlagResult)) -> + let args = doTheThing @ someFlag + + let expected = + { + DoTheThing = "foo" + SomeFlag = someFlagResult + } + + args, expected + ) + |> List.map TestCaseData + + [] + let ``Long-form args`` (args : string list, expected : ManyLongForms) = + let getEnvVar (_ : string) = failwith "do not call" + + ManyLongForms.parse' getEnvVar args |> shouldEqual expected + + [] + let ``Long-form args can't be referred to by their original name`` () = + let getEnvVar (_ : string) = failwith "do not call" + + let exc = + Assert.Throws (fun () -> + ManyLongForms.parse' getEnvVar [ "--do-the-thing=foo" ] |> ignore + ) + + exc.Message + |> shouldEqual """Unable to process argument --do-the-thing=foo as key --do-the-thing and value foo""" + + [] + let ``Long-form args help text`` () = + let getEnvVar (_ : string) = failwith "do not call" + + let exc = + Assert.Throws (fun () -> ManyLongForms.parse' getEnvVar [ "--help" ] |> ignore) + + exc.Message + |> shouldEqual + """Help text requested. +--do-something-else / --anotherarg string +--turn-it-on / --dont-turn-it-off bool""" + + [] + let ``Can collect *all* non-help args into positional args with includeFlagLike`` () = + let getEnvVar (_ : string) = failwith "do not call" + + FlagsIntoPositionalArgs.parse' getEnvVar [ "--a" ; "foo" ; "--b=false" ; "--c" ; "hi" ; "--" ; "--help" ] + |> shouldEqual + { + A = "foo" + GrabEverything = [ "--b=false" ; "--c" ; "hi" ; "--help" ] + } + + // Users might consider this eccentric! + // But we're only a simple arg parser; we don't look around to see whether this is "almost" + // a valid parse. + FlagsIntoPositionalArgs.parse' getEnvVar [ "--a" ; "--b=false" ; "--c" ; "hi" ; "--" ; "--help" ] + |> shouldEqual + { + A = "--b=false" + GrabEverything = [ "--c" ; "hi" ; "--help" ] + } + + [] + let ``Can collect non-help args into positional args with Choice`` () = + let getEnvVar (_ : string) = failwith "do not call" + + FlagsIntoPositionalArgsChoice.parse' getEnvVar [ "--a" ; "foo" ; "--b=false" ; "--c" ; "hi" ; "--" ; "--help" ] + |> shouldEqual + { + A = "foo" + GrabEverything = + [ + Choice1Of2 "--b=false" + Choice1Of2 "--c" + Choice1Of2 "hi" + Choice2Of2 "--help" + ] + } + + [] + let ``Can collect non-help args into positional args, and we parse on the way`` () = + let getEnvVar (_ : string) = failwith "do not call" + + FlagsIntoPositionalArgsInt.parse' getEnvVar [ "3" ; "--a" ; "foo" ; "5" ; "--" ; "98" ] + |> shouldEqual + { + A = "foo" + GrabEverything = [ 3 ; 5 ; 98 ] + } + + [] + let ``Can collect non-help args into positional args with Choice, and we parse on the way`` () = + let getEnvVar (_ : string) = failwith "do not call" + + FlagsIntoPositionalArgsIntChoice.parse' getEnvVar [ "3" ; "--a" ; "foo" ; "5" ; "--" ; "98" ] + |> shouldEqual + { + A = "foo" + GrabEverything = [ Choice1Of2 3 ; Choice1Of2 5 ; Choice2Of2 98 ] + } + + [] + let ``Can refuse to collect non-help args with PositionalArgs false`` () = + let getEnvVar (_ : string) = failwith "do not call" + + let exc = + Assert.Throws (fun () -> + FlagsIntoPositionalArgs'.parse' + getEnvVar + [ "--a" ; "foo" ; "--b=false" ; "--c" ; "hi" ; "--" ; "--help" ] + |> ignore + ) + + exc.Message + |> shouldEqual """Unable to process argument --b=false as key --b and value false""" + + let exc = + Assert.Throws (fun () -> + FlagsIntoPositionalArgs'.parse' getEnvVar [ "--a" ; "--b=false" ; "--c=hi" ; "--" ; "--help" ] + |> ignore + ) + + // Again perhaps eccentric! + // Again, we don't try to detect that the user has missed out the desired argument to `--a`. + exc.Message + |> shouldEqual """Unable to process argument --c=hi as key --c and value hi""" diff --git a/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/WoofWare.Whippet.Plugin.ArgParser.Test/TestSurface.fs b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/WoofWare.Whippet.Plugin.ArgParser.Test/TestSurface.fs new file mode 100644 index 0000000..e3bd734 --- /dev/null +++ b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/WoofWare.Whippet.Plugin.ArgParser.Test/TestSurface.fs @@ -0,0 +1,26 @@ +namespace WoofWare.Whippet.Plugin.ArgParser.Test + +open NUnit.Framework +open WoofWare.Whippet.Plugin.ArgParser +open ApiSurface + +[] +module TestAttributeSurface = + let assembly = typeof.Assembly + + [] + let ``Ensure API surface has not been modified`` () = ApiSurface.assertIdentical assembly + + (* + [] + let ``Check version against remote`` () = + MonotonicVersion.validate assembly "WoofWare.Whippet.Plugin.ArgParser.Attributes" + *) + + [] + let ``Update API surface`` () = + ApiSurface.writeAssemblyBaseline assembly + + [] + let ``Ensure public API is fully documented`` () = + DocCoverage.assertFullyDocumented assembly diff --git a/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/WoofWare.Whippet.Plugin.ArgParser.Test/WoofWare.Whippet.Plugin.ArgParser.Test.fsproj b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/WoofWare.Whippet.Plugin.ArgParser.Test/WoofWare.Whippet.Plugin.ArgParser.Test.fsproj new file mode 100644 index 0000000..c99a93c --- /dev/null +++ b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/WoofWare.Whippet.Plugin.ArgParser.Test/WoofWare.Whippet.Plugin.ArgParser.Test.fsproj @@ -0,0 +1,27 @@ + + + + net8.0 + false + true + + + + + + + + + + + + + + + + + + + + + diff --git a/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/WoofWare.Whippet.Plugin.ArgParser.fsproj b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/WoofWare.Whippet.Plugin.ArgParser.fsproj new file mode 100644 index 0000000..63b263b --- /dev/null +++ b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/WoofWare.Whippet.Plugin.ArgParser.fsproj @@ -0,0 +1,40 @@ + + + + net8.0 + true + Patrick Stevens + Copyright (c) Patrick Stevens 2024 + Whippet F# source generator plugin, for generating arg parsers. + git + https://github.com/Smaug123/WoofWare.Whippet + MIT + README.md + fsharp;source-generator;source-gen;whippet;arguments;arg-parser + true + FS3559 + WoofWare.Whippet.Plugin.ArgParser + true + true + NU5118 + + + + + True + / + README.md + + + + + + + + + + + + + + diff --git a/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/version.json b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/version.json new file mode 100644 index 0000000..4398299 --- /dev/null +++ b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/version.json @@ -0,0 +1,14 @@ +{ + "version": "0.1", + "publicReleaseRefSpec": [ + "^refs/heads/main$" + ], + "pathFilters": [ + "./", + ":/WoofWare.Whippet.Core/", + ":/WoofWare.Whippet.Fantomas/", + ":/Plugins/ArgParser/WoofWare.Whippet.Plugins.ArgParser.Attributes/", + ":/global.json", + ":/Directory.Build.props" + ] +} diff --git a/Plugins/Json/WoofWare.Whippet.Plugin.Json.Attributes/Attributes.fs b/Plugins/Json/WoofWare.Whippet.Plugin.Json.Attributes/Attributes.fs new file mode 100644 index 0000000..864c8de --- /dev/null +++ b/Plugins/Json/WoofWare.Whippet.Plugin.Json.Attributes/Attributes.fs @@ -0,0 +1,37 @@ +namespace WoofWare.Whippet.Plugin.Json + +open System + +/// Attribute indicating a record type to which the "Add JSON serializer" Whippet +/// generator should apply during build. +/// The purpose of this generator is to create methods (possibly extension methods) of the form +/// `{TypeName}.toJsonNode : {TypeName} -> System.Text.Json.Nodes.JsonNode`. +/// +/// If you supply isExtensionMethod = false, you will get a module rather than extension methods. +/// Extension methods can only be consumed from F#, but the benefit is that they don't use up the module name. +/// (If you set this to `false`, we create a module called "{TypeName}"). +type JsonSerializeAttribute (isExtensionMethod : bool) = + inherit Attribute () + + /// The default value of `isExtensionMethod`, the optional argument to the JsonSerializeAttribute constructor. + static member DefaultIsExtensionMethod = true + + /// Shorthand for the "isExtensionMethod = false" constructor; see documentation there for details. + new () = JsonSerializeAttribute JsonSerializeAttribute.DefaultIsExtensionMethod + +/// Attribute indicating a record type to which the "Add JSON parse" Whippet +/// generator should apply during build. +/// The purpose of this generator is to create methods (possibly extension methods) of the form +/// `{TypeName}.jsonParse : System.Text.Json.Nodes.JsonNode -> {TypeName}`. +/// +/// If you supply isExtensionMethod = false, you will get extension methods. +/// Extension methods can only be consumed from F#, but the benefit is that they don't use up the module name +/// (If you set this to `false`, we create a module called "{TypeName}"). +type JsonParseAttribute (isExtensionMethod : bool) = + inherit Attribute () + + /// The default value of `isExtensionMethod`, the optional argument to the JsonParseAttribute constructor. + static member DefaultIsExtensionMethod = true + + /// Shorthand for the "isExtensionMethod = false" constructor; see documentation there for details. + new () = JsonParseAttribute JsonParseAttribute.DefaultIsExtensionMethod diff --git a/Plugins/Json/WoofWare.Whippet.Plugin.Json.Attributes/README.md b/Plugins/Json/WoofWare.Whippet.Plugin.Json.Attributes/README.md new file mode 100644 index 0000000..688e57a --- /dev/null +++ b/Plugins/Json/WoofWare.Whippet.Plugin.Json.Attributes/README.md @@ -0,0 +1,6 @@ +# WoofWare.Whippet.Plugin.Json.Attributes + +This is a very slim runtime dependency which consumers of WoofWare.Whippet.Plugin.Json may optionally take. +This dependency contains attributes which control that source generator, +although you may instead omit this dependency and control the generator entirely through configuration in consumer's `.fsproj`. +Please see WoofWare.Whippet.Plugin.Json's README for further information. diff --git a/Plugins/Json/WoofWare.Whippet.Plugin.Json.Attributes/SurfaceBaseline.txt b/Plugins/Json/WoofWare.Whippet.Plugin.Json.Attributes/SurfaceBaseline.txt new file mode 100644 index 0000000..714152e --- /dev/null +++ b/Plugins/Json/WoofWare.Whippet.Plugin.Json.Attributes/SurfaceBaseline.txt @@ -0,0 +1,10 @@ +WoofWare.Whippet.Plugin.Json.JsonParseAttribute inherit System.Attribute +WoofWare.Whippet.Plugin.Json.JsonParseAttribute..ctor [constructor]: bool +WoofWare.Whippet.Plugin.Json.JsonParseAttribute..ctor [constructor]: unit +WoofWare.Whippet.Plugin.Json.JsonParseAttribute.DefaultIsExtensionMethod [static property]: [read-only] bool +WoofWare.Whippet.Plugin.Json.JsonParseAttribute.get_DefaultIsExtensionMethod [static method]: unit -> bool +WoofWare.Whippet.Plugin.Json.JsonSerializeAttribute inherit System.Attribute +WoofWare.Whippet.Plugin.Json.JsonSerializeAttribute..ctor [constructor]: bool +WoofWare.Whippet.Plugin.Json.JsonSerializeAttribute..ctor [constructor]: unit +WoofWare.Whippet.Plugin.Json.JsonSerializeAttribute.DefaultIsExtensionMethod [static property]: [read-only] bool +WoofWare.Whippet.Plugin.Json.JsonSerializeAttribute.get_DefaultIsExtensionMethod [static method]: unit -> bool \ No newline at end of file diff --git a/Plugins/Json/WoofWare.Whippet.Plugin.Json.Attributes/WoofWare.Whippet.Plugin.Json.Attributes.fsproj b/Plugins/Json/WoofWare.Whippet.Plugin.Json.Attributes/WoofWare.Whippet.Plugin.Json.Attributes.fsproj new file mode 100644 index 0000000..8cf891f --- /dev/null +++ b/Plugins/Json/WoofWare.Whippet.Plugin.Json.Attributes/WoofWare.Whippet.Plugin.Json.Attributes.fsproj @@ -0,0 +1,33 @@ + + + + netstandard2.0 + true + Patrick Stevens + Copyright (c) Patrick Stevens 2024 + Attributes to accompany the WoofWare.Whippet.Plugin.Json source generator, to indicate what you want your types to be doing. + git + https://github.com/Smaug123/WoofWare.Whippet + MIT + README.md + fsharp;source-generator;source-gen;whippet;arguments;arg-parser + true + FS3559 + WoofWare.Whippet.Plugin.Json.Attributes + + + + + + + + True + / + README.md + + + + + + + diff --git a/Plugins/Json/WoofWare.Whippet.Plugin.Json.Attributes/version.json b/Plugins/Json/WoofWare.Whippet.Plugin.Json.Attributes/version.json new file mode 100644 index 0000000..47ae36c --- /dev/null +++ b/Plugins/Json/WoofWare.Whippet.Plugin.Json.Attributes/version.json @@ -0,0 +1,11 @@ +{ + "version": "0.2", + "publicReleaseRefSpec": [ + "^refs/heads/main$" + ], + "pathFilters": [ + "./", + ":/global.json", + ":/Directory.Build.props" + ] +} \ No newline at end of file diff --git a/Plugins/Json/WoofWare.Whippet.Plugin.Json.Consumer/GeneratedJson.fs b/Plugins/Json/WoofWare.Whippet.Plugin.Json.Consumer/GeneratedJson.fs new file mode 100644 index 0000000..9df23fe --- /dev/null +++ b/Plugins/Json/WoofWare.Whippet.Plugin.Json.Consumer/GeneratedJson.fs @@ -0,0 +1,483 @@ +namespace ConsumePlugin + +open System.Text.Json.Serialization +open WoofWare.Whippet.Plugin.Json + +/// Module containing JSON serializing extension members for the InternalTypeNotExtensionSerial type +[] +module internal InternalTypeNotExtensionSerialJsonSerializeExtension = + /// Extension methods for JSON parsing + type InternalTypeNotExtensionSerial with + + /// Serialize to a JSON node + static member toJsonNode (input : InternalTypeNotExtensionSerial) : System.Text.Json.Nodes.JsonNode = + let node = System.Text.Json.Nodes.JsonObject () + + do + node.Add ( + (Literals.something), + (input.InternalThing2 |> System.Text.Json.Nodes.JsonValue.Create) + ) + + node :> _ +namespace ConsumePlugin + +open System.Text.Json.Serialization +open WoofWare.Whippet.Plugin.Json + +/// Module containing JSON serializing extension members for the InternalTypeExtension type +[] +module internal InternalTypeExtensionJsonSerializeExtension = + /// Extension methods for JSON parsing + type InternalTypeExtension with + + /// Serialize to a JSON node + static member toJsonNode (input : InternalTypeExtension) : System.Text.Json.Nodes.JsonNode = + let node = System.Text.Json.Nodes.JsonObject () + do node.Add ((Literals.something), (input.ExternalThing |> System.Text.Json.Nodes.JsonValue.Create)) + node :> _ + +namespace ConsumePlugin + +/// Module containing JSON parsing extension members for the InnerType type +[] +module InnerTypeJsonParseExtension = + /// Extension methods for JSON parsing + type InnerType with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : InnerType = + let arg_0 = + (match node.[(Literals.something)] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ((Literals.something)) + ) + ) + | v -> v) + .AsValue() + .GetValue () + + { + Thing = arg_0 + } +namespace ConsumePlugin + +/// Module containing JSON parsing extension members for the JsonRecordType type +[] +module JsonRecordTypeJsonParseExtension = + /// Extension methods for JSON parsing + type JsonRecordType with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : JsonRecordType = + let arg_5 = + (match node.["f"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("f") + ) + ) + | v -> v) + .AsArray () + |> Seq.map (fun elt -> elt.AsValue().GetValue ()) + |> Array.ofSeq + + let arg_4 = + (match node.["e"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("e") + ) + ) + | v -> v) + .AsArray () + |> Seq.map (fun elt -> elt.AsValue().GetValue ()) + |> Array.ofSeq + + let arg_3 = + InnerType.jsonParse ( + match node.["d"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("d") + ) + ) + | v -> v + ) + + let arg_2 = + (match node.["hi"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("hi") + ) + ) + | v -> v) + .AsArray () + |> Seq.map (fun elt -> elt.AsValue().GetValue ()) + |> List.ofSeq + + let arg_1 = + (match node.["another-thing"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("another-thing") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_0 = + (match node.["a"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("a") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + { + A = arg_0 + B = arg_1 + C = arg_2 + D = arg_3 + E = arg_4 + F = arg_5 + } +namespace ConsumePlugin + +/// Module containing JSON parsing extension members for the InternalTypeNotExtension type +[] +module internal InternalTypeNotExtensionJsonParseExtension = + /// Extension methods for JSON parsing + type InternalTypeNotExtension with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : InternalTypeNotExtension = + let arg_0 = + (match node.[(Literals.something)] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ((Literals.something)) + ) + ) + | v -> v) + .AsValue() + .GetValue () + + { + InternalThing = arg_0 + } +namespace ConsumePlugin + +/// Module containing JSON parsing extension members for the InternalTypeExtension type +[] +module internal InternalTypeExtensionJsonParseExtension = + /// Extension methods for JSON parsing + type InternalTypeExtension with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : InternalTypeExtension = + let arg_0 = + (match node.[(Literals.something)] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ((Literals.something)) + ) + ) + | v -> v) + .AsValue() + .GetValue () + + { + ExternalThing = arg_0 + } +namespace ConsumePlugin + +/// Module containing JSON parsing extension members for the ToGetExtensionMethod type +[] +module ToGetExtensionMethodJsonParseExtension = + /// Extension methods for JSON parsing + type ToGetExtensionMethod with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : ToGetExtensionMethod = + let arg_20 = System.Numerics.BigInteger.Parse (node.["whiskey"].ToJsonString ()) + + let arg_19 = + (match node.["victor"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("victor") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_18 = + (match node.["uniform"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("uniform") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_17 = + (match node.["tango"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("tango") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_16 = + (match node.["quebec"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("quebec") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_15 = + (match node.["papa"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("papa") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_14 = + (match node.["oscar"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("oscar") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_13 = + (match node.["november"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("november") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_12 = + (match node.["mike"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("mike") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_11 = + (match node.["lima"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("lima") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_10 = + (match node.["kilo"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("kilo") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_9 = + (match node.["juliette"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("juliette") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_8 = + (match node.["india"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("india") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_7 = + (match node.["hotel"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("hotel") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_6 = + (match node.["golf"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("golf") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_5 = + (match node.["foxtrot"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("foxtrot") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_4 = + (match node.["echo"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("echo") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_3 = + (match node.["delta"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("delta") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_2 = + (match node.["charlie"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("charlie") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_1 = + (match node.["bravo"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("bravo") + ) + ) + | v -> v) + .AsValue() + .GetValue () + |> System.Uri + + let arg_0 = + (match node.["alpha"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("alpha") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + { + Alpha = arg_0 + Bravo = arg_1 + Charlie = arg_2 + Delta = arg_3 + Echo = arg_4 + Foxtrot = arg_5 + Golf = arg_6 + Hotel = arg_7 + India = arg_8 + Juliette = arg_9 + Kilo = arg_10 + Lima = arg_11 + Mike = arg_12 + November = arg_13 + Oscar = arg_14 + Papa = arg_15 + Quebec = arg_16 + Tango = arg_17 + Uniform = arg_18 + Victor = arg_19 + Whiskey = arg_20 + } diff --git a/Plugins/Json/WoofWare.Whippet.Plugin.Json.Consumer/GeneratedPureGymDto.fs b/Plugins/Json/WoofWare.Whippet.Plugin.Json.Consumer/GeneratedPureGymDto.fs new file mode 100644 index 0000000..7337948 --- /dev/null +++ b/Plugins/Json/WoofWare.Whippet.Plugin.Json.Consumer/GeneratedPureGymDto.fs @@ -0,0 +1,1116 @@ +namespace PureGym + +open System +open System.Text.Json.Serialization +open WoofWare.Whippet.Plugin.Json + +/// Module containing JSON serializing extension members for the Member type +[] +module MemberJsonSerializeExtension = + /// Extension methods for JSON parsing + type Member with + + /// Serialize to a JSON node + static member toJsonNode (input : Member) : System.Text.Json.Nodes.JsonNode = + let node = System.Text.Json.Nodes.JsonObject () + + do + node.Add ("id", (input.Id |> System.Text.Json.Nodes.JsonValue.Create)) + + node.Add ( + "compoundMemberId", + (input.CompoundMemberId |> System.Text.Json.Nodes.JsonValue.Create) + ) + + node.Add ("firstName", (input.FirstName |> System.Text.Json.Nodes.JsonValue.Create)) + node.Add ("lastName", (input.LastName |> System.Text.Json.Nodes.JsonValue.Create)) + node.Add ("homeGymId", (input.HomeGymId |> System.Text.Json.Nodes.JsonValue.Create)) + node.Add ("homeGymName", (input.HomeGymName |> System.Text.Json.Nodes.JsonValue.Create)) + node.Add ("emailAddress", (input.EmailAddress |> System.Text.Json.Nodes.JsonValue.Create)) + node.Add ("gymAccessPin", (input.GymAccessPin |> System.Text.Json.Nodes.JsonValue.Create)) + node.Add ("dateofBirth", (input.DateOfBirth |> System.Text.Json.Nodes.JsonValue.Create)) + node.Add ("mobileNumber", (input.MobileNumber |> System.Text.Json.Nodes.JsonValue.Create)) + node.Add ("postCode", (input.Postcode |> System.Text.Json.Nodes.JsonValue.Create)) + node.Add ("membershipName", (input.MembershipName |> System.Text.Json.Nodes.JsonValue.Create)) + node.Add ("membershipLevel", (input.MembershipLevel |> System.Text.Json.Nodes.JsonValue.Create)) + node.Add ("suspendedReason", (input.SuspendedReason |> System.Text.Json.Nodes.JsonValue.Create)) + node.Add ("memberStatus", (input.MemberStatus |> System.Text.Json.Nodes.JsonValue.Create)) + + node :> _ + +namespace PureGym + +/// Module containing JSON parsing extension members for the GymOpeningHours type +[] +module GymOpeningHoursJsonParseExtension = + /// Extension methods for JSON parsing + type GymOpeningHours with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : GymOpeningHours = + let arg_1 = + (match node.["openingHours"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("openingHours") + ) + ) + | v -> v) + .AsArray () + |> Seq.map (fun elt -> elt.AsValue().GetValue ()) + |> List.ofSeq + + let arg_0 = + (match node.["isAlwaysOpen"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("isAlwaysOpen") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + { + IsAlwaysOpen = arg_0 + OpeningHours = arg_1 + } +namespace PureGym + +/// Module containing JSON parsing extension members for the GymAccessOptions type +[] +module GymAccessOptionsJsonParseExtension = + /// Extension methods for JSON parsing + type GymAccessOptions with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : GymAccessOptions = + let arg_1 = + (match node.["qrCodeAccess"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("qrCodeAccess") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_0 = + (match node.["pinAccess"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("pinAccess") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + { + PinAccess = arg_0 + QrCodeAccess = arg_1 + } +namespace PureGym + +/// Module containing JSON parsing extension members for the GymLocation type +[] +module GymLocationJsonParseExtension = + /// Extension methods for JSON parsing + type GymLocation with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : GymLocation = + let arg_1 = + try + (match node.["latitude"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("latitude") + ) + ) + | v -> v) + .AsValue() + .GetValue () + with :? System.InvalidOperationException as exc -> + if exc.Message.Contains "cannot be converted to" then + if + System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString + then + (match node.["latitude"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("latitude") + ) + ) + | v -> v) + .AsValue() + .GetValue () + |> System.Double.Parse + else + reraise () + else + reraise () + |> LanguagePrimitives.FloatWithMeasure + + let arg_0 = + try + (match node.["longitude"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("longitude") + ) + ) + | v -> v) + .AsValue() + .GetValue () + with :? System.InvalidOperationException as exc -> + if exc.Message.Contains "cannot be converted to" then + if + System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString + then + (match node.["longitude"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("longitude") + ) + ) + | v -> v) + .AsValue() + .GetValue () + |> System.Double.Parse + else + reraise () + else + reraise () + + { + Longitude = arg_0 + Latitude = arg_1 + } +namespace PureGym + +/// Module containing JSON parsing extension members for the GymAddress type +[] +module GymAddressJsonParseExtension = + /// Extension methods for JSON parsing + type GymAddress with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : GymAddress = + let arg_5 = + (match node.["postcode"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("postcode") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_4 = + match node.["county"] with + | null -> None + | v -> v.AsValue().GetValue () |> Some + + let arg_3 = + (match node.["town"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("town") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_2 = + match node.["addressLine3"] with + | null -> None + | v -> v.AsValue().GetValue () |> Some + + let arg_1 = + match node.["addressLine2"] with + | null -> None + | v -> v.AsValue().GetValue () |> Some + + let arg_0 = + (match node.["addressLine1"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("addressLine1") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + { + AddressLine1 = arg_0 + AddressLine2 = arg_1 + AddressLine3 = arg_2 + Town = arg_3 + County = arg_4 + Postcode = arg_5 + } +namespace PureGym + +/// Module containing JSON parsing extension members for the Gym type +[] +module GymJsonParseExtension = + /// Extension methods for JSON parsing + type Gym with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : Gym = + let arg_10 = + (match node.["reopenDate"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("reopenDate") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_9 = + (match node.["timeZone"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("timeZone") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_8 = + GymLocation.jsonParse ( + match node.["location"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("location") + ) + ) + | v -> v + ) + + let arg_7 = + GymAccessOptions.jsonParse ( + match node.["accessOptions"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("accessOptions") + ) + ) + | v -> v + ) + + let arg_6 = + GymOpeningHours.jsonParse ( + match node.["gymOpeningHours"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("gymOpeningHours") + ) + ) + | v -> v + ) + + let arg_5 = + (match node.["emailAddress"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("emailAddress") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_4 = + (match node.["phoneNumber"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("phoneNumber") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_3 = + GymAddress.jsonParse ( + match node.["address"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("address") + ) + ) + | v -> v + ) + + let arg_2 = + (match node.["status"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("status") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_1 = + (match node.["id"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("id") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_0 = + (match node.["name"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("name") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + { + Name = arg_0 + Id = arg_1 + Status = arg_2 + Address = arg_3 + PhoneNumber = arg_4 + EmailAddress = arg_5 + GymOpeningHours = arg_6 + AccessOptions = arg_7 + Location = arg_8 + TimeZone = arg_9 + ReopenDate = arg_10 + } +namespace PureGym + +/// Module containing JSON parsing extension members for the Member type +[] +module MemberJsonParseExtension = + /// Extension methods for JSON parsing + type Member with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : Member = + let arg_14 = + (match node.["memberStatus"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("memberStatus") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_13 = + (match node.["suspendedReason"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("suspendedReason") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_12 = + (match node.["membershipLevel"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("membershipLevel") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_11 = + (match node.["membershipName"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("membershipName") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_10 = + (match node.["postCode"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("postCode") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_9 = + (match node.["mobileNumber"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("mobileNumber") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_8 = + (match node.["dateofBirth"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("dateofBirth") + ) + ) + | v -> v) + .AsValue() + .GetValue () + |> System.DateOnly.Parse + + let arg_7 = + (match node.["gymAccessPin"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("gymAccessPin") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_6 = + (match node.["emailAddress"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("emailAddress") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_5 = + (match node.["homeGymName"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("homeGymName") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_4 = + (match node.["homeGymId"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("homeGymId") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_3 = + (match node.["lastName"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("lastName") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_2 = + (match node.["firstName"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("firstName") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_1 = + (match node.["compoundMemberId"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("compoundMemberId") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_0 = + (match node.["id"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("id") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + { + Id = arg_0 + CompoundMemberId = arg_1 + FirstName = arg_2 + LastName = arg_3 + HomeGymId = arg_4 + HomeGymName = arg_5 + EmailAddress = arg_6 + GymAccessPin = arg_7 + DateOfBirth = arg_8 + MobileNumber = arg_9 + Postcode = arg_10 + MembershipName = arg_11 + MembershipLevel = arg_12 + SuspendedReason = arg_13 + MemberStatus = arg_14 + } +namespace PureGym + +/// Module containing JSON parsing extension members for the GymAttendance type +[] +module GymAttendanceJsonParseExtension = + /// Extension methods for JSON parsing + type GymAttendance with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : GymAttendance = + let arg_8 = + (match node.["maximumCapacity"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("maximumCapacity") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_7 = + (match node.["lastRefreshedPeopleInClasses"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("lastRefreshedPeopleInClasses") + ) + ) + | v -> v) + .AsValue() + .GetValue () + |> System.DateTime.Parse + + let arg_6 = + (match node.["lastRefreshed"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("lastRefreshed") + ) + ) + | v -> v) + .AsValue() + .GetValue () + |> System.DateTime.Parse + + let arg_5 = + (match node.["attendanceTime"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("attendanceTime") + ) + ) + | v -> v) + .AsValue() + .GetValue () + |> System.DateTime.Parse + + let arg_4 = + (match node.["isApproximate"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("isApproximate") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_3 = + match node.["totalPeopleSuffix"] with + | null -> None + | v -> v.AsValue().GetValue () |> Some + + let arg_2 = + (match node.["totalPeopleInClasses"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("totalPeopleInClasses") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_1 = + (match node.["totalPeopleInGym"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("totalPeopleInGym") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_0 = + (match node.["description"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("description") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + { + Description = arg_0 + TotalPeopleInGym = arg_1 + TotalPeopleInClasses = arg_2 + TotalPeopleSuffix = arg_3 + IsApproximate = arg_4 + AttendanceTime = arg_5 + LastRefreshed = arg_6 + LastRefreshedPeopleInClasses = arg_7 + MaximumCapacity = arg_8 + } +namespace PureGym + +/// Module containing JSON parsing extension members for the MemberActivityDto type +[] +module MemberActivityDtoJsonParseExtension = + /// Extension methods for JSON parsing + type MemberActivityDto with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : MemberActivityDto = + let arg_5 = + (match node.["lastRefreshed"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("lastRefreshed") + ) + ) + | v -> v) + .AsValue() + .GetValue () + |> System.DateTime.Parse + + let arg_4 = + (match node.["isEstimated"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("isEstimated") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_3 = + (match node.["totalClasses"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("totalClasses") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_2 = + (match node.["totalVisits"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("totalVisits") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_1 = + (match node.["averageDuration"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("averageDuration") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_0 = + (match node.["totalDuration"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("totalDuration") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + { + TotalDuration = arg_0 + AverageDuration = arg_1 + TotalVisits = arg_2 + TotalClasses = arg_3 + IsEstimated = arg_4 + LastRefreshed = arg_5 + } +namespace PureGym + +/// Module containing JSON parsing extension members for the SessionsAggregate type +[] +module SessionsAggregateJsonParseExtension = + /// Extension methods for JSON parsing + type SessionsAggregate with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : SessionsAggregate = + let arg_2 = + (match node.["Duration"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("Duration") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_1 = + (match node.["Visits"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("Visits") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_0 = + (match node.["Activities"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("Activities") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + { + Activities = arg_0 + Visits = arg_1 + Duration = arg_2 + } +namespace PureGym + +/// Module containing JSON parsing extension members for the VisitGym type +[] +module VisitGymJsonParseExtension = + /// Extension methods for JSON parsing + type VisitGym with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : VisitGym = + let arg_2 = + (match node.["Status"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("Status") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_1 = + (match node.["Name"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("Name") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_0 = + (match node.["Id"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("Id") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + { + Id = arg_0 + Name = arg_1 + Status = arg_2 + } +namespace PureGym + +/// Module containing JSON parsing extension members for the Visit type +[] +module VisitJsonParseExtension = + /// Extension methods for JSON parsing + type Visit with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : Visit = + let arg_3 = + VisitGym.jsonParse ( + match node.["Gym"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("Gym") + ) + ) + | v -> v + ) + + let arg_2 = + (match node.["Duration"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("Duration") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_1 = + (match node.["StartTime"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("StartTime") + ) + ) + | v -> v) + .AsValue() + .GetValue () + |> System.DateTime.Parse + + let arg_0 = + (match node.["IsDurationEstimated"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("IsDurationEstimated") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + { + IsDurationEstimated = arg_0 + StartTime = arg_1 + Duration = arg_2 + Gym = arg_3 + } +namespace PureGym + +/// Module containing JSON parsing extension members for the SessionsSummary type +[] +module SessionsSummaryJsonParseExtension = + /// Extension methods for JSON parsing + type SessionsSummary with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : SessionsSummary = + let arg_1 = + SessionsAggregate.jsonParse ( + match node.["ThisWeek"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("ThisWeek") + ) + ) + | v -> v + ) + + let arg_0 = + SessionsAggregate.jsonParse ( + match node.["Total"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("Total") + ) + ) + | v -> v + ) + + { + Total = arg_0 + ThisWeek = arg_1 + } +namespace PureGym + +/// Module containing JSON parsing extension members for the Sessions type +[] +module SessionsJsonParseExtension = + /// Extension methods for JSON parsing + type Sessions with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : Sessions = + let arg_1 = + (match node.["Visits"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("Visits") + ) + ) + | v -> v) + .AsArray () + |> Seq.map (fun elt -> Visit.jsonParse elt) + |> List.ofSeq + + let arg_0 = + SessionsSummary.jsonParse ( + match node.["Summary"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("Summary") + ) + ) + | v -> v + ) + + { + Summary = arg_0 + Visits = arg_1 + } +namespace PureGym + +/// Module containing JSON parsing extension members for the UriThing type +[] +module UriThingJsonParseExtension = + /// Extension methods for JSON parsing + type UriThing with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : UriThing = + let arg_0 = + (match node.["someUri"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("someUri") + ) + ) + | v -> v) + .AsValue() + .GetValue () + |> System.Uri + + { + SomeUri = arg_0 + } diff --git a/Plugins/Json/WoofWare.Whippet.Plugin.Json.Consumer/GeneratedSerializationAndDeserialization.fs b/Plugins/Json/WoofWare.Whippet.Plugin.Json.Consumer/GeneratedSerializationAndDeserialization.fs new file mode 100644 index 0000000..121f912 --- /dev/null +++ b/Plugins/Json/WoofWare.Whippet.Plugin.Json.Consumer/GeneratedSerializationAndDeserialization.fs @@ -0,0 +1,977 @@ +namespace ConsumePlugin + +open System +open System.Collections.Generic +open System.Text.Json.Serialization + +/// Module containing JSON serializing extension members for the InnerTypeWithBoth type +[] +module InnerTypeWithBothJsonSerializeExtension = + /// Extension methods for JSON parsing + type InnerTypeWithBoth with + + /// Serialize to a JSON node + static member toJsonNode (input : InnerTypeWithBoth) : System.Text.Json.Nodes.JsonNode = + let node = System.Text.Json.Nodes.JsonObject () + + do + node.Add (("it's-a-me"), (input.Thing |> System.Text.Json.Nodes.JsonValue.Create)) + + node.Add ( + "map", + (input.Map + |> (fun field -> + let ret = System.Text.Json.Nodes.JsonObject () + + for (KeyValue (key, value)) in field do + ret.Add (key.ToString (), System.Text.Json.Nodes.JsonValue.Create value) + + ret + )) + ) + + node.Add ( + "readOnlyDict", + (input.ReadOnlyDict + |> (fun field -> + let ret = System.Text.Json.Nodes.JsonObject () + + for (KeyValue (key, value)) in field do + ret.Add ( + key.ToString (), + (fun field -> + let arr = System.Text.Json.Nodes.JsonArray () + + for mem in field do + arr.Add (System.Text.Json.Nodes.JsonValue.Create mem) + + arr + ) + value + ) + + ret + )) + ) + + node.Add ( + "dict", + (input.Dict + |> (fun field -> + let ret = System.Text.Json.Nodes.JsonObject () + + for (KeyValue (key, value)) in field do + ret.Add (key.ToString (), System.Text.Json.Nodes.JsonValue.Create value) + + ret + )) + ) + + node.Add ( + "concreteDict", + (input.ConcreteDict + |> (fun field -> + let ret = System.Text.Json.Nodes.JsonObject () + + for (KeyValue (key, value)) in field do + ret.Add (key.ToString (), InnerTypeWithBoth.toJsonNode value) + + ret + )) + ) + + node :> _ +namespace ConsumePlugin + +open System +open System.Collections.Generic +open System.Text.Json.Serialization + +/// Module containing JSON serializing extension members for the SomeEnum type +[] +module SomeEnumJsonSerializeExtension = + /// Extension methods for JSON parsing + type SomeEnum with + + /// Serialize to a JSON node + static member toJsonNode (input : SomeEnum) : System.Text.Json.Nodes.JsonNode = + match input with + | SomeEnum.Blah -> System.Text.Json.Nodes.JsonValue.Create 1 + | SomeEnum.Thing -> System.Text.Json.Nodes.JsonValue.Create 0 + | v -> failwith (sprintf "Unrecognised value for enum: %O" v) +namespace ConsumePlugin + +open System +open System.Collections.Generic +open System.Text.Json.Serialization + +/// Module containing JSON serializing extension members for the JsonRecordTypeWithBoth type +[] +module JsonRecordTypeWithBothJsonSerializeExtension = + /// Extension methods for JSON parsing + type JsonRecordTypeWithBoth with + + /// Serialize to a JSON node + static member toJsonNode (input : JsonRecordTypeWithBoth) : System.Text.Json.Nodes.JsonNode = + let node = System.Text.Json.Nodes.JsonObject () + + do + node.Add ("a", (input.A |> System.Text.Json.Nodes.JsonValue.Create)) + node.Add ("b", (input.B |> System.Text.Json.Nodes.JsonValue.Create)) + + node.Add ( + "c", + (input.C + |> (fun field -> + let arr = System.Text.Json.Nodes.JsonArray () + + for mem in field do + arr.Add (System.Text.Json.Nodes.JsonValue.Create mem) + + arr + )) + ) + + node.Add ("d", (input.D |> InnerTypeWithBoth.toJsonNode)) + + node.Add ( + "e", + (input.E + |> (fun field -> + let arr = System.Text.Json.Nodes.JsonArray () + + for mem in field do + arr.Add (System.Text.Json.Nodes.JsonValue.Create mem) + + arr + )) + ) + + node.Add ( + "arr", + (input.Arr + |> (fun field -> + let arr = System.Text.Json.Nodes.JsonArray () + + for mem in field do + arr.Add (System.Text.Json.Nodes.JsonValue.Create mem) + + arr + )) + ) + + node.Add ("byte", (input.Byte |> System.Text.Json.Nodes.JsonValue.Create>)) + node.Add ("sbyte", (input.Sbyte |> System.Text.Json.Nodes.JsonValue.Create>)) + node.Add ("i", (input.I |> System.Text.Json.Nodes.JsonValue.Create>)) + node.Add ("i32", (input.I32 |> System.Text.Json.Nodes.JsonValue.Create>)) + node.Add ("i64", (input.I64 |> System.Text.Json.Nodes.JsonValue.Create>)) + node.Add ("u", (input.U |> System.Text.Json.Nodes.JsonValue.Create>)) + node.Add ("u32", (input.U32 |> System.Text.Json.Nodes.JsonValue.Create>)) + node.Add ("u64", (input.U64 |> System.Text.Json.Nodes.JsonValue.Create>)) + node.Add ("f", (input.F |> System.Text.Json.Nodes.JsonValue.Create>)) + node.Add ("f32", (input.F32 |> System.Text.Json.Nodes.JsonValue.Create>)) + node.Add ("single", (input.Single |> System.Text.Json.Nodes.JsonValue.Create>)) + + node.Add ( + "intMeasureOption", + (input.IntMeasureOption + |> (fun field -> + match field with + | None -> null :> System.Text.Json.Nodes.JsonNode + | Some field -> + (System.Text.Json.Nodes.JsonValue.Create> field) + :> System.Text.Json.Nodes.JsonNode + )) + ) + + node.Add ( + "intMeasureNullable", + (input.IntMeasureNullable + |> (fun field -> + if field.HasValue then + System.Text.Json.Nodes.JsonValue.Create> field.Value + :> System.Text.Json.Nodes.JsonNode + else + null :> System.Text.Json.Nodes.JsonNode + )) + ) + + node.Add ("enum", (input.Enum |> SomeEnum.toJsonNode)) + + node.Add ( + "timestamp", + (input.Timestamp + |> (fun field -> field.ToString "o" |> System.Text.Json.Nodes.JsonValue.Create)) + ) + + node.Add ("unit", (input.Unit |> (fun value -> System.Text.Json.Nodes.JsonObject ()))) + + node :> _ +namespace ConsumePlugin + +open System +open System.Collections.Generic +open System.Text.Json.Serialization + +/// Module containing JSON serializing extension members for the FirstDu type +[] +module FirstDuJsonSerializeExtension = + /// Extension methods for JSON parsing + type FirstDu with + + /// Serialize to a JSON node + static member toJsonNode (input : FirstDu) : System.Text.Json.Nodes.JsonNode = + let node = System.Text.Json.Nodes.JsonObject () + + match input with + | FirstDu.EmptyCase -> node.Add ("type", System.Text.Json.Nodes.JsonValue.Create "emptyCase") + | FirstDu.Case1 arg0 -> + node.Add ("type", System.Text.Json.Nodes.JsonValue.Create "case1") + let dataNode = System.Text.Json.Nodes.JsonObject () + dataNode.Add ("data", System.Text.Json.Nodes.JsonValue.Create arg0) + node.Add ("data", dataNode) + | FirstDu.Case2 (arg0, arg1) -> + node.Add ("type", System.Text.Json.Nodes.JsonValue.Create "case2") + let dataNode = System.Text.Json.Nodes.JsonObject () + dataNode.Add ("record", JsonRecordTypeWithBoth.toJsonNode arg0) + dataNode.Add ("i", System.Text.Json.Nodes.JsonValue.Create arg1) + node.Add ("data", dataNode) + + node :> _ +namespace ConsumePlugin + +open System +open System.Collections.Generic +open System.Text.Json.Serialization + +/// Module containing JSON serializing extension members for the HeaderAndValue type +[] +module HeaderAndValueJsonSerializeExtension = + /// Extension methods for JSON parsing + type HeaderAndValue with + + /// Serialize to a JSON node + static member toJsonNode (input : HeaderAndValue) : System.Text.Json.Nodes.JsonNode = + let node = System.Text.Json.Nodes.JsonObject () + + do + node.Add ("header", (input.Header |> System.Text.Json.Nodes.JsonValue.Create)) + node.Add ("value", (input.Value |> System.Text.Json.Nodes.JsonValue.Create)) + + node :> _ +namespace ConsumePlugin + +open System +open System.Collections.Generic +open System.Text.Json.Serialization + +/// Module containing JSON serializing extension members for the Foo type +[] +module FooJsonSerializeExtension = + /// Extension methods for JSON parsing + type Foo with + + /// Serialize to a JSON node + static member toJsonNode (input : Foo) : System.Text.Json.Nodes.JsonNode = + let node = System.Text.Json.Nodes.JsonObject () + + do + node.Add ( + "message", + (input.Message + |> (fun field -> + match field with + | None -> null :> System.Text.Json.Nodes.JsonNode + | Some field -> HeaderAndValue.toJsonNode field + )) + ) + + node :> _ +namespace ConsumePlugin + +open System +open System.Collections.Generic +open System.Text.Json.Serialization + +/// Module containing JSON serializing extension members for the CollectRemaining type +[] +module CollectRemainingJsonSerializeExtension = + /// Extension methods for JSON parsing + type CollectRemaining with + + /// Serialize to a JSON node + static member toJsonNode (input : CollectRemaining) : System.Text.Json.Nodes.JsonNode = + let node = System.Text.Json.Nodes.JsonObject () + + do + node.Add ( + "message", + (input.Message + |> (fun field -> + match field with + | None -> null :> System.Text.Json.Nodes.JsonNode + | Some field -> HeaderAndValue.toJsonNode field + )) + ) + + for KeyValue (key, value) in input.Rest do + node.Add (key, id value) + + node :> _ +namespace ConsumePlugin + +open System +open System.Collections.Generic +open System.Text.Json.Serialization + +/// Module containing JSON serializing extension members for the OuterCollectRemaining type +[] +module OuterCollectRemainingJsonSerializeExtension = + /// Extension methods for JSON parsing + type OuterCollectRemaining with + + /// Serialize to a JSON node + static member toJsonNode (input : OuterCollectRemaining) : System.Text.Json.Nodes.JsonNode = + let node = System.Text.Json.Nodes.JsonObject () + + do + for KeyValue (key, value) in input.Others do + node.Add (key, System.Text.Json.Nodes.JsonValue.Create value) + + node.Add ("remaining", (input.Remaining |> CollectRemaining.toJsonNode)) + + node :> _ + +namespace ConsumePlugin + +/// Module containing JSON parsing extension members for the InnerTypeWithBoth type +[] +module InnerTypeWithBothJsonParseExtension = + /// Extension methods for JSON parsing + type InnerTypeWithBoth with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : InnerTypeWithBoth = + let arg_4 = + (match node.["concreteDict"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("concreteDict") + ) + ) + | v -> v) + .AsObject () + |> Seq.map (fun kvp -> + let key = (kvp.Key) + let value = InnerTypeWithBoth.jsonParse (kvp.Value) + key, value + ) + |> Seq.map System.Collections.Generic.KeyValuePair + |> System.Collections.Generic.Dictionary + + let arg_3 = + (match node.["dict"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("dict") + ) + ) + | v -> v) + .AsObject () + |> Seq.map (fun kvp -> + let key = (kvp.Key) |> System.Uri + let value = (kvp.Value).AsValue().GetValue () + key, value + ) + |> dict + + let arg_2 = + (match node.["readOnlyDict"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("readOnlyDict") + ) + ) + | v -> v) + .AsObject () + |> Seq.map (fun kvp -> + let key = (kvp.Key) + + let value = + (kvp.Value).AsArray () + |> Seq.map (fun elt -> elt.AsValue().GetValue ()) + |> List.ofSeq + + key, value + ) + |> readOnlyDict + + let arg_1 = + (match node.["map"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("map") + ) + ) + | v -> v) + .AsObject () + |> Seq.map (fun kvp -> + let key = (kvp.Key) + let value = (kvp.Value).AsValue().GetValue () |> System.Uri + key, value + ) + |> Map.ofSeq + + let arg_0 = + (match node.[("it's-a-me")] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" (("it's-a-me")) + ) + ) + | v -> v) + .AsValue() + .GetValue () + |> System.Guid.Parse + + { + Thing = arg_0 + Map = arg_1 + ReadOnlyDict = arg_2 + Dict = arg_3 + ConcreteDict = arg_4 + } +namespace ConsumePlugin + +/// Module containing JSON parsing extension members for the SomeEnum type +[] +module SomeEnumJsonParseExtension = + /// Extension methods for JSON parsing + type SomeEnum with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : SomeEnum = + match node.GetValueKind () with + | System.Text.Json.JsonValueKind.Number -> node.AsValue().GetValue () |> enum + | System.Text.Json.JsonValueKind.String -> + match node.AsValue().GetValue().ToLowerInvariant () with + | "blah" -> SomeEnum.Blah + | "thing" -> SomeEnum.Thing + | v -> failwith ("Unrecognised value for enum: %i" + v) + | _ -> failwith ("Unrecognised kind for enum of type: " + "SomeEnum") +namespace ConsumePlugin + +/// Module containing JSON parsing extension members for the JsonRecordTypeWithBoth type +[] +module JsonRecordTypeWithBothJsonParseExtension = + /// Extension methods for JSON parsing + type JsonRecordTypeWithBoth with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : JsonRecordTypeWithBoth = + let arg_21 = () + + let arg_20 = + (match node.["timestamp"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("timestamp") + ) + ) + | v -> v) + .AsValue() + .GetValue () + |> System.DateTimeOffset.Parse + + let arg_19 = + SomeEnum.jsonParse ( + match node.["enum"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("enum") + ) + ) + | v -> v + ) + + let arg_18 = + match node.["intMeasureNullable"] with + | null -> System.Nullable () + | v -> + v.AsValue().GetValue () + |> LanguagePrimitives.Int32WithMeasure + |> System.Nullable + + let arg_17 = + match node.["intMeasureOption"] with + | null -> None + | v -> + v.AsValue().GetValue () + |> LanguagePrimitives.Int32WithMeasure + |> Some + + let arg_16 = + (match node.["single"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("single") + ) + ) + | v -> v) + .AsValue() + .GetValue () + |> LanguagePrimitives.Float32WithMeasure + + let arg_15 = + (match node.["f32"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("f32") + ) + ) + | v -> v) + .AsValue() + .GetValue () + |> LanguagePrimitives.Float32WithMeasure + + let arg_14 = + (match node.["f"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("f") + ) + ) + | v -> v) + .AsValue() + .GetValue () + |> LanguagePrimitives.FloatWithMeasure + + let arg_13 = + (match node.["u64"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("u64") + ) + ) + | v -> v) + .AsValue() + .GetValue () + |> LanguagePrimitives.UInt64WithMeasure + + let arg_12 = + (match node.["u32"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("u32") + ) + ) + | v -> v) + .AsValue() + .GetValue () + |> LanguagePrimitives.UInt32WithMeasure + + let arg_11 = + (match node.["u"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("u") + ) + ) + | v -> v) + .AsValue() + .GetValue () + |> LanguagePrimitives.UInt32WithMeasure + + let arg_10 = + (match node.["i64"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("i64") + ) + ) + | v -> v) + .AsValue() + .GetValue () + |> LanguagePrimitives.Int64WithMeasure + + let arg_9 = + (match node.["i32"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("i32") + ) + ) + | v -> v) + .AsValue() + .GetValue () + |> LanguagePrimitives.Int32WithMeasure + + let arg_8 = + (match node.["i"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("i") + ) + ) + | v -> v) + .AsValue() + .GetValue () + |> LanguagePrimitives.Int32WithMeasure + + let arg_7 = + (match node.["sbyte"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("sbyte") + ) + ) + | v -> v) + .AsValue() + .GetValue () + |> LanguagePrimitives.SByteWithMeasure + + let arg_6 = + (match node.["byte"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("byte") + ) + ) + | v -> v) + .AsValue() + .GetValue () + |> LanguagePrimitives.ByteWithMeasure + + let arg_5 = + (match node.["arr"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("arr") + ) + ) + | v -> v) + .AsArray () + |> Seq.map (fun elt -> elt.AsValue().GetValue ()) + |> Array.ofSeq + + let arg_4 = + (match node.["e"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("e") + ) + ) + | v -> v) + .AsArray () + |> Seq.map (fun elt -> elt.AsValue().GetValue ()) + |> Array.ofSeq + + let arg_3 = + InnerTypeWithBoth.jsonParse ( + match node.["d"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("d") + ) + ) + | v -> v + ) + + let arg_2 = + (match node.["c"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("c") + ) + ) + | v -> v) + .AsArray () + |> Seq.map (fun elt -> elt.AsValue().GetValue ()) + |> List.ofSeq + + let arg_1 = + (match node.["b"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("b") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_0 = + (match node.["a"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("a") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + { + A = arg_0 + B = arg_1 + C = arg_2 + D = arg_3 + E = arg_4 + Arr = arg_5 + Byte = arg_6 + Sbyte = arg_7 + I = arg_8 + I32 = arg_9 + I64 = arg_10 + U = arg_11 + U32 = arg_12 + U64 = arg_13 + F = arg_14 + F32 = arg_15 + Single = arg_16 + IntMeasureOption = arg_17 + IntMeasureNullable = arg_18 + Enum = arg_19 + Timestamp = arg_20 + Unit = arg_21 + } +namespace ConsumePlugin + +/// Module containing JSON parsing extension members for the FirstDu type +[] +module FirstDuJsonParseExtension = + /// Extension methods for JSON parsing + type FirstDu with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : FirstDu = + let ty = + (match node.["type"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("type") + ) + ) + | v -> v) + |> (fun v -> v.GetValue ()) + + match ty with + | "emptyCase" -> FirstDu.EmptyCase + | "case1" -> + let node = + (match node.["data"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("data") + ) + ) + | v -> v) + + FirstDu.Case1 ( + (match node.["data"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("data") + ) + ) + | v -> v) + .AsValue() + .GetValue () + ) + | "case2" -> + let node = + (match node.["data"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("data") + ) + ) + | v -> v) + + FirstDu.Case2 ( + JsonRecordTypeWithBoth.jsonParse ( + match node.["record"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("record") + ) + ) + | v -> v + ), + (match node.["i"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("i") + ) + ) + | v -> v) + .AsValue() + .GetValue () + ) + | v -> failwith ("Unrecognised 'type' field value: " + v) +namespace ConsumePlugin + +/// Module containing JSON parsing extension members for the HeaderAndValue type +[] +module HeaderAndValueJsonParseExtension = + /// Extension methods for JSON parsing + type HeaderAndValue with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : HeaderAndValue = + let arg_1 = + (match node.["value"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("value") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_0 = + (match node.["header"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("header") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + { + Header = arg_0 + Value = arg_1 + } +namespace ConsumePlugin + +/// Module containing JSON parsing extension members for the Foo type +[] +module FooJsonParseExtension = + /// Extension methods for JSON parsing + type Foo with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : Foo = + let arg_0 = + match node.["message"] with + | null -> None + | v -> HeaderAndValue.jsonParse v |> Some + + { + Message = arg_0 + } +namespace ConsumePlugin + +/// Module containing JSON parsing extension members for the CollectRemaining type +[] +module CollectRemainingJsonParseExtension = + /// Extension methods for JSON parsing + type CollectRemaining with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : CollectRemaining = + let arg_1 = + let result = + System.Collections.Generic.Dictionary () + + let node = node.AsObject () + + for KeyValue (key, value) in node do + if key = "message" then () else result.Add (key, node.[key]) + + result + + let arg_0 = + match node.["message"] with + | null -> None + | v -> HeaderAndValue.jsonParse v |> Some + + { + Message = arg_0 + Rest = arg_1 + } +namespace ConsumePlugin + +/// Module containing JSON parsing extension members for the OuterCollectRemaining type +[] +module OuterCollectRemainingJsonParseExtension = + /// Extension methods for JSON parsing + type OuterCollectRemaining with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : OuterCollectRemaining = + let arg_1 = + CollectRemaining.jsonParse ( + match node.["remaining"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("remaining") + ) + ) + | v -> v + ) + + let arg_0 = + let result = System.Collections.Generic.Dictionary () + let node = node.AsObject () + + for KeyValue (key, value) in node do + if key = "remaining" then + () + else + result.Add ( + key, + (match node.[key] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" (key) + ) + ) + | v -> v) + .AsValue() + .GetValue () + ) + + result + + { + Others = arg_0 + Remaining = arg_1 + } diff --git a/Plugins/Json/WoofWare.Whippet.Plugin.Json.Consumer/JsonRecord.fs b/Plugins/Json/WoofWare.Whippet.Plugin.Json.Consumer/JsonRecord.fs new file mode 100644 index 0000000..153250b --- /dev/null +++ b/Plugins/Json/WoofWare.Whippet.Plugin.Json.Consumer/JsonRecord.fs @@ -0,0 +1,83 @@ +namespace ConsumePlugin + +open System.Text.Json.Serialization +open WoofWare.Whippet.Plugin.Json + +module Literals = + [] + let something = "something" + +[] +type InnerType = + { + [] + Thing : string + } + +/// My whatnot +[] +type JsonRecordType = + { + /// A thing! + A : int + /// Another thing! + [] + B : string + [] + C : int list + D : InnerType + E : string array + F : int[] + } + +[] +type internal InternalTypeNotExtension = + { + [] + InternalThing : string + } + +[] +type internal InternalTypeNotExtensionSerial = + { + [] + InternalThing2 : string + } + +[] +[] +type internal InternalTypeExtension = + { + [] + ExternalThing : string + } + +[] +type ToGetExtensionMethod = + { + Alpha : string + Bravo : System.Uri + Charlie : float + Delta : float32 + Echo : single + Foxtrot : double + Golf : int64 + Hotel : uint64 + India : int + Juliette : uint + Kilo : int32 + Lima : uint32 + Mike : int16 + November : uint16 + Oscar : int8 + Papa : uint8 + Quebec : byte + Tango : sbyte + Uniform : decimal + Victor : char + Whiskey : bigint + } + +[] +module ToGetExtensionMethod = + let thisModuleWouldClash = 3 diff --git a/Plugins/Json/WoofWare.Whippet.Plugin.Json.Consumer/PureGymDto.fs b/Plugins/Json/WoofWare.Whippet.Plugin.Json.Consumer/PureGymDto.fs new file mode 100644 index 0000000..9181c6a --- /dev/null +++ b/Plugins/Json/WoofWare.Whippet.Plugin.Json.Consumer/PureGymDto.fs @@ -0,0 +1,190 @@ +// Copied from https://gitea.patrickstevens.co.uk/patrick/puregym-unofficial-dotnet/src/commit/2741c5e36cf0bdb203b12b78a8062e25af9d89c7/PureGym/Api.fs + +namespace PureGym + +open System +open System.Text.Json.Serialization +open WoofWare.Whippet.Plugin.Json + +[] +type GymOpeningHours = + { + IsAlwaysOpen : bool + OpeningHours : string list + } + +[] +type GymAccessOptions = + { + PinAccess : bool + QrCodeAccess : bool + } + +[] +type measure + +[] +type GymLocation = + { + [] + Longitude : float + [] + Latitude : float + } + +[] +type GymAddress = + { + [] + AddressLine1 : string + AddressLine2 : string option + AddressLine3 : string option + [] + Town : string + County : string option + [] + Postcode : string + } + +[] +type Gym = + { + [] + Name : string + [] + Id : int + [] + Status : int + [] + Address : GymAddress + [] + PhoneNumber : string + [] + EmailAddress : string + [] + GymOpeningHours : GymOpeningHours + [] + AccessOptions : GymAccessOptions + [] + Location : GymLocation + [] + TimeZone : string + ReopenDate : string + } + +[] +[] +type Member = + { + Id : int + CompoundMemberId : string + FirstName : string + LastName : string + HomeGymId : int + HomeGymName : string + EmailAddress : string + GymAccessPin : string + [] + DateOfBirth : DateOnly + MobileNumber : string + [] + Postcode : string + MembershipName : string + MembershipLevel : int + SuspendedReason : int + MemberStatus : int + } + +[] +type GymAttendance = + { + [] + Description : string + [] + TotalPeopleInGym : int + [] + TotalPeopleInClasses : int + TotalPeopleSuffix : string option + [] + IsApproximate : bool + AttendanceTime : DateTime + LastRefreshed : DateTime + LastRefreshedPeopleInClasses : DateTime + MaximumCapacity : int + } + +[] +type MemberActivityDto = + { + [] + TotalDuration : int + [] + AverageDuration : int + [] + TotalVisits : int + [] + TotalClasses : int + [] + IsEstimated : bool + [] + LastRefreshed : DateTime + } + +[] +type SessionsAggregate = + { + [] + Activities : int + [] + Visits : int + [] + Duration : int + } + +[] +type VisitGym = + { + [] + Id : int + [] + Name : string + [] + Status : string + } + +[] +type Visit = + { + [] + IsDurationEstimated : bool + [] + StartTime : DateTime + [] + Duration : int + [] + Gym : VisitGym + } + +[] +type SessionsSummary = + { + [] + Total : SessionsAggregate + [] + ThisWeek : SessionsAggregate + } + +[] +type Sessions = + { + [] + Summary : SessionsSummary + [] + Visits : Visit list + } + +[] +type UriThing = + { + SomeUri : Uri + } diff --git a/Plugins/Json/WoofWare.Whippet.Plugin.Json.Consumer/SerializationAndDeserialization.fs b/Plugins/Json/WoofWare.Whippet.Plugin.Json.Consumer/SerializationAndDeserialization.fs new file mode 100644 index 0000000..d62bbc3 --- /dev/null +++ b/Plugins/Json/WoofWare.Whippet.Plugin.Json.Consumer/SerializationAndDeserialization.fs @@ -0,0 +1,94 @@ +namespace ConsumePlugin + +open System +open System.Collections.Generic +open System.Text.Json.Serialization + +[] +[] +type InnerTypeWithBoth = + { + [] + Thing : Guid + Map : Map + ReadOnlyDict : IReadOnlyDictionary + Dict : IDictionary + ConcreteDict : Dictionary + } + +[] +[] +type SomeEnum = + | Blah = 1 + | Thing = 0 + +[] +type measure + +[] +[] +type JsonRecordTypeWithBoth = + { + A : int + B : string + C : int list + D : InnerTypeWithBoth + E : string array + Arr : int[] + Byte : byte + Sbyte : sbyte + I : int + I32 : int32 + I64 : int64 + U : uint + U32 : uint32 + U64 : uint64 + F : float + F32 : float32 + Single : single + IntMeasureOption : int option + IntMeasureNullable : int Nullable + Enum : SomeEnum + Timestamp : DateTimeOffset + Unit : unit + } + +[] +[] +type FirstDu = + | EmptyCase + | Case1 of data : string + | Case2 of record : JsonRecordTypeWithBoth * i : int + +[] +[] +type HeaderAndValue = + { + Header : string + Value : string + } + +[] +[] +type Foo = + { + Message : HeaderAndValue option + } + +[] +[] +type CollectRemaining = + { + Message : HeaderAndValue option + [] + Rest : Dictionary + } + +[] +[] +type OuterCollectRemaining = + { + [] + Others : Dictionary + Remaining : CollectRemaining + } diff --git a/Plugins/Json/WoofWare.Whippet.Plugin.Json.Consumer/WoofWare.Whippet.Plugin.Json.Consumer.fsproj b/Plugins/Json/WoofWare.Whippet.Plugin.Json.Consumer/WoofWare.Whippet.Plugin.Json.Consumer.fsproj new file mode 100644 index 0000000..c6966fa --- /dev/null +++ b/Plugins/Json/WoofWare.Whippet.Plugin.Json.Consumer/WoofWare.Whippet.Plugin.Json.Consumer.fsproj @@ -0,0 +1,31 @@ + + + + net8.0 + true + false + + + + + + JsonRecord.fs + + + + SerializationAndDeserialization.fs + + + + PureGymDto.fs + + + + + + + + + + + diff --git a/Plugins/Json/WoofWare.Whippet.Plugin.Json/DesiredGenerator.fs b/Plugins/Json/WoofWare.Whippet.Plugin.Json/DesiredGenerator.fs new file mode 100644 index 0000000..65e8cd9 --- /dev/null +++ b/Plugins/Json/WoofWare.Whippet.Plugin.Json/DesiredGenerator.fs @@ -0,0 +1,15 @@ +namespace WoofWare.Whippet.Plugin.Json + +type internal DesiredGenerator = + | JsonParse of extensionMethod : bool option + | JsonSerialize of extensionMethod : bool option + + static member Parse (s : string) = + match s with + | "JsonParse" -> DesiredGenerator.JsonParse None + | "JsonParse(true)" -> DesiredGenerator.JsonParse (Some true) + | "JsonParse(false)" -> DesiredGenerator.JsonParse (Some false) + | "JsonSerialize" -> DesiredGenerator.JsonSerialize None + | "JsonSerialize(true)" -> DesiredGenerator.JsonSerialize (Some true) + | "JsonSerialize(false)" -> DesiredGenerator.JsonSerialize (Some false) + | _ -> failwith $"Failed to parse as a generator specification: %s{s}" diff --git a/Plugins/Json/WoofWare.Whippet.Plugin.Json/JsonParseGenerator.fs b/Plugins/Json/WoofWare.Whippet.Plugin.Json/JsonParseGenerator.fs new file mode 100644 index 0000000..34142c2 --- /dev/null +++ b/Plugins/Json/WoofWare.Whippet.Plugin.Json/JsonParseGenerator.fs @@ -0,0 +1,783 @@ +namespace WoofWare.Whippet.Plugin.Json + +open System +open System.Text +open Fantomas.FCS.Syntax +open Fantomas.FCS.SyntaxTrivia +open WoofWare.Whippet.Core +open WoofWare.Whippet.Fantomas + +type internal JsonParseOutputSpec = + { + ExtensionMethods : bool + } + +[] +module internal JsonParseGenerator = + open Fantomas.FCS.Text.Range + + type JsonParseOption = + { + JsonNumberHandlingArg : SynExpr option + } + + static member None = + { + JsonNumberHandlingArg = None + } + + /// (match {indexed} with | null -> raise (System.Collections.Generic.KeyNotFoundException ({propertyName} not found)) | v -> v) + let assertNotNull (propertyName : SynExpr) (indexed : SynExpr) = + let raiseExpr = + SynExpr.applyFunction + (SynExpr.createIdent "sprintf") + (SynExpr.CreateConst "Required key '%s' not found on JSON object") + |> SynExpr.applyTo (SynExpr.paren propertyName) + |> SynExpr.paren + |> SynExpr.applyFunction ( + SynExpr.createLongIdent [ "System" ; "Collections" ; "Generic" ; "KeyNotFoundException" ] + ) + |> SynExpr.paren + |> SynExpr.applyFunction (SynExpr.createIdent "raise") + + [ + SynMatchClause.create SynPat.createNull raiseExpr + SynMatchClause.create (SynPat.named "v") (SynExpr.createIdent "v") + ] + |> SynExpr.createMatch indexed + |> SynExpr.paren + + /// {node}.AsValue().GetValue<{typeName}> () + /// If `propertyName` is Some, uses `assertNotNull {node}` instead of `{node}`. + let asValueGetValue (propertyName : SynExpr option) (typeName : string) (node : SynExpr) : SynExpr = + match propertyName with + | None -> node + | Some propertyName -> assertNotNull propertyName node + |> SynExpr.callMethod "AsValue" + |> SynExpr.callGenericMethod' "GetValue" typeName + + let asValueGetValueIdent (propertyName : SynExpr option) (typeName : LongIdent) (node : SynExpr) : SynExpr = + match propertyName with + | None -> node + | Some propertyName -> assertNotNull propertyName node + |> SynExpr.callMethod "AsValue" + |> SynExpr.callGenericMethod (SynLongIdent.createS "GetValue") [ SynType.createLongIdent typeName ] + + /// {node}.AsObject() + /// If `propertyName` is Some, uses `assertNotNull {node}` instead of `{node}`. + let asObject (propertyName : SynExpr option) (node : SynExpr) : SynExpr = + match propertyName with + | None -> node + | Some propertyName -> assertNotNull propertyName node + |> SynExpr.callMethod "AsObject" + + /// {type}.jsonParse {node} + let typeJsonParse (typeName : LongIdent) (node : SynExpr) : SynExpr = + node + |> SynExpr.applyFunction (SynExpr.createLongIdent' (typeName @ [ Ident.create "jsonParse" ])) + + /// collectionType is e.g. "List"; we'll be calling `ofSeq` on it. + /// body is the body of a lambda which takes a parameter `elt`. + /// {assertNotNull node}.AsArray() + /// |> Seq.map (fun elt -> {body}) + /// |> {collectionType}.ofSeq + let asArrayMapped + (propertyName : SynExpr option) + (collectionType : string) + (node : SynExpr) + (body : SynExpr) + : SynExpr + = + match propertyName with + | None -> node + | Some propertyName -> assertNotNull propertyName node + |> SynExpr.callMethod "AsArray" + |> SynExpr.pipeThroughFunction ( + SynExpr.applyFunction (SynExpr.createLongIdent [ "Seq" ; "map" ]) (SynExpr.createLambda "elt" body) + ) + |> SynExpr.pipeThroughFunction (SynExpr.createLongIdent [ collectionType ; "ofSeq" ]) + + let dotParse (typeName : LongIdent) : LongIdent = + List.append typeName [ Ident.create "Parse" ] + + /// fun kvp -> let key = {key(kvp)} in let value = {value(kvp)} in (key, value)) + /// The inputs will be fed with appropriate SynExprs to apply them to the `kvp.Key` and `kvp.Value` args. + let dictionaryMapper (key : SynExpr -> SynExpr) (value : SynExpr -> SynExpr) : SynExpr = + let keyArg = SynExpr.createLongIdent [ "kvp" ; "Key" ] |> SynExpr.paren + + let valueArg = SynExpr.createLongIdent [ "kvp" ; "Value" ] |> SynExpr.paren + + // No need to paren here, we're on the LHS of a `let` + SynExpr.tupleNoParen [ SynExpr.createIdent "key" ; SynExpr.createIdent "value" ] + |> SynExpr.createLet [ SynBinding.basic [ Ident.create "value" ] [] (value valueArg) ] + |> SynExpr.createLet [ SynBinding.basic [ Ident.create "key" ] [] (key keyArg) ] + |> SynExpr.createLambda "kvp" + + /// A conforming JSON object has only strings as keys. But it would be reasonable to allow the user + /// to parse these as URIs, for example. + let parseKeyString (desiredType : SynType) (key : SynExpr) : SynExpr = + match desiredType with + | String -> key + | Uri -> + key + |> SynExpr.pipeThroughFunction (SynExpr.createLongIdent [ "System" ; "Uri" ]) + | _ -> + failwithf + $"Unable to parse the key type %+A{desiredType} of a JSON object. Keys are strings, and this plugin does not know how to convert to that from a string." + + let private parseNumberType + (options : JsonParseOption) + (propertyName : SynExpr option) + (node : SynExpr) + (typeName : LongIdent) + = + let basic = asValueGetValueIdent propertyName typeName node + + match options.JsonNumberHandlingArg with + | None -> basic + | Some option -> + let cond = + SynExpr.DotGet (SynExpr.createIdent "exc", range0, SynLongIdent.createS "Message", range0) + |> SynExpr.callMethodArg "Contains" (SynExpr.CreateConst "cannot be converted to") + + let handler = + asValueGetValue propertyName "string" node + |> SynExpr.pipeThroughFunction (SynExpr.createLongIdent' (typeName |> dotParse)) + |> SynExpr.ifThenElse + (SynExpr.equals + option + (SynExpr.createLongIdent + [ + "System" + "Text" + "Json" + "Serialization" + "JsonNumberHandling" + "AllowReadingFromString" + ])) + SynExpr.reraise + |> SynExpr.ifThenElse cond SynExpr.reraise + + basic + |> SynExpr.pipeThroughTryWith + (SynPat.IsInst ( + SynType.LongIdent (SynLongIdent.createS' [ "System" ; "InvalidOperationException" ]), + range0 + )) + handler + + /// Given `node.["town"]`, for example, choose how to obtain a JSON value from it. + /// The property name is used in error messages at runtime to show where a JSON + /// parse error occurred; supply `None` to indicate "don't validate". + let rec parseNode + (propertyName : SynExpr option) + (options : JsonParseOption) + (fieldType : SynType) + (node : SynExpr) + : SynExpr + = + // TODO: parsing format for DateTime etc + match fieldType with + | DateOnly -> + node + |> asValueGetValue propertyName "string" + |> SynExpr.pipeThroughFunction (SynExpr.createLongIdent [ "System" ; "DateOnly" ; "Parse" ]) + | Uri -> + node + |> asValueGetValue propertyName "string" + |> SynExpr.pipeThroughFunction (SynExpr.createLongIdent [ "System" ; "Uri" ]) + | Guid -> + node + |> asValueGetValue propertyName "string" + |> SynExpr.pipeThroughFunction (SynExpr.createLongIdent [ "System" ; "Guid" ; "Parse" ]) + | DateTime -> + node + |> asValueGetValue propertyName "string" + |> SynExpr.pipeThroughFunction (SynExpr.createLongIdent [ "System" ; "DateTime" ; "Parse" ]) + | DateTimeOffset -> + node + |> asValueGetValue propertyName "string" + |> SynExpr.pipeThroughFunction (SynExpr.createLongIdent [ "System" ; "DateTimeOffset" ; "Parse" ]) + | NumberType typeName -> parseNumberType options propertyName node typeName + | PrimitiveType typeName -> asValueGetValueIdent propertyName typeName node + | OptionType ty -> + let someClause = + parseNode None options ty (SynExpr.createIdent "v") + |> SynExpr.pipeThroughFunction (SynExpr.createIdent "Some") + |> SynMatchClause.create (SynPat.named "v") + + [ + SynMatchClause.create SynPat.createNull (SynExpr.createIdent "None") + someClause + ] + |> SynExpr.createMatch node + | NullableType ty -> + let someClause = + parseNode None options ty (SynExpr.createIdent "v") + |> SynExpr.pipeThroughFunction (SynExpr.createLongIdent [ "System" ; "Nullable" ]) + |> SynMatchClause.create (SynPat.named "v") + + [ + SynMatchClause.create + SynPat.createNull + (SynExpr.applyFunction (SynExpr.createLongIdent [ "System" ; "Nullable" ]) (SynExpr.CreateConst ())) + someClause + ] + |> SynExpr.createMatch node + | ListType ty -> + parseNode None options ty (SynExpr.createIdent "elt") + |> asArrayMapped propertyName "List" node + | ArrayType ty -> + parseNode None options ty (SynExpr.createIdent "elt") + |> asArrayMapped propertyName "Array" node + | IDictionaryType (keyType, valueType) -> + node + |> asObject propertyName + |> SynExpr.pipeThroughFunction ( + SynExpr.applyFunction + (SynExpr.createLongIdent [ "Seq" ; "map" ]) + (dictionaryMapper (parseKeyString keyType) (parseNode None options valueType)) + ) + |> SynExpr.pipeThroughFunction (SynExpr.createIdent "dict") + | DictionaryType (keyType, valueType) -> + node + |> asObject propertyName + |> SynExpr.pipeThroughFunction ( + SynExpr.applyFunction + (SynExpr.createLongIdent [ "Seq" ; "map" ]) + (dictionaryMapper (parseKeyString keyType) (parseNode None options valueType)) + ) + |> SynExpr.pipeThroughFunction ( + SynExpr.applyFunction + (SynExpr.createLongIdent [ "Seq" ; "map" ]) + (SynExpr.createLongIdent [ "System" ; "Collections" ; "Generic" ; "KeyValuePair" ]) + ) + |> SynExpr.pipeThroughFunction ( + SynExpr.createLongIdent [ "System" ; "Collections" ; "Generic" ; "Dictionary" ] + ) + | IReadOnlyDictionaryType (keyType, valueType) -> + node + |> asObject propertyName + |> SynExpr.pipeThroughFunction ( + SynExpr.applyFunction + (SynExpr.createLongIdent [ "Seq" ; "map" ]) + (dictionaryMapper (parseKeyString keyType) (parseNode None options valueType)) + ) + |> SynExpr.pipeThroughFunction (SynExpr.createIdent "readOnlyDict") + | MapType (keyType, valueType) -> + node + |> asObject propertyName + |> SynExpr.pipeThroughFunction ( + SynExpr.applyFunction + (SynExpr.createLongIdent [ "Seq" ; "map" ]) + (dictionaryMapper (parseKeyString keyType) (parseNode None options valueType)) + ) + |> SynExpr.pipeThroughFunction (SynExpr.createLongIdent [ "Map" ; "ofSeq" ]) + | BigInt -> + node + |> SynExpr.callMethod "ToJsonString" + |> SynExpr.paren + |> SynExpr.applyFunction (SynExpr.createLongIdent [ "System" ; "Numerics" ; "BigInteger" ; "Parse" ]) + | Measure (_measure, primType) -> + parseNumberType options propertyName node primType + |> SynExpr.pipeThroughFunction (Measure.getLanguagePrimitivesMeasure primType) + | JsonNode -> node + | UnitType -> SynExpr.CreateConst () + | _ -> + // Let's just hope that we've also got our own type annotation! + let typeName = + match fieldType with + | SynType.LongIdent ident -> ident.LongIdent + | _ -> failwith $"Unrecognised type: %+A{fieldType}" + + match propertyName with + | None -> node + | Some propertyName -> assertNotNull propertyName node + |> typeJsonParse typeName + + /// propertyName is probably a string literal, but it could be a [] variable + /// The result of this function is the body of a let-binding (not including the LHS of that let-binding). + let createParseRhs (options : JsonParseOption) (propertyName : SynExpr) (fieldType : SynType) : SynExpr = + let objectToParse = SynExpr.createIdent "node" |> SynExpr.index propertyName + parseNode (Some propertyName) options fieldType objectToParse + + let isJsonNumberHandling (literal : LongIdent) : bool = + match List.rev literal |> List.map (fun ident -> ident.idText) with + | [ _ ; "JsonNumberHandling" ] + | [ _ ; "JsonNumberHandling" ; "Serialization" ] + | [ _ ; "JsonNumberHandling" ; "Serialization" ; "Json" ] + | [ _ ; "JsonNumberHandling" ; "Serialization" ; "Json" ; "Text" ] + | [ _ ; "JsonNumberHandling" ; "Serialization" ; "Json" ; "Text" ; "System" ] -> true + | _ -> false + + /// `populateNode` will be inserted before we return the `node` variable. + /// + /// That is, we give you access to a `JsonNode` called `node`, + /// and you must return a `typeName`. + let scaffolding (spec : JsonParseOutputSpec) (typeName : LongIdent) (functionBody : SynExpr) : SynModuleDecl = + let xmlDoc = PreXmlDoc.create "Parse from a JSON node." + + let returnInfo = SynType.createLongIdent typeName + + let inputArg = "node" + let functionName = Ident.create "jsonParse" + + let arg = + SynPat.named inputArg + |> SynPat.annotateType (SynType.createLongIdent' [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonNode" ]) + + if spec.ExtensionMethods then + let binding = + SynBinding.basic [ functionName ] [ arg ] functionBody + |> SynBinding.withXmlDoc xmlDoc + |> SynBinding.withReturnAnnotation returnInfo + |> SynMemberDefn.staticMember + + let componentInfo = + SynComponentInfo.createLong typeName + |> SynComponentInfo.withDocString (PreXmlDoc.create "Extension methods for JSON parsing") + + let containingType = + SynTypeDefnRepr.augmentation () + |> SynTypeDefn.create componentInfo + |> SynTypeDefn.withMemberDefns [ binding ] + + SynModuleDecl.Types ([ containingType ], range0) + else + SynBinding.basic [ functionName ] [ arg ] functionBody + |> SynBinding.withXmlDoc xmlDoc + |> SynBinding.withReturnAnnotation returnInfo + |> SynModuleDecl.createLet + + let getParseOptions (fieldAttrs : SynAttribute list) = + (JsonParseOption.None, fieldAttrs) + ||> List.fold (fun options attr -> + if + (SynLongIdent.toString attr.TypeName) + .EndsWith ("JsonNumberHandling", StringComparison.Ordinal) + then + let qualifiedEnumValue = + match SynExpr.stripOptionalParen attr.ArgExpr with + | SynExpr.LongIdent (_, SynLongIdent (ident, _, _), _, _) when isJsonNumberHandling ident -> + // Make sure it's fully qualified + SynExpr.createLongIdent + [ + "System" + "Text" + "Json" + "Serialization" + "JsonNumberHandling" + "AllowReadingFromString" + ] + | _ -> attr.ArgExpr + + { + JsonNumberHandlingArg = Some qualifiedEnumValue + } + else + options + ) + + let createRecordMaker (spec : JsonParseOutputSpec) (fields : SynFieldData list) = + let propertyFields = + fields + |> List.map (fun fieldData -> + let propertyNameAttr = + fieldData.Attrs + |> List.tryFind (fun attr -> + (SynLongIdent.toString attr.TypeName) + .EndsWith ("JsonPropertyName", StringComparison.Ordinal) + ) + + let extensionDataAttr = + fieldData.Attrs + |> List.tryFind (fun attr -> + (SynLongIdent.toString attr.TypeName) + .EndsWith ("JsonExtensionData", StringComparison.Ordinal) + ) + + let propertyName = + match propertyNameAttr with + | None -> + let sb = StringBuilder fieldData.Ident.idText.Length + + sb.Append (Char.ToLowerInvariant fieldData.Ident.idText.[0]) + |> ignore + + if fieldData.Ident.idText.Length > 1 then + sb.Append (fieldData.Ident.idText.Substring 1) |> ignore + + sb.ToString () |> SynExpr.CreateConst + | Some name -> name.ArgExpr + + propertyName, extensionDataAttr + ) + + let namedPropertyFields = + propertyFields + |> List.choose (fun (name, extension) -> + match extension with + | Some _ -> None + | None -> Some name + ) + + let isNamedPropertyField = + match namedPropertyFields with + | [] -> SynExpr.CreateConst false + | _ -> + namedPropertyFields + |> List.map (fun fieldName -> SynExpr.equals (SynExpr.createIdent "key") fieldName) + |> List.reduce SynExpr.booleanOr + + let assignments = + List.zip fields propertyFields + |> List.mapi (fun i (fieldData, (propertyName, extensionDataAttr)) -> + let options = getParseOptions fieldData.Attrs + + let accIdent = Ident.create $"arg_%i{i}" + + match extensionDataAttr with + | Some _ -> + // Can't go through the usual parse logic here, because that will try and identify the node that's + // been labelled. The whole point of JsonExtensionData is that there is no such node! + let valType = + match fieldData.Type with + | DictionaryType (String, v) -> v + | _ -> failwith "Expected JsonExtensionData to be Dictionary" + + SynExpr.ifThenElse + isNamedPropertyField + (SynExpr.callMethodArg + "Add" + (SynExpr.tuple + [ + SynExpr.createIdent "key" + createParseRhs options (SynExpr.createIdent "key") valType + ]) + (SynExpr.createIdent "result")) + (SynExpr.CreateConst ()) + |> SynExpr.createForEach + (SynPat.nameWithArgs "KeyValue" [ SynPat.named "key" ; SynPat.named "value" ]) + (SynExpr.createIdent "node") + |> fun forEach -> [ forEach ; SynExpr.createIdent "result" ] + |> SynExpr.sequential + |> SynExpr.createLet + [ + SynBinding.basic + [ Ident.create "result" ] + [] + (SynExpr.typeApp + [ SynType.string ; valType ] + (SynExpr.createLongIdent [ "System" ; "Collections" ; "Generic" ; "Dictionary" ]) + |> SynExpr.applyTo (SynExpr.CreateConst ())) + + SynBinding.basic + [ Ident.create "node" ] + [] + (SynExpr.createIdent "node" |> SynExpr.callMethod "AsObject") + ] + |> SynBinding.basic [ accIdent ] [] + | None -> + + createParseRhs options propertyName fieldData.Type + |> SynBinding.basic [ accIdent ] [] + ) + + let finalConstruction = + fields + |> List.mapi (fun i fieldData -> SynLongIdent.createI fieldData.Ident, SynExpr.createIdent $"arg_%i{i}") + |> AstHelper.instantiateRecord + + (finalConstruction, assignments) + ||> List.fold (fun final assignment -> SynExpr.createLet [ assignment ] final) + + let createUnionMaker (spec : JsonParseOutputSpec) (typeName : LongIdent) (fields : UnionCase list) = + fields + |> List.map (fun case -> + let propertyName = JsonSerializeGenerator.getPropertyName case.Name case.Attributes + + let body = + if case.Fields.IsEmpty then + SynExpr.createLongIdent' (typeName @ [ case.Name ]) + else + case.Fields + |> List.map (fun field -> + let propertyName = JsonSerializeGenerator.getPropertyName field.Ident field.Attrs + let options = getParseOptions field.Attrs + createParseRhs options propertyName field.Type + ) + |> SynExpr.tuple + |> SynExpr.applyFunction (SynExpr.createLongIdent' (typeName @ [ case.Name ])) + |> SynExpr.createLet + [ + SynExpr.index (SynExpr.CreateConst "data") (SynExpr.createIdent "node") + |> assertNotNull (SynExpr.CreateConst "data") + |> SynBinding.basic [ Ident.create "node" ] [] + ] + + match propertyName with + | SynExpr.Const (synConst, _) -> + SynMatchClause.SynMatchClause ( + SynPat.createConst synConst, + None, + body, + range0, + DebugPointAtTarget.Yes, + { + ArrowRange = Some range0 + BarRange = Some range0 + } + ) + | _ -> + SynMatchClause.create (SynPat.named "x") body + |> SynMatchClause.withWhere (SynExpr.equals (SynExpr.createIdent "x") propertyName) + ) + |> fun l -> + l + @ [ + let fail = + SynExpr.plus (SynExpr.CreateConst "Unrecognised 'type' field value: ") (SynExpr.createIdent "v") + |> SynExpr.paren + |> SynExpr.applyFunction (SynExpr.createIdent "failwith") + + SynMatchClause.SynMatchClause ( + SynPat.named "v", + None, + fail, + range0, + DebugPointAtTarget.Yes, + { + ArrowRange = Some range0 + BarRange = Some range0 + } + ) + ] + |> SynExpr.createMatch (SynExpr.createIdent "ty") + |> SynExpr.createLet + [ + let property = SynExpr.CreateConst "type" + + SynExpr.createIdent "node" + |> SynExpr.index property + |> assertNotNull property + |> SynExpr.pipeThroughFunction ( + SynExpr.createLambda "v" (SynExpr.callGenericMethod' "GetValue" "string" (SynExpr.createIdent "v")) + ) + |> SynBinding.basic [ Ident.create "ty" ] [] + ] + + let createEnumMaker + (spec : JsonParseOutputSpec) + (typeName : LongIdent) + (fields : (Ident * SynExpr) list) + : SynExpr + = + let numberKind = + [ "System" ; "Text" ; "Json" ; "JsonValueKind" ; "Number" ] + |> List.map Ident.create + + let stringKind = + [ "System" ; "Text" ; "Json" ; "JsonValueKind" ; "String" ] + |> List.map Ident.create + + let fail = + SynExpr.plus + (SynExpr.CreateConst "Unrecognised kind for enum of type: ") + (SynExpr.CreateConst (typeName |> List.map _.idText |> String.concat ".")) + |> SynExpr.paren + |> SynExpr.applyFunction (SynExpr.createIdent "failwith") + + let failString = + SynExpr.plus (SynExpr.CreateConst "Unrecognised value for enum: %i") (SynExpr.createIdent "v") + |> SynExpr.paren + |> SynExpr.applyFunction (SynExpr.createIdent "failwith") + + let parseString = + fields + |> List.map (fun (ident, _) -> + SynMatchClause.create + (SynPat.createConst ( + SynConst.String (ident.idText.ToLowerInvariant (), SynStringKind.Regular, range0) + )) + (SynExpr.createLongIdent' (typeName @ [ ident ])) + ) + |> fun l -> l @ [ SynMatchClause.create (SynPat.named "v") failString ] + |> SynExpr.createMatch ( + asValueGetValue None "string" (SynExpr.createIdent "node") + |> SynExpr.callMethod "ToLowerInvariant" + ) + + [ + SynMatchClause.create + (SynPat.identWithArgs numberKind (SynArgPats.create [])) + (asValueGetValue None "int" (SynExpr.createIdent "node") + |> SynExpr.pipeThroughFunction ( + SynExpr.typeApp [ SynType.createLongIdent typeName ] (SynExpr.createIdent "enum") + )) + SynMatchClause.create (SynPat.identWithArgs stringKind (SynArgPats.create [])) parseString + SynMatchClause.create (SynPat.named "_") fail + ] + |> SynExpr.createMatch (SynExpr.callMethod "GetValueKind" (SynExpr.createIdent "node")) + + let createModule (namespaceId : LongIdent) (spec : JsonParseOutputSpec) (typeDefn : SynTypeDefn) = + let (SynTypeDefn (synComponentInfo, synTypeDefnRepr, _members, _implicitCtor, _, _)) = + typeDefn + + let (SynComponentInfo (_attributes, _typeParams, _constraints, ident, _, _preferPostfix, access, _)) = + synComponentInfo + + let attributes = + if spec.ExtensionMethods then + [ SynAttribute.autoOpen ] + else + [ SynAttribute.requireQualifiedAccess ; SynAttribute.compilationRepresentation ] + + let xmlDoc = + let fullyQualified = ident |> Seq.map (fun i -> i.idText) |> String.concat "." + + let description = + if spec.ExtensionMethods then + "extension members" + else + "methods" + + $"Module containing JSON parsing %s{description} for the %s{fullyQualified} type" + |> PreXmlDoc.create + + let moduleName = + if spec.ExtensionMethods then + match ident with + | [] -> failwith "unexpectedly got an empty identifier for record name" + | ident -> + let expanded = + List.last ident + |> fun i -> i.idText + |> fun s -> s + "JsonParseExtension" + |> Ident.create + + List.take (List.length ident - 1) ident @ [ expanded ] + else + ident + + let info = + SynComponentInfo.createLong moduleName + |> SynComponentInfo.withDocString xmlDoc + |> SynComponentInfo.setAccessibility access + |> SynComponentInfo.addAttributes attributes + + let decl = + match synTypeDefnRepr with + | SynTypeDefnRepr.Simple (SynTypeDefnSimpleRepr.Record (_accessibility, fields, _range), _) -> + fields |> List.map SynField.extractWithIdent |> createRecordMaker spec + | SynTypeDefnRepr.Simple (SynTypeDefnSimpleRepr.Union (_accessibility, cases, _range), _) -> + let optionGet (i : Ident option) = + match i with + | None -> + failwith "WoofWare.Whippet.Plugin.Json requires union cases to have identifiers on each field." + | Some i -> i + + cases + |> List.map UnionCase.ofSynUnionCase + |> List.map (UnionCase.mapIdentFields optionGet) + |> createUnionMaker spec ident + | SynTypeDefnRepr.Simple (SynTypeDefnSimpleRepr.Enum (cases, _range), _) -> + cases + |> List.map (fun c -> + match c with + | SynEnumCase.SynEnumCase (_, SynIdent.SynIdent (ident, _), value, _, _, _) -> ident, value + ) + |> createEnumMaker spec ident + | _ -> failwithf "Not a record or union type" + + [ scaffolding spec ident decl ] + |> SynModuleDecl.nestedModule info + |> List.singleton + |> SynModuleOrNamespace.createNamespace namespaceId + +/// Whippet generator that provides a method (possibly an extension method) for a record type, +/// containing a JSON parse function. +[] +type JsonParseGenerator () = + + interface IGenerateRawFromRaw with + member _.GenerateRawFromRaw (context : RawSourceGenerationArgs) = + if not (context.FilePath.EndsWith (".fs", StringComparison.Ordinal)) then + null + else + + let targetedTypes = + context.Parameters + |> Seq.map (fun (KeyValue (k, v)) -> k, v.Split '!' |> Array.toList |> List.map DesiredGenerator.Parse) + |> Map.ofSeq + + let ast = Ast.parse (Encoding.UTF8.GetString context.FileContents) + + let relevantTypes = + Ast.getTypes ast + |> List.map (fun (name, defns) -> + defns + |> List.choose (fun defn -> + if SynTypeDefn.isRecord defn then Some defn + elif SynTypeDefn.isDu defn then Some defn + elif SynTypeDefn.isEnum defn then Some defn + else None + ) + |> fun defns -> name, defns + ) + + let namespaceAndTypes = + relevantTypes + |> List.choose (fun (ns, types) -> + types + |> List.choose (fun typeDef -> + match SynTypeDefn.getAttribute typeof.Name typeDef with + | None -> + let name = SynTypeDefn.getName typeDef |> List.map _.idText |> String.concat "." + + match Map.tryFind name targetedTypes with + | Some desired -> + desired + |> List.tryPick (fun generator -> + match generator with + | DesiredGenerator.JsonParse arg -> + let spec = + { + ExtensionMethods = + arg + |> Option.defaultValue + JsonParseAttribute.DefaultIsExtensionMethod + } + + Some (typeDef, spec) + | _ -> None + ) + | _ -> None + + | Some attr -> + let arg = + match SynExpr.stripOptionalParen attr.ArgExpr with + | SynExpr.Const (SynConst.Bool value, _) -> value + | SynExpr.Const (SynConst.Unit, _) -> JsonParseAttribute.DefaultIsExtensionMethod + | arg -> + failwith + $"Unrecognised argument %+A{arg} to [<%s{nameof JsonParseAttribute}>]. Literals are not supported. Use `true` or `false` (or unit) only." + + let spec = + { + ExtensionMethods = arg + } + + Some (typeDef, spec) + ) + |> function + | [] -> None + | ty -> Some (ns, ty) + ) + + let modules = + namespaceAndTypes + |> List.collect (fun (ns, types) -> + types |> List.map (fun (ty, spec) -> JsonParseGenerator.createModule ns spec ty) + ) + + Ast.render modules |> Option.toObj diff --git a/Plugins/Json/WoofWare.Whippet.Plugin.Json/JsonSerializeGenerator.fs b/Plugins/Json/WoofWare.Whippet.Plugin.Json/JsonSerializeGenerator.fs new file mode 100644 index 0000000..b617b4b --- /dev/null +++ b/Plugins/Json/WoofWare.Whippet.Plugin.Json/JsonSerializeGenerator.fs @@ -0,0 +1,602 @@ +namespace WoofWare.Whippet.Plugin.Json + +open System +open System.Text +open Fantomas.FCS.Syntax +open WoofWare.Whippet.Core +open WoofWare.Whippet.Fantomas + +type internal JsonSerializeOutputSpec = + { + ExtensionMethods : bool + } + +[] +module internal JsonSerializeGenerator = + open Fantomas.FCS.Text.Range + + + // The absolutely galaxy-brained implementation of JsonValue has `JsonValue.Parse "null"` + // identically equal to null. We have to work around this later, but we might as well just + // be efficient here and whip up the null directly. + let private jsonNull () = + SynExpr.createNull () + |> SynExpr.upcast' (SynType.createLongIdent' [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonNode" ]) + + /// Given `input.Ident`, for example, choose how to add it to the ambient `node`. + /// The result is a line like `(fun ident -> InnerType.toJsonNode ident)` or `(fun ident -> JsonValue.Create ident)`. + /// Returns also a bool which is true if the resulting SynExpr represents something of type JsonNode. + let rec serializeNode (fieldType : SynType) : SynExpr * bool = + // TODO: serialization format for DateTime etc + match fieldType with + | DateOnly + | DateTime + | NumberType _ + | Measure _ + | PrimitiveType _ + | Guid + | Uri -> + // JsonValue.Create + SynExpr.createLongIdent [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonValue" ; "Create" ] + |> SynExpr.typeApp [ fieldType ] + |> fun e -> e, false + | DateTimeOffset -> + // fun field -> field.ToString("o") |> JsonValue.Create + let create = + SynExpr.createLongIdent [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonValue" ; "Create" ] + |> SynExpr.typeApp [ SynType.named "string" ] + + SynExpr.createIdent "field" + |> SynExpr.callMethodArg "ToString" (SynExpr.CreateConst "o") + |> SynExpr.pipeThroughFunction create + |> SynExpr.createLambda "field" + |> fun e -> e, false + | NullableType ty -> + // fun field -> if field.HasValue then {serializeNode ty} field.Value else JsonValue.Create null + let inner, innerIsJsonNode = serializeNode ty + + SynExpr.applyFunction inner (SynExpr.createLongIdent [ "field" ; "Value" ]) + |> SynExpr.upcast' (SynType.createLongIdent' [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonNode" ]) + |> SynExpr.ifThenElse (SynExpr.createLongIdent [ "field" ; "HasValue" ]) (jsonNull ()) + |> SynExpr.createLambda "field" + |> fun e -> e, innerIsJsonNode + | OptionType ty -> + // fun field -> match field with | None -> JsonValue.Create null | Some v -> {serializeNode ty} field + let noneClause = jsonNull () |> SynMatchClause.create (SynPat.named "None") + + let someClause = + let inner, innerIsJsonNode = serializeNode ty + let target = SynExpr.applyFunction inner (SynExpr.createIdent "field") + + if innerIsJsonNode then + target + else + target + |> SynExpr.paren + |> SynExpr.upcast' (SynType.createLongIdent' [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonNode" ]) + |> SynMatchClause.create (SynPat.nameWithArgs "Some" [ SynPat.named "field" ]) + + [ noneClause ; someClause ] + |> SynExpr.createMatch (SynExpr.createIdent "field") + |> SynExpr.createLambda "field" + |> fun e -> e, true + | ArrayType ty + | ListType ty -> + // fun field -> + // let arr = JsonArray () + // for mem in field do arr.Add ({serializeNode} mem) + // arr + [ + SynExpr.ForEach ( + DebugPointAtFor.Yes range0, + DebugPointAtInOrTo.Yes range0, + SeqExprOnly.SeqExprOnly false, + true, + SynPat.named "mem", + SynExpr.createIdent "field", + SynExpr.applyFunction + (SynExpr.createLongIdent [ "arr" ; "Add" ]) + (SynExpr.paren (SynExpr.applyFunction (fst (serializeNode ty)) (SynExpr.createIdent "mem"))), + range0 + ) + SynExpr.createIdent "arr" + ] + |> SynExpr.sequential + |> SynExpr.createLet + [ + SynExpr.createLongIdent [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonArray" ] + |> SynExpr.applyTo (SynExpr.CreateConst ()) + |> SynBinding.basic [ Ident.create "arr" ] [] + ] + |> SynExpr.createLambda "field" + |> fun e -> e, false + | IDictionaryType (_keyType, valueType) + | DictionaryType (_keyType, valueType) + | IReadOnlyDictionaryType (_keyType, valueType) + | MapType (_keyType, valueType) -> + // fun field -> + // let ret = JsonObject () + // for (KeyValue(key, value)) in field do + // ret.Add (key.ToString (), {serializeNode} value) + // ret + [ + SynExpr.ForEach ( + DebugPointAtFor.Yes range0, + DebugPointAtInOrTo.Yes range0, + SeqExprOnly.SeqExprOnly false, + true, + SynPat.paren (SynPat.nameWithArgs "KeyValue" [ SynPat.named "key" ; SynPat.named "value" ]), + SynExpr.createIdent "field", + SynExpr.applyFunction + (SynExpr.createLongIdent [ "ret" ; "Add" ]) + (SynExpr.tuple + [ + SynExpr.createLongIdent [ "key" ; "ToString" ] + |> SynExpr.applyTo (SynExpr.CreateConst ()) + SynExpr.applyFunction (fst (serializeNode valueType)) (SynExpr.createIdent "value") + ]), + range0 + ) + SynExpr.createIdent "ret" + ] + |> SynExpr.sequential + |> SynExpr.createLet + [ + SynExpr.createLongIdent [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonObject" ] + |> SynExpr.applyTo (SynExpr.CreateConst ()) + |> SynBinding.basic [ Ident.create "ret" ] [] + ] + |> SynExpr.createLambda "field" + |> fun e -> e, false + | JsonNode -> SynExpr.createIdent "id", true + | UnitType -> + SynExpr.createLambda + "value" + (SynExpr.createLongIdent [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonObject" ] + |> SynExpr.applyTo (SynExpr.CreateConst ())), + false + | _ -> + // {type}.toJsonNode + let typeName = + match fieldType with + | SynType.LongIdent ident -> ident.LongIdent + | _ -> failwith $"Unrecognised type: %+A{fieldType}" + + SynExpr.createLongIdent' (typeName @ [ Ident.create "toJsonNode" ]), true + + /// propertyName is probably a string literal, but it could be a [] variable + /// `node.Add ({propertyName}, {toJsonNode})` + let createSerializeRhsRecord (propertyName : SynExpr) (fieldId : Ident) (fieldType : SynType) : SynExpr = + [ + propertyName + SynExpr.pipeThroughFunction + (fst (serializeNode fieldType)) + (SynExpr.createLongIdent' [ Ident.create "input" ; fieldId ]) + |> SynExpr.paren + ] + |> SynExpr.tuple + |> SynExpr.applyFunction (SynExpr.createLongIdent [ "node" ; "Add" ]) + + let getPropertyName (fieldId : Ident) (attrs : SynAttribute list) : SynExpr = + let propertyNameAttr = + attrs + |> List.tryFind (fun attr -> + (SynLongIdent.toString attr.TypeName) + .EndsWith ("JsonPropertyName", StringComparison.Ordinal) + ) + + match propertyNameAttr with + | None -> + let sb = StringBuilder fieldId.idText.Length + sb.Append (Char.ToLowerInvariant fieldId.idText.[0]) |> ignore + + if fieldId.idText.Length > 1 then + sb.Append fieldId.idText.[1..] |> ignore + + sb.ToString () |> SynExpr.CreateConst + | Some name -> name.ArgExpr + + let getIsJsonExtension (attrs : SynAttribute list) : bool = + attrs + |> List.tryFind (fun attr -> + (SynLongIdent.toString attr.TypeName) + .EndsWith ("JsonExtensionData", StringComparison.Ordinal) + ) + |> Option.isSome + + /// `populateNode` will be inserted before we return the `node` variable. + /// + /// That is, we give you access to a `JsonObject` called `node`, + /// and you have access to a variable `inputArgName` which is of type `typeName`. + /// Your job is to provide a `populateNode` expression which has the side effect + /// of mutating `node` to faithfully reflect the value of `inputArgName`. + let scaffolding + (spec : JsonSerializeOutputSpec) + (typeName : LongIdent) + (inputArgName : Ident) + (populateNode : SynExpr) + : SynModuleDecl + = + let xmlDoc = PreXmlDoc.create "Serialize to a JSON node" + + let returnInfo = + SynLongIdent.createS' [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonNode" ] + |> SynType.LongIdent + + let functionName = Ident.create "toJsonNode" + + let assignments = + [ + populateNode + SynExpr.Upcast (SynExpr.createIdent "node", SynType.Anon range0, range0) + ] + |> SynExpr.sequential + |> SynExpr.createLet + [ + SynExpr.createLongIdent [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonObject" ] + |> SynExpr.applyTo (SynExpr.CreateConst ()) + |> SynBinding.basic [ Ident.create "node" ] [] + ] + + let pattern = + SynPat.namedI inputArgName + |> SynPat.annotateType (SynType.LongIdent (SynLongIdent.create typeName)) + + if spec.ExtensionMethods then + let componentInfo = + SynComponentInfo.createLong typeName + |> SynComponentInfo.withDocString (PreXmlDoc.create "Extension methods for JSON parsing") + + let memberDef = + assignments + |> SynBinding.basic [ functionName ] [ pattern ] + |> SynBinding.withXmlDoc xmlDoc + |> SynBinding.withReturnAnnotation returnInfo + |> SynMemberDefn.staticMember + + let containingType = + SynTypeDefnRepr.augmentation () + |> SynTypeDefn.create componentInfo + |> SynTypeDefn.withMemberDefns [ memberDef ] + + SynModuleDecl.Types ([ containingType ], range0) + else + assignments + |> SynBinding.basic [ functionName ] [ pattern ] + |> SynBinding.withReturnAnnotation returnInfo + |> SynBinding.withXmlDoc xmlDoc + |> SynModuleDecl.createLet + + let recordModule (spec : JsonSerializeOutputSpec) (_typeName : LongIdent) (fields : SynField list) = + let fields = fields |> List.map SynField.extractWithIdent + + fields + |> List.map (fun fieldData -> + let propertyName = getPropertyName fieldData.Ident fieldData.Attrs + let isJsonExtension = getIsJsonExtension fieldData.Attrs + + if isJsonExtension then + let valType = + match fieldData.Type with + | DictionaryType (String, v) -> v + | _ -> failwith "Expected JsonExtensionData to be a Dictionary" + + let serialise = fst (serializeNode valType) + + SynExpr.createIdent "node" + |> SynExpr.callMethodArg + "Add" + (SynExpr.tuple + [ + SynExpr.createIdent "key" + SynExpr.applyFunction serialise (SynExpr.createIdent "value") + ]) + |> SynExpr.createForEach + (SynPat.identWithArgs + [ Ident.create "KeyValue" ] + (SynArgPats.create [ SynPat.named "key" ; SynPat.named "value" ])) + (SynExpr.createLongIdent' [ Ident.create "input" ; fieldData.Ident ]) + else + createSerializeRhsRecord propertyName fieldData.Ident fieldData.Type + ) + |> SynExpr.sequential + |> fun expr -> SynExpr.Do (expr, range0) + + let unionModule (spec : JsonSerializeOutputSpec) (typeName : LongIdent) (cases : SynUnionCase list) = + let inputArg = Ident.create "input" + let fields = cases |> List.map UnionCase.ofSynUnionCase + + fields + |> List.map (fun unionCase -> + let propertyName = getPropertyName unionCase.Name unionCase.Attributes + + let caseNames = unionCase.Fields |> List.mapi (fun i _ -> $"arg%i{i}") + + let argPats = SynArgPats.createNamed caseNames + + let pattern = + SynPat.LongIdent ( + SynLongIdent.create (typeName @ [ unionCase.Name ]), + None, + None, + argPats, + None, + range0 + ) + + let typeLine = + [ + SynExpr.CreateConst "type" + SynExpr.applyFunction + (SynExpr.createLongIdent [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonValue" ; "Create" ]) + propertyName + ] + |> SynExpr.tuple + |> SynExpr.applyFunction (SynExpr.createLongIdent [ "node" ; "Add" ]) + + let dataNode = + SynExpr.createLongIdent [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonObject" ] + |> SynExpr.applyTo (SynExpr.CreateConst ()) + |> SynBinding.basic [ Ident.create "dataNode" ] [] + + let dataBindings = + (unionCase.Fields, caseNames) + ||> List.zip + |> List.map (fun (fieldData, caseName) -> + let propertyName = getPropertyName (Option.get fieldData.Ident) fieldData.Attrs + + let node = + SynExpr.applyFunction (fst (serializeNode fieldData.Type)) (SynExpr.createIdent caseName) + + [ propertyName ; node ] + |> SynExpr.tuple + |> SynExpr.applyFunction (SynExpr.createLongIdent [ "dataNode" ; "Add" ]) + ) + + let assignToNode = + [ SynExpr.CreateConst "data" ; SynExpr.createIdent "dataNode" ] + |> SynExpr.tuple + |> SynExpr.applyFunction (SynExpr.createLongIdent [ "node" ; "Add" ]) + + let dataNode = + SynExpr.sequential (dataBindings @ [ assignToNode ]) + |> SynExpr.createLet [ dataNode ] + + let action = + [ + yield typeLine + if not dataBindings.IsEmpty then + yield dataNode + ] + |> SynExpr.sequential + + SynMatchClause.create pattern action + ) + |> SynExpr.createMatch (SynExpr.createIdent' inputArg) + + let enumModule + (spec : JsonSerializeOutputSpec) + (typeName : LongIdent) + (cases : (Ident * SynExpr) list) + : SynModuleDecl + = + let fail = + SynExpr.CreateConst "Unrecognised value for enum: %O" + |> SynExpr.applyFunction (SynExpr.createIdent "sprintf") + |> SynExpr.applyTo (SynExpr.createIdent "v") + |> SynExpr.paren + |> SynExpr.applyFunction (SynExpr.createIdent "failwith") + + let body = + cases + |> List.map (fun (caseName, value) -> + value + |> SynExpr.applyFunction ( + SynExpr.createLongIdent [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonValue" ; "Create" ] + ) + |> SynMatchClause.create (SynPat.identWithArgs (typeName @ [ caseName ]) (SynArgPats.create [])) + ) + |> fun l -> l @ [ SynMatchClause.create (SynPat.named "v") fail ] + |> SynExpr.createMatch (SynExpr.createIdent "input") + + let xmlDoc = PreXmlDoc.create "Serialize to a JSON node" + + let returnInfo = + SynLongIdent.createS' [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonNode" ] + |> SynType.LongIdent + + let functionName = Ident.create "toJsonNode" + + let pattern = + SynPat.named "input" + |> SynPat.annotateType (SynType.LongIdent (SynLongIdent.create typeName)) + + if spec.ExtensionMethods then + let componentInfo = + SynComponentInfo.createLong typeName + |> SynComponentInfo.withDocString (PreXmlDoc.create "Extension methods for JSON parsing") + + let memberDef = + body + |> SynBinding.basic [ functionName ] [ pattern ] + |> SynBinding.withXmlDoc xmlDoc + |> SynBinding.withReturnAnnotation returnInfo + |> SynMemberDefn.staticMember + + let containingType = + SynTypeDefnRepr.augmentation () + |> SynTypeDefn.create componentInfo + |> SynTypeDefn.withMemberDefns [ memberDef ] + + SynModuleDecl.Types ([ containingType ], range0) + else + body + |> SynBinding.basic [ functionName ] [ pattern ] + |> SynBinding.withReturnAnnotation returnInfo + |> SynBinding.withXmlDoc xmlDoc + |> SynModuleDecl.createLet + + let createModule + (namespaceId : LongIdent) + (opens : SynOpenDeclTarget list) + (spec : JsonSerializeOutputSpec) + (typeDefn : SynTypeDefn) + = + let (SynTypeDefn (synComponentInfo, synTypeDefnRepr, _members, _implicitCtor, _, _)) = + typeDefn + + let (SynComponentInfo (_attributes, _typeParams, _constraints, ident, _, _preferPostfix, access, _)) = + synComponentInfo + + let attributes = + if spec.ExtensionMethods then + [ SynAttribute.autoOpen ] + else + [ SynAttribute.requireQualifiedAccess ; SynAttribute.compilationRepresentation ] + + let xmlDoc = + let fullyQualified = ident |> Seq.map (fun i -> i.idText) |> String.concat "." + + let description = + if spec.ExtensionMethods then + "extension members" + else + "methods" + + $"Module containing JSON serializing %s{description} for the %s{fullyQualified} type" + |> PreXmlDoc.create + + let moduleName = + if spec.ExtensionMethods then + match ident with + | [] -> failwith "unexpectedly got an empty identifier for type name" + | ident -> + let expanded = + List.last ident + |> fun i -> i.idText + |> fun s -> s + "JsonSerializeExtension" + |> Ident.create + + List.take (List.length ident - 1) ident @ [ expanded ] + else + ident + + let info = + SynComponentInfo.createLong moduleName + |> SynComponentInfo.addAttributes attributes + |> SynComponentInfo.setAccessibility access + |> SynComponentInfo.withDocString xmlDoc + + let decls = + match synTypeDefnRepr with + | SynTypeDefnRepr.Simple (SynTypeDefnSimpleRepr.Record (_accessibility, recordFields, _range), _) -> + recordModule spec ident recordFields + |> scaffolding spec ident (Ident.create "input") + | SynTypeDefnRepr.Simple (SynTypeDefnSimpleRepr.Union (_accessibility, unionFields, _range), _) -> + unionModule spec ident unionFields + |> scaffolding spec ident (Ident.create "input") + | SynTypeDefnRepr.Simple (SynTypeDefnSimpleRepr.Enum (cases, _range), _) -> + cases + |> List.map (fun c -> + match c with + | SynEnumCase.SynEnumCase (_, SynIdent.SynIdent (ident, _), value, _, _, _) -> ident, value + ) + |> enumModule spec ident + | ty -> failwithf "Unsupported type: got %O" ty + + [ + yield! opens |> List.map SynModuleDecl.openAny + yield decls |> List.singleton |> SynModuleDecl.nestedModule info + ] + |> SynModuleOrNamespace.createNamespace namespaceId + +/// Whippet generator that provides a method (possibly an extension method) for a record type, +/// containing a JSON serialization function. +[] +type JsonSerializeGenerator () = + + interface IGenerateRawFromRaw with + member _.GenerateRawFromRaw (context : RawSourceGenerationArgs) = + if not (context.FilePath.EndsWith (".fs", StringComparison.Ordinal)) then + null + else + + let targetedTypes = + context.Parameters + |> Seq.map (fun (KeyValue (k, v)) -> k, v.Split '!' |> Array.toList |> List.map DesiredGenerator.Parse) + |> Map.ofSeq + + let ast = Ast.parse (Encoding.UTF8.GetString context.FileContents) + + let relevantTypes = + Ast.getTypes ast + |> List.map (fun (name, defns) -> + defns + |> List.choose (fun defn -> + if SynTypeDefn.isRecord defn then Some defn + elif SynTypeDefn.isDu defn then Some defn + elif SynTypeDefn.isEnum defn then Some defn + else None + ) + |> fun defns -> name, defns + ) + + let namespaceAndTypes = + relevantTypes + |> List.choose (fun (ns, types) -> + types + |> List.choose (fun typeDef -> + match SynTypeDefn.getAttribute typeof.Name typeDef with + | None -> + let name = SynTypeDefn.getName typeDef |> List.map _.idText |> String.concat "." + + match Map.tryFind name targetedTypes with + | Some desired -> + desired + |> List.tryPick (fun generator -> + match generator with + | DesiredGenerator.JsonSerialize arg -> + let spec = + { + ExtensionMethods = + arg + |> Option.defaultValue + JsonSerializeAttribute.DefaultIsExtensionMethod + } + + Some (typeDef, spec) + | _ -> None + ) + | _ -> None + + | Some attr -> + let arg = + match SynExpr.stripOptionalParen attr.ArgExpr with + | SynExpr.Const (SynConst.Bool value, _) -> value + | SynExpr.Const (SynConst.Unit, _) -> JsonSerializeAttribute.DefaultIsExtensionMethod + | arg -> + failwith + $"Unrecognised argument %+A{arg} to [<%s{nameof JsonSerializeAttribute}>]. Literals are not supported. Use `true` or `false` (or unit) only." + + let spec = + { + ExtensionMethods = arg + } + + Some (typeDef, spec) + ) + |> function + | [] -> None + | ty -> Some (ns, ty) + ) + + let opens = AstHelper.extractOpens ast + + let modules = + namespaceAndTypes + |> List.collect (fun (ns, types) -> + types + |> List.map (fun (ty, spec) -> JsonSerializeGenerator.createModule ns opens spec ty) + ) + + Ast.render modules |> Option.toObj diff --git a/Plugins/Json/WoofWare.Whippet.Plugin.Json/README.md b/Plugins/Json/WoofWare.Whippet.Plugin.Json/README.md new file mode 100644 index 0000000..3534884 --- /dev/null +++ b/Plugins/Json/WoofWare.Whippet.Plugin.Json/README.md @@ -0,0 +1,223 @@ +# WoofWare.Whippet.Plugin.Json + +This is a [Whippet](https://github.com/Smaug123/WoofWare.Whippet) plugin defining JSON parse and serialise methods. + +It is a copy of the corresponding [Myriad](https://github.com/MoiraeSoftware/myriad) JSON plugin in [WoofWare.Myriad](https://github.com/Smaug123/WoofWare.Myriad), taken from commit d59ebdfccb87a06579fb99008a15f58ea8be394e. + +## What's the point? + +`System.Text.Json`, in a `PublishAot` context, relies on C# source generators. +The default reflection-heavy implementations have the necessary code trimmed away, and result in a runtime exception. +But C# source generators [are entirely unsupported in F#](https://github.com/dotnet/fsharp/issues/14300). + +These generators handle going from your strongly-typed domain objects to `System.Text.Json.Nodes.JsonNode`, and back. + +## Usage: `JsonParse` + +Define a `Dto.fs` file like the following: + +```fsharp +namespace MyNamespace + +open WoofWare.Whippet.Plugin.Json + +[] +type InnerType = + { + [] + Thing : string + } + +/// My whatnot +[] +type JsonRecordType = + { + /// A thing! + A : int + /// Another thing! + B : string + [] + C : int list + D : InnerType + } +``` + +In your fsproj: + +```xml + + + + + Dto.fs + + + + + + + + + + + +``` + +The generator will produce a file somewhat like the following: + +```fsharp +/// Module containing JSON parsing methods for the InnerType type +[] +[] +module InnerType = + /// Parse from a JSON node. + let jsonParse (node: System.Text.Json.Nodes.JsonNode) : InnerType = + let Thing = node.["something"].AsValue().GetValue() + { Thing = Thing } +namespace UsePlugin + +/// Module containing JSON parsing methods for the JsonRecordType type +[] +module JsonRecordTypeExtension = + type JsonRecordType with + /// Parse from a JSON node. + let jsonParse (node: System.Text.Json.Nodes.JsonNode) : JsonRecordType = + let D = InnerType.jsonParse node.["d"] + + let C = + node.["hi"].AsArray() |> Seq.map (fun elt -> elt.GetValue()) |> List.ofSeq + + let B = node.["b"].AsValue().GetValue() + let A = node.["a"].AsValue().GetValue() + { A = A; B = B; C = C; D = D } +``` + +You may instead choose to define attributes with the correct name yourself (if you don't want to take a dependency on the `WoofWare.Whippet.Plugin.Json.Attributes` package). +Alternatively, you may omit the attributes and the runtime dependency, and control the generator entirely through the fsproj file: + +```xml + + + + + Dto.fs + JsonParse + JsonParse(false) + + + + + + + + + +``` + +(This plugin follows a standard convention taken by `WoofWare.Whippet.Plugin` plugins, +where you use Whippet parameters with the same name as each input type, +whose contents are a `!`-delimited list of the generators which you wish to apply to that input type.) + +## Usage: `JsonSerialize` + +Define a `Dto.fs` file like the following: + +```fsharp +namespace MyNamespace + +open WoofWare.Whippet.Plugin.Json + +[] +type InnerTypeWithBoth = + { + [] + Thing : string + ReadOnlyDict : IReadOnlyDictionary + } +``` + +In your fsproj: + +```xml + + + + + Dto.fs + + + + + + + + + + + +``` + +The generator will produce a file somewhat like the following: + +```fsharp +namespace UsePlugin + +/// Module containing JSON parsing methods for the JsonRecordType type +[] +module JsonRecordTypeExtension = + type InnerTypeWithBoth with + let toJsonNode (input : InnerTypeWithBoth) : System.Text.Json.Nodes.JsonNode = + let node = System.Text.Json.Nodes.JsonObject () + + do + node.Add (("it's-a-me"), System.Text.Json.Nodes.JsonValue.Create input.Thing) + + node.Add ( + "ReadOnlyDict", + (fun field -> + let ret = System.Text.Json.Nodes.JsonObject () + + for (KeyValue (key, value)) in field do + ret.Add (key.ToString (), System.Text.Json.Nodes.JsonValue.Create value) + + ret + ) input.Map + ) + + node +``` + +You may instead choose to define attributes with the correct name yourself (if you don't want to take a dependency on the `WoofWare.Whippet.Plugin.Json.Attributes` package). +Alternatively, you may omit the attributes and the runtime dependency, and control the generator entirely through the fsproj file: + +```xml + + + + + Dto.fs + JsonSerialize + JsonSerialize(false) + + + + + + + + + +``` + +(This plugin follows a standard convention taken by `WoofWare.Whippet.Plugin` plugins, +where you use Whippet parameters with the same name as each input type, +whose contents are a `!`-delimited list of the generators which you wish to apply to that input type.) + +## Notes + +* The plugin includes an *opinionated* de/serializer for discriminated unions. (Any such serializer must be opinionated, because JSON does not natively model DUs.) +* Supply the optional boolean arg `false` to the `[]`/`[]` attributes, or pass it via `JsonParse(false)`, to get a genuine module that can be consumed from C#. diff --git a/Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/PureGymDtos.fs b/Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/PureGymDtos.fs new file mode 100644 index 0000000..50883c2 --- /dev/null +++ b/Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/PureGymDtos.fs @@ -0,0 +1,264 @@ +namespace WoofWare.Whippet.Plugin.Json.Test + +open PureGym +open System + +[] +module PureGymDtos = + + let gymOpeningHoursCases = + [ + """{"openingHours": [], "isAlwaysOpen": false}""", + { + GymOpeningHours.OpeningHours = [] + IsAlwaysOpen = false + } + """{"openingHours": ["something"], "isAlwaysOpen": false}""", + { + GymOpeningHours.OpeningHours = [ "something" ] + IsAlwaysOpen = false + } + ] + + let gymAccessOptionsCases = + List.allPairs [ true ; false ] [ true ; false ] + |> List.map (fun (a, b) -> + let s = sprintf """{"pinAccess": %b, "qrCodeAccess": %b}""" a b + + s, + { + GymAccessOptions.PinAccess = a + QrCodeAccess = b + } + ) + + let gymAddressCases = + [ + """{"addressLine1": "", "postCode": "hi", "town": ""}""", + { + GymAddress.AddressLine1 = "" + AddressLine2 = None + AddressLine3 = None + County = None + Postcode = "hi" + Town = "" + } + """{"addressLine1": "", "addressLine2": null, "postCode": "hi", "town": ""}""", + { + GymAddress.AddressLine1 = "" + AddressLine2 = None + AddressLine3 = None + County = None + Postcode = "hi" + Town = "" + } + ] + + let gymLocationCases = + [ + """{"latitude": 1.0, "longitude": 3.0}""", + { + GymLocation.Latitude = 1.0 + Longitude = 3.0 + } + ] + + let gymCases = + let ovalJson = + """{"name":"London Oval","id":19,"status":2,"address":{"addressLine1":"Canterbury Court","addressLine2":"Units 4, 4A, 5 And 5A","addressLine3":"Kennington Park","town":"LONDON","county":null,"postcode":"SW9 6DE"},"phoneNumber":"+44 3444770005","emailAddress":"info.londonoval@puregym.com","staffMembers":null,"gymOpeningHours":{"isAlwaysOpen":true,"openingHours":[]},"reasonsToJoin":null,"accessOptions":{"pinAccess":true,"qrCodeAccess":true},"virtualTourUrl":null,"personalTrainersUrl":null,"webViewUrl":null,"floorPlanUrl":null,"location":{"longitude":"-0.110252","latitude":"51.480401"},"timeZone":"Europe/London","reopenDate":"2021-04-12T00:00:00+01 Europe/London"}""" + + let oval = + { + Gym.Name = "London Oval" + Id = 19 + Status = 2 + Address = + { + AddressLine1 = "Canterbury Court" + AddressLine2 = Some "Units 4, 4A, 5 And 5A" + AddressLine3 = Some "Kennington Park" + Town = "LONDON" + County = None + Postcode = "SW9 6DE" + } + PhoneNumber = "+44 3444770005" + EmailAddress = "info.londonoval@puregym.com" + GymOpeningHours = + { + IsAlwaysOpen = true + OpeningHours = [] + } + AccessOptions = + { + PinAccess = true + QrCodeAccess = true + } + Location = + { + Longitude = -0.110252 + Latitude = 51.480401 + } + TimeZone = "Europe/London" + ReopenDate = "2021-04-12T00:00:00+01 Europe/London" + } + + [ ovalJson, oval ] + + let memberCases = + let me = + { + Id = 1234567 + CompoundMemberId = "12A123456" + FirstName = "Patrick" + LastName = "Stevens" + HomeGymId = 19 + HomeGymName = "London Oval" + EmailAddress = "someone@somewhere" + GymAccessPin = "00000000" + DateOfBirth = DateOnly (1994, 01, 02) + MobileNumber = "+44 1234567" + Postcode = "W1A 1AA" + MembershipName = "Corporate" + MembershipLevel = 12 + SuspendedReason = 0 + MemberStatus = 2 + } + + let meJson = + """{ + "id": 1234567, + "compoundMemberId": "12A123456", + "firstName": "Patrick", + "lastName": "Stevens", + "homeGymId": 19, + "homeGymName": "London Oval", + "emailAddress": "someone@somewhere", + "gymAccessPin": "00000000", + "dateofBirth": "1994-01-02", + "mobileNumber": "+44 1234567", + "postCode": "W1A 1AA", + "membershipName": "Corporate", + "membershipLevel": 12, + "suspendedReason": 0, + "memberStatus": 2 +}""" + + [ meJson, me ] + + let gymAttendanceCases = + let json = + """{ + "description": "65", + "totalPeopleInGym": 65, + "totalPeopleInClasses": 2, + "totalPeopleSuffix": null, + "isApproximate": false, + "attendanceTime": "2023-12-27T18:54:09.5101697", + "lastRefreshed": "2023-12-27T18:54:09.5101697Z", + "lastRefreshedPeopleInClasses": "2023-12-27T18:50:26.0782286Z", + "maximumCapacity": 0 +}""" + + let expected = + { + Description = "65" + TotalPeopleInGym = 65 + TotalPeopleInClasses = 2 + TotalPeopleSuffix = None + IsApproximate = false + AttendanceTime = + DateTime (2023, 12, 27, 18, 54, 09, 510, 169, DateTimeKind.Utc) + + TimeSpan.FromTicks 7L + LastRefreshed = + DateTime (2023, 12, 27, 18, 54, 09, 510, 169, DateTimeKind.Utc) + + TimeSpan.FromTicks 7L + LastRefreshedPeopleInClasses = + DateTime (2023, 12, 27, 18, 50, 26, 078, 228, DateTimeKind.Utc) + + TimeSpan.FromTicks 6L + MaximumCapacity = 0 + } + + [ json, expected ] + + let memberActivityDtoCases = + let json = + """{"totalDuration":2217,"averageDuration":48,"totalVisits":46,"totalClasses":0,"isEstimated":false,"lastRefreshed":"2023-12-27T19:00:56.0309892Z"}""" + + let value = + { + TotalDuration = 2217 + AverageDuration = 48 + TotalVisits = 46 + TotalClasses = 0 + IsEstimated = false + LastRefreshed = + DateTime (2023, 12, 27, 19, 00, 56, 030, 989, DateTimeKind.Utc) + + TimeSpan.FromTicks 2L + } + + [ json, value ] + + let sessionsCases = + let json = + """{ + "Summary":{"Total":{"Activities":0,"Visits":10,"Duration":445},"ThisWeek":{"Activities":0,"Visits":0,"Duration":0}}, + "Visits":[ + {"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-21T10:12:00","Duration":50,"Name":null}, + {"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-20T12:05:00","Duration":80,"Name":null}, + {"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-17T19:37:00","Duration":46,"Name":null}, + {"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-16T12:19:00","Duration":37,"Name":null}, + {"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-15T11:14:00","Duration":47,"Name":null}, + {"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-13T10:30:00","Duration":36,"Name":null}, + {"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-10T16:18:00","Duration":32,"Name":null}, + {"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-05T22:36:00","Duration":40,"Name":null}, + {"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-03T17:59:00","Duration":48,"Name":null}, + {"IsDurationEstimated":false,"Gym":{"Id":19,"Name":"London Oval","Status":"Blocked","Location":null,"GymAccess":null,"ContactInfo":null,"TimeZone":null},"StartTime":"2023-12-01T21:41:00","Duration":29,"Name":null}], + "Activities":[]} +""" + + let singleVisit startTime duration = + { + IsDurationEstimated = false + Gym = + { + Id = 19 + Name = "London Oval" + Status = "Blocked" + } + StartTime = startTime + Duration = duration + } + + let expected = + { + Summary = + { + Total = + { + Activities = 0 + Visits = 10 + Duration = 445 + } + ThisWeek = + { + Activities = 0 + Visits = 0 + Duration = 0 + } + } + Visits = + [ + singleVisit (DateTime (2023, 12, 21, 10, 12, 00)) 50 + singleVisit (DateTime (2023, 12, 20, 12, 05, 00)) 80 + singleVisit (DateTime (2023, 12, 17, 19, 37, 00)) 46 + singleVisit (DateTime (2023, 12, 16, 12, 19, 00)) 37 + singleVisit (DateTime (2023, 12, 15, 11, 14, 00)) 47 + singleVisit (DateTime (2023, 12, 13, 10, 30, 00)) 36 + singleVisit (DateTime (2023, 12, 10, 16, 18, 00)) 32 + singleVisit (DateTime (2023, 12, 05, 22, 36, 00)) 40 + singleVisit (DateTime (2023, 12, 03, 17, 59, 00)) 48 + singleVisit (DateTime (2023, 12, 01, 21, 41, 00)) 29 + ] + } + + [ json, expected ] diff --git a/Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/TestExtensionMethod.fs b/Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/TestExtensionMethod.fs new file mode 100644 index 0000000..743e3ee --- /dev/null +++ b/Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/TestExtensionMethod.fs @@ -0,0 +1,74 @@ +namespace WoofWare.Whippet.Plugin.Json.Test + +open System +open System.Numerics +open System.Text.Json.Nodes +open ConsumePlugin +open NUnit.Framework +open FsUnitTyped + +[] +module TestExtensionMethod = + + [] + let ``Parse via extension method`` () = + let json = + """{ + "alpha": "hello!", + "bravo": "https://example.com", + "charlie": 0.3341, + "delta": 110033.4, + "echo": -0.000993, + "foxtrot": -999999999999, + "golf": -123456789101112, + "hotel": 18446744073709551615, + "india": 99884, + "juliette": 12223334, + "kilo": -2147483642, + "lima": 4294967293, + "mike": -32767, + "november": 65533, + "oscar": -125, + "papa": 253, + "quebec": 254, + "tango": -3, + "uniform": 1004443.300988393349583009, + "victor": "x", + "whiskey": 123456123456123456123456123456123456123456 +}""" + |> JsonNode.Parse + + let expected = + { + Alpha = "hello!" + Bravo = Uri "https://example.com" + Charlie = 0.3341 + Delta = 110033.4f + Echo = -0.000993f + Foxtrot = -999999999999.0 + Golf = -123456789101112L + Hotel = 18446744073709551615UL + India = 99884 + Juliette = 12223334u + Kilo = -2147483642 + Lima = 4294967293u + Mike = -32767s + November = 65533us + Oscar = -125y + Papa = 253uy + Quebec = 254uy + Tango = -3y + Uniform = 1004443.300988393349583009m + Victor = 'x' + Whiskey = + let mutable i = BigInteger 0 + + for _ = 0 to 6 do + i <- i * BigInteger 1000000 + BigInteger 123456 + + i + } + + let actual = ToGetExtensionMethod.jsonParse json + + actual |> shouldEqual expected diff --git a/Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/TestJsonParse.fs b/Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/TestJsonParse.fs new file mode 100644 index 0000000..a2496c6 --- /dev/null +++ b/Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/TestJsonParse.fs @@ -0,0 +1,63 @@ +namespace WoofWare.Whippet.Plugin.Json.Test + +open System.Text.Json.Nodes +open ConsumePlugin +open NUnit.Framework +open FsUnitTyped + +[] +module TestJsonParse = + let _canSeePastExtensionMethod = ToGetExtensionMethod.thisModuleWouldClash + + [] + let ``Single example`` () = + let s = + """ +{ + "a": 3, "another-thing": "hello", "hi": [6, 1], "d": {"something": "oh hi"}, + "e": ["something", "else"], "f": [] +} +""" + + let expected = + { + A = 3 + B = "hello" + C = [ 6 ; 1 ] + D = + { + Thing = "oh hi" + } + E = [| "something" ; "else" |] + F = [||] + } + + let actual = s |> JsonNode.Parse |> JsonRecordType.jsonParse + actual |> shouldEqual expected + + [] + let ``Inner example`` () = + let s = + """{ + "something": "oh hi" +}""" + + let expected = + { + Thing = "oh hi" + } + + let actual = s |> JsonNode.Parse |> InnerType.jsonParse + actual |> shouldEqual expected + + [] + [] + [] + [] + [] + [] + let ``Can deserialise enum`` (str : string, expected : SomeEnum) = + sprintf "\"%s\"" str + |> JsonNode.Parse + |> SomeEnum.jsonParse + |> shouldEqual expected diff --git a/Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/TestJsonSerde.fs b/Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/TestJsonSerde.fs new file mode 100644 index 0000000..2127649 --- /dev/null +++ b/Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/TestJsonSerde.fs @@ -0,0 +1,474 @@ +namespace WoofWare.Whippet.Plugin.Json.Test + +open System +open System.Collections.Generic +open System.Text.Json.Nodes +open FsCheck.Random +open Microsoft.FSharp.Reflection +open NUnit.Framework +open FsCheck +open FsUnitTyped +open ConsumePlugin + +[] +module TestJsonSerde = + + let uriGen : Gen = + gen { + let! suffix = Arb.generate + return Uri $"https://example.com/%i{suffix}" + } + + let rec innerGen (count : int) : Gen = + gen { + let! guid = Arb.generate + let! mapKeys = Gen.listOf Arb.generate> + let mapKeys = mapKeys |> List.map _.Get |> List.distinct + let! mapValues = Gen.listOfLength mapKeys.Length uriGen + let map = List.zip mapKeys mapValues |> Map.ofList + + let! concreteDictKeys = + if count > 0 then + Gen.listOf Arb.generate> + else + Gen.constant [] + + let concreteDictKeys = + concreteDictKeys + |> List.map _.Get + |> List.distinct + |> fun x -> List.take (min 3 x.Length) x + + let! concreteDictValues = + if count > 0 then + Gen.listOfLength concreteDictKeys.Length (innerGen (count - 1)) + else + Gen.constant [] + + let concreteDict = + List.zip concreteDictKeys concreteDictValues + |> List.map KeyValuePair + |> Dictionary + + let! readOnlyDictKeys = Gen.listOf Arb.generate> + let readOnlyDictKeys = readOnlyDictKeys |> List.map _.Get |> List.distinct + let! readOnlyDictValues = Gen.listOfLength readOnlyDictKeys.Length (Gen.listOf Arb.generate) + let readOnlyDict = List.zip readOnlyDictKeys readOnlyDictValues |> readOnlyDict + + let! dictKeys = Gen.listOf uriGen + let! dictValues = Gen.listOfLength dictKeys.Length Arb.generate + let dict = List.zip dictKeys dictValues |> dict + + return + { + Thing = guid + Map = map + ReadOnlyDict = readOnlyDict + Dict = dict + ConcreteDict = concreteDict + } + } + + let outerGen : Gen = + gen { + let! a = Arb.generate + let! b = Arb.generate> + let! c = Gen.listOf Arb.generate + let! depth = Gen.choose (0, 2) + let! d = innerGen depth + let! e = Gen.arrayOf Arb.generate> + let! arr = Gen.arrayOf Arb.generate + let! byte = Arb.generate + let! sbyte = Arb.generate + let! i = Arb.generate + let! i32 = Arb.generate + let! i64 = Arb.generate + let! u = Arb.generate + let! u32 = Arb.generate + let! u64 = Arb.generate + let! f = Arb.generate |> Gen.filter (fun s -> Double.IsFinite (s / 1.0)) + let! f32 = Arb.generate |> Gen.filter (fun s -> Single.IsFinite (s / 1.0f)) + let! single = Arb.generate |> Gen.filter (fun s -> Single.IsFinite (s / 1.0f)) + let! intMeasureOption = Arb.generate + let! intMeasureNullable = Arb.generate + let! someEnum = Gen.choose (0, 1) + let! timestamp = Arb.generate + + return + { + A = a + B = b.Get + C = c + D = d + E = e |> Array.map _.Get + Arr = arr + Byte = byte + Sbyte = sbyte + I = i + I32 = i32 + I64 = i64 + U = u + U32 = u32 + U64 = u64 + F = f + F32 = f32 + Single = single + IntMeasureOption = intMeasureOption + IntMeasureNullable = intMeasureNullable + Enum = enum someEnum + Timestamp = timestamp + Unit = () + } + } + + [] + let ``It just works`` () = + let property (o : JsonRecordTypeWithBoth) : bool = + o + |> JsonRecordTypeWithBoth.toJsonNode + |> fun s -> s.ToJsonString () + |> JsonNode.Parse + |> JsonRecordTypeWithBoth.jsonParse + |> shouldEqual o + + true + + property |> Prop.forAll (Arb.fromGen outerGen) |> Check.QuickThrowOnFailure + + [] + let ``Single example of big record`` () = + let guid = Guid.Parse "dfe24db5-9f8d-447b-8463-4c0bcf1166d5" + + let data = + { + A = 3 + B = "hello!" + C = [ 1 ; -9 ] + D = + { + Thing = guid + Map = Map.ofList [] + ReadOnlyDict = readOnlyDict [] + Dict = dict [] + ConcreteDict = Dictionary () + } + E = [| "I'm-a-string" |] + Arr = [| -18883 ; 9100 |] + Byte = 87uy + Sbyte = 89y + I = 199993345 + I32 = -485832 + I64 = -13458625689L + U = 458582u + U32 = 857362147u + U64 = 1234567892123414596UL + F = 8833345667.1 + F32 = 1000.98f + Single = 0.334f + IntMeasureOption = Some 981 + IntMeasureNullable = Nullable -883 + Enum = enum 1 + Timestamp = DateTimeOffset (2024, 07, 01, 17, 54, 00, TimeSpan.FromHours 1.0) + Unit = () + } + + let expected = + """{ + "a": 3, + "b": "hello!", + "c": [1, -9], + "d": { + "it\u0027s-a-me": "dfe24db5-9f8d-447b-8463-4c0bcf1166d5", + "map": {}, + "readOnlyDict": {}, + "dict": {}, + "concreteDict": {} + }, + "e": ["I\u0027m-a-string"], + "arr": [-18883, 9100], + "byte": 87, + "sbyte": 89, + "i": 199993345, + "i32": -485832, + "i64": -13458625689, + "u": 458582, + "u32": 857362147, + "u64": 1234567892123414596, + "f": 8833345667.1, + "f32": 1000.98, + "single": 0.334, + "intMeasureOption": 981, + "intMeasureNullable": -883, + "enum": 1, + "timestamp": "2024-07-01T17:54:00.0000000\u002B01:00", + "unit": {} +} +""" + |> fun s -> s.ToCharArray () + |> Array.filter (fun c -> not (Char.IsWhiteSpace c)) + |> fun s -> new String (s) + + JsonRecordTypeWithBoth.toJsonNode(data).ToJsonString () |> shouldEqual expected + JsonRecordTypeWithBoth.jsonParse (JsonNode.Parse expected) |> shouldEqual data + + [] + let ``Guids are treated just like strings`` () = + let guidStr = "b1e7496e-6e79-4158-8579-a01de355d3b2" + let guid = Guid.Parse guidStr + + let node = + { + Thing = guid + Map = Map.empty + ReadOnlyDict = readOnlyDict [] + Dict = dict [] + ConcreteDict = Dictionary () + } + |> InnerTypeWithBoth.toJsonNode + + node.ToJsonString () + |> shouldEqual ( + sprintf """{"it\u0027s-a-me":"%s","map":{},"readOnlyDict":{},"dict":{},"concreteDict":{}}""" guidStr + ) + + type Generators = + static member TestCase () = + { new Arbitrary() with + override x.Generator = innerGen 5 + } + + let sanitiseInner (r : InnerTypeWithBoth) : InnerTypeWithBoth = + { + Thing = r.Thing + Map = r.Map + ReadOnlyDict = r.ReadOnlyDict + Dict = r.Dict + ConcreteDict = r.ConcreteDict + } + + let sanitiseRec (r : JsonRecordTypeWithBoth) : JsonRecordTypeWithBoth = + { r with + B = if isNull r.B then "" else r.B + C = + if Object.ReferenceEquals (r.C, (null : obj)) then + [] + else + r.C + D = sanitiseInner r.D + E = if isNull r.E then [||] else r.E + Arr = + if Object.ReferenceEquals (r.Arr, (null : obj)) then + [||] + else + r.Arr + } + + let duGen = + gen { + let! case = Gen.choose (0, 2) + + match case with + | 0 -> return FirstDu.EmptyCase + | 1 -> + let! s = Arb.generate> + return FirstDu.Case1 s.Get + | 2 -> + let! i = Arb.generate + let! record = outerGen + return FirstDu.Case2 (record, i) + | _ -> return failwith $"unexpected: %i{case}" + } + + [] + let ``Discriminated union works`` () = + let property (du : FirstDu) : unit = + du + |> FirstDu.toJsonNode + |> fun s -> s.ToJsonString () + |> JsonNode.Parse + |> FirstDu.jsonParse + |> shouldEqual du + + property |> Prop.forAll (Arb.fromGen duGen) |> Check.QuickThrowOnFailure + + [] + let ``DU generator covers all cases`` () = + let rand = Random () + let cases = FSharpType.GetUnionCases typeof + let counts = Array.zeroCreate cases.Length + + let decompose = FSharpValue.PreComputeUnionTagReader typeof + + let mutable i = 0 + + while i < 10_000 && Array.exists (fun i -> i = 0) counts do + let du = Gen.eval 10 (StdGen.StdGen (rand.Next (), rand.Next ())) duGen + let tag = decompose du + counts.[tag] <- counts.[tag] + 1 + i <- i + 1 + + for i in counts do + i |> shouldBeGreaterThan 0 + + let dict<'a, 'b when 'a : equality> (xs : ('a * 'b) seq) : Dictionary<'a, 'b> = + let result = Dictionary () + + for k, v in xs do + result.Add (k, v) + + result + + let inline makeJsonArr< ^t, ^u when ^u : (static member op_Implicit : ^t -> JsonNode) and ^u :> JsonNode> + (arr : ^t seq) + : JsonNode + = + let result = JsonArray () + + for a in arr do + result.Add a + + result :> JsonNode + + let normalise (d : Dictionary<'a, 'b>) : ('a * 'b) list = + d |> Seq.map (fun (KeyValue (a, b)) -> a, b) |> Seq.toList |> List.sortBy fst + + [] + let ``Can collect extension data`` () = + let str = + """{ + "message": { "header": "hi", "value": "bye" }, + "something": 3, + "arr": ["egg", "toast"], + "str": "whatnot" +}""" + |> JsonNode.Parse + + let expected = + { + Rest = + [ + "something", JsonNode.op_Implicit 3 + "arr", makeJsonArr [| "egg" ; "toast" |] + "str", JsonNode.op_Implicit "whatnot" + ] + |> dict + Message = + Some + { + Header = "hi" + Value = "bye" + } + } + + let actual = CollectRemaining.jsonParse str + + actual.Message |> shouldEqual expected.Message + + normalise actual.Rest + |> List.map (fun (k, v) -> k, v.ToJsonString ()) + |> shouldEqual (normalise expected.Rest |> List.map (fun (k, v) -> k, v.ToJsonString ())) + + [] + let ``Can write out extension data`` () = + let expected = + """{"message":{"header":"hi","value":"bye"},"something":3,"arr":["egg","toast"],"str":"whatnot"}""" + + let toWrite = + { + Rest = + [ + "something", JsonNode.op_Implicit 3 + "arr", makeJsonArr [| "egg" ; "toast" |] + "str", JsonNode.op_Implicit "whatnot" + ] + |> dict + Message = + Some + { + Header = "hi" + Value = "bye" + } + } + + let actual = CollectRemaining.toJsonNode toWrite |> fun s -> s.ToJsonString () + + actual |> shouldEqual expected + + [] + let ``Can collect extension data, nested`` () = + let str = + """{ + "thing": 99, + "baz": -123, + "remaining": { + "message": { "header": "hi", "value": "bye" }, + "something": 3, + "arr": ["egg", "toast"], + "str": "whatnot" + } +}""" + |> JsonNode.Parse + + let expected : OuterCollectRemaining = + { + Remaining = + { + Message = + Some + { + Header = "hi" + Value = "bye" + } + Rest = + [ + "something", JsonNode.op_Implicit 3 + "arr", makeJsonArr [| "egg" ; "toast" |] + "str", JsonNode.op_Implicit "whatnot" + ] + |> dict + } + Others = [ "thing", 99 ; "baz", -123 ] |> dict + } + + let actual = OuterCollectRemaining.jsonParse str + + normalise actual.Others |> shouldEqual (normalise expected.Others) + + let actual = actual.Remaining + let expected = expected.Remaining + + actual.Message |> shouldEqual expected.Message + + normalise actual.Rest + |> List.map (fun (k, v) -> k, v.ToJsonString ()) + |> shouldEqual (normalise expected.Rest |> List.map (fun (k, v) -> k, v.ToJsonString ())) + + [] + let ``Can write out extension data, nested`` () = + let expected = + """{"thing":99,"baz":-123,"remaining":{"message":{"header":"hi","value":"bye"},"something":3,"arr":["egg","toast"],"str":"whatnot"}}""" + + let toWrite : OuterCollectRemaining = + { + Others = [ "thing", 99 ; "baz", -123 ] |> dict + Remaining = + { + Rest = + [ + "something", JsonNode.op_Implicit 3 + "arr", makeJsonArr [| "egg" ; "toast" |] + "str", JsonNode.op_Implicit "whatnot" + ] + |> dict + Message = + Some + { + Header = "hi" + Value = "bye" + } + } + } + + let actual = OuterCollectRemaining.toJsonNode toWrite |> fun s -> s.ToJsonString () + + actual |> shouldEqual expected diff --git a/Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/TestPureGymJson.fs b/Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/TestPureGymJson.fs new file mode 100644 index 0000000..c571fd9 --- /dev/null +++ b/Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/TestPureGymJson.fs @@ -0,0 +1,71 @@ +namespace WoofWare.Whippet.Plugin.Json.Test + +open System +open System.Text.Json.Nodes +open NUnit.Framework +open FsUnitTyped +open PureGym + +[] +module TestPureGymJson = + + let gymOpeningHoursCases = PureGymDtos.gymOpeningHoursCases |> List.map TestCaseData + + [] + let ``GymOpeningHours JSON parse`` (json : string, expected : GymOpeningHours) = + JsonNode.Parse json |> GymOpeningHours.jsonParse |> shouldEqual expected + + let gymAccessOptionsCases = + PureGymDtos.gymAccessOptionsCases |> List.map TestCaseData + + [] + let ``GymAccessOptions JSON parse`` (json : string, expected : GymAccessOptions) = + JsonNode.Parse json |> GymAccessOptions.jsonParse |> shouldEqual expected + + let gymLocationCases = PureGymDtos.gymLocationCases |> List.map TestCaseData + + [] + let ``GymLocation JSON parse`` (json : string, expected : GymLocation) = + JsonNode.Parse json |> GymLocation.jsonParse |> shouldEqual expected + + let gymAddressCases = PureGymDtos.gymAddressCases |> List.map TestCaseData + + [] + let ``GymAddress JSON parse`` (json : string, expected : GymAddress) = + JsonNode.Parse (json, Nullable (JsonNodeOptions (PropertyNameCaseInsensitive = true))) + |> GymAddress.jsonParse + |> shouldEqual expected + + let gymCases = PureGymDtos.gymCases |> List.map TestCaseData + + [] + let ``Gym JSON parse`` (json : string, expected : Gym) = + JsonNode.Parse json |> Gym.jsonParse |> shouldEqual expected + + let memberCases = PureGymDtos.memberCases |> List.map TestCaseData + + [] + let ``Member JSON parse`` (json : string, expected : Member) = + json |> JsonNode.Parse |> Member.jsonParse |> shouldEqual expected + + let gymAttendanceCases = PureGymDtos.gymAttendanceCases |> List.map TestCaseData + + [] + let ``GymAttendance JSON parse`` (json : string, expected : GymAttendance) = + json |> JsonNode.Parse |> GymAttendance.jsonParse |> shouldEqual expected + + let memberActivityDtoCases = + PureGymDtos.memberActivityDtoCases |> List.map TestCaseData + + [] + let ``MemberActivityDto JSON parse`` (json : string, expected : MemberActivityDto) = + json |> JsonNode.Parse |> MemberActivityDto.jsonParse |> shouldEqual expected + + let sessionsCases = PureGymDtos.sessionsCases |> List.map TestCaseData + + [] + let ``Sessions JSON parse`` (json : string, expected : Sessions) = + json + |> fun o -> JsonNode.Parse (o, Nullable (JsonNodeOptions (PropertyNameCaseInsensitive = true))) + |> Sessions.jsonParse + |> shouldEqual expected diff --git a/Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/TestSurface.fs b/Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/TestSurface.fs new file mode 100644 index 0000000..d6d6698 --- /dev/null +++ b/Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/TestSurface.fs @@ -0,0 +1,26 @@ +namespace WoofWare.Whippet.Plugin.Json.Test + +open NUnit.Framework +open WoofWare.Whippet.Plugin.Json +open ApiSurface + +[] +module TestAttributeSurface = + let assembly = typeof.Assembly + + [] + let ``Ensure API surface has not been modified`` () = ApiSurface.assertIdentical assembly + + (* + [] + let ``Check version against remote`` () = + MonotonicVersion.validate assembly "WoofWare.Whippet.Plugin.Json.Attributes" + *) + + [] + let ``Update API surface`` () = + ApiSurface.writeAssemblyBaseline assembly + + [] + let ``Ensure public API is fully documented`` () = + DocCoverage.assertFullyDocumented assembly diff --git a/Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/WoofWare.Whippet.Plugin.Json.Test.fsproj b/Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/WoofWare.Whippet.Plugin.Json.Test.fsproj new file mode 100644 index 0000000..3442d47 --- /dev/null +++ b/Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/WoofWare.Whippet.Plugin.Json.Test.fsproj @@ -0,0 +1,31 @@ + + + + net8.0 + false + true + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.fsproj b/Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.fsproj new file mode 100644 index 0000000..136bbef --- /dev/null +++ b/Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.fsproj @@ -0,0 +1,40 @@ + + + + net8.0 + true + Patrick Stevens + Copyright (c) Patrick Stevens 2024 + Whippet F# source generator plugin, for generating JSON parse and serialize methods. + git + https://github.com/Smaug123/WoofWare.Whippet + MIT + README.md + fsharp;source-generator;source-gen;whippet;json;serialize;deserialize;serde + true + FS3559 + WoofWare.Whippet.Plugin.Json + true + true + NU5118 + + + + + + + + True + / + README.md + + + + + + + + + + + diff --git a/Plugins/Json/WoofWare.Whippet.Plugin.Json/version.json b/Plugins/Json/WoofWare.Whippet.Plugin.Json/version.json new file mode 100644 index 0000000..ea8d55f --- /dev/null +++ b/Plugins/Json/WoofWare.Whippet.Plugin.Json/version.json @@ -0,0 +1,14 @@ +{ + "version": "0.1", + "publicReleaseRefSpec": [ + "^refs/heads/main$" + ], + "pathFilters": [ + "./", + ":/WoofWare.Whippet.Core/", + ":/WoofWare.Whippet.Fantomas/", + ":/Plugins/Json/WoofWare.Whippet.Plugins.Json.Attributes/", + ":/global.json", + ":/Directory.Build.props" + ] +} diff --git a/README.md b/README.md index 89c2554..71fce23 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,81 @@ Whippet is a source generator for F#, inspired by [Myriad](https://github.com/MoiraeSoftware/myriad). -It is currently vapourware; please do not use it. -With some modest changes to [WoofWare.Myriad.Plugins](https://github.com/Smaug123/WoofWare.Myriad/) I was able to use Whippet to generate source files in that repo. -However, it currently lacks any of Myriad's ease of invocation, and indeed any of the future features intended to distinguish Whippet from Myriad. +It is currently vapourware; please do not use it, because its API surface and features are liable to change completely without notice. +It currently lacks most of the future features intended to distinguish Whippet from Myriad. Differentiating features: -* Whippet expands the range of information available to a source-generating plugin. Eventually we intend for it to supply type-checking information. -* Whippet will eventually support the Fantomas [Oak](https://fsprojects.github.io/fantomas/docs/end-users/GeneratingCode.html) format, rather than just a plain AST. +* Whippet expands the range of information available to a source-generating plugin. Eventually (in the far future) we intend for it to supply type-checking information. +* Whippet will eventually support the Fantomas [Oak](https://fsprojects.github.io/fantomas/docs/end-users/GeneratingCode.html) format, rather than just a plain AST. It already does support this, in the sense that the only interface to Whippet is "we give you bytes, you give us text", so you're free to use an Oak already; but we give you no help with this. * Whippet is intended to be more modular, providing various different helper assemblies a plugin author can optionally use depending on what features they want. + +## How to use + +The simplest invocation is as follows. + +### Import the source generator framework + +In your `.fsproj`, take the following `PackageReference`, setting the `Version=""` to the latest version available on NuGet. + +```xml + +``` + +(`PrivateAssets="all"` is necessary to prevent the Whippet dependency from flowing to the consumers of your package.) + +### Import the plugin you wish to call + +```xml + +``` + +Note the important `WhippetPlugin="true"` which is how Whippet determines which packages to search for plugins, +and the `PrivateAssets="all"` again to prevent this dependency from flowing to your consumers. + +### Configure the source generator + +The simplest possible configuration is as follows: + +```xml + + + + Args.fs + + +``` + +Here, you wrote the `Args.fs` file, and have specified that the `GeneratedArgs.fs` file is to be generated using `Args.fs` +as input. + +### Advanced configuration of source generators + +A plugin author may choose to have their plugin be configurable, by recognising parameters passed through the fsproj. + +```xml + + + + swagger-gitea.json + true + Gitea + + +``` + +Any key prefixed with `WhippetParam` will have that prefix removed and the string value passed in to the generator +through the `Parameters` field on the plugin's args. +(MSBuild only allows strings here, so the `"true"` in the above example is a string, not a boolean. +If you want more advanced inputs to your plugin, you will have to create a parser yourself.) + +## Standalone tool + +The standalone tool takes the following arguments: + +* A path to an `fsproj` file. +* (positional args) A list of DLLs from which to load plugins. (Currently I strongly recommend only using one plugin per fsproj file; it's completely untested to use more!) + +The tool uses MSBuild to load the fsproj file to discover what files need to be generated. +(This duplicates a bunch of work, because you're presumably executing the tool during a build anyway!) +The tool then loads the plugins, and reflectively determines which source generators contained therein should run on each file. diff --git a/WoofWare.Whippet.App/AppContext.fs b/WoofWare.Whippet.App/AppContext.fs new file mode 100644 index 0000000..cac6a6b --- /dev/null +++ b/WoofWare.Whippet.App/AppContext.fs @@ -0,0 +1,33 @@ +namespace WoofWare.Whippet + +open System +open System.IO +open System.Reflection + +// Fix for https://github.com/Smaug123/unofficial-nunit-runner/issues/8 +// (This tells the DLL loader to look next to the input DLL for dependencies.) +/// Context manager to set the AppContext.BaseDirectory of the executing DLL. +type SetBaseDir (testDll : FileInfo) = + let oldBaseDir = AppContext.BaseDirectory + + let setData = + let appContext = Type.GetType "System.AppContext" + + if Object.ReferenceEquals (appContext, (null : obj)) then + ignore + else + + let setDataMethod = + appContext.GetMethod ("SetData", BindingFlags.Static ||| BindingFlags.Public) + + if Object.ReferenceEquals (setDataMethod, (null : obj)) then + ignore + else + + fun (k, v) -> setDataMethod.Invoke ((null : obj), [| k ; v |]) |> unbox + + do setData ("APP_CONTEXT_BASE_DIRECTORY", testDll.Directory.FullName) + + interface IDisposable with + member _.Dispose () = + setData ("APP_CONTEXT_BASE_DIRECTORY", oldBaseDir) diff --git a/WoofWare.Whippet.App/Context.fs b/WoofWare.Whippet.App/Context.fs new file mode 100644 index 0000000..9d1d0a0 --- /dev/null +++ b/WoofWare.Whippet.App/Context.fs @@ -0,0 +1,26 @@ +namespace WoofWare.Whippet + +open System.IO +open System.Reflection +open System.Runtime.Loader + +type Ctx (dll : FileInfo, runtimes : DirectoryInfo list) = + inherit AssemblyLoadContext () + + override this.Load (target : AssemblyName) : Assembly = + let path = Path.Combine (dll.Directory.FullName, $"%s{target.Name}.dll") + + if File.Exists path then + this.LoadFromAssemblyPath path + else + + runtimes + |> List.tryPick (fun di -> + let path = Path.Combine (di.FullName, $"%s{target.Name}.dll") + + if File.Exists path then + this.LoadFromAssemblyPath path |> Some + else + None + ) + |> Option.defaultValue null diff --git a/WoofWare.Whippet/Program.fs b/WoofWare.Whippet.App/Program.fs similarity index 71% rename from WoofWare.Whippet/Program.fs rename to WoofWare.Whippet.App/Program.fs index a1139e5..6698b7a 100644 --- a/WoofWare.Whippet/Program.fs +++ b/WoofWare.Whippet.App/Program.fs @@ -10,27 +10,27 @@ open WoofWare.Whippet.Core type Args = { - PluginDll : FileInfo InputFile : FileInfo + Plugins : FileInfo list } type WhippetTarget = { InputSource : FileInfo GeneratedDest : FileInfo + Params : Map } module Program = let parseArgs (argv : string array) = let inputFile = argv.[0] |> FileInfo - let pluginDll = argv.[1] |> FileInfo { InputFile = inputFile - PluginDll = pluginDll + Plugins = argv.[1..] |> Seq.map FileInfo |> Seq.toList } - let getGenerateRawFromRaw (host : obj) : (RawSourceGenerationArgs -> string) option = + let getGenerateRawFromRaw (host : obj) : (RawSourceGenerationArgs -> string option) option = let pluginType = host.GetType () let generateRawFromRaw = @@ -71,7 +71,14 @@ module Program = failwith $"Expected GenerateRawFromRaw method to have return type `string`, but was: %s{retType.FullName}" - fun args -> generateRawFromRaw.Invoke (host, [| args |]) |> unbox + fun args -> + let args = + Activator.CreateInstance ( + pars.[0].ParameterType, + [| box args.FilePath ; box args.FileContents ; box args.Parameters |] + ) + + generateRawFromRaw.Invoke (host, [| args |]) |> unbox |> Option.ofObj |> Some [] @@ -113,17 +120,50 @@ module Program = | None -> None | Some myriadFile -> + let pars = + metadata + |> Map.toSeq + |> Seq.choose (fun (key, value) -> + if key.StartsWith ("WhippetParam", StringComparison.Ordinal) then + Some (key.Substring "WhippetParam".Length, value) + else + None + ) + |> Map.ofSeq + + let inputSource = + FileInfo (Path.Combine (Path.GetDirectoryName desiredProject.ProjectFileName, myriadFile)) + + let generatedDest = FileInfo fullPath + + if inputSource.FullName = generatedDest.FullName then + failwith $"Input source %s{inputSource.FullName} was identical to output path; aborting." + { - GeneratedDest = FileInfo fullPath - InputSource = - FileInfo (Path.Combine (Path.GetDirectoryName desiredProject.ProjectFileName, myriadFile)) + GeneratedDest = generatedDest + InputSource = inputSource + Params = pars } |> Some ) - Console.Error.WriteLine $"Loading plugin: %s{args.PluginDll.FullName}" + let runtime = + DotnetRuntime.locate (Assembly.GetExecutingAssembly().Location |> FileInfo) - let pluginAssembly = Assembly.LoadFrom args.PluginDll.FullName + let pluginDll = + match args.Plugins with + | [] -> failwith "must supply a plugin!" + | [ plugin ] -> plugin + | _ -> failwith "We don't yet support running more than one Whippet plugin in a given project file" + + // TODO: should ideally loop over files, not plugins, so we fully generate a file before moving on to the next + // one + + Console.Error.WriteLine $"Loading plugin: %s{pluginDll.FullName}" + + let ctx = Ctx (pluginDll, runtime) + + let pluginAssembly = ctx.LoadFromAssemblyPath pluginDll.FullName // We will look up any member called GenerateRawFromRaw and/or GenerateFromRaw. // It's your responsibility to decide whether to do anything with this call; you return null if you don't want @@ -159,13 +199,15 @@ module Program = { RawSourceGenerationArgs.FilePath = item.InputSource.FullName FileContents = fileContents + Parameters = item.Params } let result = generateRawFromRaw args match result with - | null -> () - | result -> + | None + | Some null -> () + | Some result -> Console.Error.WriteLine $"Writing output for generator %s{plugin.Name} to file %s{item.GeneratedDest.FullName}" diff --git a/WoofWare.Whippet.App/RuntimeConfig.fs b/WoofWare.Whippet.App/RuntimeConfig.fs new file mode 100644 index 0000000..2fbdbfc --- /dev/null +++ b/WoofWare.Whippet.App/RuntimeConfig.fs @@ -0,0 +1,47 @@ +namespace WoofWare.Whippet + +open System + +type FrameworkDescription = + { + Name : string + Version : string + } + +type RuntimeOptions = + { + Tfm : string + Framework : FrameworkDescription option + Frameworks : FrameworkDescription list option + RollForward : string option + } + +type RuntimeConfig = + { + RuntimeOptions : RuntimeOptions + } + +[] +type RollForward = + | Minor + | Major + | LatestPatch + | LatestMinor + | LatestMajor + | Disable + + static member Parse (s : string) : RollForward = + if s.Equals ("minor", StringComparison.OrdinalIgnoreCase) then + RollForward.Minor + elif s.Equals ("major", StringComparison.OrdinalIgnoreCase) then + RollForward.Major + elif s.Equals ("latestpatch", StringComparison.OrdinalIgnoreCase) then + RollForward.LatestPatch + elif s.Equals ("latestminor", StringComparison.OrdinalIgnoreCase) then + RollForward.LatestMinor + elif s.Equals ("latestmajor", StringComparison.OrdinalIgnoreCase) then + RollForward.LatestMajor + elif s.Equals ("disable", StringComparison.OrdinalIgnoreCase) then + RollForward.Disable + else + failwith $"Could not interpret '%s{s}' as a RollForward" diff --git a/WoofWare.Whippet.App/RuntimeConfigGen.fs b/WoofWare.Whippet.App/RuntimeConfigGen.fs new file mode 100644 index 0000000..047c288 --- /dev/null +++ b/WoofWare.Whippet.App/RuntimeConfigGen.fs @@ -0,0 +1,103 @@ +namespace WoofWare.Whippet + +(* File originally generated by Myriad. *) + +/// Module containing JSON parsing methods for the FrameworkDescription type +[] +module FrameworkDescription = + /// Parse from a JSON node. + let jsonParse (node : System.Text.Json.Nodes.JsonNode) : FrameworkDescription = + let arg_1 = + (match node.["version"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("version") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_0 = + (match node.["name"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("name") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + { + Name = arg_0 + Version = arg_1 + } +namespace WoofWare.Whippet + +/// Module containing JSON parsing methods for the RuntimeOptions type +[] +module RuntimeOptions = + /// Parse from a JSON node. + let jsonParse (node : System.Text.Json.Nodes.JsonNode) : RuntimeOptions = + let arg_3 = + match node.["rollForward"] with + | null -> None + | v -> v.AsValue().GetValue () |> Some + + let arg_2 = + match node.["frameworks"] with + | null -> None + | v -> + v.AsArray () + |> Seq.map (fun elt -> FrameworkDescription.jsonParse elt) + |> List.ofSeq + |> Some + + let arg_1 = + match node.["framework"] with + | null -> None + | v -> FrameworkDescription.jsonParse v |> Some + + let arg_0 = + (match node.["tfm"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("tfm") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + { + Tfm = arg_0 + Framework = arg_1 + Frameworks = arg_2 + RollForward = arg_3 + } +namespace WoofWare.Whippet + +/// Module containing JSON parsing methods for the RuntimeConfig type +[] +module RuntimeConfig = + /// Parse from a JSON node. + let jsonParse (node : System.Text.Json.Nodes.JsonNode) : RuntimeConfig = + let arg_0 = + RuntimeOptions.jsonParse ( + match node.["runtimeOptions"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("runtimeOptions") + ) + ) + | v -> v + ) + + { + RuntimeOptions = arg_0 + } diff --git a/WoofWare.Whippet.App/RuntimeLocator.fs b/WoofWare.Whippet.App/RuntimeLocator.fs new file mode 100644 index 0000000..2b39cfd --- /dev/null +++ b/WoofWare.Whippet.App/RuntimeLocator.fs @@ -0,0 +1,104 @@ +namespace WoofWare.Whippet + +open System +open System.IO +open WoofWare.DotnetRuntimeLocator + +/// Functions for locating .NET runtimes. +[] +module DotnetRuntime = + let private selectRuntime + (config : RuntimeOptions) + (f : DotnetEnvironmentInfo) + : Choice option + = + let rollForward = + match Environment.GetEnvironmentVariable "DOTNET_ROLL_FORWARD" with + | null -> + config.RollForward + |> Option.map RollForward.Parse + |> Option.defaultValue RollForward.Minor + | s -> RollForward.Parse s + + let desiredVersions = + match config.Framework with + | Some f -> [ Version f.Version, f.Name ] + | None -> + + match config.Frameworks with + | Some f -> f |> List.map (fun f -> Version f.Version, f.Name) + | None -> + failwith + "Could not deduce a framework version due to lack of either Framework or Frameworks in runtimeconfig" + + let compatiblyNamedRuntimes = + f.Frameworks + |> Seq.collect (fun availableFramework -> + desiredVersions + |> List.choose (fun (desiredVersion, desiredName) -> + if desiredName = availableFramework.Name then + Some + {| + Desired = desiredVersion + Name = desiredName + Installed = availableFramework + InstalledVersion = Version availableFramework.Version + |} + else + None + ) + ) + |> Seq.toList + + match rollForward with + | RollForward.Minor -> + let available = + compatiblyNamedRuntimes + |> Seq.filter (fun data -> + data.InstalledVersion.Major = data.Desired.Major + && data.InstalledVersion.Minor >= data.Desired.Minor + ) + |> Seq.groupBy (fun data -> data.Name) + |> Seq.map (fun (name, data) -> + let data = + data + |> Seq.minBy (fun data -> data.InstalledVersion.Minor, data.InstalledVersion.Build) + + name, data.Installed + ) + // TODO: how do we select between many available frameworks? + |> Seq.tryHead + + match available with + | Some (_, f) -> Some (Choice1Of2 f) + | None -> + // TODO: maybe we can ask the SDK. But we keep on trucking: maybe we're self-contained, + // and we'll actually find all the runtime next to the DLL. + None + | _ -> failwith "non-minor RollForward not supported yet; please shout if you want it" + + /// Given an executable DLL, locate the .NET runtime that can best run it. + let locate (dll : FileInfo) : DirectoryInfo list = + let runtimeConfig = + let name = + if not (dll.Name.EndsWith (".dll", StringComparison.OrdinalIgnoreCase)) then + failwith $"Expected DLL %s{dll.FullName} to end in .dll" + + dll.Name.Substring (0, dll.Name.Length - 4) + + Path.Combine (dll.Directory.FullName, $"%s{name}.runtimeconfig.json") + |> File.ReadAllText + |> System.Text.Json.Nodes.JsonNode.Parse + |> RuntimeConfig.jsonParse + |> fun f -> f.RuntimeOptions + + let availableRuntimes = DotnetEnvironmentInfo.Get () + + let runtime = selectRuntime runtimeConfig availableRuntimes + + match runtime with + | None -> + // Keep on trucking: let's be optimistic and hope that we're self-contained. + [ dll.Directory ] + | Some (Choice1Of2 runtime) -> [ dll.Directory ; DirectoryInfo $"%s{runtime.Path}/%s{runtime.Version}" ] + | Some (Choice2Of2 sdk) -> [ dll.Directory ; DirectoryInfo sdk.Path ] diff --git a/WoofWare.Whippet.App/WoofWare.Whippet.App.fsproj b/WoofWare.Whippet.App/WoofWare.Whippet.App.fsproj new file mode 100644 index 0000000..d642a3e --- /dev/null +++ b/WoofWare.Whippet.App/WoofWare.Whippet.App.fsproj @@ -0,0 +1,27 @@ + + + + Exe + net8.0 + true + + + + + + + + + + + + + + + + + + + + + diff --git a/WoofWare.Whippet.Core/Domain.fs b/WoofWare.Whippet.Core/Domain.fs index 90da65d..e629ada 100644 --- a/WoofWare.Whippet.Core/Domain.fs +++ b/WoofWare.Whippet.Core/Domain.fs @@ -1,5 +1,7 @@ namespace WoofWare.Whippet.Core +open System.Collections.Generic + (* These types should take no dependencies and should only change additively; otherwise consumers will break! *) @@ -18,6 +20,8 @@ type RawSourceGenerationArgs = FilePath : string /// Contents of the file; you might want to `System.Text.Encoding.UTF8.GetString` this. FileContents : byte[] + /// Extra parameters as supplied through the project file with {ParamValue}. + Parameters : IReadOnlyDictionary } /// We provide this interface as a helper to give you compile-time safety, but you don't have to use it. diff --git a/WoofWare.Whippet.Core/SurfaceBaseline.txt b/WoofWare.Whippet.Core/SurfaceBaseline.txt index e3ee1b6..9ac5513 100644 --- a/WoofWare.Whippet.Core/SurfaceBaseline.txt +++ b/WoofWare.Whippet.Core/SurfaceBaseline.txt @@ -1,11 +1,13 @@ WoofWare.Whippet.Core.IGenerateRawFromRaw - interface with 1 member(s) WoofWare.Whippet.Core.IGenerateRawFromRaw.GenerateRawFromRaw [method]: WoofWare.Whippet.Core.RawSourceGenerationArgs -> string -WoofWare.Whippet.Core.RawSourceGenerationArgs inherit obj, implements WoofWare.Whippet.Core.RawSourceGenerationArgs System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.Whippet.Core.RawSourceGenerationArgs System.IComparable, System.IComparable, System.Collections.IStructuralComparable -WoofWare.Whippet.Core.RawSourceGenerationArgs..ctor [constructor]: (string, System.Byte []) +WoofWare.Whippet.Core.RawSourceGenerationArgs inherit obj, implements WoofWare.Whippet.Core.RawSourceGenerationArgs System.IEquatable, System.Collections.IStructuralEquatable +WoofWare.Whippet.Core.RawSourceGenerationArgs..ctor [constructor]: (string, System.Byte [], System.Collections.Generic.IReadOnlyDictionary) WoofWare.Whippet.Core.RawSourceGenerationArgs.Equals [method]: (WoofWare.Whippet.Core.RawSourceGenerationArgs, System.Collections.IEqualityComparer) -> bool WoofWare.Whippet.Core.RawSourceGenerationArgs.FileContents [property]: [read-only] System.Byte [] WoofWare.Whippet.Core.RawSourceGenerationArgs.FilePath [property]: [read-only] string WoofWare.Whippet.Core.RawSourceGenerationArgs.get_FileContents [method]: unit -> System.Byte [] WoofWare.Whippet.Core.RawSourceGenerationArgs.get_FilePath [method]: unit -> string +WoofWare.Whippet.Core.RawSourceGenerationArgs.get_Parameters [method]: unit -> System.Collections.Generic.IReadOnlyDictionary +WoofWare.Whippet.Core.RawSourceGenerationArgs.Parameters [property]: [read-only] System.Collections.Generic.IReadOnlyDictionary WoofWare.Whippet.Core.WhippetGeneratorAttribute inherit System.Attribute WoofWare.Whippet.Core.WhippetGeneratorAttribute..ctor [constructor]: unit \ No newline at end of file diff --git a/WoofWare.Whippet.Fantomas/Ast.fs b/WoofWare.Whippet.Fantomas/Ast.fs index 2afa744..f7870ce 100644 --- a/WoofWare.Whippet.Fantomas/Ast.fs +++ b/WoofWare.Whippet.Fantomas/Ast.fs @@ -45,7 +45,8 @@ module Ast = let cfg = FormatConfig.Default - CodeFormatter.FormatASTAsync (parseTree, cfg) |> Async.RunSynchronously |> Some + let output = CodeFormatter.FormatASTAsync (parseTree, cfg) |> Async.RunSynchronously + Some output /// For each namespace in the AST, returns the types defined therein. let getTypes (ast : ParsedInput) : (LongIdent * SynTypeDefn list) list = diff --git a/WoofWare.Whippet.Fantomas/Measure.fs b/WoofWare.Whippet.Fantomas/Measure.fs new file mode 100644 index 0000000..9e2762a --- /dev/null +++ b/WoofWare.Whippet.Fantomas/Measure.fs @@ -0,0 +1,26 @@ +namespace WoofWare.Whippet.Fantomas + +open Fantomas.FCS.Syntax + +/// Methods for manipulating units of measure. +[] +module Measure = + + /// Get the function that adds an arbitrary measure to the given fully-qualified type. + /// For example, ["System" ; "Single"] would result in `LanguagePrimitives.Float32WithMeasure`. + let getLanguagePrimitivesMeasure (typeName : LongIdent) : SynExpr = + match typeName |> List.map _.idText with + | [ "System" ; "Single" ] -> [ "LanguagePrimitives" ; "Float32WithMeasure" ] + | [ "System" ; "Double" ] -> [ "LanguagePrimitives" ; "FloatWithMeasure" ] + | [ "System" ; "Byte" ] -> [ "LanguagePrimitives" ; "ByteWithMeasure" ] + | [ "System" ; "SByte" ] -> [ "LanguagePrimitives" ; "SByteWithMeasure" ] + | [ "System" ; "Int16" ] -> [ "LanguagePrimitives" ; "Int16WithMeasure" ] + | [ "System" ; "Int32" ] -> [ "LanguagePrimitives" ; "Int32WithMeasure" ] + | [ "System" ; "Int64" ] -> [ "LanguagePrimitives" ; "Int64WithMeasure" ] + | [ "System" ; "UInt16" ] -> [ "LanguagePrimitives" ; "UInt16WithMeasure" ] + | [ "System" ; "UInt32" ] -> [ "LanguagePrimitives" ; "UInt32WithMeasure" ] + | [ "System" ; "UInt64" ] -> [ "LanguagePrimitives" ; "UInt64WithMeasure" ] + | l -> + let l = String.concat "." l + failwith $"unrecognised type for measure: %s{l}" + |> SynExpr.createLongIdent diff --git a/WoofWare.Whippet.Fantomas/SurfaceBaseline.txt b/WoofWare.Whippet.Fantomas/SurfaceBaseline.txt index 42e6fc5..a80779e 100644 --- a/WoofWare.Whippet.Fantomas/SurfaceBaseline.txt +++ b/WoofWare.Whippet.Fantomas/SurfaceBaseline.txt @@ -66,6 +66,8 @@ WoofWare.Whippet.Fantomas.InterfaceType.Inherits [property]: [read-only] Fantoma WoofWare.Whippet.Fantomas.InterfaceType.Members [property]: [read-only] WoofWare.Whippet.Fantomas.MemberInfo list WoofWare.Whippet.Fantomas.InterfaceType.Name [property]: [read-only] Fantomas.FCS.Syntax.Ident list WoofWare.Whippet.Fantomas.InterfaceType.Properties [property]: [read-only] WoofWare.Whippet.Fantomas.PropertyInfo list +WoofWare.Whippet.Fantomas.Measure inherit obj +WoofWare.Whippet.Fantomas.Measure.getLanguagePrimitivesMeasure [static method]: Fantomas.FCS.Syntax.Ident list -> Fantomas.FCS.Syntax.SynExpr WoofWare.Whippet.Fantomas.MemberInfo inherit obj WoofWare.Whippet.Fantomas.MemberInfo..ctor [constructor]: (Fantomas.FCS.Syntax.SynType, Fantomas.FCS.Syntax.SynAccess option, WoofWare.Whippet.Fantomas.TupledArg list, Fantomas.FCS.Syntax.Ident, Fantomas.FCS.Syntax.SynAttribute list, Fantomas.FCS.Xml.PreXmlDoc option, bool, bool) WoofWare.Whippet.Fantomas.MemberInfo.Accessibility [property]: [read-only] Fantomas.FCS.Syntax.SynAccess option diff --git a/WoofWare.Whippet.Fantomas/WoofWare.Whippet.Fantomas.fsproj b/WoofWare.Whippet.Fantomas/WoofWare.Whippet.Fantomas.fsproj index 249de32..3f4460c 100644 --- a/WoofWare.Whippet.Fantomas/WoofWare.Whippet.Fantomas.fsproj +++ b/WoofWare.Whippet.Fantomas/WoofWare.Whippet.Fantomas.fsproj @@ -48,6 +48,7 @@ + True / diff --git a/WoofWare.Whippet.Fantomas/version.json b/WoofWare.Whippet.Fantomas/version.json index 9731be2..1be86b5 100644 --- a/WoofWare.Whippet.Fantomas/version.json +++ b/WoofWare.Whippet.Fantomas/version.json @@ -1,5 +1,5 @@ { - "version": "0.2", + "version": "0.3", "publicReleaseRefSpec": [ "^refs/heads/main$" ], diff --git a/WoofWare.Whippet.sln b/WoofWare.Whippet.sln index 5cecdae..7e98ac1 100644 --- a/WoofWare.Whippet.sln +++ b/WoofWare.Whippet.sln @@ -10,6 +10,24 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WoofWare.Whippet.Fantomas", EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WoofWare.Whippet.Fantomas.Test", "WoofWare.Whippet.Fantomas.Test\WoofWare.Whippet.Fantomas.Test.fsproj", "{E220B17E-D608-43CB-B117-329BA240B13B}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WoofWare.Whippet.Plugin.ArgParser", "Plugins\ArgParser\WoofWare.Whippet.Plugin.ArgParser\WoofWare.Whippet.Plugin.ArgParser.fsproj", "{C8165033-31E4-43A1-AE30-D2F2B1217374}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WoofWare.Whippet.Plugin.ArgParser.Attributes", "Plugins\ArgParser\WoofWare.Whippet.Plugin.ArgParser.Attributes\WoofWare.Whippet.Plugin.ArgParser.Attributes.fsproj", "{6AD1F2B6-4E91-4587-AF93-7EABAE9D3203}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WoofWare.Whippet.Plugin.ArgParser.Consumer", "Plugins\ArgParser\WoofWare.Whippet.Plugin.ArgParser.Consumer\WoofWare.Whippet.Plugin.ArgParser.Consumer.fsproj", "{7CD49A84-0B37-4CCC-BE5D-05BE828B5B97}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WoofWare.Whippet.Plugin.ArgParser.Test", "Plugins\ArgParser\WoofWare.Whippet.Plugin.ArgParser\WoofWare.Whippet.Plugin.ArgParser.Test\WoofWare.Whippet.Plugin.ArgParser.Test.fsproj", "{DFF9FCEE-CD1A-450E-A71D-BD0CD267D5B4}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WoofWare.Whippet.Plugin.Json", "Plugins\Json\WoofWare.Whippet.Plugin.Json\WoofWare.Whippet.Plugin.Json.fsproj", "{8164E85B-3E7F-4F0B-8E25-CFA67E189668}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WoofWare.Whippet.Plugin.Json.Consumer", "Plugins\Json\WoofWare.Whippet.Plugin.Json.Consumer\WoofWare.Whippet.Plugin.Json.Consumer.fsproj", "{9DE5BA14-F8F8-4E3F-8C32-2A3439A906AC}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WoofWare.Whippet.Plugin.Json.Test", "Plugins\Json\WoofWare.Whippet.Plugin.Json\WoofWare.Whippet.Plugin.Json.Test\WoofWare.Whippet.Plugin.Json.Test.fsproj", "{0BBF5FEB-0BC6-4EDA-94E8-44DCDFDBB1CF}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WoofWare.Whippet.Plugin.Json.Attributes", "Plugins\Json\WoofWare.Whippet.Plugin.Json.Attributes\WoofWare.Whippet.Plugin.Json.Attributes.fsproj", "{649938B1-9993-4422-A9D9-5075323833E3}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WoofWare.Whippet.App", "WoofWare.Whippet.App\WoofWare.Whippet.App.fsproj", "{A2258153-1C1F-4B25-B49A-BCC8EA4A3278}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -36,5 +54,41 @@ Global {E220B17E-D608-43CB-B117-329BA240B13B}.Debug|Any CPU.Build.0 = Debug|Any CPU {E220B17E-D608-43CB-B117-329BA240B13B}.Release|Any CPU.ActiveCfg = Release|Any CPU {E220B17E-D608-43CB-B117-329BA240B13B}.Release|Any CPU.Build.0 = Release|Any CPU + {C8165033-31E4-43A1-AE30-D2F2B1217374}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C8165033-31E4-43A1-AE30-D2F2B1217374}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C8165033-31E4-43A1-AE30-D2F2B1217374}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C8165033-31E4-43A1-AE30-D2F2B1217374}.Release|Any CPU.Build.0 = Release|Any CPU + {6AD1F2B6-4E91-4587-AF93-7EABAE9D3203}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6AD1F2B6-4E91-4587-AF93-7EABAE9D3203}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6AD1F2B6-4E91-4587-AF93-7EABAE9D3203}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6AD1F2B6-4E91-4587-AF93-7EABAE9D3203}.Release|Any CPU.Build.0 = Release|Any CPU + {7CD49A84-0B37-4CCC-BE5D-05BE828B5B97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7CD49A84-0B37-4CCC-BE5D-05BE828B5B97}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7CD49A84-0B37-4CCC-BE5D-05BE828B5B97}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7CD49A84-0B37-4CCC-BE5D-05BE828B5B97}.Release|Any CPU.Build.0 = Release|Any CPU + {DFF9FCEE-CD1A-450E-A71D-BD0CD267D5B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DFF9FCEE-CD1A-450E-A71D-BD0CD267D5B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DFF9FCEE-CD1A-450E-A71D-BD0CD267D5B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DFF9FCEE-CD1A-450E-A71D-BD0CD267D5B4}.Release|Any CPU.Build.0 = Release|Any CPU + {8164E85B-3E7F-4F0B-8E25-CFA67E189668}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8164E85B-3E7F-4F0B-8E25-CFA67E189668}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8164E85B-3E7F-4F0B-8E25-CFA67E189668}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8164E85B-3E7F-4F0B-8E25-CFA67E189668}.Release|Any CPU.Build.0 = Release|Any CPU + {9DE5BA14-F8F8-4E3F-8C32-2A3439A906AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9DE5BA14-F8F8-4E3F-8C32-2A3439A906AC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9DE5BA14-F8F8-4E3F-8C32-2A3439A906AC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9DE5BA14-F8F8-4E3F-8C32-2A3439A906AC}.Release|Any CPU.Build.0 = Release|Any CPU + {0BBF5FEB-0BC6-4EDA-94E8-44DCDFDBB1CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0BBF5FEB-0BC6-4EDA-94E8-44DCDFDBB1CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0BBF5FEB-0BC6-4EDA-94E8-44DCDFDBB1CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0BBF5FEB-0BC6-4EDA-94E8-44DCDFDBB1CF}.Release|Any CPU.Build.0 = Release|Any CPU + {649938B1-9993-4422-A9D9-5075323833E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {649938B1-9993-4422-A9D9-5075323833E3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {649938B1-9993-4422-A9D9-5075323833E3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {649938B1-9993-4422-A9D9-5075323833E3}.Release|Any CPU.Build.0 = Release|Any CPU + {A2258153-1C1F-4B25-B49A-BCC8EA4A3278}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2258153-1C1F-4B25-B49A-BCC8EA4A3278}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2258153-1C1F-4B25-B49A-BCC8EA4A3278}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2258153-1C1F-4B25-B49A-BCC8EA4A3278}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/WoofWare.Whippet/WoofWare.Whippet.fsproj b/WoofWare.Whippet/WoofWare.Whippet.fsproj index 3ae6bfc..dcb5b21 100644 --- a/WoofWare.Whippet/WoofWare.Whippet.fsproj +++ b/WoofWare.Whippet/WoofWare.Whippet.fsproj @@ -1,38 +1,44 @@ - Exe - net8.0 - Patrick Stevens - Copyright (c) Patrick Stevens 2024 - A source generator for F#. - git - https://github.com/Smaug123/WoofWare.Whippet - MIT - README.md - fsharp;source-generator;source-gen - true - FS3559 - WoofWare.Whippet + Library + net8.0 + Patrick Stevens + Copyright (c) Patrick Stevens 2024 + A source generator for F#. + git + https://github.com/Smaug123/WoofWare.Whippet + MIT + README.md + fsharp;source-generator;source-gen + true + FS3559 + WoofWare.Whippet + true + NU5118 + true - True \ + + + - - - - - - - - - + + + Build;_CopyFilesMarkedCopyLocal + + + + + + + diff --git a/WoofWare.Whippet/build/WoofWare.Whippet.targets b/WoofWare.Whippet/build/WoofWare.Whippet.targets new file mode 100644 index 0000000..51919fd --- /dev/null +++ b/WoofWare.Whippet/build/WoofWare.Whippet.targets @@ -0,0 +1,49 @@ + + + + + + all + + + + + + $(MSBuildThisFileDirectory)../tools/net8.0/any/WoofWare.Whippet.App.dll + + + + + + + %(ReferencePath.NuGetPackageId) + + + + $([System.IO.Path]::GetFileNameWithoutExtension('%(ReferencePath.Filename)')) + + + + + %(WhippetPlugin.FullPath) + + + + + + @(WhippetPluginArgs->'"%(Path)"', ' ') + + + + + +