diff --git a/.github/workflows/dotnet.yaml b/.github/workflows/dotnet.yaml index 86ef06f..e7e1da5 100644 --- a/.github/workflows/dotnet.yaml +++ b/.github/workflows/dotnet.yaml @@ -180,6 +180,16 @@ jobs: with: name: nuget-package-argparser path: Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/bin/Release/WoofWare.Whippet.Plugin.ArgParser.*.nupkg + - name: Upload NuGet artifact (httpclient attrs) + uses: actions/upload-artifact@v4 + with: + name: nuget-package-httpclient-attrs + path: Plugins/ArgParser/WoofWare.Whippet.Plugin.HttpClient.Attributes/bin/Release/WoofWare.Whippet.Plugin.HttpClient.*.nupkg + - name: Upload NuGet artifact (httpclient plugin) + uses: actions/upload-artifact@v4 + with: + name: nuget-package-httpclient + path: Plugins/ArgParser/WoofWare.Whippet.Plugin.HttpClient/bin/Release/WoofWare.Whippet.Plugin.HttpClient.*.nupkg expected-pack: needs: [nuget-pack] @@ -473,3 +483,69 @@ jobs: nuget-key: ${{ secrets.NUGET_API_KEY }} nupkg-dir: packed/ dotnet: ${{ steps.dotnet-identify.outputs.dotnet }} + + nuget-publish-httpclient-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-httpclient + 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.HttpClient + nuget-key: ${{ secrets.NUGET_API_KEY }} + nupkg-dir: packed/ + dotnet: ${{ steps.dotnet-identify.outputs.dotnet }} + + nuget-publish-httpclient-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-httpclient-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.HttpClient.Attributes + nuget-key: ${{ secrets.NUGET_API_KEY }} + nupkg-dir: packed/ + dotnet: ${{ steps.dotnet-identify.outputs.dotnet }} 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 index 63b263b..143a584 100644 --- a/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/WoofWare.Whippet.Plugin.ArgParser.fsproj +++ b/Plugins/ArgParser/WoofWare.Whippet.Plugin.ArgParser/WoofWare.Whippet.Plugin.ArgParser.fsproj @@ -1,7 +1,7 @@  - net8.0 + netstandard2.1 true Patrick Stevens Copyright (c) Patrick Stevens 2024 diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Attributes/Attributes.fs b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Attributes/Attributes.fs new file mode 100644 index 0000000..527e231 --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Attributes/Attributes.fs @@ -0,0 +1,16 @@ +namespace WoofWare.Whippet.Plugin.HttpClient + +/// Attribute indicating a record type to which the "create HTTP client" Whippet +/// generator should apply during build. +/// This generator is intended to replicate much of the functionality of RestEase, +/// i.e. to stamp out HTTP REST clients from interfaces defining the API. +/// +/// If you supply isExtensionMethod = false, you will get a genuine module (which can +/// be consumed from C#) rather than extension methods. +type HttpClientAttribute (isExtensionMethod : bool) = + inherit System.Attribute () + /// The default value of `isExtensionMethod`, the optional argument to the HttpClientAttribute constructor. + static member DefaultIsExtensionMethod = true + + /// Shorthand for the "isExtensionMethod = false" constructor; see documentation there for details. + new () = HttpClientAttribute HttpClientAttribute.DefaultIsExtensionMethod diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Attributes/README.md b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Attributes/README.md new file mode 100644 index 0000000..3a4211b --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Attributes/README.md @@ -0,0 +1,6 @@ +# WoofWare.Whippet.Plugin.HttpClient.Attributes + +This is a very slim runtime dependency which consumers of WoofWare.Whippet.Plugin.HttpClient 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.HttpClient's README for further information. diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Attributes/RestEase.fs b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Attributes/RestEase.fs new file mode 100644 index 0000000..15f9302 --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Attributes/RestEase.fs @@ -0,0 +1,88 @@ +namespace WoofWare.Whippet.Plugin.HttpClient + +open System + +/// Module containing duplicates of the supported RestEase attributes, in case you don't want +/// to take a dependency on RestEase. +module RestEase = + /// Indicates that a method represents an HTTP Get query to the specified endpoint. + type GetAttribute (path : string) = + inherit Attribute () + + /// Indicates that a method represents an HTTP Post query to the specified endpoint. + type PostAttribute (path : string) = + inherit Attribute () + + /// Indicates that a method represents an HTTP Delete query to the specified endpoint. + type DeleteAttribute (path : string) = + inherit Attribute () + + /// Indicates that a method represents an HTTP Head query to the specified endpoint. + type HeadAttribute (path : string) = + inherit Attribute () + + /// Indicates that a method represents an HTTP Options query to the specified endpoint. + type OptionsAttribute (path : string) = + inherit Attribute () + + /// Indicates that a method represents an HTTP Put query to the specified endpoint. + type PutAttribute (path : string) = + inherit Attribute () + + /// Indicates that a method represents an HTTP Patch query to the specified endpoint. + type PatchAttribute (path : string) = + inherit Attribute () + + /// Indicates that a method represents an HTTP Trace query to the specified endpoint. + type TraceAttribute (path : string) = + inherit Attribute () + + /// Indicates that this argument to a method is interpolated into the HTTP request at runtime + /// by setting a query parameter (with the given name) to the value of the annotated argument. + type QueryAttribute (paramName : string) = + inherit Attribute () + new () = QueryAttribute null + + /// Indicates that this interface represents a REST client which accesses an API whose paths are + /// all relative to the given address. + /// + /// We will essentially unconditionally append a slash to this for you, on the grounds that you probably don't + /// intend the base path *itself* to be an endpoint. + type BaseAddressAttribute (addr : string) = + inherit Attribute () + + /// Indicates that this interface member causes the interface to set a header with the given name, + /// whose value is obtained whenever required by a fresh call to the interface member. + type HeaderAttribute (header : string, value : string option) = + inherit Attribute () + new (header : string) = HeaderAttribute (header, None) + new (header : string, value : string) = HeaderAttribute (header, Some value) + + /// Indicates that this argument to a method is interpolated into the request path at runtime + /// by writing it into the templated string that specifies the HTTP query e.g. in the `[]`. + type PathAttribute (path : string option) = + inherit Attribute () + new (path : string) = PathAttribute (Some path) + new () = PathAttribute None + + /// Indicates that this argument to a method is passed to the remote API by being serialised into the request + /// body. + type BodyAttribute () = + inherit Attribute () + + /// This is interpolated into every URL, between the BaseAddress and the path specified by e.g. []. + /// Note that if the []-specified path starts with a slash, the BasePath is ignored, because then [] + /// is considered to be relative to the URL root (i.e. the host part of the BaseAddress). + /// Similarly, if the [] starts with a slash, then any path component of the BaseAddress is ignored. + /// + /// We will essentially unconditionally append a slash to this for you, on the grounds that you probably don't + /// intend the base path *itself* to be an endpoint. + /// + /// Can contain {placeholders}; hopefully your methods define values for those placeholders with [] + /// attributes! + type BasePathAttribute (path : string) = + inherit Attribute () + + /// Indicates that this REST endpoint may return a non-success status code but we still want to consume its output. + type AllowAnyStatusCodeAttribute () = + inherit Attribute () diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Attributes/SurfaceBaseline.txt b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Attributes/SurfaceBaseline.txt new file mode 100644 index 0000000..68ea002 --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Attributes/SurfaceBaseline.txt @@ -0,0 +1,41 @@ +WoofWare.Whippet.Plugin.HttpClient.HttpClientAttribute inherit System.Attribute +WoofWare.Whippet.Plugin.HttpClient.HttpClientAttribute..ctor [constructor]: bool +WoofWare.Whippet.Plugin.HttpClient.HttpClientAttribute..ctor [constructor]: unit +WoofWare.Whippet.Plugin.HttpClient.HttpClientAttribute.DefaultIsExtensionMethod [static property]: [read-only] bool +WoofWare.Whippet.Plugin.HttpClient.HttpClientAttribute.get_DefaultIsExtensionMethod [static method]: unit -> bool +WoofWare.Whippet.Plugin.HttpClient.RestEase inherit obj +WoofWare.Whippet.Plugin.HttpClient.RestEase+AllowAnyStatusCodeAttribute inherit System.Attribute +WoofWare.Whippet.Plugin.HttpClient.RestEase+AllowAnyStatusCodeAttribute..ctor [constructor]: unit +WoofWare.Whippet.Plugin.HttpClient.RestEase+BaseAddressAttribute inherit System.Attribute +WoofWare.Whippet.Plugin.HttpClient.RestEase+BaseAddressAttribute..ctor [constructor]: string +WoofWare.Whippet.Plugin.HttpClient.RestEase+BasePathAttribute inherit System.Attribute +WoofWare.Whippet.Plugin.HttpClient.RestEase+BasePathAttribute..ctor [constructor]: string +WoofWare.Whippet.Plugin.HttpClient.RestEase+BodyAttribute inherit System.Attribute +WoofWare.Whippet.Plugin.HttpClient.RestEase+BodyAttribute..ctor [constructor]: unit +WoofWare.Whippet.Plugin.HttpClient.RestEase+DeleteAttribute inherit System.Attribute +WoofWare.Whippet.Plugin.HttpClient.RestEase+DeleteAttribute..ctor [constructor]: string +WoofWare.Whippet.Plugin.HttpClient.RestEase+GetAttribute inherit System.Attribute +WoofWare.Whippet.Plugin.HttpClient.RestEase+GetAttribute..ctor [constructor]: string +WoofWare.Whippet.Plugin.HttpClient.RestEase+HeadAttribute inherit System.Attribute +WoofWare.Whippet.Plugin.HttpClient.RestEase+HeadAttribute..ctor [constructor]: string +WoofWare.Whippet.Plugin.HttpClient.RestEase+HeaderAttribute inherit System.Attribute +WoofWare.Whippet.Plugin.HttpClient.RestEase+HeaderAttribute..ctor [constructor]: (string, string option) +WoofWare.Whippet.Plugin.HttpClient.RestEase+HeaderAttribute..ctor [constructor]: (string, string) +WoofWare.Whippet.Plugin.HttpClient.RestEase+HeaderAttribute..ctor [constructor]: string +WoofWare.Whippet.Plugin.HttpClient.RestEase+OptionsAttribute inherit System.Attribute +WoofWare.Whippet.Plugin.HttpClient.RestEase+OptionsAttribute..ctor [constructor]: string +WoofWare.Whippet.Plugin.HttpClient.RestEase+PatchAttribute inherit System.Attribute +WoofWare.Whippet.Plugin.HttpClient.RestEase+PatchAttribute..ctor [constructor]: string +WoofWare.Whippet.Plugin.HttpClient.RestEase+PathAttribute inherit System.Attribute +WoofWare.Whippet.Plugin.HttpClient.RestEase+PathAttribute..ctor [constructor]: string +WoofWare.Whippet.Plugin.HttpClient.RestEase+PathAttribute..ctor [constructor]: string option +WoofWare.Whippet.Plugin.HttpClient.RestEase+PathAttribute..ctor [constructor]: unit +WoofWare.Whippet.Plugin.HttpClient.RestEase+PostAttribute inherit System.Attribute +WoofWare.Whippet.Plugin.HttpClient.RestEase+PostAttribute..ctor [constructor]: string +WoofWare.Whippet.Plugin.HttpClient.RestEase+PutAttribute inherit System.Attribute +WoofWare.Whippet.Plugin.HttpClient.RestEase+PutAttribute..ctor [constructor]: string +WoofWare.Whippet.Plugin.HttpClient.RestEase+QueryAttribute inherit System.Attribute +WoofWare.Whippet.Plugin.HttpClient.RestEase+QueryAttribute..ctor [constructor]: string +WoofWare.Whippet.Plugin.HttpClient.RestEase+QueryAttribute..ctor [constructor]: unit +WoofWare.Whippet.Plugin.HttpClient.RestEase+TraceAttribute inherit System.Attribute +WoofWare.Whippet.Plugin.HttpClient.RestEase+TraceAttribute..ctor [constructor]: string \ No newline at end of file diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Attributes/WoofWare.Whippet.Plugin.HttpClient.Attributes.fsproj b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Attributes/WoofWare.Whippet.Plugin.HttpClient.Attributes.fsproj new file mode 100644 index 0000000..f5e5d5f --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Attributes/WoofWare.Whippet.Plugin.HttpClient.Attributes.fsproj @@ -0,0 +1,19 @@ + + + + netstandard2.0 + true + + + + + + + + + + + + + + diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Attributes/version.json b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Attributes/version.json new file mode 100644 index 0000000..790e212 --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Attributes/version.json @@ -0,0 +1,11 @@ +{ + "version": "0.1", + "publicReleaseRefSpec": [ + "^refs/heads/main$" + ], + "pathFilters": [ + "./", + ":/global.json", + ":/Directory.Build.props" + ] +} diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Consumer/AssemblyInfo.fs b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Consumer/AssemblyInfo.fs new file mode 100644 index 0000000..fc8ebde --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Consumer/AssemblyInfo.fs @@ -0,0 +1,5 @@ +namespace ConsumePlugin.AssemblyInfo + +[] + +do () diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Consumer/GeneratedPureGymDto.fs b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Consumer/GeneratedPureGymDto.fs new file mode 100644 index 0000000..7337948 --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.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/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Consumer/GeneratedRestClient.fs b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Consumer/GeneratedRestClient.fs new file mode 100644 index 0000000..2d97f98 --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Consumer/GeneratedRestClient.fs @@ -0,0 +1,1669 @@ +namespace PureGym + +open System +open System.Threading +open System.Threading.Tasks +open System.IO +open System.Net +open System.Net.Http +open WoofWare.Whippet.Plugin.HttpClient +open RestEase + +/// Module for constructing a REST client. +[] +module PureGymApi = + /// Create a REST client. + let make (client : System.Net.Http.HttpClient) : IPureGymApi = + { new IPureGymApi with + member _.GetGyms (ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri (("v1/gyms/"), System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseStream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + + let! jsonNode = + System.Text.Json.Nodes.JsonNode.ParseAsync (responseStream, cancellationToken = ct) + |> Async.AwaitTask + + return jsonNode.AsArray () |> Seq.map (fun elt -> Gym.jsonParse elt) |> List.ofSeq + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.GetGymAttendance (gymId : int, ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ( + "v1/gyms/{gym_id}/attendance" + .Replace ("{gym_id}", gymId.ToString () |> System.Uri.EscapeDataString), + System.UriKind.Relative + ) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseStream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + + let! jsonNode = + System.Text.Json.Nodes.JsonNode.ParseAsync (responseStream, cancellationToken = ct) + |> Async.AwaitTask + + return GymAttendance.jsonParse jsonNode + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.GetGymAttendance' (gymId : int, ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ( + "v1/gyms/{gym_id}/attendance" + .Replace ("{gym_id}", gymId.ToString () |> System.Uri.EscapeDataString), + System.UriKind.Relative + ) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseStream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + + let! jsonNode = + System.Text.Json.Nodes.JsonNode.ParseAsync (responseStream, cancellationToken = ct) + |> Async.AwaitTask + + return GymAttendance.jsonParse jsonNode + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.GetMember (ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ("v1/member", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseStream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + + let! jsonNode = + System.Text.Json.Nodes.JsonNode.ParseAsync (responseStream, cancellationToken = ct) + |> Async.AwaitTask + + return Member.jsonParse jsonNode + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.GetGym (gym : int, ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ( + "v1/gyms/{gym}" + .Replace ("{gym}", gym.ToString () |> System.Uri.EscapeDataString), + System.UriKind.Relative + ) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseStream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + + let! jsonNode = + System.Text.Json.Nodes.JsonNode.ParseAsync (responseStream, cancellationToken = ct) + |> Async.AwaitTask + + return Gym.jsonParse jsonNode + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.GetMemberActivity (ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ("v1/member/activity", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseStream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + + let! jsonNode = + System.Text.Json.Nodes.JsonNode.ParseAsync (responseStream, cancellationToken = ct) + |> Async.AwaitTask + + return MemberActivityDto.jsonParse jsonNode + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.GetUrl (ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ("some/url", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseStream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + + let! jsonNode = + System.Text.Json.Nodes.JsonNode.ParseAsync (responseStream, cancellationToken = ct) + |> Async.AwaitTask + + return UriThing.jsonParse jsonNode + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.PostStringToString (foo : Map option, ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ("some/url", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Post, + RequestUri = uri + ) + + let queryParams = + new System.Net.Http.StringContent ( + foo + |> (fun field -> + match field with + | None -> null :> System.Text.Json.Nodes.JsonNode + | Some field -> + ((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 + ) + field) + :> System.Text.Json.Nodes.JsonNode + ) + |> (fun node -> if isNull node then "null" else node.ToJsonString ()) + ) + + do httpMessage.Content <- queryParams + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseStream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + + let! jsonNode = + System.Text.Json.Nodes.JsonNode.ParseAsync (responseStream, cancellationToken = ct) + |> Async.AwaitTask + + return + match jsonNode with + | null -> None + | v -> + v.AsObject () + |> Seq.map (fun kvp -> + let key = (kvp.Key) + let value = (kvp.Value).AsValue().GetValue () + key, value + ) + |> Map.ofSeq + |> Some + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.GetSessions (fromDate : DateOnly, toDate : DateOnly, ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ( + ("/v2/gymSessions/member" + + (if "/v2/gymSessions/member".IndexOf (char 63) >= 0 then + "&" + else + "?") + + "fromDate=" + + ((fromDate.ToString "yyyy-MM-dd") |> System.Uri.EscapeDataString) + + "&toDate=" + + ((toDate.ToString "yyyy-MM-dd") |> System.Uri.EscapeDataString)), + System.UriKind.Relative + ) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseStream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + + let! jsonNode = + System.Text.Json.Nodes.JsonNode.ParseAsync (responseStream, cancellationToken = ct) + |> Async.AwaitTask + + return Sessions.jsonParse jsonNode + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.GetSessionsWithQuery (fromDate : DateOnly, toDate : DateOnly, ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ( + ("/v2/gymSessions/member?foo=1" + + (if "/v2/gymSessions/member?foo=1".IndexOf (char 63) >= 0 then + "&" + else + "?") + + "fromDate=" + + ((fromDate.ToString "yyyy-MM-dd") |> System.Uri.EscapeDataString) + + "&toDate=" + + ((toDate.ToString "yyyy-MM-dd") |> System.Uri.EscapeDataString)), + System.UriKind.Relative + ) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseStream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + + let! jsonNode = + System.Text.Json.Nodes.JsonNode.ParseAsync (responseStream, cancellationToken = ct) + |> Async.AwaitTask + + return Sessions.jsonParse jsonNode + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.CreateUserString (user : string, ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ("users/new", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Post, + RequestUri = uri + ) + + let queryParams = new System.Net.Http.StringContent (user) + do httpMessage.Content <- queryParams + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask + return responseString + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.CreateUserStream (user : System.IO.Stream, ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ("users/new", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Post, + RequestUri = uri + ) + + let queryParams = new System.Net.Http.StreamContent (user) + do httpMessage.Content <- queryParams + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseStream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + return responseStream + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.CreateUserByteArr (user : byte[], ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ("users/new", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Post, + RequestUri = uri + ) + + let queryParams = new System.Net.Http.ByteArrayContent (user) + do httpMessage.Content <- queryParams + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseStream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + return responseStream + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.CreateUserByteArr' (user : array, ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ("users/new", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Post, + RequestUri = uri + ) + + let queryParams = new System.Net.Http.ByteArrayContent (user) + do httpMessage.Content <- queryParams + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseStream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + return responseStream + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.CreateUserByteArr'' (user : byte array, ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ("users/new", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Post, + RequestUri = uri + ) + + let queryParams = new System.Net.Http.ByteArrayContent (user) + do httpMessage.Content <- queryParams + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseStream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + return responseStream + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.CreateUserSerialisedBody (user : PureGym.Member, ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ("users/new", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Post, + RequestUri = uri + ) + + let queryParams = + new System.Net.Http.StringContent ( + user + |> PureGym.Member.toJsonNode + |> (fun node -> if isNull node then "null" else node.ToJsonString ()) + ) + + do httpMessage.Content <- queryParams + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask + return responseString + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.CreateUserSerialisedUrlBody (user : Uri, ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ("users/new", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Post, + RequestUri = uri + ) + + let queryParams = + new System.Net.Http.StringContent ( + user + |> System.Text.Json.Nodes.JsonValue.Create + |> (fun node -> if isNull node then "null" else node.ToJsonString ()) + ) + + do httpMessage.Content <- queryParams + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask + return responseString + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.CreateUserSerialisedIntBody (user : int, ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ("users/new", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Post, + RequestUri = uri + ) + + let queryParams = + new System.Net.Http.StringContent ( + user + |> System.Text.Json.Nodes.JsonValue.Create + |> (fun node -> if isNull node then "null" else node.ToJsonString ()) + ) + + do httpMessage.Content <- queryParams + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask + return responseString + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.CreateUserHttpContent (user : System.Net.Http.HttpContent, ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ("users/new", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Post, + RequestUri = uri + ) + + do httpMessage.Content <- user + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask + return responseString + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.GetPathParam (parameter : string, ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ( + "endpoint/{param}" + .Replace ("{param}", parameter.ToString () |> System.Uri.EscapeDataString), + System.UriKind.Relative + ) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask + return responseString + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.GetStream (ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ("endpoint", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseStream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + return responseStream + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.GetStream' (ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ("endpoint", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseStream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + return responseStream + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.GetStream'' (ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ("endpoint", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseStream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + return responseStream + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.GetResponseMessage (ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ("endpoint", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + return response + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.GetResponseMessage' (ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ("endpoint", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + return response + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.GetResponseMessage'' (ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ("endpoint", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + return response + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.GetResponseMessage''' (ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ("endpoint", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + return response + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.GetResponse (ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ("endpoint", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask + let! responseStream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + + let! jsonNode = + System.Text.Json.Nodes.JsonNode.ParseAsync (responseStream, cancellationToken = ct) + |> Async.AwaitTask + + return + new RestEase.Response<_> ( + responseString, + response, + (fun () -> (MemberActivityDto.jsonParse jsonNode)) + ) + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.GetResponse' (ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ("endpoint", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask + let! responseStream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + + let! jsonNode = + System.Text.Json.Nodes.JsonNode.ParseAsync (responseStream, cancellationToken = ct) + |> Async.AwaitTask + + return + new RestEase.Response<_> ( + responseString, + response, + (fun () -> (MemberActivityDto.jsonParse jsonNode)) + ) + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.GetResponse'' (ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ("endpoint", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask + let! responseStream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + + let! jsonNode = + System.Text.Json.Nodes.JsonNode.ParseAsync (responseStream, cancellationToken = ct) + |> Async.AwaitTask + + return + new RestEase.Response<_> ( + responseString, + response, + (fun () -> (MemberActivityDto.jsonParse jsonNode)) + ) + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.GetResponse''' (ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ("endpoint", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask + let! responseStream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + + let! jsonNode = + System.Text.Json.Nodes.JsonNode.ParseAsync (responseStream, cancellationToken = ct) + |> Async.AwaitTask + + return + new RestEase.Response<_> ( + responseString, + response, + (fun () -> (MemberActivityDto.jsonParse jsonNode)) + ) + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.GetWithAnyReturnCode (ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ("endpoint", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + return response + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.GetWithoutAnyReturnCode (ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/" + | v -> v), + System.Uri ("endpoint", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + return response + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + } +namespace PureGym + +open System +open System.Threading +open System.Threading.Tasks +open System.IO +open System.Net +open System.Net.Http +open WoofWare.Whippet.Plugin.HttpClient +open RestEase + +/// Extension methods for constructing a REST client. +[] +module internal ApiWithoutBaseAddressHttpClientExtension = + /// Extension methods for HTTP clients + type IApiWithoutBaseAddress with + + /// Create a REST client. + static member make (client : System.Net.Http.HttpClient) : IApiWithoutBaseAddress = + { new IApiWithoutBaseAddress with + member _.GetPathParam (parameter : string, ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> + raise ( + System.ArgumentNullException ( + nameof (client.BaseAddress), + "No base address was supplied on the type, and no BaseAddress was on the HttpClient." + ) + ) + | v -> v), + System.Uri ( + "endpoint/{param}" + .Replace ("{param}", parameter.ToString () |> System.Uri.EscapeDataString), + System.UriKind.Relative + ) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask + return responseString + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + } +namespace PureGym + +open System +open System.Threading +open System.Threading.Tasks +open System.IO +open System.Net +open System.Net.Http +open WoofWare.Whippet.Plugin.HttpClient +open RestEase + +/// Extension methods for constructing a REST client. +[] +module ApiWithBasePathHttpClientExtension = + /// Extension methods for HTTP clients + type IApiWithBasePath with + + /// Create a REST client. + static member make (client : System.Net.Http.HttpClient) : IApiWithBasePath = + { new IApiWithBasePath with + member _.GetPathParam (parameter : string, cancellationToken : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + System.Uri ( + (match client.BaseAddress with + | null -> + raise ( + System.ArgumentNullException ( + nameof (client.BaseAddress), + "No base address was supplied on the type, and no BaseAddress was on the HttpClient." + ) + ) + | v -> v), + System.Uri ("foo/", System.UriKind.Relative) + ), + System.Uri ( + "endpoint/{param}" + .Replace ("{param}", parameter.ToString () |> System.Uri.EscapeDataString), + System.UriKind.Relative + ) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask + return responseString + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = cancellationToken)) + } +namespace PureGym + +open System +open System.Threading +open System.Threading.Tasks +open System.IO +open System.Net +open System.Net.Http +open WoofWare.Whippet.Plugin.HttpClient +open RestEase + +/// Extension methods for constructing a REST client. +[] +module ApiWithBasePathAndAddressHttpClientExtension = + /// Extension methods for HTTP clients + type IApiWithBasePathAndAddress with + + /// Create a REST client. + static member make (client : System.Net.Http.HttpClient) : IApiWithBasePathAndAddress = + { new IApiWithBasePathAndAddress with + member _.GetPathParam (parameter : string, ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/thing/" + | v -> v), + System.Uri ("foo/", System.UriKind.Relative) + ), + System.Uri ( + "endpoint/{param}" + .Replace ("{param}", parameter.ToString () |> System.Uri.EscapeDataString), + System.UriKind.Relative + ) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask + return responseString + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + } +namespace PureGym + +open System +open System.Threading +open System.Threading.Tasks +open System.IO +open System.Net +open System.Net.Http +open WoofWare.Whippet.Plugin.HttpClient +open RestEase + +/// Extension methods for constructing a REST client. +[] +module ApiWithAbsoluteBasePathHttpClientExtension = + /// Extension methods for HTTP clients + type IApiWithAbsoluteBasePath with + + /// Create a REST client. + static member make (client : System.Net.Http.HttpClient) : IApiWithAbsoluteBasePath = + { new IApiWithAbsoluteBasePath with + member _.GetPathParam (parameter : string, cancellationToken : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + System.Uri ( + (match client.BaseAddress with + | null -> + raise ( + System.ArgumentNullException ( + nameof (client.BaseAddress), + "No base address was supplied on the type, and no BaseAddress was on the HttpClient." + ) + ) + | v -> v), + System.Uri ("/foo/", System.UriKind.Relative) + ), + System.Uri ( + "endpoint/{param}" + .Replace ("{param}", parameter.ToString () |> System.Uri.EscapeDataString), + System.UriKind.Relative + ) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask + return responseString + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = cancellationToken)) + } +namespace PureGym + +open System +open System.Threading +open System.Threading.Tasks +open System.IO +open System.Net +open System.Net.Http +open WoofWare.Whippet.Plugin.HttpClient +open RestEase + +/// Extension methods for constructing a REST client. +[] +module ApiWithAbsoluteBasePathAndAddressHttpClientExtension = + /// Extension methods for HTTP clients + type IApiWithAbsoluteBasePathAndAddress with + + /// Create a REST client. + static member make (client : System.Net.Http.HttpClient) : IApiWithAbsoluteBasePathAndAddress = + { new IApiWithAbsoluteBasePathAndAddress with + member _.GetPathParam (parameter : string, ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/thing/" + | v -> v), + System.Uri ("/foo/", System.UriKind.Relative) + ), + System.Uri ( + "endpoint/{param}" + .Replace ("{param}", parameter.ToString () |> System.Uri.EscapeDataString), + System.UriKind.Relative + ) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask + return responseString + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + } +namespace PureGym + +open System +open System.Threading +open System.Threading.Tasks +open System.IO +open System.Net +open System.Net.Http +open WoofWare.Whippet.Plugin.HttpClient +open RestEase + +/// Extension methods for constructing a REST client. +[] +module ApiWithBasePathAndAbsoluteEndpointHttpClientExtension = + /// Extension methods for HTTP clients + type IApiWithBasePathAndAbsoluteEndpoint with + + /// Create a REST client. + static member make (client : System.Net.Http.HttpClient) : IApiWithBasePathAndAbsoluteEndpoint = + { new IApiWithBasePathAndAbsoluteEndpoint with + member _.GetPathParam (parameter : string, cancellationToken : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + System.Uri ( + (match client.BaseAddress with + | null -> + raise ( + System.ArgumentNullException ( + nameof (client.BaseAddress), + "No base address was supplied on the type, and no BaseAddress was on the HttpClient." + ) + ) + | v -> v), + System.Uri ("foo/", System.UriKind.Relative) + ), + System.Uri ( + "/endpoint/{param}" + .Replace ("{param}", parameter.ToString () |> System.Uri.EscapeDataString), + System.UriKind.Relative + ) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask + return responseString + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = cancellationToken)) + } +namespace PureGym + +open System +open System.Threading +open System.Threading.Tasks +open System.IO +open System.Net +open System.Net.Http +open WoofWare.Whippet.Plugin.HttpClient +open RestEase + +/// Extension methods for constructing a REST client. +[] +module ApiWithBasePathAndAddressAndAbsoluteEndpointHttpClientExtension = + /// Extension methods for HTTP clients + type IApiWithBasePathAndAddressAndAbsoluteEndpoint with + + /// Create a REST client. + static member make (client : System.Net.Http.HttpClient) : IApiWithBasePathAndAddressAndAbsoluteEndpoint = + { new IApiWithBasePathAndAddressAndAbsoluteEndpoint with + member _.GetPathParam (parameter : string, ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/thing/" + | v -> v), + System.Uri ("foo/", System.UriKind.Relative) + ), + System.Uri ( + "/endpoint/{param}" + .Replace ("{param}", parameter.ToString () |> System.Uri.EscapeDataString), + System.UriKind.Relative + ) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask + return responseString + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + } +namespace PureGym + +open System +open System.Threading +open System.Threading.Tasks +open System.IO +open System.Net +open System.Net.Http +open WoofWare.Whippet.Plugin.HttpClient +open RestEase + +/// Extension methods for constructing a REST client. +[] +module ApiWithAbsoluteBasePathAndAbsoluteEndpointHttpClientExtension = + /// Extension methods for HTTP clients + type IApiWithAbsoluteBasePathAndAbsoluteEndpoint with + + /// Create a REST client. + static member make (client : System.Net.Http.HttpClient) : IApiWithAbsoluteBasePathAndAbsoluteEndpoint = + { new IApiWithAbsoluteBasePathAndAbsoluteEndpoint with + member _.GetPathParam (parameter : string, cancellationToken : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + System.Uri ( + (match client.BaseAddress with + | null -> + raise ( + System.ArgumentNullException ( + nameof (client.BaseAddress), + "No base address was supplied on the type, and no BaseAddress was on the HttpClient." + ) + ) + | v -> v), + System.Uri ("/foo/", System.UriKind.Relative) + ), + System.Uri ( + "/endpoint/{param}" + .Replace ("{param}", parameter.ToString () |> System.Uri.EscapeDataString), + System.UriKind.Relative + ) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask + return responseString + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = cancellationToken)) + } +namespace PureGym + +open System +open System.Threading +open System.Threading.Tasks +open System.IO +open System.Net +open System.Net.Http +open WoofWare.Whippet.Plugin.HttpClient +open RestEase + +/// Extension methods for constructing a REST client. +[] +module ApiWithAbsoluteBasePathAndAddressAndAbsoluteEndpointHttpClientExtension = + /// Extension methods for HTTP clients + type IApiWithAbsoluteBasePathAndAddressAndAbsoluteEndpoint with + + /// Create a REST client. + static member make + (client : System.Net.Http.HttpClient) + : IApiWithAbsoluteBasePathAndAddressAndAbsoluteEndpoint + = + { new IApiWithAbsoluteBasePathAndAddressAndAbsoluteEndpoint with + member _.GetPathParam (parameter : string, ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + System.Uri ( + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com/thing/" + | v -> v), + System.Uri ("/foo/", System.UriKind.Relative) + ), + System.Uri ( + "/endpoint/{param}" + .Replace ("{param}", parameter.ToString () |> System.Uri.EscapeDataString), + System.UriKind.Relative + ) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask + return responseString + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + } +namespace PureGym + +open System +open System.Threading +open System.Threading.Tasks +open System.IO +open System.Net +open System.Net.Http +open WoofWare.Whippet.Plugin.HttpClient +open RestEase + +/// Extension methods for constructing a REST client. +[] +module ApiWithHeadersHttpClientExtension = + /// Extension methods for HTTP clients + type IApiWithHeaders with + + /// Create a REST client. The input functions will be re-evaluated on every HTTP request to obtain the required values for the corresponding header properties. + static member make + (someHeader : unit -> string) + (someOtherHeader : unit -> int) + (client : System.Net.Http.HttpClient) + : IApiWithHeaders + = + { new IApiWithHeaders with + member _.SomeHeader : string = someHeader () + member _.SomeOtherHeader : int = someOtherHeader () + + member this.GetPathParam (parameter : string, ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> + raise ( + System.ArgumentNullException ( + nameof (client.BaseAddress), + "No base address was supplied on the type, and no BaseAddress was on the HttpClient." + ) + ) + | v -> v), + System.Uri ( + "endpoint/{param}" + .Replace ("{param}", parameter.ToString () |> System.Uri.EscapeDataString), + System.UriKind.Relative + ) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + do httpMessage.Headers.Add ("X-Foo", this.SomeHeader.ToString ()) + do httpMessage.Headers.Add ("Authorization", this.SomeOtherHeader.ToString ()) + do httpMessage.Headers.Add ("Header-Name", "Header-Value") + do httpMessage.Headers.Add ("Something-Else", "val") + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask + return responseString + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + } +namespace PureGym + +open System +open System.Threading +open System.Threading.Tasks +open System.IO +open System.Net +open System.Net.Http +open WoofWare.Whippet.Plugin.HttpClient +open RestEase + +/// Extension methods for constructing a REST client. +[] +module ApiWithHeaders2HttpClientExtension = + /// Extension methods for HTTP clients + type IApiWithHeaders2 with + + /// Create a REST client. The input functions will be re-evaluated on every HTTP request to obtain the required values for the corresponding header properties. + static member make + (someHeader : unit -> string) + (someOtherHeader : unit -> int) + (client : System.Net.Http.HttpClient) + : IApiWithHeaders2 + = + { new IApiWithHeaders2 with + member _.SomeHeader : string = someHeader () + member _.SomeOtherHeader : int = someOtherHeader () + + member this.GetPathParam (parameter : string, ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> + raise ( + System.ArgumentNullException ( + nameof (client.BaseAddress), + "No base address was supplied on the type, and no BaseAddress was on the HttpClient." + ) + ) + | v -> v), + System.Uri ( + "endpoint/{param}" + .Replace ("{param}", parameter.ToString () |> System.Uri.EscapeDataString), + System.UriKind.Relative + ) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + do httpMessage.Headers.Add ("X-Foo", this.SomeHeader.ToString ()) + do httpMessage.Headers.Add ("Authorization", this.SomeOtherHeader.ToString ()) + do httpMessage.Headers.Add ("Header-Name", "Header-Value") + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseString = response.Content.ReadAsStringAsync ct |> Async.AwaitTask + return responseString + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + } diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Consumer/GeneratedVault.fs b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Consumer/GeneratedVault.fs new file mode 100644 index 0000000..4c62a18 --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Consumer/GeneratedVault.fs @@ -0,0 +1,291 @@ +namespace ConsumePlugin + +open System +open System.Collections.Generic +open System.Text.Json.Serialization +open System.Threading +open System.Threading.Tasks +open WoofWare.Whippet.Plugin.Json +open WoofWare.Whippet.Plugin.HttpClient +open RestEase + +/// Module for constructing a REST client. +[] +module VaultClient = + /// Create a REST client. + let make (client : System.Net.Http.HttpClient) : IVaultClient = + { new IVaultClient with + member _.GetSecret + (jwt : JwtVaultResponse, path : string, mountPoint : string, ct : CancellationToken option) + = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> + raise ( + System.ArgumentNullException ( + nameof (client.BaseAddress), + "No base address was supplied on the type, and no BaseAddress was on the HttpClient." + ) + ) + | v -> v), + System.Uri ( + "v1/{mountPoint}/{path}" + .Replace("{path}", path.ToString () |> System.Uri.EscapeDataString) + .Replace ("{mountPoint}", mountPoint.ToString () |> System.Uri.EscapeDataString), + System.UriKind.Relative + ) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseStream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + + let! jsonNode = + System.Text.Json.Nodes.JsonNode.ParseAsync (responseStream, cancellationToken = ct) + |> Async.AwaitTask + + return JwtSecretResponse.jsonParse jsonNode + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.GetJwt (role : string, jwt : string, ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> + raise ( + System.ArgumentNullException ( + nameof (client.BaseAddress), + "No base address was supplied on the type, and no BaseAddress was on the HttpClient." + ) + ) + | v -> v), + System.Uri ("v1/auth/jwt/login", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseStream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + + let! jsonNode = + System.Text.Json.Nodes.JsonNode.ParseAsync (responseStream, cancellationToken = ct) + |> Async.AwaitTask + + return JwtVaultResponse.jsonParse jsonNode + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + } +namespace ConsumePlugin + +open System +open System.Collections.Generic +open System.Text.Json.Serialization +open System.Threading +open System.Threading.Tasks +open WoofWare.Whippet.Plugin.Json +open WoofWare.Whippet.Plugin.HttpClient +open RestEase + +/// Module for constructing a REST client. +[] +module VaultClientNonExtensionMethod = + /// Create a REST client. + let make (client : System.Net.Http.HttpClient) : IVaultClientNonExtensionMethod = + { new IVaultClientNonExtensionMethod with + member _.GetSecret + (jwt : JwtVaultResponse, path : string, mountPoint : string, ct : CancellationToken option) + = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> + raise ( + System.ArgumentNullException ( + nameof (client.BaseAddress), + "No base address was supplied on the type, and no BaseAddress was on the HttpClient." + ) + ) + | v -> v), + System.Uri ( + "v1/{mountPoint}/{path}" + .Replace("{path}", path.ToString () |> System.Uri.EscapeDataString) + .Replace ("{mountPoint}", mountPoint.ToString () |> System.Uri.EscapeDataString), + System.UriKind.Relative + ) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseStream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + + let! jsonNode = + System.Text.Json.Nodes.JsonNode.ParseAsync (responseStream, cancellationToken = ct) + |> Async.AwaitTask + + return JwtSecretResponse.jsonParse jsonNode + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.GetJwt (role : string, jwt : string, ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> + raise ( + System.ArgumentNullException ( + nameof (client.BaseAddress), + "No base address was supplied on the type, and no BaseAddress was on the HttpClient." + ) + ) + | v -> v), + System.Uri ("v1/auth/jwt/login", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseStream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + + let! jsonNode = + System.Text.Json.Nodes.JsonNode.ParseAsync (responseStream, cancellationToken = ct) + |> Async.AwaitTask + + return JwtVaultResponse.jsonParse jsonNode + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + } +namespace ConsumePlugin + +open System +open System.Collections.Generic +open System.Text.Json.Serialization +open System.Threading +open System.Threading.Tasks +open WoofWare.Whippet.Plugin.Json +open WoofWare.Whippet.Plugin.HttpClient +open RestEase + +/// Extension methods for constructing a REST client. +[] +module VaultClientExtensionMethodHttpClientExtension = + /// Extension methods for HTTP clients + type IVaultClientExtensionMethod with + + /// Create a REST client. + static member make (client : System.Net.Http.HttpClient) : IVaultClientExtensionMethod = + { new IVaultClientExtensionMethod with + member _.GetSecret + (jwt : JwtVaultResponse, path : string, mountPoint : string, ct : CancellationToken option) + = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> + raise ( + System.ArgumentNullException ( + nameof (client.BaseAddress), + "No base address was supplied on the type, and no BaseAddress was on the HttpClient." + ) + ) + | v -> v), + System.Uri ( + "v1/{mountPoint}/{path}" + .Replace("{path}", path.ToString () |> System.Uri.EscapeDataString) + .Replace ("{mountPoint}", mountPoint.ToString () |> System.Uri.EscapeDataString), + System.UriKind.Relative + ) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseStream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + + let! jsonNode = + System.Text.Json.Nodes.JsonNode.ParseAsync (responseStream, cancellationToken = ct) + |> Async.AwaitTask + + return JwtSecretResponse.jsonParse jsonNode + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + member _.GetJwt (role : string, jwt : string, ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let uri = + System.Uri ( + (match client.BaseAddress with + | null -> + raise ( + System.ArgumentNullException ( + nameof (client.BaseAddress), + "No base address was supplied on the type, and no BaseAddress was on the HttpClient." + ) + ) + | v -> v), + System.Uri ("v1/auth/jwt/login", System.UriKind.Relative) + ) + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = uri + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! responseStream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + + let! jsonNode = + System.Text.Json.Nodes.JsonNode.ParseAsync (responseStream, cancellationToken = ct) + |> Async.AwaitTask + + return JwtVaultResponse.jsonParse jsonNode + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + } diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Consumer/GeneratedVaultDto.fs b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Consumer/GeneratedVaultDto.fs new file mode 100644 index 0000000..5a197d7 --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Consumer/GeneratedVaultDto.fs @@ -0,0 +1,445 @@ +namespace ConsumePlugin + +/// Module containing JSON parsing extension members for the JwtVaultAuthResponse type +[] +module JwtVaultAuthResponseJsonParseExtension = + /// Extension methods for JSON parsing + type JwtVaultAuthResponse with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : JwtVaultAuthResponse = + let arg_10 = + (match node.["num_uses"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("num_uses") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_9 = + (match node.["orphan"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("orphan") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_8 = + (match node.["entity_id"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("entity_id") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_7 = + (match node.["token_type"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("token_type") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_6 = + (match node.["renewable"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("renewable") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_5 = + (match node.["lease_duration"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("lease_duration") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_4 = + (match node.["identity_policies"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("identity_policies") + ) + ) + | v -> v) + .AsArray () + |> Seq.map (fun elt -> elt.AsValue().GetValue ()) + |> List.ofSeq + + let arg_3 = + (match node.["token_policies"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("token_policies") + ) + ) + | v -> v) + .AsArray () + |> Seq.map (fun elt -> elt.AsValue().GetValue ()) + |> List.ofSeq + + let arg_2 = + (match node.["policies"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("policies") + ) + ) + | v -> v) + .AsArray () + |> Seq.map (fun elt -> elt.AsValue().GetValue ()) + |> List.ofSeq + + let arg_1 = + (match node.["accessor"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("accessor") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_0 = + (match node.["client_token"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("client_token") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + { + ClientToken = arg_0 + Accessor = arg_1 + Policies = arg_2 + TokenPolicies = arg_3 + IdentityPolicies = arg_4 + LeaseDuration = arg_5 + Renewable = arg_6 + TokenType = arg_7 + EntityId = arg_8 + Orphan = arg_9 + NumUses = arg_10 + } +namespace ConsumePlugin + +/// Module containing JSON parsing extension members for the JwtVaultResponse type +[] +module JwtVaultResponseJsonParseExtension = + /// Extension methods for JSON parsing + type JwtVaultResponse with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : JwtVaultResponse = + let arg_4 = + JwtVaultAuthResponse.jsonParse ( + match node.["auth"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("auth") + ) + ) + | v -> v + ) + + let arg_3 = + (match node.["lease_duration"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("lease_duration") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_2 = + (match node.["renewable"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("renewable") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_1 = + (match node.["lease_id"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("lease_id") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_0 = + (match node.["request_id"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("request_id") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + { + RequestId = arg_0 + LeaseId = arg_1 + Renewable = arg_2 + LeaseDuration = arg_3 + Auth = arg_4 + } +namespace ConsumePlugin + +/// Module containing JSON parsing extension members for the JwtSecretResponse type +[] +module JwtSecretResponseJsonParseExtension = + /// Extension methods for JSON parsing + type JwtSecretResponse with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : JwtSecretResponse = + let arg_11 = + (match node.["data8"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("data8") + ) + ) + | v -> v) + .AsObject () + |> Seq.map (fun kvp -> + let key = (kvp.Key) + let value = (kvp.Value).AsValue().GetValue () |> System.Uri + key, value + ) + |> Seq.map System.Collections.Generic.KeyValuePair + |> System.Collections.Generic.Dictionary + + let arg_10 = + (match node.["data7"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("data7") + ) + ) + | v -> v) + .AsObject () + |> Seq.map (fun kvp -> + let key = (kvp.Key) + let value = (kvp.Value).AsValue().GetValue () + key, value + ) + |> Map.ofSeq + + let arg_9 = + (match node.["data6"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("data6") + ) + ) + | v -> v) + .AsObject () + |> Seq.map (fun kvp -> + let key = (kvp.Key) |> System.Uri + let value = (kvp.Value).AsValue().GetValue () + key, value + ) + |> dict + + let arg_8 = + (match node.["data5"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("data5") + ) + ) + | v -> v) + .AsObject () + |> Seq.map (fun kvp -> + let key = (kvp.Key) |> System.Uri + let value = (kvp.Value).AsValue().GetValue () + key, value + ) + |> readOnlyDict + + let arg_7 = + (match node.["data4"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("data4") + ) + ) + | v -> v) + .AsObject () + |> Seq.map (fun kvp -> + let key = (kvp.Key) + let value = (kvp.Value).AsValue().GetValue () + key, value + ) + |> Map.ofSeq + + let arg_6 = + (match node.["data3"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("data3") + ) + ) + | v -> v) + .AsObject () + |> Seq.map (fun kvp -> + let key = (kvp.Key) + let value = (kvp.Value).AsValue().GetValue () + key, value + ) + |> Seq.map System.Collections.Generic.KeyValuePair + |> System.Collections.Generic.Dictionary + + let arg_5 = + (match node.["data2"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("data2") + ) + ) + | v -> v) + .AsObject () + |> Seq.map (fun kvp -> + let key = (kvp.Key) + let value = (kvp.Value).AsValue().GetValue () + key, value + ) + |> dict + + let arg_4 = + (match node.["data"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("data") + ) + ) + | v -> v) + .AsObject () + |> Seq.map (fun kvp -> + let key = (kvp.Key) + let value = (kvp.Value).AsValue().GetValue () + key, value + ) + |> readOnlyDict + + let arg_3 = + (match node.["lease_duration"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("lease_duration") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_2 = + (match node.["renewable"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("renewable") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_1 = + (match node.["lease_id"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("lease_id") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + let arg_0 = + (match node.["request_id"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("request_id") + ) + ) + | v -> v) + .AsValue() + .GetValue () + + { + RequestId = arg_0 + LeaseId = arg_1 + Renewable = arg_2 + LeaseDuration = arg_3 + Data = arg_4 + Data2 = arg_5 + Data3 = arg_6 + Data4 = arg_7 + Data5 = arg_8 + Data6 = arg_9 + Data7 = arg_10 + Data8 = arg_11 + } diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Consumer/PureGymDto.fs b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Consumer/PureGymDto.fs new file mode 100644 index 0000000..9181c6a --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.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/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Consumer/RestApiExample.fs b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Consumer/RestApiExample.fs new file mode 100644 index 0000000..650d40d --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Consumer/RestApiExample.fs @@ -0,0 +1,205 @@ +namespace PureGym + +open System +open System.Threading +open System.Threading.Tasks +open System.IO +open System.Net +open System.Net.Http +open WoofWare.Whippet.Plugin.HttpClient +open RestEase + +[] +[] +type IPureGymApi = + [] + abstract GetGyms : ?ct : CancellationToken -> Task + + [] + abstract GetGymAttendance : [] gymId : int * ?ct : CancellationToken -> Task + + [] + abstract GetGymAttendance' : [] gymId : int * ?ct : CancellationToken -> Task + + [] + abstract GetMember : ?ct : CancellationToken -> Member Task + + [] + abstract GetGym : [] gym : int * ?ct : CancellationToken -> Task + + [] + abstract GetMemberActivity : ?ct : CancellationToken -> Task + + [] + abstract GetUrl : ?ct : CancellationToken -> Task + + [] + abstract PostStringToString : + [] foo : Map option * ?ct : CancellationToken -> Task option> + + // We'll use this one to check handling of absolute URIs too + [] + abstract GetSessions : + [] fromDate : DateOnly * [] toDate : DateOnly * ?ct : CancellationToken -> Task + + [] + abstract GetSessionsWithQuery : + [] fromDate : DateOnly * [] toDate : DateOnly * ?ct : CancellationToken -> Task + + // An example from RestEase's own docs + [] + abstract CreateUserString : [] user : string * ?ct : CancellationToken -> Task + + [] + abstract CreateUserStream : [] user : System.IO.Stream * ?ct : CancellationToken -> Task + + [] + abstract CreateUserByteArr : [] user : byte[] * ?ct : CancellationToken -> Task + + [] + abstract CreateUserByteArr' : [] user : array * ?ct : CancellationToken -> Task + + [] + abstract CreateUserByteArr'' : [] user : byte array * ?ct : CancellationToken -> Task + + [] + abstract CreateUserSerialisedBody : [] user : PureGym.Member * ?ct : CancellationToken -> Task + + [] + abstract CreateUserSerialisedUrlBody : [] user : Uri * ?ct : CancellationToken -> Task + + [] + abstract CreateUserSerialisedIntBody : [] user : int * ?ct : CancellationToken -> Task + + [] + abstract CreateUserHttpContent : + [] user : System.Net.Http.HttpContent * ?ct : CancellationToken -> Task + + [] + abstract GetPathParam : [] parameter : string * ?ct : CancellationToken -> Task + + [] + abstract GetStream : ?ct : CancellationToken -> Task + + [] + abstract GetStream' : ?ct : CancellationToken -> Task + + [] + abstract GetStream'' : ?ct : CancellationToken -> Task + + [] + abstract GetResponseMessage : ?ct : CancellationToken -> Task + + [] + abstract GetResponseMessage' : ?ct : CancellationToken -> Task + + [] + abstract GetResponseMessage'' : ?ct : CancellationToken -> Task + + [] + abstract GetResponseMessage''' : ?ct : CancellationToken -> Task + + [] + abstract GetResponse : ?ct : CancellationToken -> Task> + + [] + abstract GetResponse' : ?ct : CancellationToken -> Task> + + [] + abstract GetResponse'' : ?ct : CancellationToken -> Task + + [] + abstract GetResponse''' : ?ct : CancellationToken -> Task + + [] + [] + abstract GetWithAnyReturnCode : ?ct : CancellationToken -> Task + + [] + abstract GetWithoutAnyReturnCode : ?ct : CancellationToken -> Task + +[] +type internal IApiWithoutBaseAddress = + [] + abstract GetPathParam : [] parameter : string * ?ct : CancellationToken -> Task + +[] +[] +type IApiWithBasePath = + // Example where we use the bundled attributes rather than RestEase's + [] + abstract GetPathParam : [] parameter : string * ?cancellationToken : CancellationToken -> Task + +[] +[] +[] +type IApiWithBasePathAndAddress = + [] + abstract GetPathParam : [] parameter : string * ?ct : CancellationToken -> Task + +[] +[] +type IApiWithAbsoluteBasePath = + // Example where we use the bundled attributes rather than RestEase's + [] + abstract GetPathParam : [] parameter : string * ?cancellationToken : CancellationToken -> Task + +[] +[] +[] +type IApiWithAbsoluteBasePathAndAddress = + [] + abstract GetPathParam : [] parameter : string * ?ct : CancellationToken -> Task + +[] +[] +type IApiWithBasePathAndAbsoluteEndpoint = + // Example where we use the bundled attributes rather than RestEase's + [] + abstract GetPathParam : [] parameter : string * ?cancellationToken : CancellationToken -> Task + +[] +[] +[] +type IApiWithBasePathAndAddressAndAbsoluteEndpoint = + [] + abstract GetPathParam : [] parameter : string * ?ct : CancellationToken -> Task + +[] +[] +type IApiWithAbsoluteBasePathAndAbsoluteEndpoint = + // Example where we use the bundled attributes rather than RestEase's + [] + abstract GetPathParam : [] parameter : string * ?cancellationToken : CancellationToken -> Task + +[] +[] +[] +type IApiWithAbsoluteBasePathAndAddressAndAbsoluteEndpoint = + [] + abstract GetPathParam : [] parameter : string * ?ct : CancellationToken -> Task + +[] +[] +type IApiWithHeaders = + [
] + abstract SomeHeader : string + + [
] + abstract SomeOtherHeader : int + + [] + [] + abstract GetPathParam : [] parameter : string * ?ct : CancellationToken -> Task + +[] +[] +type IApiWithHeaders2 = + [
] + abstract SomeHeader : string + + [
] + abstract SomeOtherHeader : int + + [] + abstract GetPathParam : [] parameter : string * ?ct : CancellationToken -> Task diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Consumer/Vault.fs b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Consumer/Vault.fs new file mode 100644 index 0000000..de07169 --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Consumer/Vault.fs @@ -0,0 +1,53 @@ +namespace ConsumePlugin + +open System +open System.Collections.Generic +open System.Text.Json.Serialization +open System.Threading +open System.Threading.Tasks +open WoofWare.Whippet.Plugin.Json +open WoofWare.Whippet.Plugin.HttpClient +open RestEase + +[] +type IVaultClient = + [] + abstract GetSecret : + jwt : JwtVaultResponse * + [] path : string * + [] mountPoint : string * + ?ct : CancellationToken -> + Task + + [] + abstract GetJwt : role : string * jwt : string * ?ct : CancellationToken -> Task + +[] +type IVaultClientNonExtensionMethod = + [] + abstract GetSecret : + jwt : JwtVaultResponse * + [] path : string * + [] mountPoint : string * + ?ct : CancellationToken -> + Task + + [] + abstract GetJwt : role : string * jwt : string * ?ct : CancellationToken -> Task + +[] +type IVaultClientExtensionMethod = + [] + abstract GetSecret : + jwt : JwtVaultResponse * + [] path : string * + [] mountPoint : string * + ?ct : CancellationToken -> + Task + + [] + abstract GetJwt : role : string * jwt : string * ?ct : CancellationToken -> Task + +[] +type VaultClientExtensionMethod = + static member thisClashes = 99 diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Consumer/VaultDto.fs b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Consumer/VaultDto.fs new file mode 100644 index 0000000..1e61588 --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Consumer/VaultDto.fs @@ -0,0 +1,64 @@ +namespace ConsumePlugin + +open System +open System.Collections.Generic +open System.Text.Json.Serialization +open WoofWare.Whippet.Plugin.Json +open WoofWare.Whippet.Plugin.HttpClient + +[] +type JwtVaultAuthResponse = + { + [] + ClientToken : string + Accessor : string + Policies : string list + [] + TokenPolicies : string list + [] + IdentityPolicies : string list + [] + LeaseDuration : int + Renewable : bool + [] + TokenType : string + [] + EntityId : string + Orphan : bool + [] + NumUses : int + } + +[] +type JwtVaultResponse = + { + [] + RequestId : string + [] + LeaseId : string + Renewable : bool + [] + LeaseDuration : int + Auth : JwtVaultAuthResponse + } + +[] +type JwtSecretResponse = + { + [] + RequestId : string + [] + LeaseId : string + Renewable : bool + [] + LeaseDuration : int + Data : IReadOnlyDictionary + // These ones aren't actually part of the Vault response, but are here for tests + Data2 : IDictionary + Data3 : Dictionary + Data4 : Map + Data5 : IReadOnlyDictionary + Data6 : IDictionary + Data7 : Map + Data8 : Dictionary + } diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Consumer/WoofWare.Whippet.Plugin.HttpClient.Consumer.fsproj b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Consumer/WoofWare.Whippet.Plugin.HttpClient.Consumer.fsproj new file mode 100644 index 0000000..374274f --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient.Consumer/WoofWare.Whippet.Plugin.HttpClient.Consumer.fsproj @@ -0,0 +1,42 @@ + + + + net8.0 + false + + + + + + + PureGymDto.fs + + + + RestApiExample.fs + + + + VaultDto.fs + + + + Vault.fs + + + + + + + + + + + + + + + + + + diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/DesiredGenerator.fs b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/DesiredGenerator.fs new file mode 100644 index 0000000..e95eb98 --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/DesiredGenerator.fs @@ -0,0 +1,11 @@ +namespace WoofWare.Whippet.Plugin.HttpClient + +type internal DesiredGenerator = + | HttpClient of extensionMethod : bool option + + static member Parse (s : string) = + match s with + | "HttpClient" -> DesiredGenerator.HttpClient None + | "HttpClient(true)" -> DesiredGenerator.HttpClient (Some true) + | "HttpClient(false)" -> DesiredGenerator.HttpClient (Some false) + | _ -> failwith $"Failed to parse as a generator specification: %s{s}" diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/HttpClientGenerator.fs b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/HttpClientGenerator.fs new file mode 100644 index 0000000..805ea26 --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/HttpClientGenerator.fs @@ -0,0 +1,1008 @@ +namespace WoofWare.Whippet.Plugin.HttpClient + +open System +open System.IO +open System.Net.Http +open Fantomas.FCS.Syntax +open WoofWare.Whippet.Fantomas +open WoofWare.Whippet.Core +open WoofWare.Whippet.Plugin.Json + +type internal HttpClientGeneratorOutputSpec = + { + ExtensionMethods : bool + } + +[] +module internal HttpClientGenerator = + open Fantomas.FCS.Text.Range + + let outputFile = FileInfo "/tmp/output.txt" + + // do + // use _ = File.Create outputFile.FullName + // () + + let log (line : string) = + // use w = outputFile.AppendText () + // w.WriteLine line + () + + [] + type PathSpec = + | Verbatim of string + | MatchArgName + + type HttpAttribute = + // TODO: Format parameter to these attrs + | Query of string option + | Path of PathSpec + | Body + + type Parameter = + { + Attributes : HttpAttribute list + IsOptional : bool + Id : Ident option + Type : SynType + } + + [] + type BodyParamMethods = + | StringContent + | StreamContent + | ByteArrayContent + | HttpContent + | Serialise of SynType + + override this.ToString () = + match this with + | BodyParamMethods.Serialise _ -> "ToString" + | BodyParamMethods.ByteArrayContent -> "ByteArrayContent" + | BodyParamMethods.StringContent -> "StringContent" + | BodyParamMethods.StreamContent -> "StreamContent" + | BodyParamMethods.HttpContent -> "HttpContent" + + type MemberInfo = + { + /// E.g. HttpMethod.Get + HttpMethod : HttpMethod + /// E.g. SynExpr.Const "v1/gyms/{gym_id}/attendance" + UrlTemplate : SynExpr + TaskReturnType : SynType + Args : Parameter list + Identifier : Ident + EnsureSuccessHttpCode : bool + BaseAddress : SynExpr option + BasePath : SynExpr option + Accessibility : SynAccess option + /// Headers which apply *only* to this endpoint. + /// For example, SynConst "Authorization" and SynConst "token BLAH". + Headers : (SynExpr * SynExpr) list + } + + let httpMethodString (m : HttpMethod) : string = + if m = HttpMethod.Get then "Get" + elif m = HttpMethod.Post then "Post" + elif m = HttpMethod.Delete then "Delete" + elif m = HttpMethod.Patch then "Post" + elif m = HttpMethod.Options then "Options" + elif m = HttpMethod.Head then "Head" + elif m = HttpMethod.Put then "Put" + elif m = HttpMethod.Trace then "Trace" + else failwith $"Unrecognised method: %+A{m}" + + /// E.g. converts `[]` to (HttpMethod.Get, SynExpr.Const "blah") + let extractHttpInformation (mem : Ident) (attrs : SynAttribute list) : HttpMethod * SynExpr = + let matchingAttrs = + attrs + |> List.choose (fun attr -> + match attr.TypeName.LongIdent |> List.last |> _.idText with + | "Get" + | "GetAttribute" -> Some (HttpMethod.Get, attr.ArgExpr) + | "Post" + | "PostAttribute" -> Some (HttpMethod.Post, attr.ArgExpr) + | "Put" + | "PutAttribute" -> Some (HttpMethod.Put, attr.ArgExpr) + | "Delete" + | "DeleteAttribute" -> Some (HttpMethod.Delete, attr.ArgExpr) + | "Head" + | "HeadAttribute" -> Some (HttpMethod.Head, attr.ArgExpr) + | "Options" + | "OptionsAttribute" -> Some (HttpMethod.Options, attr.ArgExpr) + | "Patch" + | "PatchAttribute" -> Some (HttpMethod.Patch, attr.ArgExpr) + | "Trace" + | "TraceAttribute" -> Some (HttpMethod.Trace, attr.ArgExpr) + | _ -> None + ) + + match matchingAttrs with + | [ (meth, arg) ] -> meth, arg + | [] -> + let allAttrs = + attrs + |> List.map (fun attr -> attr.TypeName.LongIdent |> List.map _.idText |> String.concat ".") + |> String.concat "\n" + + failwith + $"Required exactly one recognised RestEase attribute on member %s{mem.idText}, but got none.\n%s{allAttrs}" + | matchingAttrs -> + failwith + $"Required exactly one recognised RestEase attribute on member %s{mem.idText}, but got %i{matchingAttrs.Length}" + + /// Get the args associated with the Header attributes within the list. + let extractHeaderInformation (attrs : SynAttribute list) : SynExpr list list = + attrs + |> List.choose (fun attr -> + match attr.TypeName.LongIdent |> List.last |> _.idText with + | "Header" + | "HeaderAttribute" -> + match attr.ArgExpr with + | SynExpr.Paren (SynExpr.Tuple (_, [ v1 ; v2 ], _, _), _, _, _) -> + Some [ SynExpr.stripOptionalParen v1 ; SynExpr.stripOptionalParen v2 ] + | e -> Some [ SynExpr.stripOptionalParen e ] + | _ -> None + ) + + let shouldAllowAnyStatusCode (attrs : SynAttribute list) : bool = + attrs + |> List.exists (fun attr -> + match SynLongIdent.toString attr.TypeName with + | "AllowAnyStatusCode" + | "AllowAnyStatusCodeAttribute" + | "RestEase.AllowAnyStatusCode" + | "RestEase.AllowAnyStatusCodeAttribute" -> true + | _ -> false + ) + + /// constantHeaders are a list of (headerName, headerValue) + /// variableHeaders are a list of (headerName, selfPropertyToGetValueOf) + let constructMember + (constantHeaders : (SynExpr * SynExpr) list) + (variableHeaders : (SynExpr * Ident) list) + (info : MemberInfo) + : SynMemberDefn + = + let args = + info.Args + |> List.map (fun arg -> + let argName = + match arg.Id with + | None -> failwith "TODO: create an arg name" + | Some id -> id + + let argType = + if arg.IsOptional then + SynType.appPostfix "option" arg.Type + else + arg.Type + + // We'll be tupling these up anyway, so don't need the parens + // around the type annotations. + argName, SynPat.annotateTypeNoParen argType (SynPat.namedI argName) + ) + + let cancellationTokenArg = + match List.tryLast args with + | None -> failwith $"expected an optional cancellation token as final arg in %s{info.Identifier.idText}" + | Some (arg, _) -> arg + + let requestUriTrailer = + (info.UrlTemplate, info.Args) + ||> List.fold (fun template arg -> + (template, arg.Attributes) + ||> List.fold (fun template attr -> + match attr with + | HttpAttribute.Path spec -> + let varName = + match arg.Id with + | None -> failwith "TODO: anonymous args" + | Some id -> id + + let substituteId = + match spec with + | PathSpec.Verbatim s -> s + | PathSpec.MatchArgName -> varName.idText + + template + |> SynExpr.callMethodArg + "Replace" + (SynExpr.tuple + [ + SynExpr.CreateConst ("{" + substituteId + "}") + SynExpr.callMethod "ToString" (SynExpr.createIdent' varName) + |> SynExpr.pipeThroughFunction ( + SynExpr.createLongIdent [ "System" ; "Uri" ; "EscapeDataString" ] + ) + ]) + | _ -> template + ) + ) + + /// List of (query-param-key, parameter-which-provides-value) + let queryParams = + info.Args + |> List.collect (fun arg -> + arg.Attributes + |> List.choose (fun attr -> + match attr with + | Query None -> + let name = + match arg.Id with + | None -> + failwith + "Expected a name for the argument we're trying to use as an anonymous query parameter" + | Some name -> name.idText + + Some (name, arg) + | Query (Some name) -> Some (name, arg) + | _ -> None + ) + ) + + let requestUriTrailer = + match queryParams with + | [] -> requestUriTrailer + | (firstKey, firstValue) :: queryParams -> + let firstValueId = + match firstValue.Id with + | None -> failwith "Unable to get parameter variable name from anonymous parameter" + | Some id -> id + + let urlSeparator = + let questionMark = SynExpr.CreateConst '?' + + let containsQuestion = + info.UrlTemplate + |> SynExpr.callMethodArg "IndexOf" questionMark + |> SynExpr.greaterThanOrEqual (SynExpr.CreateConst 0) + + SynExpr.ifThenElse containsQuestion (SynExpr.CreateConst "?") (SynExpr.CreateConst "&") + |> SynExpr.paren + + let prefix = + SynExpr.createIdent' firstValueId + |> SynExpr.toString firstValue.Type + |> SynExpr.paren + |> SynExpr.pipeThroughFunction (SynExpr.createLongIdent [ "System" ; "Uri" ; "EscapeDataString" ]) + |> SynExpr.paren + |> SynExpr.plus (SynExpr.plus urlSeparator (SynExpr.CreateConst (firstKey + "="))) + + (prefix, queryParams) + ||> List.fold (fun uri (paramKey, paramValue) -> + let paramValueId = + match paramValue.Id with + | None -> failwith "Unable to get parameter variable name from anonymous parameter" + | Some id -> id + + SynExpr.toString paramValue.Type (SynExpr.createIdent' paramValueId) + |> SynExpr.paren + |> SynExpr.pipeThroughFunction (SynExpr.createLongIdent [ "System" ; "Uri" ; "EscapeDataString" ]) + |> SynExpr.paren + |> SynExpr.plus (SynExpr.plus uri (SynExpr.CreateConst ("&" + paramKey + "="))) + ) + |> SynExpr.plus requestUriTrailer + |> SynExpr.paren + + let requestUri = + let uriIdent = SynExpr.createLongIdent [ "System" ; "Uri" ] + + let baseAddress = SynExpr.createLongIdent [ "client" ; "BaseAddress" ] + + let baseAddress = + [ + SynMatchClause.create + SynPat.createNull + (match info.BaseAddress with + | None -> + [ + SynExpr.applyFunction (SynExpr.createIdent "nameof") (SynExpr.paren baseAddress) + SynExpr.CreateConst + "No base address was supplied on the type, and no BaseAddress was on the HttpClient." + ] + |> SynExpr.tuple + |> SynExpr.applyFunction (SynExpr.createLongIdent [ "System" ; "ArgumentNullException" ]) + |> SynExpr.paren + |> SynExpr.applyFunction (SynExpr.createIdent "raise") + | Some expr -> SynExpr.applyFunction uriIdent expr) + SynMatchClause.create (SynPat.named "v") (SynExpr.createIdent "v") + ] + |> SynExpr.createMatch baseAddress + |> SynExpr.paren + + let baseAddress = + match info.BasePath with + | None -> baseAddress + | Some basePath -> + [ + yield baseAddress + + yield + SynExpr.applyFunction + uriIdent + (SynExpr.tuple + [ basePath ; SynExpr.createLongIdent [ "System" ; "UriKind" ; "Relative" ] ]) + ] + |> SynExpr.tuple + |> SynExpr.applyFunction uriIdent + + [ + yield baseAddress + + yield + SynExpr.applyFunction + uriIdent + (SynExpr.tuple + [ + requestUriTrailer + SynExpr.createLongIdent [ "System" ; "UriKind" ; "Relative" ] + ]) + ] + |> SynExpr.tuple + |> SynExpr.applyFunction uriIdent + + let bodyParams = + info.Args + |> List.collect (fun arg -> + arg.Attributes + |> List.choose (fun attr -> + match attr with + | HttpAttribute.Body -> Some arg + | _ -> None + ) + ) + + let bodyParam = + match bodyParams with + | [] -> None + | [ x ] -> + // TODO: body serialisation method + let paramName = + match x.Id with + | None -> failwith "Anonymous [] parameter is unsupported" + | Some id -> id + + match x.Type with + | Stream -> Some (BodyParamMethods.StreamContent, paramName) + | String -> Some (BodyParamMethods.StringContent, paramName) + | ArrayType Byte -> Some (BodyParamMethods.ByteArrayContent, paramName) + | HttpContent -> Some (BodyParamMethods.HttpContent, paramName) + | ty -> Some (BodyParamMethods.Serialise ty, paramName) + | _ -> failwith "You can only have at most one [] parameter on a method." + + let httpReqMessageConstructor = + [ + SynExpr.equals + (SynExpr.createIdent "Method") + (SynExpr.createLongIdent + [ "System" ; "Net" ; "Http" ; "HttpMethod" ; httpMethodString info.HttpMethod ]) + SynExpr.equals (SynExpr.createIdent "RequestUri") (SynExpr.createIdent "uri") + ] + |> SynExpr.tupleNoParen + + let returnExpr = + match info.TaskReturnType with + | HttpResponseMessage -> SynExpr.createIdent "response" + | String -> SynExpr.createIdent "responseString" + | Stream -> SynExpr.createIdent "responseStream" + | RestEaseResponseType contents -> + let deserialiser = + JsonParseGenerator.parseNode + None + JsonParseGenerator.JsonParseOption.None + contents + (SynExpr.createIdent "jsonNode") + |> SynExpr.paren + |> SynExpr.createThunk + + // new RestEase.Response (content : string, response : HttpResponseMessage, deserialiser : unit -> 'T) + SynExpr.createNew + (SynType.app' (SynType.createLongIdent' [ "RestEase" ; "Response" ]) [ SynType.Anon range0 ]) + (SynExpr.tupleNoParen + [ + SynExpr.createIdent "responseString" + SynExpr.createIdent "response" + deserialiser + ]) + | retType -> + JsonParseGenerator.parseNode + None + JsonParseGenerator.JsonParseOption.None + retType + (SynExpr.createIdent "jsonNode") + + let contentTypeHeader, memberHeaders = + info.Headers + |> List.partition (fun (headerName, headerValue) -> + match headerName |> SynExpr.stripOptionalParen with + | SynExpr.Const (SynConst.String ("Content-Type", _, _), _) -> true + | _ -> false + ) + + let contentTypeHeader = + match contentTypeHeader with + | [] -> None + | [ _, ct ] -> Some (SynExpr.stripOptionalParen ct) + | _ -> failwith "Unexpectedly got multiple Content-Type headers" + + let createStringContent (contents : SynExpr) = + SynExpr.createNew + (SynType.createLongIdent' [ "System" ; "Net" ; "Http" ; "StringContent" ]) + (SynExpr.tupleNoParen + [ + yield contents + match contentTypeHeader with + | None -> () + | Some ch -> + yield SynExpr.createNull () + // Sigh, Gitea in particular passes "json" here + match ch with + | SynExpr.Const (SynConst.String ("json", _, _), _) -> + yield SynExpr.CreateConst "application/json" + | SynExpr.Const (SynConst.String ("html", _, _), _) -> yield SynExpr.CreateConst "text/html" + | _ -> yield ch + ]) + + let handleBodyParams = + match bodyParam with + | None -> [] + | Some (bodyParamType, bodyParamName) -> + match bodyParamType with + | BodyParamMethods.StringContent -> + [ + Let ("queryParams", createStringContent (SynExpr.createIdent' bodyParamName)) + Do ( + SynExpr.assign + (SynLongIdent.createS' [ "httpMessage" ; "Content" ]) + (SynExpr.createIdent "queryParams") + ) + ] + | BodyParamMethods.StreamContent + | BodyParamMethods.ByteArrayContent -> + [ + Let ( + "queryParams", + SynExpr.createNew + (SynType.createLongIdent' + [ "System" ; "Net" ; "Http" ; (bodyParamType : BodyParamMethods).ToString () ]) + (SynExpr.createIdent' bodyParamName) + ) + Do ( + SynExpr.assign + (SynLongIdent.createS' [ "httpMessage" ; "Content" ]) + (SynExpr.createIdent "queryParams") + ) + ] + | BodyParamMethods.HttpContent -> + [ + Do ( + SynExpr.assign + (SynLongIdent.createS' [ "httpMessage" ; "Content" ]) + (SynExpr.createIdent' bodyParamName) + ) + ] + | BodyParamMethods.Serialise ty -> + [ + Let ( + "queryParams", + createStringContent ( + SynExpr.createIdent' bodyParamName + |> SynExpr.pipeThroughFunction (fst (JsonSerializeGenerator.serializeNode ty)) + |> SynExpr.pipeThroughFunction ( + SynExpr.createLambda + "node" + (SynExpr.ifThenElse + (SynExpr.applyFunction + (SynExpr.createIdent "isNull") + (SynExpr.createIdent "node")) + (SynExpr.applyFunction + (SynExpr.createLongIdent [ "node" ; "ToJsonString" ]) + (SynExpr.CreateConst ())) + (SynExpr.CreateConst "null")) + ) + ) + ) + Do ( + SynExpr.assign + (SynLongIdent.createS' [ "httpMessage" ; "Content" ]) + (SynExpr.createIdent "queryParams") + ) + ] + + let implementation = + let responseString = + LetBang ( + "responseString", + SynExpr.awaitTask ( + SynExpr.applyFunction + (SynExpr.createLongIdent [ "response" ; "Content" ; "ReadAsStringAsync" ]) + (SynExpr.createIdent "ct") + ) + ) + + let responseStream = + LetBang ( + "responseStream", + SynExpr.awaitTask ( + SynExpr.applyFunction + (SynExpr.createLongIdent [ "response" ; "Content" ; "ReadAsStreamAsync" ]) + (SynExpr.createIdent "ct") + ) + ) + + let jsonNode = + LetBang ( + "jsonNode", + SynExpr.awaitTask ( + SynExpr.applyFunction + (SynExpr.createLongIdent + [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonNode" ; "ParseAsync" ]) + (SynExpr.tuple + [ + SynExpr.createIdent "responseStream" + SynExpr.equals (SynExpr.createIdent "cancellationToken") (SynExpr.createIdent "ct") + ]) + ) + ) + + let setVariableHeaders = + variableHeaders + |> List.map (fun (headerName, callToGetValue) -> + [ + headerName + SynExpr.applyFunction + (SynExpr.createLongIdent' + [ Ident.create "this" ; callToGetValue ; Ident.create "ToString" ]) + (SynExpr.CreateConst ()) + ] + |> SynExpr.tuple + |> SynExpr.applyFunction (SynExpr.createLongIdent [ "httpMessage" ; "Headers" ; "Add" ]) + |> Do + ) + + let setConstantHeaders = + constantHeaders + |> List.map (fun (headerName, headerValue) -> + SynExpr.applyFunction + (SynExpr.createLongIdent [ "httpMessage" ; "Headers" ; "Add" ]) + (SynExpr.tuple [ headerName ; headerValue ]) + |> Do + ) + + let setMemberHeaders = + memberHeaders + |> List.map (fun (headerName, headerValue) -> + // Best-effort: assume this is a message header. + SynExpr.applyFunction + (SynExpr.createLongIdent [ "httpMessage" ; "Headers" ; "Add" ]) + (SynExpr.tuple [ headerName ; headerValue ]) + |> Do + ) + + [ + yield LetBang ("ct", SynExpr.createLongIdent [ "Async" ; "CancellationToken" ]) + yield Let ("uri", requestUri) + yield + Use ( + "httpMessage", + SynExpr.createNew + (SynType.createLongIdent' [ "System" ; "Net" ; "Http" ; "HttpRequestMessage" ]) + httpReqMessageConstructor + ) + + yield! handleBodyParams + + yield! setVariableHeaders + yield! setConstantHeaders + yield! setMemberHeaders + + yield + LetBang ( + "response", + SynExpr.awaitTask ( + SynExpr.applyFunction + (SynExpr.createLongIdent [ "client" ; "SendAsync" ]) + (SynExpr.tuple [ SynExpr.createIdent "httpMessage" ; SynExpr.createIdent "ct" ]) + ) + ) + if info.EnsureSuccessHttpCode then + yield + Let ( + "response", + SynExpr.applyFunction + (SynExpr.createLongIdent [ "response" ; "EnsureSuccessStatusCode" ]) + (SynExpr.CreateConst ()) + ) + match info.TaskReturnType with + | HttpResponseMessage -> () + | RestEaseResponseType _ -> + yield responseString + yield responseStream + yield jsonNode + | String -> yield responseString + | Stream -> yield responseStream + | UnitType -> + // What we're returning doesn't depend on the content, so don't bother! + () + | _ -> + yield responseStream + yield jsonNode + ] + |> SynExpr.createCompExpr "async" returnExpr + |> SynExpr.startAsTask cancellationTokenArg + + let thisIdent = + if variableHeaders.IsEmpty then "_" else "this" + |> Ident.create + + let args = args |> List.map snd |> SynPat.tuple |> List.singleton + + SynBinding.basic [ thisIdent ; info.Identifier ] args implementation + |> SynBinding.withAccessibility info.Accessibility + |> SynMemberDefn.memberImplementation + + let getHttpAttributes (attrs : SynAttribute list) : HttpAttribute list = + attrs + |> List.choose (fun attr -> + match attr.TypeName.LongIdent |> List.last |> _.idText with + | "Query" + | "QueryAttribute" -> + match attr.ArgExpr with + | SynExpr.Const (SynConst.Unit, _) -> Some (HttpAttribute.Query None) + | SynExpr.Const (SynConst.String (s, SynStringKind.Regular, _), _) -> + Some (HttpAttribute.Query (Some s)) + | SynExpr.Const (a, _) -> failwith $"unrecognised constant arg to the Query attribute: %+A{a}" + | _ -> None + | "Path" + | "PathAttribute" -> + match attr.ArgExpr |> SynExpr.stripOptionalParen with + | SynExpr.Const (SynConst.String (s, SynStringKind.Regular, _), _) -> + Some (HttpAttribute.Path (PathSpec.Verbatim s)) + | SynExpr.Const (SynConst.Unit, _) -> Some (HttpAttribute.Path PathSpec.MatchArgName) + | SynExpr.Const (a, _) -> failwith $"unrecognised constant arg to the Path attribute: %+A{a}" + | _ -> None + | "Body" + | "BodyAttribute" -> + match attr.ArgExpr with + | SynExpr.Const (SynConst.Unit, _) -> Some HttpAttribute.Body + | SynExpr.Const (a, _) -> failwith $"unrecognised constant arg to the Body attribute: %+A{a}" + | _ -> None + | _ -> None + ) + + let extractBasePath (attrs : SynAttribute list) : SynExpr option = + attrs + |> List.tryPick (fun attr -> + match attr.TypeName.LongIdent |> List.last |> _.idText with + | "BasePath" + | "BasePathAttribute" -> Some attr.ArgExpr + | _ -> None + ) + + let extractBaseAddress (attrs : SynAttribute list) : SynExpr option = + attrs + |> List.tryPick (fun attr -> + match attr.TypeName.LongIdent |> List.last |> _.idText with + | "BaseAddress" + | "BaseAddressAttribute" -> Some attr.ArgExpr + | _ -> None + ) + + let insertTrailingSlash (path : SynExpr) : SynExpr = + match path |> SynExpr.stripOptionalParen with + | SynExpr.Const (SynConst.String (s, _, _), _) -> + if s.EndsWith '/' then + path + else + SynExpr.CreateConst (s + "/") + | _ -> SynExpr.plus (SynExpr.paren path) (SynExpr.CreateConst "/") + + let createModule + (opens : SynOpenDeclTarget list) + (ns : LongIdent) + (interfaceType : SynTypeDefn, spec : HttpClientGeneratorOutputSpec) + : SynModuleOrNamespace + = + let interfaceType = AstHelper.parseInterface interfaceType + + if not (List.isEmpty interfaceType.Inherits) then + failwith + "HttpClientGenerator does not support inheritance. Remove the `inherit` keyword if you want to use this generator." + + let constantHeaders = + interfaceType.Attributes + |> extractHeaderInformation + |> List.map (fun exprs -> + match exprs with + | [ key ; value ] -> key, value + | [] -> + failwith + "Expected constant header parameters to be of the form [
], but got no args" + | [ _ ] -> + failwith + "Expected constant header parameters to be of the form [
], but got only one arg" + | _ -> + failwith + "Expected constant header parameters to be of the form [
], but got more than two args" + ) + + let baseAddress = + extractBaseAddress interfaceType.Attributes + // We artificially insert a trailing slash because this is almost certainly + // not meant to be an endpoint itself. + |> Option.map insertTrailingSlash + + let basePath = + extractBasePath interfaceType.Attributes + // We artificially insert a trailing slash because this is almost certainly + // not meant to be an endpoint itself. + |> Option.map insertTrailingSlash + + let properties = + interfaceType.Properties + |> List.map (fun pi -> + let headerInfo = + match extractHeaderInformation pi.Attributes with + | [ [ x ] ] -> x + | [ _ ] -> + failwith + "Expected exactly one Header parameter on the member, with exactly one arg; got one Header parameter with non-1-many args" + | [] -> + failwith + "Expected exactly one Header parameter on the member, with exactly one arg; got no Header parameters" + | _ -> + failwith + "Expected exactly one Header parameter on the member, with exactly one arg; got multiple Header parameters" + + headerInfo, pi + ) + + let nonPropertyMembers = + let properties = properties |> List.map (fun (header, pi) -> header, pi.Identifier) + + interfaceType.Members + |> List.map (fun mem -> + let httpMethod, url = extractHttpInformation mem.Identifier mem.Attributes + + let specificHeaders = + extractHeaderInformation mem.Attributes + |> List.map (fun l -> + match l with + | [ x ; y ] -> x, y + | _ -> + failwith + $"Expected Header attribute on member %s{mem.Identifier.idText} to have exactly two arguments." + ) + + let shouldEnsureSuccess = not (shouldAllowAnyStatusCode mem.Attributes) + + let returnType = + match mem.ReturnType with + | Task ty -> ty + | a -> failwith $"Method must return a generic Task; returned %+A{a}" + + if mem.IsMutable then + failwith $"mutable methods not supported (identifier: %+A{mem.Identifier})" + + if mem.IsInline then + failwith $"inline methods not supported (identifier: %+A{mem.Identifier})" + + let args = + match mem.Args with + | [ args ] -> + args.Args + |> List.map (fun arg -> + { + Attributes = arg.Attributes |> getHttpAttributes + IsOptional = arg.IsOptional + Id = arg.Id + Type = arg.Type + } + ) + | [] -> failwith $"Expected %+A{mem.Identifier} to have tupled args, but it had no args." + | _ -> + failwith + $"Expected %+A{mem.Identifier} to have tupled args, but it was curried: %+A{mem.Args}." + + { + HttpMethod = httpMethod + UrlTemplate = url + TaskReturnType = returnType + Args = args + Identifier = mem.Identifier + EnsureSuccessHttpCode = shouldEnsureSuccess + BaseAddress = baseAddress + BasePath = basePath + Accessibility = mem.Accessibility + Headers = specificHeaders + } + ) + |> List.map (constructMember constantHeaders properties) + + let propertyMembers = + properties + |> List.map (fun (_, pi) -> + SynExpr.createLongIdent' [ Ident.lowerFirstLetter pi.Identifier ] + |> SynExpr.applyTo (SynExpr.CreateConst ()) + |> SynBinding.basic [ Ident.create "_" ; pi.Identifier ] [] + |> SynBinding.withReturnAnnotation pi.Type + |> SynBinding.setInline pi.IsInline + |> SynBinding.withAccessibility pi.Accessibility + |> SynMemberDefn.memberImplementation + ) + + let members = propertyMembers @ nonPropertyMembers + + let docString = + (if spec.ExtensionMethods then + "Extension methods" + else + "Module") + |> sprintf "%s for constructing a REST client." + |> PreXmlDoc.create + + let interfaceImpl = + SynExpr.ObjExpr ( + SynType.createLongIdent interfaceType.Name, + None, + Some range0, + [], + members, + [], + range0, + range0 + ) + + let headerArgs = + properties + |> List.map (fun (_, pi) -> + SynPat.namedI (Ident.lowerFirstLetter pi.Identifier) + |> SynPat.annotateType (SynType.funFromDomain (SynType.named "unit") pi.Type) + ) + + let clientCreationArg = + SynPat.named "client" + |> SynPat.annotateType (SynType.createLongIdent' [ "System" ; "Net" ; "Http" ; "HttpClient" ]) + + let xmlDoc = + if properties.IsEmpty then + "Create a REST client." + else + "Create a REST client. The input functions will be re-evaluated on every HTTP request to obtain the required values for the corresponding header properties." + |> PreXmlDoc.create + + let returnInfo = SynType.createLongIdent interfaceType.Name + + let nameWithoutLeadingI = + List.last interfaceType.Name + |> _.idText + |> fun s -> + if s.StartsWith 'I' then + s.Substring 1 + else + failwith $"Expected interface type to start with 'I', but was: %s{s}" + + let createFunc = + if spec.ExtensionMethods then + let binding = + SynBinding.basic [ Ident.create "make" ] (headerArgs @ [ clientCreationArg ]) interfaceImpl + |> SynBinding.withXmlDoc xmlDoc + |> SynBinding.withReturnAnnotation returnInfo + |> SynMemberDefn.staticMember + + let componentInfo = + SynComponentInfo.createLong interfaceType.Name + |> SynComponentInfo.withDocString (PreXmlDoc.create "Extension methods for HTTP clients") + + let containingType = + SynTypeDefnRepr.augmentation () + |> SynTypeDefn.create componentInfo + |> SynTypeDefn.withMemberDefns [ binding ] + + SynModuleDecl.createTypes [ containingType ] + + else + SynBinding.basic [ Ident.create "make" ] (headerArgs @ [ clientCreationArg ]) interfaceImpl + |> SynBinding.withXmlDoc xmlDoc + |> SynBinding.withReturnAnnotation returnInfo + |> SynModuleDecl.createLet + + let moduleName = + if spec.ExtensionMethods then + Ident.create (nameWithoutLeadingI + "HttpClientExtension") + else + Ident.create nameWithoutLeadingI + + let attribs = + if spec.ExtensionMethods then + [ SynAttribute.autoOpen ] + else + [ SynAttribute.compilationRepresentation ; SynAttribute.requireQualifiedAccess ] + + let modInfo = + SynComponentInfo.create moduleName + |> SynComponentInfo.withDocString docString + |> SynComponentInfo.addAttributes attribs + |> SynComponentInfo.setAccessibility interfaceType.Accessibility + + [ + for openStatement in opens do + yield SynModuleDecl.openAny openStatement + yield SynModuleDecl.nestedModule modInfo [ createFunc ] + ] + |> SynModuleOrNamespace.createNamespace ns + +/// Whippet generator that provides an HTTP client for an interface type using RestEase annotations. +[] +type HttpClientGenerator () = + + 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 (System.Text.Encoding.UTF8.GetString context.FileContents) + + let types = Ast.getTypes ast + + let opens = AstHelper.extractOpens ast + + let namespaceAndTypes = + types + |> 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.HttpClient arg -> + let spec = + { + ExtensionMethods = + arg + |> Option.defaultValue + HttpClientAttribute.DefaultIsExtensionMethod + } + + Some (typeDef, spec) + ) + | _ -> None + | Some attr -> + let arg = + match SynExpr.stripOptionalParen attr.ArgExpr with + | SynExpr.Const (SynConst.Bool value, _) -> value + | SynExpr.Const (SynConst.Unit, _) -> HttpClientAttribute.DefaultIsExtensionMethod + | arg -> + failwith + $"Unrecognised argument %+A{arg} to [<%s{nameof HttpClientAttribute}>]. 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 (HttpClientGenerator.createModule opens ns)) + + Ast.render modules |> Option.toObj diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/README.md b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/README.md new file mode 100644 index 0000000..220e9bd --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/README.md @@ -0,0 +1,130 @@ +# WoofWare.Whippet.Plugin.HttpClient + +This is a [Whippet](https://github.com/Smaug123/WoofWare.Whippet) plugin for defining [RestEase](https://github.com/canton7/RestEase)-style HTTP clients. + +It is a copy of the corresponding [Myriad](https://github.com/MoiraeSoftware/myriad) HttpClient plugin in [WoofWare.Myriad](https://github.com/Smaug123/WoofWare.Myriad), taken from commit d59ebdfccb87a06579fb99008a15f58ea8be394e. + +## Usage + +Define a file like `Client.fs`: + +```fsharp +open System.Threading.Tasks +open WoofWare.Whippet.Plugin.HttpClient + +[] +type IPureGymApi = + [] + abstract GetGyms : ?ct : CancellationToken -> Task + + [] + abstract GetGymAttendance : [] gymId : int * ?ct : CancellationToken -> Task + + [] + abstract GetMember : ?ct : CancellationToken -> Task + + [] + abstract GetGym : [] gymId : int * ?ct : CancellationToken -> Task + + [] + abstract GetMemberActivity : ?ct : CancellationToken -> Task + + [] + abstract GetSessions : + [] fromDate : DateTime * [] toDate : DateTime * ?ct : CancellationToken -> Task +``` + +In your fsproj: + +```xml + + + + + Client.fs + + + + + + + + + + + +``` + +The generator produces a type like this (here I'm showing the `isExtensionMethod = false` version): + +```fsharp +/// Module for constructing a REST client. +[] +[] +module PureGymApi = + /// Create a REST client. + let make (client : System.Net.Http.HttpClient) : IPureGymApi = + { new IPureGymApi with + member _.GetGyms (ct : CancellationToken option) = + async { + let! ct = Async.CancellationToken + + let httpMessage = + new System.Net.Http.HttpRequestMessage ( + Method = System.Net.Http.HttpMethod.Get, + RequestUri = System.Uri (client.BaseAddress.ToString () + "v1/gyms/") + ) + + let! response = client.SendAsync (httpMessage, ct) |> Async.AwaitTask + let response = response.EnsureSuccessStatusCode () + let! stream = response.Content.ReadAsStreamAsync ct |> Async.AwaitTask + + let! node = + System.Text.Json.Nodes.JsonNode.ParseAsync (stream, cancellationToken = ct) + |> Async.AwaitTask + + return node.AsArray () |> Seq.map (fun elt -> Gym.jsonParse elt) |> List.ofSeq + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + + // (more methods here) + } +``` + +You tell the generator to generate a client using the `[]` attribute. +You may instead choose to define an attribute with the correct name yourself (if you don't want to take a dependency on the `WoofWare.Whippet.Plugin.RestEase.Attributes` package), +and use the RestEase attributes directly from RestEase by taking a dependency on RestEase. + +Alternatively, you may omit the `[]` attribute entirely, and control the generator through the fsproj file: + +```xml + + + + + Client.fs + HttpClient + HttpClient(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 assumes access to the `WoofWare.Whippet.Plugin.Json` generators in some situations. If you find the result does not compile due to the lack of `.jsonParse` methods, you might want to generate them using that plugin. +* Supply the optional boolean arg `false` to the `[]` attribute, or pass it via `HttpClient(false)`, to get a genuine module that can be consumed from C# (rather than an extension method). diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/HttpClient.fs b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/HttpClient.fs new file mode 100644 index 0000000..5b478fa --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/HttpClient.fs @@ -0,0 +1,21 @@ +namespace WoofWare.Whippet.Plugin.HttpClient.Test + +open System.Net.Http + +/// Simple implementation of an HttpClient. +type HttpClientMock (result : HttpRequestMessage -> Async) = + inherit HttpClient () + + override this.SendAsync (message, ct) = + Async.StartAsTask (result message, cancellationToken = ct) + +[] +module HttpClientMock = + let makeNoUri (handler : HttpRequestMessage -> Async) = + let result = new HttpClientMock (handler) + result + + let make (baseUrl : System.Uri) (handler : HttpRequestMessage -> Async) = + let result = makeNoUri handler + result.BaseAddress <- baseUrl + result diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/PureGymDtos.fs b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/PureGymDtos.fs new file mode 100644 index 0000000..4db8b58 --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/PureGymDtos.fs @@ -0,0 +1,264 @@ +namespace WoofWare.Whippet.Plugin.HttpClient.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/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/TestAllowAnyStatusCode.fs b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/TestAllowAnyStatusCode.fs new file mode 100644 index 0000000..ae2af18 --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/TestAllowAnyStatusCode.fs @@ -0,0 +1,62 @@ +namespace WoofWare.Whippet.Plugin.HttpClient.Test + +open System +open System.Net +open System.Net.Http +open NUnit.Framework +open FsUnitTyped +open PureGym + +[] +module TestAllowAnyStatusCode = + + [] + let ``Without AllowAnyStatusCode we throw`` () = + let proc (message : HttpRequestMessage) : HttpResponseMessage Async = + async { + message.Method |> shouldEqual HttpMethod.Get + let content = new StringContent ("nothing was here :(") + let resp = new HttpResponseMessage (HttpStatusCode.NotFound) + resp.Content <- content + return resp + } + + use client = HttpClientMock.make (Uri "https://example.com") proc + let api = PureGymApi.make client + + let exc = + async { + let! message = Async.AwaitTask (api.GetWithoutAnyReturnCode ()) |> Async.Catch + + match message with + | Choice1Of2 _ -> return failwith "test failure" + | Choice2Of2 exc -> return exc + } + |> Async.RunSynchronously + + let exc = + match exc with + | :? AggregateException as exc -> exc + | exc -> failwith $"Test failure: expected AggregateException, got %+A{exc}" + + match exc.InnerException with + | :? HttpRequestException as exc -> exc.Message.Contains "404 (Not Found)" |> shouldEqual true + | e -> failwith $"Test failure: %+A{e}" + + [] + let ``With AllowAnyStatusCode we do not throw`` () = + let proc (message : HttpRequestMessage) : HttpResponseMessage Async = + async { + message.Method |> shouldEqual HttpMethod.Get + let content = new StringContent ("nothing was here :(") + let resp = new HttpResponseMessage (HttpStatusCode.NotFound) + resp.Content <- content + return resp + } + + use client = HttpClientMock.make (Uri "https://example.com") proc + let api = PureGymApi.make client + + let message = api.GetWithAnyReturnCode().Result + message.StatusCode |> shouldEqual HttpStatusCode.NotFound + message.Content.ReadAsStringAsync().Result |> shouldEqual "nothing was here :(" diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/TestBasePath.fs b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/TestBasePath.fs new file mode 100644 index 0000000..f6364c1 --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/TestBasePath.fs @@ -0,0 +1,170 @@ +namespace WoofWare.Whippet.Plugin.HttpClient.Test + +open System +open System.Net +open System.Net.Http +open NUnit.Framework +open PureGym +open FsUnitTyped + +[] +module TestBasePath = + let replyWithUrl (message : HttpRequestMessage) : HttpResponseMessage Async = + async { + message.Method |> shouldEqual HttpMethod.Get + let content = new StringContent (message.RequestUri.ToString ()) + let resp = new HttpResponseMessage (HttpStatusCode.OK) + resp.Content <- content + return resp + } + + [] + let ``Base address is respected`` () = + use client = HttpClientMock.makeNoUri replyWithUrl + let api = PureGymApi.make client + + let observedUri = api.GetPathParam("param").Result + observedUri |> shouldEqual "https://whatnot.com/endpoint/param" + + [] + let ``Without a base address attr but with BaseAddress on client, request goes through`` () = + use client = HttpClientMock.make (Uri "https://baseaddress.com") replyWithUrl + let api = IApiWithoutBaseAddress.make client + + let observedUri = api.GetPathParam("param").Result + observedUri |> shouldEqual "https://baseaddress.com/endpoint/param" + + [] + let ``Base address on client takes precedence`` () = + use client = HttpClientMock.make (Uri "https://baseaddress.com") replyWithUrl + let api = PureGymApi.make client + + let observedUri = api.GetPathParam("param").Result + observedUri |> shouldEqual "https://baseaddress.com/endpoint/param" + + [] + let ``Without a base address attr or BaseAddress on client, request throws`` () = + use client = HttpClientMock.makeNoUri replyWithUrl + let api = IApiWithoutBaseAddress.make client + + let observedExc = + async { + let! result = api.GetPathParam "param" |> Async.AwaitTask |> Async.Catch + + match result with + | Choice1Of2 _ -> return failwith "test failure" + | Choice2Of2 exc -> return exc + } + |> Async.RunSynchronously + + let observedExc = + match observedExc with + | :? AggregateException as exc -> + match exc.InnerException with + | :? ArgumentNullException as exc -> exc + | _ -> failwith "test failure" + | _ -> failwith "test failure" + + observedExc.Message + |> shouldEqual + "No base address was supplied on the type, and no BaseAddress was on the HttpClient. (Parameter 'BaseAddress')" + + [] + let ``Relative base path, no base address, relative attribute`` () : unit = + do + use client = HttpClientMock.makeNoUri replyWithUrl + let api = IApiWithBasePath.make client + + let exc = + Assert.Throws (fun () -> api.GetPathParam("hi").Result |> ignore) + + exc.InnerException.Message + |> shouldEqual + "No base address was supplied on the type, and no BaseAddress was on the HttpClient. (Parameter 'BaseAddress')" + + use client = HttpClientMock.make (Uri "https://whatnot.com/thing/") replyWithUrl + let api = IApiWithBasePath.make client + let result = api.GetPathParam("hi").Result + result |> shouldEqual "https://whatnot.com/thing/foo/endpoint/hi" + + [] + let ``Relative base path, base address, relative attribute`` () : unit = + use client = HttpClientMock.makeNoUri replyWithUrl + let api = IApiWithBasePathAndAddress.make client + let result = api.GetPathParam("hi").Result + result |> shouldEqual "https://whatnot.com/thing/foo/endpoint/hi" + + [] + let ``Absolute base path, no base address, relative attribute`` () : unit = + do + use client = HttpClientMock.makeNoUri replyWithUrl + let api = IApiWithAbsoluteBasePath.make client + + let exc = + Assert.Throws (fun () -> api.GetPathParam("hi").Result |> ignore) + + exc.InnerException.Message + |> shouldEqual + "No base address was supplied on the type, and no BaseAddress was on the HttpClient. (Parameter 'BaseAddress')" + + use client = HttpClientMock.make (Uri "https://whatnot.com/thing/") replyWithUrl + let api = IApiWithAbsoluteBasePath.make client + let result = api.GetPathParam("hi").Result + result |> shouldEqual "https://whatnot.com/foo/endpoint/hi" + + [] + let ``Absolute base path, base address, relative attribute`` () : unit = + use client = HttpClientMock.makeNoUri replyWithUrl + let api = IApiWithAbsoluteBasePathAndAddress.make client + let result = api.GetPathParam("hi").Result + result |> shouldEqual "https://whatnot.com/foo/endpoint/hi" + + [] + let ``Relative base path, no base address, absolute attribute`` () : unit = + do + use client = HttpClientMock.makeNoUri replyWithUrl + let api = IApiWithBasePathAndAbsoluteEndpoint.make client + + let exc = + Assert.Throws (fun () -> api.GetPathParam("hi").Result |> ignore) + + exc.InnerException.Message + |> shouldEqual + "No base address was supplied on the type, and no BaseAddress was on the HttpClient. (Parameter 'BaseAddress')" + + use client = HttpClientMock.make (Uri "https://whatnot.com/thing/") replyWithUrl + let api = IApiWithBasePathAndAbsoluteEndpoint.make client + let result = api.GetPathParam("hi").Result + result |> shouldEqual "https://whatnot.com/endpoint/hi" + + [] + let ``Relative base path, base address, absolute attribute`` () : unit = + use client = HttpClientMock.makeNoUri replyWithUrl + let api = IApiWithBasePathAndAddressAndAbsoluteEndpoint.make client + let result = api.GetPathParam("hi").Result + result |> shouldEqual "https://whatnot.com/endpoint/hi" + + [] + let ``Absolute base path, no base address, absolute attribute`` () : unit = + do + use client = HttpClientMock.makeNoUri replyWithUrl + let api = IApiWithAbsoluteBasePathAndAbsoluteEndpoint.make client + + let exc = + Assert.Throws (fun () -> api.GetPathParam("hi").Result |> ignore) + + exc.InnerException.Message + |> shouldEqual + "No base address was supplied on the type, and no BaseAddress was on the HttpClient. (Parameter 'BaseAddress')" + + use client = HttpClientMock.make (Uri "https://whatnot.com/thing/") replyWithUrl + let api = IApiWithAbsoluteBasePathAndAbsoluteEndpoint.make client + let result = api.GetPathParam("hi").Result + result |> shouldEqual "https://whatnot.com/endpoint/hi" + + [] + let ``Absolute base path, base address, absolute attribute`` () : unit = + use client = HttpClientMock.makeNoUri replyWithUrl + let api = IApiWithAbsoluteBasePathAndAddressAndAbsoluteEndpoint.make client + let result = api.GetPathParam("hi").Result + result |> shouldEqual "https://whatnot.com/endpoint/hi" diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/TestBodyParam.fs b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/TestBodyParam.fs new file mode 100644 index 0000000..db3ca10 --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/TestBodyParam.fs @@ -0,0 +1,188 @@ +namespace WoofWare.Whippet.Plugin.HttpClient.Test + +open System +open System.IO +open System.Net +open System.Net.Http +open NUnit.Framework +open PureGym +open FsUnitTyped + +[] +module TestBodyParam = + + [] + let ``Body param of string`` () = + let proc (message : HttpRequestMessage) : HttpResponseMessage Async = + async { + message.Method |> shouldEqual HttpMethod.Post + let! content = message.Content.ReadAsStringAsync () |> Async.AwaitTask + let content = new StringContent (content) + let resp = new HttpResponseMessage (HttpStatusCode.OK) + resp.Content <- content + return resp + } + + use client = HttpClientMock.make (Uri "https://example.com") proc + let api = PureGymApi.make client + + let observedUri = api.CreateUserString("username?not!url%encoded").Result + observedUri |> shouldEqual "username?not!url%encoded" + + [] + let ``Body param of stream`` () = + let proc (message : HttpRequestMessage) : HttpResponseMessage Async = + async { + message.Method |> shouldEqual HttpMethod.Post + let! content = message.Content.ReadAsStreamAsync () |> Async.AwaitTask + let content = new StreamContent (content) + let resp = new HttpResponseMessage (HttpStatusCode.OK) + resp.Content <- content + return resp + } + + let contents = [| 1uy ; 2uy ; 3uy ; 4uy |] + + use client = HttpClientMock.make (Uri "https://example.com") proc + let api = PureGymApi.make client + + use stream = new MemoryStream (contents) + let observedContent = api.CreateUserStream(stream).Result + let buf = Array.zeroCreate 10 + let written = observedContent.ReadAtLeast (buf.AsSpan (), 5, false) + buf |> Array.take written |> shouldEqual contents + + [] + let ``Body param of HttpContent`` () = + let mutable observedContent = None + + let proc (message : HttpRequestMessage) : HttpResponseMessage Async = + async { + message.Method |> shouldEqual HttpMethod.Post + let resp = new HttpResponseMessage (HttpStatusCode.OK) + observedContent <- Some message.Content + resp.Content <- new StringContent ("oh hi") + return resp + } + + use client = HttpClientMock.make (Uri "https://example.com") proc + let api = PureGymApi.make client + + use content = new StringContent ("hello!") + + api.CreateUserHttpContent(content).Result |> shouldEqual "oh hi" + Object.ReferenceEquals (Option.get observedContent, content) |> shouldEqual true + + [] + [] + [] + let ``Body param of byte arr`` (case : string) = + let proc (message : HttpRequestMessage) : HttpResponseMessage Async = + async { + message.Method |> shouldEqual HttpMethod.Post + let! content = message.Content.ReadAsStreamAsync () |> Async.AwaitTask + let content = new StreamContent (content) + let resp = new HttpResponseMessage (HttpStatusCode.OK) + resp.Content <- content + return resp + } + + use client = HttpClientMock.make (Uri "https://example.com") proc + let api = PureGymApi.make client + + let contents = [| 1uy ; 2uy ; 3uy ; 4uy |] + + let observedContent = + match case with + | "ByteArr" -> api.CreateUserByteArr(contents).Result + | "ByteArr'" -> api.CreateUserByteArr'(contents).Result + | "ByteArr''" -> api.CreateUserByteArr''(contents).Result + | _ -> failwith $"Unrecognised case: %s{case}" + + let buf = Array.zeroCreate 10 + let written = observedContent.ReadAtLeast (buf.AsSpan (), 5, false) + buf |> Array.take written |> shouldEqual contents + + [] + let ``Body param of serialised thing`` () = + let proc (message : HttpRequestMessage) : HttpResponseMessage Async = + async { + message.Method |> shouldEqual HttpMethod.Post + let! content = message.Content.ReadAsStringAsync () |> Async.AwaitTask + let content = new StringContent ("Done! " + content) + let resp = new HttpResponseMessage (HttpStatusCode.OK) + resp.Content <- content + return resp + } + + use client = HttpClientMock.make (Uri "https://example.com") proc + let api = PureGymApi.make client + + let expected = + { + Id = 3 + CompoundMemberId = "compound!" + FirstName = "Patrick" + LastName = "Stevens" + HomeGymId = 100 + HomeGymName = "Big Boy Gym" + EmailAddress = "woof@ware" + GymAccessPin = "l3tm31n" + // To the reader: what's the significance of this date? + // answer rot13: ghevatpbzchgnovyvglragfpurvqhatfceboyrzcncre + DateOfBirth = DateOnly (1936, 05, 28) + MobileNumber = "+44-GHOST-BUSTERS" + Postcode = "W1A 111" + MembershipName = "mario" + MembershipLevel = 4 + SuspendedReason = 1090 + MemberStatus = -3 + } + + let result = api.CreateUserSerialisedBody(expected).Result + + result.StartsWith ("Done! ", StringComparison.Ordinal) |> shouldEqual true + let result = result.[6..] + + result + |> System.Text.Json.Nodes.JsonNode.Parse + |> PureGym.Member.jsonParse + |> shouldEqual expected + + [] + let ``Body param of primitive: int`` () = + let proc (message : HttpRequestMessage) : HttpResponseMessage Async = + async { + message.Method |> shouldEqual HttpMethod.Post + let! content = message.Content.ReadAsStringAsync () |> Async.AwaitTask + let content = new StringContent ("Done! " + content) + let resp = new HttpResponseMessage (HttpStatusCode.OK) + resp.Content <- content + return resp + } + + use client = HttpClientMock.make (Uri "https://example.com") proc + let api = PureGymApi.make client + + let result = api.CreateUserSerialisedIntBody(3).Result + + result |> shouldEqual "Done! 3" + + [] + let ``Body param of primitive: Uri`` () = + let proc (message : HttpRequestMessage) : HttpResponseMessage Async = + async { + message.Method |> shouldEqual HttpMethod.Post + let! content = message.Content.ReadAsStringAsync () |> Async.AwaitTask + let content = new StringContent ("Done! " + content) + let resp = new HttpResponseMessage (HttpStatusCode.OK) + resp.Content <- content + return resp + } + + use client = HttpClientMock.make (Uri "https://example.com") proc + let api = PureGymApi.make client + + let result = api.CreateUserSerialisedUrlBody(Uri "https://mything.com/blah").Result + + result |> shouldEqual "Done! \"https://mything.com/blah\"" diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/TestPathParam.fs b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/TestPathParam.fs new file mode 100644 index 0000000..30af14f --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/TestPathParam.fs @@ -0,0 +1,36 @@ +namespace WoofWare.Whippet.Plugin.HttpClient.Test + +open System +open System.Net +open System.Net.Http +open NUnit.Framework +open FsUnitTyped +open PureGym + +[] +module TestPathParam = + + [] + let ``Path params are escaped`` () = + let proc (message : HttpRequestMessage) : HttpResponseMessage Async = + async { + message.Method |> shouldEqual HttpMethod.Get + + let expectedUriPrefix = "https://example.com/endpoint/" + + let actualUri = message.RequestUri.ToString () + + if not (actualUri.StartsWith (expectedUriPrefix, StringComparison.Ordinal)) then + failwith $"wrong prefix on %s{actualUri}" + + let content = new StringContent (actualUri.Substring expectedUriPrefix.Length) + let resp = new HttpResponseMessage (HttpStatusCode.OK) + resp.Content <- content + return resp + } + + use client = HttpClientMock.make (Uri "https://example.com") proc + let api = PureGymApi.make client + + api.GetPathParam("hello/world?(hi)").Result + |> shouldEqual "hello%2Fworld%3F%28hi%29" diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/TestPureGymRestApi.fs b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/TestPureGymRestApi.fs new file mode 100644 index 0000000..ca7c775 --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/TestPureGymRestApi.fs @@ -0,0 +1,321 @@ +namespace WoofWare.Whippet.Plugin.HttpClient.Test + +open System +open System.Net +open System.Net.Http +open NUnit.Framework +open PureGym +open FsUnitTyped + +[] +module TestPureGymRestApi = + // several of these, to check behaviour around treatment of initial slashes + let baseUris = + [ + // Everything is relative to the root: + "https://example.com" + // Everything is also relative to the root, because `foo` is not a subdir: + "https://example.com/foo" + // Everything is relative to `foo`, because `foo` is a subdir + "https://example.com/foo/" + ] + |> List.map Uri + + let gymsCases = + PureGymDtos.gymCases + |> List.collect (fun (json, gym) -> [ $"[%s{json}]", [ gym ] ; $"[%s{json}, %s{json}]", [ gym ; gym ] ]) + |> List.allPairs baseUris + |> List.map TestCaseData + + [] + let ``Test GetGyms`` (baseUri : Uri, (json : string, expected : Gym list)) = + let proc (message : HttpRequestMessage) : HttpResponseMessage Async = + async { + message.Method |> shouldEqual HttpMethod.Get + + // URI is relative in the attribute on the IPureGymApi member, + // so this never gets redirected + let expectedUri = + match baseUri.ToString () with + | "https://example.com/" -> "https://example.com/v1/gyms/" + | "https://example.com/foo" -> "https://example.com/v1/gyms/" + | "https://example.com/foo/" -> "https://example.com/foo/v1/gyms/" + | s -> failwith $"Unrecognised base URI: %s{s}" + + message.RequestUri.ToString () |> shouldEqual expectedUri + + let content = new StringContent (json) + let resp = new HttpResponseMessage (HttpStatusCode.OK) + resp.Content <- content + return resp + } + + use client = HttpClientMock.make baseUri proc + let api = PureGymApi.make client + + api.GetGyms().Result |> shouldEqual expected + + let gymAttendanceCases = + PureGymDtos.gymAttendanceCases + |> List.allPairs baseUris + |> List.map TestCaseData + + [] + let ``Test GetGymAttendance`` (baseUri : Uri, (json : string, expected : GymAttendance)) = + let requestedGym = 3 + + let proc (message : HttpRequestMessage) : HttpResponseMessage Async = + async { + message.Method |> shouldEqual HttpMethod.Get + + // URI is relative in the attribute on the IPureGymApi member, + // so this never gets redirected + let expectedUri = + match baseUri.ToString () with + | "https://example.com/" -> $"https://example.com/v1/gyms/%i{requestedGym}/attendance" + | "https://example.com/foo" -> $"https://example.com/v1/gyms/%i{requestedGym}/attendance" + | "https://example.com/foo/" -> $"https://example.com/foo/v1/gyms/%i{requestedGym}/attendance" + | s -> failwith $"Unrecognised base URI: %s{s}" + + message.RequestUri.ToString () |> shouldEqual expectedUri + + let content = new StringContent (json) + let resp = new HttpResponseMessage (HttpStatusCode.OK) + resp.Content <- content + return resp + } + + use client = HttpClientMock.make baseUri proc + let api = PureGymApi.make client + + api.GetGymAttendance(requestedGym).Result |> shouldEqual expected + api.GetGymAttendance'(requestedGym).Result |> shouldEqual expected + + let memberCases = + PureGymDtos.memberCases |> List.allPairs baseUris |> List.map TestCaseData + + [] + let ``Test GetMember`` (baseUri : Uri, (json : string, expected : Member)) = + let proc (message : HttpRequestMessage) : HttpResponseMessage Async = + async { + message.Method |> shouldEqual HttpMethod.Get + + // URI is relative in the attribute on the IPureGymApi member, + // so this never gets redirected + let expectedUri = + match baseUri.ToString () with + | "https://example.com/" -> "https://example.com/v1/member" + | "https://example.com/foo" -> "https://example.com/v1/member" + | "https://example.com/foo/" -> "https://example.com/foo/v1/member" + | s -> failwith $"Unrecognised base URI: %s{s}" + + message.RequestUri.ToString () |> shouldEqual expectedUri + + let content = new StringContent (json) + let resp = new HttpResponseMessage (HttpStatusCode.OK) + resp.Content <- content + return resp + } + + use client = HttpClientMock.make baseUri proc + let api = PureGymApi.make client + + api.GetMember().Result |> shouldEqual expected + + let gymCases = + PureGymDtos.gymCases |> List.allPairs baseUris |> List.map TestCaseData + + [] + let ``Test GetGym`` (baseUri : Uri, (json : string, expected : Gym)) = + let requestedGym = 3 + + let proc (message : HttpRequestMessage) : HttpResponseMessage Async = + async { + message.Method |> shouldEqual HttpMethod.Get + + // URI is relative in the attribute on the IPureGymApi member, + // so this never gets redirected + let expectedUri = + match baseUri.ToString () with + | "https://example.com/" -> $"https://example.com/v1/gyms/%i{requestedGym}" + | "https://example.com/foo" -> $"https://example.com/v1/gyms/%i{requestedGym}" + | "https://example.com/foo/" -> $"https://example.com/foo/v1/gyms/%i{requestedGym}" + | s -> failwith $"Unrecognised base URI: %s{s}" + + message.RequestUri.ToString () |> shouldEqual expectedUri + + let content = new StringContent (json) + let resp = new HttpResponseMessage (HttpStatusCode.OK) + resp.Content <- content + return resp + } + + use client = HttpClientMock.make baseUri proc + let api = PureGymApi.make client + + api.GetGym(requestedGym).Result |> shouldEqual expected + + let memberActivityCases = + PureGymDtos.memberActivityDtoCases + |> List.allPairs baseUris + |> List.map TestCaseData + + [] + let ``Test GetMemberActivity`` (baseUri : Uri, (json : string, expected : MemberActivityDto)) = + let proc (message : HttpRequestMessage) : HttpResponseMessage Async = + async { + message.Method |> shouldEqual HttpMethod.Get + + // URI is relative in the attribute on the IPureGymApi member, + // so this never gets redirected + let expectedUri = + match baseUri.ToString () with + | "https://example.com/" -> "https://example.com/v1/member/activity" + | "https://example.com/foo" -> "https://example.com/v1/member/activity" + | "https://example.com/foo/" -> "https://example.com/foo/v1/member/activity" + | s -> failwith $"Unrecognised base URI: %s{s}" + + message.RequestUri.ToString () |> shouldEqual expectedUri + + let content = new StringContent (json) + let resp = new HttpResponseMessage (HttpStatusCode.OK) + resp.Content <- content + return resp + } + + use client = HttpClientMock.make baseUri proc + let api = PureGymApi.make client + + api.GetMemberActivity().Result |> shouldEqual expected + + let dates = + [ + for month = 1 to 3 do + // span the number 12, to catch muddling up month and day + for day = 11 to 13 do + yield DateOnly (2023, month, day) + ] + + let sessionsCases = + PureGymDtos.sessionsCases + |> List.allPairs dates + |> List.allPairs dates + |> List.allPairs baseUris + |> List.map TestCaseData + + let inline dateOnlyToString (d : DateOnly) : string = + let month = if d.Month < 10 then $"0%i{d.Month}" else $"%i{d.Month}" + let day = if d.Day < 10 then $"0%i{d.Day}" else $"%i{d.Day}" + $"{d.Year}-{month}-{day}" + + [] + let ``Test GetSessions`` + (baseUri : Uri, (startDate : DateOnly, (endDate : DateOnly, (json : string, expected : Sessions)))) + = + let proc (message : HttpRequestMessage) : HttpResponseMessage Async = + async { + message.Method |> shouldEqual HttpMethod.Get + + // This one is specified as being absolute, in its attribute on the IPureGymApi type + let expectedUri = + let fromDate = dateOnlyToString startDate + let toDate = dateOnlyToString endDate + $"https://example.com/v2/gymSessions/member?fromDate=%s{fromDate}&toDate=%s{toDate}" + + message.RequestUri.ToString () |> shouldEqual expectedUri + + let content = new StringContent (json) + let resp = new HttpResponseMessage (HttpStatusCode.OK) + resp.Content <- content + return resp + } + + use client = HttpClientMock.make baseUri proc + let api = PureGymApi.make client + + api.GetSessions(startDate, endDate).Result |> shouldEqual expected + + [] + let ``Test GetSessionsWithQuery`` + (baseUri : Uri, (startDate : DateOnly, (endDate : DateOnly, (json : string, expected : Sessions)))) + = + let proc (message : HttpRequestMessage) : HttpResponseMessage Async = + async { + message.Method |> shouldEqual HttpMethod.Get + + // This one is specified as being absolute, in its attribute on the IPureGymApi type + let expectedUri = + let fromDate = dateOnlyToString startDate + let toDate = dateOnlyToString endDate + $"https://example.com/v2/gymSessions/member?foo=1&fromDate=%s{fromDate}&toDate=%s{toDate}" + + message.RequestUri.ToString () |> shouldEqual expectedUri + + let content = new StringContent (json) + let resp = new HttpResponseMessage (HttpStatusCode.OK) + resp.Content <- content + return resp + } + + use client = HttpClientMock.make baseUri proc + let api = PureGymApi.make client + + api.GetSessionsWithQuery(startDate, endDate).Result |> shouldEqual expected + + [] + let ``URI example`` () = + let proc (message : HttpRequestMessage) : HttpResponseMessage Async = + async { + message.Method |> shouldEqual HttpMethod.Get + + message.RequestUri.ToString () |> shouldEqual "https://whatnot.com/some/url" + + let content = + new StringContent ("""{"someUri": "https://patrick@en.wikipedia.org/wiki/foo"}""") + + let resp = new HttpResponseMessage (HttpStatusCode.OK) + resp.Content <- content + return resp + } + + use client = HttpClientMock.makeNoUri proc + let api = PureGymApi.make client + + let uri = api.GetUrl().Result.SomeUri + uri.ToString () |> shouldEqual "https://patrick@en.wikipedia.org/wiki/foo" + uri.UserInfo |> shouldEqual "patrick" + uri.Host |> shouldEqual "en.wikipedia.org" + + [] + [] + let ``Map option example`` (isSome : bool) = + let proc (message : HttpRequestMessage) : HttpResponseMessage Async = + async { + message.Method |> shouldEqual HttpMethod.Post + + message.RequestUri.ToString () |> shouldEqual "https://whatnot.com/some/url" + let! content = message.Content.ReadAsStringAsync () |> Async.AwaitTask + + if isSome then + content |> shouldEqual """{"hi":"bye"}""" + else + content |> shouldEqual "null" + + let content = new StringContent (content) + + let resp = new HttpResponseMessage (HttpStatusCode.OK) + resp.Content <- content + return resp + } + + use client = HttpClientMock.makeNoUri proc + let api = PureGymApi.make client + + let expected = + if isSome then + [ "hi", "bye" ] |> Map.ofList |> Some + else + None + + let actual = api.PostStringToString(expected).Result + actual |> shouldEqual expected diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/TestReturnTypes.fs b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/TestReturnTypes.fs new file mode 100644 index 0000000..9d7207b --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/TestReturnTypes.fs @@ -0,0 +1,121 @@ +namespace WoofWare.Whippet.Plugin.HttpClient.Test + +open System +open System.IO +open System.Net +open FsUnitTyped +open System.Net.Http +open PureGym +open NUnit.Framework + +[] +module TestReturnTypes = + + [] + let ``String return`` () = + let proc (message : HttpRequestMessage) : HttpResponseMessage Async = + async { + message.Method |> shouldEqual HttpMethod.Get + let content = new StringContent ("this is not a JSON string") + let resp = new HttpResponseMessage (HttpStatusCode.OK) + resp.Content <- content + return resp + } + + use client = HttpClientMock.make (Uri "https://example.com") proc + let api = PureGymApi.make client + + api.GetPathParam("hi").Result |> shouldEqual "this is not a JSON string" + + [] + [] + [] + let ``Stream return`` (case : string) = + let result = [| 1uy ; 2uy ; 3uy ; 4uy |] + + let proc (message : HttpRequestMessage) : HttpResponseMessage Async = + async { + message.Method |> shouldEqual HttpMethod.Get + let result = new MemoryStream (result) + let content = new StreamContent (result) + let resp = new HttpResponseMessage (HttpStatusCode.OK) + resp.Content <- content + return resp + } + + use client = HttpClientMock.make (Uri "https://example.com") proc + let api = PureGymApi.make client + + use stream = + match case with + | "GetStream" -> api.GetStream().Result + | "GetStream'" -> api.GetStream'().Result + | "GetStream''" -> api.GetStream''().Result + | _ -> failwith $"unrecognised case: %s{case}" + + let buf = Array.zeroCreate 10 + let written = stream.ReadAtLeast (buf.AsSpan (), 10, false) + Array.take written buf |> shouldEqual result + + [] + [] + [] + [] + let ``HttpResponseMessage return`` (case : string) = + let mutable responseMessage = None + + let proc (message : HttpRequestMessage) : HttpResponseMessage Async = + async { + message.Method |> shouldEqual HttpMethod.Get + let content = new StringContent ("a response!") + let resp = new HttpResponseMessage (HttpStatusCode.OK) + resp.Content <- content + responseMessage <- Some resp + return resp + } + + use client = HttpClientMock.make (Uri "https://example.com") proc + let api = PureGymApi.make client + + let message = + match case with + | "GetResponseMessage" -> api.GetResponseMessage().Result + | "GetResponseMessage'" -> api.GetResponseMessage'().Result + | "GetResponseMessage''" -> api.GetResponseMessage''().Result + | "GetResponseMessage'''" -> api.GetResponseMessage'''().Result + | _ -> failwith $"unrecognised case: %s{case}" + + Object.ReferenceEquals (message, Option.get responseMessage) |> shouldEqual true + + [">] + [">] + [] + [] + let ``Response return`` (case : string) = + for json, memberDto in PureGymDtos.memberActivityDtoCases do + let mutable responseMessage = None + + let proc (message : HttpRequestMessage) : HttpResponseMessage Async = + async { + message.Method |> shouldEqual HttpMethod.Get + let content = new StringContent (json) + let resp = new HttpResponseMessage (HttpStatusCode.OK) + resp.Content <- content + responseMessage <- Some resp + return resp + } + + use client = HttpClientMock.make (Uri "https://example.com") proc + let api = PureGymApi.make client + + let response = + match case with + | "Task" -> api.GetResponse().Result + | "Task" -> api.GetResponse'().Result + | "Response Task" -> api.GetResponse''().Result + | "RestEase.Response Task" -> api.GetResponse'''().Result + | _ -> failwith $"unrecognised case: %s{case}" + + response.ResponseMessage |> shouldEqual (Option.get responseMessage) + response.StringContent |> shouldEqual json + response.GetContent () |> shouldEqual memberDto diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/TestSurface.fs b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/TestSurface.fs new file mode 100644 index 0000000..0b3bbe6 --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/TestSurface.fs @@ -0,0 +1,26 @@ +namespace WoofWare.Whippet.Plugin.HttpClient.Test + +open NUnit.Framework +open WoofWare.Whippet.Plugin.HttpClient +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.HttpClient.Attributes" + *) + + [] + let ``Update API surface`` () = + ApiSurface.writeAssemblyBaseline assembly + + [] + let ``Ensure public API is fully documented`` () = + DocCoverage.assertFullyDocumented assembly diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/TestVariableHeader.fs b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/TestVariableHeader.fs new file mode 100644 index 0000000..3b73425 --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/TestVariableHeader.fs @@ -0,0 +1,126 @@ +namespace WoofWare.Whippet.Plugin.HttpClient.Test + +open System +open System.Net +open System.Net.Http +open System.Threading +open NUnit.Framework +open FsUnitTyped +open PureGym + +[] +module TestVariableHeader = + + [] + let ``Headers are set`` () : unit = + let proc (message : HttpRequestMessage) : HttpResponseMessage Async = + async { + message.Method |> shouldEqual HttpMethod.Get + + message.RequestUri.ToString () + |> shouldEqual "https://example.com/endpoint/param" + + let headers = + [ + for h in message.Headers do + yield $"%s{h.Key}: %s{Seq.exactlyOne h.Value}" + ] + |> String.concat "\n" + + let content = new StringContent (headers) + let resp = new HttpResponseMessage (HttpStatusCode.OK) + resp.Content <- content + return resp + } + + use client = HttpClientMock.make (Uri "https://example.com") proc + + let someHeaderCount = ref 10 + + let someHeader () = + (Interlocked.Increment someHeaderCount : int).ToString () + + let someOtherHeaderCount = ref -100 + + let someOtherHeader () = + Interlocked.Increment someOtherHeaderCount + + let api = IApiWithHeaders.make someHeader someOtherHeader client + + someHeaderCount.Value |> shouldEqual 10 + someOtherHeaderCount.Value |> shouldEqual -100 + + api.GetPathParam("param").Result.Split "\n" + |> Array.sort + |> shouldEqual + [| + "Authorization: -99" + "Header-Name: Header-Value" + "Something-Else: val" + "X-Foo: 11" + |] + + someHeaderCount.Value |> shouldEqual 11 + someOtherHeaderCount.Value |> shouldEqual -99 + + [] + let ``Headers get re-evaluated every time`` () : unit = + let proc (message : HttpRequestMessage) : HttpResponseMessage Async = + async { + message.Method |> shouldEqual HttpMethod.Get + + message.RequestUri.ToString () + |> shouldEqual "https://example.com/endpoint/param" + + let headers = + [ + for h in message.Headers do + yield $"%s{h.Key}: %s{Seq.exactlyOne h.Value}" + ] + |> String.concat "\n" + + let content = new StringContent (headers) + let resp = new HttpResponseMessage (HttpStatusCode.OK) + resp.Content <- content + return resp + } + + use client = HttpClientMock.make (Uri "https://example.com") proc + + let someHeaderCount = ref 10 + + let someHeader () = + (Interlocked.Increment someHeaderCount : int).ToString () + + let someOtherHeaderCount = ref -100 + + let someOtherHeader () = + Interlocked.Increment someOtherHeaderCount + + let api = IApiWithHeaders.make someHeader someOtherHeader client + + someHeaderCount.Value |> shouldEqual 10 + someOtherHeaderCount.Value |> shouldEqual -100 + + api.GetPathParam("param").Result.Split "\n" + |> Array.sort + |> shouldEqual + [| + "Authorization: -99" + "Header-Name: Header-Value" + "Something-Else: val" + "X-Foo: 11" + |] + + api.GetPathParam("param").Result.Split "\n" + |> Array.sort + |> shouldEqual + [| + "Authorization: -98" + "Header-Name: Header-Value" + "Something-Else: val" + "X-Foo: 12" + |] + + someHeaderCount.Value |> shouldEqual 12 + someOtherHeaderCount.Value |> shouldEqual -98 diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/TestVaultClient.fs b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/TestVaultClient.fs new file mode 100644 index 0000000..da6a821 --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/TestVaultClient.fs @@ -0,0 +1,189 @@ +namespace WoofWare.Whippet.Plugin.HttpClient.Test + +open System +open System.Net +open System.Net.Http +open NUnit.Framework +open FsUnitTyped +open ConsumePlugin + +[] +module TestVaultClient = + + let exampleVaultKeyResponseString = + """{ + "request_id": "e2470000-0000-0000-0000-000000001f47", + "lease_id": "", + "renewable": false, + "lease_duration": 0, + "data": { + "key1_1": "value1_1", + "key1_2": "value1_2" + }, + "data2": { + "key2_1": "value2_1", + "key2_2": "value2_2" + }, + "data3": { + "key3_1": "value3_1", + "key3_2": "value3_2" + }, + "data4": { + "key4_1": "value4_1", + "key4_2": "value4_2" + }, + "data5": { + "https://example.com/data5/1": "value5_1", + "https://example.com/data5/2": "value5_2" + }, + "data6": { + "https://example.com/data6/1": "value6_1", + "https://example.com/data6/2": "value6_2" + }, + "data7": { + "key7_1": 71, + "key7_2": 72 + }, + "data8": { + "key8_1": "https://example.com/data8/1", + "key8_2": "https://example.com/data8/2" + } +}""" + + let exampleVaultJwtResponseString = + """{ + "request_id": "80000000-0000-0000-0000-00000000000d", + "lease_id": "", + "renewable": false, + "lease_duration": 0, + "data": null, + "wrap_info": null, + "warnings": null, + "auth": { + "client_token": "redacted_client_token", + "accessor": "redacted_accessor", + "policies": [ + "policy1", + "default" + ], + "identity_policies": [ + "identity-policy", + "default-2" + ], + "token_policies": [ + "token-policy", + "default-3" + ], + "metadata": { + "role": "some-role" + }, + "lease_duration": 43200, + "renewable": true, + "entity_id": "20000000-0000-0000-0000-000000000007", + "token_type": "service", + "orphan": true, + "mfa_requirement": null, + "num_uses": 0 + } +}""" + + [] + [] + [] + let ``URI example`` (vaultClientId : int) = + let proc (message : HttpRequestMessage) : HttpResponseMessage Async = + async { + message.Method |> shouldEqual HttpMethod.Get + + let requestUri = message.RequestUri.ToString () + + match requestUri with + | "https://my-vault.com/v1/auth/jwt/login" -> + let content = new StringContent (exampleVaultJwtResponseString) + + let resp = new HttpResponseMessage (HttpStatusCode.OK) + resp.Content <- content + return resp + | "https://my-vault.com/v1/mount/path" -> + let content = new StringContent (exampleVaultKeyResponseString) + + let resp = new HttpResponseMessage (HttpStatusCode.OK) + resp.Content <- content + return resp + | _ -> return failwith $"bad URI: %s{requestUri}" + } + + use client = HttpClientMock.make (Uri "https://my-vault.com") proc + + let value = + match vaultClientId with + | 1 -> + let api = VaultClient.make client + let vaultResponse = api.GetJwt("role", "jwt").Result + let value = api.GetSecret(vaultResponse, "path", "mount").Result + value + | 2 -> + let api = VaultClientNonExtensionMethod.make client + let vaultResponse = api.GetJwt("role", "jwt").Result + let value = api.GetSecret(vaultResponse, "path", "mount").Result + value + | 3 -> + let api = IVaultClientExtensionMethod.make client + let vaultResponse = api.GetJwt("role", "jwt").Result + let value = api.GetSecret(vaultResponse, "path", "mount").Result + value + | _ -> failwith $"Unrecognised ID: %i{vaultClientId}" + + value.Data + |> Seq.toList + |> List.map (fun (KeyValue (k, v)) -> k, v) + |> shouldEqual [ "key1_1", "value1_1" ; "key1_2", "value1_2" ] + + value.Data2 + |> Seq.toList + |> List.map (fun (KeyValue (k, v)) -> k, v) + |> shouldEqual [ "key2_1", "value2_1" ; "key2_2", "value2_2" ] + + value.Data3 + |> Seq.toList + |> List.map (fun (KeyValue (k, v)) -> k, v) + |> shouldEqual [ "key3_1", "value3_1" ; "key3_2", "value3_2" ] + + value.Data4 + |> Seq.toList + |> List.map (fun (KeyValue (k, v)) -> k, v) + |> shouldEqual [ "key4_1", "value4_1" ; "key4_2", "value4_2" ] + + value.Data5 + |> Seq.toList + |> List.map (fun (KeyValue (k, v)) -> (k : Uri).ToString (), v) + |> shouldEqual + [ + "https://example.com/data5/1", "value5_1" + "https://example.com/data5/2", "value5_2" + ] + + value.Data6 + |> Seq.toList + |> List.map (fun (KeyValue (k, v)) -> (k : Uri).ToString (), v) + |> shouldEqual + [ + "https://example.com/data6/1", "value6_1" + "https://example.com/data6/2", "value6_2" + ] + + value.Data7 + |> Seq.toList + |> List.map (fun (KeyValue (k, v)) -> k, v) + |> shouldEqual [ "key7_1", 71 ; "key7_2", 72 ] + + value.Data8 + |> Seq.toList + |> List.map (fun (KeyValue (k, v)) -> k, (v : Uri).ToString ()) + |> shouldEqual + [ + "key8_1", "https://example.com/data8/1" + "key8_2", "https://example.com/data8/2" + ] + + let _canSeePastExtensionMethod = VaultClientExtensionMethod.thisClashes diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/WoofWare.Whippet.Plugin.HttpClient.Test.fsproj b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/WoofWare.Whippet.Plugin.HttpClient.Test.fsproj new file mode 100644 index 0000000..8bcfe3b --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.Test/WoofWare.Whippet.Plugin.HttpClient.Test.fsproj @@ -0,0 +1,39 @@ + + + + net8.0 + + false + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.fsproj b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.fsproj new file mode 100644 index 0000000..ba13b59 --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/WoofWare.Whippet.Plugin.HttpClient.fsproj @@ -0,0 +1,40 @@ + + + + netstandard2.1 + true + Patrick Stevens + Copyright (c) Patrick Stevens 2024 + Whippet F# source generator plugin, for generating RestEase-style HTTP clients. + git + https://github.com/Smaug123/WoofWare.Whippet + MIT + README.md + fsharp;source-generator;source-gen;whippet;http;restease + true + FS3559 + WoofWare.Whippet.Plugin.HttpClient + true + true + NU5118 + + + + + + + + True + / + README.md + + + + + + + + + + + diff --git a/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/version.json b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/version.json new file mode 100644 index 0000000..c5ad053 --- /dev/null +++ b/Plugins/HttpClient/WoofWare.Whippet.Plugin.HttpClient/version.json @@ -0,0 +1,13 @@ +{ + "version": "0.1", + "publicReleaseRefSpec": [ + "^refs/heads/main$" + ], + "pathFilters": [ + "./", + ":/Plugins/Json/WoofWare.Whippet.Plugin.Json", + "!:/Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.Test/", + ":/global.json", + ":/Directory.Build.props" + ] +} diff --git a/Plugins/Json/WoofWare.Whippet.Plugin.Json/JsonParseGenerator.fs b/Plugins/Json/WoofWare.Whippet.Plugin.Json/JsonParseGenerator.fs index 34142c2..6debe22 100644 --- a/Plugins/Json/WoofWare.Whippet.Plugin.Json/JsonParseGenerator.fs +++ b/Plugins/Json/WoofWare.Whippet.Plugin.Json/JsonParseGenerator.fs @@ -7,13 +7,13 @@ open Fantomas.FCS.SyntaxTrivia open WoofWare.Whippet.Core open WoofWare.Whippet.Fantomas -type internal JsonParseOutputSpec = +type JsonParseOutputSpec = { ExtensionMethods : bool } [] -module internal JsonParseGenerator = +module JsonParseGenerator = open Fantomas.FCS.Text.Range type JsonParseOption = diff --git a/Plugins/Json/WoofWare.Whippet.Plugin.Json/JsonSerializeGenerator.fs b/Plugins/Json/WoofWare.Whippet.Plugin.Json/JsonSerializeGenerator.fs index b617b4b..6f9a1f7 100644 --- a/Plugins/Json/WoofWare.Whippet.Plugin.Json/JsonSerializeGenerator.fs +++ b/Plugins/Json/WoofWare.Whippet.Plugin.Json/JsonSerializeGenerator.fs @@ -6,13 +6,13 @@ open Fantomas.FCS.Syntax open WoofWare.Whippet.Core open WoofWare.Whippet.Fantomas -type internal JsonSerializeOutputSpec = +type JsonSerializeOutputSpec = { ExtensionMethods : bool } [] -module internal JsonSerializeGenerator = +module JsonSerializeGenerator = open Fantomas.FCS.Text.Range 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 index 136bbef..57537f4 100644 --- a/Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.fsproj +++ b/Plugins/Json/WoofWare.Whippet.Plugin.Json/WoofWare.Whippet.Plugin.Json.fsproj @@ -1,7 +1,7 @@  - net8.0 + netstandard2.1 true Patrick Stevens Copyright (c) Patrick Stevens 2024 diff --git a/WoofWare.Whippet.App/Context.fs b/WoofWare.Whippet.App/Context.fs index 9d1d0a0..28c8869 100644 --- a/WoofWare.Whippet.App/Context.fs +++ b/WoofWare.Whippet.App/Context.fs @@ -8,11 +8,7 @@ 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 + let localPath = Path.Combine (dll.Directory.FullName, $"%s{target.Name}.dll") runtimes |> List.tryPick (fun di -> @@ -23,4 +19,9 @@ type Ctx (dll : FileInfo, runtimes : DirectoryInfo list) = else None ) - |> Option.defaultValue null + |> Option.defaultWith (fun () -> + if File.Exists localPath then + this.LoadFromAssemblyPath localPath + else + null + ) diff --git a/WoofWare.Whippet.App/Program.fs b/WoofWare.Whippet.App/Program.fs index 6698b7a..00986b1 100644 --- a/WoofWare.Whippet.App/Program.fs +++ b/WoofWare.Whippet.App/Program.fs @@ -150,70 +150,66 @@ module Program = let runtime = DotnetRuntime.locate (Assembly.GetExecutingAssembly().Location |> FileInfo) - 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" + let plugins = + args.Plugins + |> List.map (fun pluginDll -> + let ctx = Ctx (pluginDll, runtime) - // TODO: should ideally loop over files, not plugins, so we fully generate a file before moving on to the next - // one + let pluginAssembly = ctx.LoadFromAssemblyPath pluginDll.FullName - Console.Error.WriteLine $"Loading plugin: %s{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 + // to do anything. + // Alternatively, return the text you want to output. + // We provide you with the input file contents. + // GenerateRawFromRaw should return plain text. + // GenerateFromRaw should return a Fantomas AST. + let applicablePlugins = + pluginAssembly.ExportedTypes + |> Seq.choose (fun ty -> + if + ty.CustomAttributes + |> Seq.exists (fun attr -> + attr.AttributeType.Name = typeof.Name + ) + then + Some (ty, Activator.CreateInstance ty) + else + None + ) + |> Seq.toList - 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 - // to do anything. - // Alternatively, return the text you want to output. - // We provide you with the input file contents. - // GenerateRawFromRaw should return plain text. - // GenerateFromRaw should return a Fantomas AST. - let applicablePlugins = - pluginAssembly.ExportedTypes - |> Seq.choose (fun ty -> - if - ty.CustomAttributes - |> Seq.exists (fun attr -> attr.AttributeType.Name = typeof.Name) - then - Some (ty, Activator.CreateInstance ty) - else - None + pluginDll, applicablePlugins ) - |> Seq.toList for item in toGenerate do use output = item.GeneratedDest.Open (FileMode.Create, FileAccess.Write) use outputWriter = new StreamWriter (output, leaveOpen = true) - for plugin, hostClass in applicablePlugins do - match getGenerateRawFromRaw hostClass with - | None -> () - | Some generateRawFromRaw -> - let fileContents = File.ReadAllBytes item.InputSource.FullName + for _, applicablePlugins in plugins do + for plugin, hostClass in applicablePlugins do + match getGenerateRawFromRaw hostClass with + | None -> () + | Some generateRawFromRaw -> + let fileContents = File.ReadAllBytes item.InputSource.FullName - let args = - { - RawSourceGenerationArgs.FilePath = item.InputSource.FullName - FileContents = fileContents - Parameters = item.Params - } + let args = + { + RawSourceGenerationArgs.FilePath = item.InputSource.FullName + FileContents = fileContents + Parameters = item.Params + } - let result = generateRawFromRaw args + let result = generateRawFromRaw args - match result with - | None - | Some null -> () - | Some result -> - Console.Error.WriteLine - $"Writing output for generator %s{plugin.Name} to file %s{item.GeneratedDest.FullName}" + match result with + | None + | Some null -> () + | Some result -> + Console.Error.WriteLine + $"Writing output for generator %s{plugin.Name} to file %s{item.GeneratedDest.FullName}" - outputWriter.Write result - outputWriter.Write "\n" - - () + outputWriter.Write result + outputWriter.Write "\n" 0 diff --git a/WoofWare.Whippet.sln b/WoofWare.Whippet.sln index 7e98ac1..75e04d0 100644 --- a/WoofWare.Whippet.sln +++ b/WoofWare.Whippet.sln @@ -28,6 +28,14 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WoofWare.Whippet.Plugin.Jso EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WoofWare.Whippet.App", "WoofWare.Whippet.App\WoofWare.Whippet.App.fsproj", "{A2258153-1C1F-4B25-B49A-BCC8EA4A3278}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WoofWare.Whippet.Plugin.HttpClient", "Plugins\HttpClient\WoofWare.Whippet.Plugin.HttpClient\WoofWare.Whippet.Plugin.HttpClient.fsproj", "{53352296-A95F-4153-BABB-BF1D4B1C3531}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WoofWare.Whippet.Plugin.HttpClient.Attributes", "Plugins\HttpClient\WoofWare.Whippet.Plugin.HttpClient.Attributes\WoofWare.Whippet.Plugin.HttpClient.Attributes.fsproj", "{985634CD-739C-43E4-8469-C75C20DC7D9F}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WoofWare.Whippet.Plugin.HttpClient.Consumer", "Plugins\HttpClient\WoofWare.Whippet.Plugin.HttpClient.Consumer\WoofWare.Whippet.Plugin.HttpClient.Consumer.fsproj", "{6754719D-E942-4EBE-AB2B-6FAD997DB685}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WoofWare.Whippet.Plugin.HttpClient.Test", "Plugins\HttpClient\WoofWare.Whippet.Plugin.HttpClient\WoofWare.Whippet.Plugin.HttpClient.Test\WoofWare.Whippet.Plugin.HttpClient.Test.fsproj", "{4DDD15F1-F273-441B-92F3-76BD9C089529}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -90,5 +98,21 @@ Global {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 + {53352296-A95F-4153-BABB-BF1D4B1C3531}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {53352296-A95F-4153-BABB-BF1D4B1C3531}.Debug|Any CPU.Build.0 = Debug|Any CPU + {53352296-A95F-4153-BABB-BF1D4B1C3531}.Release|Any CPU.ActiveCfg = Release|Any CPU + {53352296-A95F-4153-BABB-BF1D4B1C3531}.Release|Any CPU.Build.0 = Release|Any CPU + {985634CD-739C-43E4-8469-C75C20DC7D9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {985634CD-739C-43E4-8469-C75C20DC7D9F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {985634CD-739C-43E4-8469-C75C20DC7D9F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {985634CD-739C-43E4-8469-C75C20DC7D9F}.Release|Any CPU.Build.0 = Release|Any CPU + {6754719D-E942-4EBE-AB2B-6FAD997DB685}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6754719D-E942-4EBE-AB2B-6FAD997DB685}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6754719D-E942-4EBE-AB2B-6FAD997DB685}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6754719D-E942-4EBE-AB2B-6FAD997DB685}.Release|Any CPU.Build.0 = Release|Any CPU + {4DDD15F1-F273-441B-92F3-76BD9C089529}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4DDD15F1-F273-441B-92F3-76BD9C089529}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4DDD15F1-F273-441B-92F3-76BD9C089529}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4DDD15F1-F273-441B-92F3-76BD9C089529}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal