diff --git a/CHANGELOG.md b/CHANGELOG.md index 93f6e00..08bcf52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ Notable changes are recorded here. +# WoofWare.Myriad.Plugins 3.0.1 + +Semantics of `HttpClient`'s URI component composition changed: +we now implicitly insert `/` characters after `[]` and `[]`, so that URI composition doesn't silently drop the last component if you didn't put a slash there. + +# WoofWare.Myriad.Plugins 2.3.9 + +`JsonParse` and `JsonSerialize` now interpret `[]`, which must be on a `Dictionary`; this collects any extra components that were present on the JSON object. + # WoofWare.Myriad.Plugins 2.2.1, WoofWare.Myriad.Plugins.Attributes 3.2.1 New generator: `ArgParser`, a basic reflection-free argument parser. diff --git a/ConsumePlugin/GeneratedRestClient.fs b/ConsumePlugin/GeneratedRestClient.fs index 5d79278..25c0b4c 100644 --- a/ConsumePlugin/GeneratedRestClient.fs +++ b/ConsumePlugin/GeneratedRestClient.fs @@ -29,7 +29,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri (("v1/gyms/"), System.UriKind.Relative) ) @@ -59,7 +59,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ( "v1/gyms/{gym_id}/attendance" @@ -93,7 +93,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ( "v1/gyms/{gym_id}/attendance" @@ -127,7 +127,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ("v1/member", System.UriKind.Relative) ) @@ -157,7 +157,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ( "v1/gyms/{gym}" @@ -191,7 +191,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ("v1/member/activity", System.UriKind.Relative) ) @@ -221,7 +221,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ("some/url", System.UriKind.Relative) ) @@ -251,7 +251,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ("some/url", System.UriKind.Relative) ) @@ -317,7 +317,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ( ("/v2/gymSessions/member" @@ -358,7 +358,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ( ("/v2/gymSessions/member?foo=1" @@ -399,7 +399,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ("users/new", System.UriKind.Relative) ) @@ -426,7 +426,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ("users/new", System.UriKind.Relative) ) @@ -453,7 +453,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ("users/new", System.UriKind.Relative) ) @@ -480,7 +480,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ("users/new", System.UriKind.Relative) ) @@ -507,7 +507,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ("users/new", System.UriKind.Relative) ) @@ -534,7 +534,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ("users/new", System.UriKind.Relative) ) @@ -567,7 +567,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ("users/new", System.UriKind.Relative) ) @@ -600,7 +600,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ("users/new", System.UriKind.Relative) ) @@ -633,7 +633,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ("users/new", System.UriKind.Relative) ) @@ -659,7 +659,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ( "endpoint/{param}" @@ -688,7 +688,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ("endpoint", System.UriKind.Relative) ) @@ -713,7 +713,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ("endpoint", System.UriKind.Relative) ) @@ -738,7 +738,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ("endpoint", System.UriKind.Relative) ) @@ -763,7 +763,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ("endpoint", System.UriKind.Relative) ) @@ -787,7 +787,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ("endpoint", System.UriKind.Relative) ) @@ -811,7 +811,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ("endpoint", System.UriKind.Relative) ) @@ -835,7 +835,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ("endpoint", System.UriKind.Relative) ) @@ -859,7 +859,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ("endpoint", System.UriKind.Relative) ) @@ -895,7 +895,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ("endpoint", System.UriKind.Relative) ) @@ -931,7 +931,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ("endpoint", System.UriKind.Relative) ) @@ -967,7 +967,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ("endpoint", System.UriKind.Relative) ) @@ -1003,7 +1003,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ("endpoint", System.UriKind.Relative) ) @@ -1026,7 +1026,7 @@ module PureGymApi = let uri = System.Uri ( (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" + | null -> System.Uri "https://whatnot.com/" | v -> v), System.Uri ("endpoint", System.UriKind.Relative) ) @@ -1116,15 +1116,18 @@ module ApiWithBasePath = 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." + 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), + | v -> v), + System.Uri ("foo/", System.UriKind.Relative) + ), System.Uri ( "endpoint/{param}" .Replace ("{param}", parameter.ToString () |> System.Web.HttpUtility.UrlEncode), @@ -1167,9 +1170,12 @@ module ApiWithBasePathAndAddress = let uri = System.Uri ( - (match client.BaseAddress with - | null -> System.Uri "https://whatnot.com" - | v -> v), + 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.Web.HttpUtility.UrlEncode), @@ -1200,6 +1206,312 @@ open System.Net open System.Net.Http open RestEase +/// Module for constructing a REST client. +[] +module ApiWithAbsoluteBasePath = + /// Create a REST client. + let 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.Web.HttpUtility.UrlEncode), + 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 RestEase + +/// Module for constructing a REST client. +[] +module ApiWithAbsoluteBasePathAndAddress = + /// Create a REST client. + let 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.Web.HttpUtility.UrlEncode), + 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 RestEase + +/// Module for constructing a REST client. +[] +module ApiWithBasePathAndAbsoluteEndpoint = + /// Create a REST client. + let 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.Web.HttpUtility.UrlEncode), + 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 RestEase + +/// Module for constructing a REST client. +[] +module ApiWithBasePathAndAddressAndAbsoluteEndpoint = + /// Create a REST client. + let 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.Web.HttpUtility.UrlEncode), + 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 RestEase + +/// Module for constructing a REST client. +[] +module ApiWithAbsoluteBasePathAndAbsoluteEndpoint = + /// Create a REST client. + let 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.Web.HttpUtility.UrlEncode), + 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 RestEase + +/// Module for constructing a REST client. +[] +module ApiWithAbsoluteBasePathAndAddressAndAbsoluteEndpoint = + /// Create a REST client. + let 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.Web.HttpUtility.UrlEncode), + 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 RestEase + /// Module for constructing a REST client. [] module ApiWithHeaders = diff --git a/ConsumePlugin/RestApiExample.fs b/ConsumePlugin/RestApiExample.fs index 02aa8c3..fab044b 100644 --- a/ConsumePlugin/RestApiExample.fs +++ b/ConsumePlugin/RestApiExample.fs @@ -122,8 +122,6 @@ type internal IApiWithoutBaseAddress = [] abstract GetPathParam : [] parameter : string * ?ct : CancellationToken -> Task -// TODO: implement BasePath support - [] [] type IApiWithBasePath = @@ -132,12 +130,54 @@ type IApiWithBasePath = 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 = diff --git a/WoofWare.Myriad.Plugins.Attributes/RestEase.fs b/WoofWare.Myriad.Plugins.Attributes/RestEase.fs index 3213c71..886e84a 100644 --- a/WoofWare.Myriad.Plugins.Attributes/RestEase.fs +++ b/WoofWare.Myriad.Plugins.Attributes/RestEase.fs @@ -45,6 +45,9 @@ module RestEase = /// 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 () @@ -61,3 +64,21 @@ module RestEase = 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 () diff --git a/WoofWare.Myriad.Plugins.Attributes/SurfaceBaseline.txt b/WoofWare.Myriad.Plugins.Attributes/SurfaceBaseline.txt index cfe922b..0064d2a 100644 --- a/WoofWare.Myriad.Plugins.Attributes/SurfaceBaseline.txt +++ b/WoofWare.Myriad.Plugins.Attributes/SurfaceBaseline.txt @@ -49,6 +49,10 @@ WoofWare.Myriad.Plugins.RemoveOptionsAttribute..ctor [constructor]: unit WoofWare.Myriad.Plugins.RestEase inherit obj WoofWare.Myriad.Plugins.RestEase+BaseAddressAttribute inherit System.Attribute WoofWare.Myriad.Plugins.RestEase+BaseAddressAttribute..ctor [constructor]: string +WoofWare.Myriad.Plugins.RestEase+BasePathAttribute inherit System.Attribute +WoofWare.Myriad.Plugins.RestEase+BasePathAttribute..ctor [constructor]: string +WoofWare.Myriad.Plugins.RestEase+BodyAttribute inherit System.Attribute +WoofWare.Myriad.Plugins.RestEase+BodyAttribute..ctor [constructor]: unit WoofWare.Myriad.Plugins.RestEase+DeleteAttribute inherit System.Attribute WoofWare.Myriad.Plugins.RestEase+DeleteAttribute..ctor [constructor]: string WoofWare.Myriad.Plugins.RestEase+GetAttribute inherit System.Attribute diff --git a/WoofWare.Myriad.Plugins.Attributes/version.json b/WoofWare.Myriad.Plugins.Attributes/version.json index fde2c04..61c07b7 100644 --- a/WoofWare.Myriad.Plugins.Attributes/version.json +++ b/WoofWare.Myriad.Plugins.Attributes/version.json @@ -1,5 +1,5 @@ { - "version": "3.5", + "version": "3.6", "publicReleaseRefSpec": [ "^refs/heads/main$" ], diff --git a/WoofWare.Myriad.Plugins.Test/TestHttpClient/TestBasePath.fs b/WoofWare.Myriad.Plugins.Test/TestHttpClient/TestBasePath.fs index d6229b7..68302de 100644 --- a/WoofWare.Myriad.Plugins.Test/TestHttpClient/TestBasePath.fs +++ b/WoofWare.Myriad.Plugins.Test/TestHttpClient/TestBasePath.fs @@ -9,18 +9,18 @@ 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`` () = - let proc (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 - } - - use client = HttpClientMock.makeNoUri proc + use client = HttpClientMock.makeNoUri replyWithUrl let api = PureGymApi.make client let observedUri = api.GetPathParam("param").Result @@ -28,38 +28,28 @@ module TestBasePath = [] let ``Without a base address attr but with BaseAddress on client, request goes through`` () = - let proc (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 - } - - use client = HttpClientMock.make (System.Uri "https://baseaddress.com") proc + use client = HttpClientMock.make (Uri "https://baseaddress.com") replyWithUrl let api = ApiWithoutBaseAddress.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`` () = - let proc (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 on client takes precedence`` () = + use client = HttpClientMock.make (Uri "https://baseaddress.com") replyWithUrl + let api = PureGymApi.make client - use client = HttpClientMock.makeNoUri proc + 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 = ApiWithoutBaseAddress.make client let observedExc = async { - let! result = api.GetPathParam ("param") |> Async.AwaitTask |> Async.Catch + let! result = api.GetPathParam "param" |> Async.AwaitTask |> Async.Catch match result with | Choice1Of2 _ -> return failwith "test failure" @@ -78,3 +68,103 @@ module TestBasePath = 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 = ApiWithBasePath.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 = ApiWithBasePath.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 = ApiWithBasePathAndAddress.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 = ApiWithAbsoluteBasePath.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 = ApiWithAbsoluteBasePath.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 = ApiWithAbsoluteBasePathAndAddress.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 = ApiWithBasePathAndAbsoluteEndpoint.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 = ApiWithBasePathAndAbsoluteEndpoint.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 = ApiWithBasePathAndAddressAndAbsoluteEndpoint.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 = ApiWithAbsoluteBasePathAndAbsoluteEndpoint.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 = ApiWithAbsoluteBasePathAndAbsoluteEndpoint.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 = ApiWithAbsoluteBasePathAndAddressAndAbsoluteEndpoint.make client + let result = api.GetPathParam("hi").Result + result |> shouldEqual "https://whatnot.com/endpoint/hi" diff --git a/WoofWare.Myriad.Plugins/HttpClientGenerator.fs b/WoofWare.Myriad.Plugins/HttpClientGenerator.fs index 7e005cd..59af320 100644 --- a/WoofWare.Myriad.Plugins/HttpClientGenerator.fs +++ b/WoofWare.Myriad.Plugins/HttpClientGenerator.fs @@ -321,15 +321,33 @@ module internal HttpClientGenerator = |> 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 + [ - baseAddress - SynExpr.applyFunction - uriIdent - (SynExpr.tuple - [ - requestUriTrailer - SynExpr.createLongIdent [ "System" ; "UriKind" ; "Relative" ] - ]) + yield baseAddress + + yield + SynExpr.applyFunction + uriIdent + (SynExpr.tuple + [ + requestUriTrailer + SynExpr.createLongIdent [ "System" ; "UriKind" ; "Relative" ] + ]) ] |> SynExpr.tuple |> SynExpr.applyFunction uriIdent @@ -647,6 +665,15 @@ module internal HttpClientGenerator = | _ -> 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) @@ -676,8 +703,17 @@ module internal HttpClientGenerator = "Expected constant header parameters to be of the form [
], but got more than two args" ) - let baseAddress = extractBaseAddress interfaceType.Attributes - let basePath = extractBasePath interfaceType.Attributes + 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 diff --git a/WoofWare.Myriad.Plugins/version.json b/WoofWare.Myriad.Plugins/version.json index eda62eb..41de08e 100644 --- a/WoofWare.Myriad.Plugins/version.json +++ b/WoofWare.Myriad.Plugins/version.json @@ -1,5 +1,5 @@ { - "version": "2.3", + "version": "3.0", "publicReleaseRefSpec": [ "^refs/heads/main$" ],