From 0d231c5200a1f7451e9f16121f75619092ec3e73 Mon Sep 17 00:00:00 2001 From: Patrick Stevens <3138005+Smaug123@users.noreply.github.com> Date: Sat, 30 Dec 2023 10:24:42 +0000 Subject: [PATCH] Respect BasePath attribute (#44) --- ConsumePlugin/GeneratedRestClient.fs | 152 ++++++++++++++++-- ConsumePlugin/RestApiExample.fs | 6 + MyriadPlugin.Test/HttpClient.fs | 6 +- MyriadPlugin.Test/MyriadPlugin.Test.fsproj | 15 +- .../TestAllowAnyStatusCode.fs | 0 .../TestHttpClient/TestBasePath.fs | 80 +++++++++ .../{ => TestHttpClient}/TestPathParam.fs | 0 .../TestPureGymRestApi.fs | 0 .../{ => TestHttpClient}/TestReturnTypes.fs | 0 .../{ => TestJsonParse}/TestJsonParse.fs | 0 .../{ => TestJsonParse}/TestPureGymJson.fs | 0 .../HttpClientGenerator.fs | 64 +++++++- 12 files changed, 297 insertions(+), 26 deletions(-) rename MyriadPlugin.Test/{ => TestHttpClient}/TestAllowAnyStatusCode.fs (100%) create mode 100644 MyriadPlugin.Test/TestHttpClient/TestBasePath.fs rename MyriadPlugin.Test/{ => TestHttpClient}/TestPathParam.fs (100%) rename MyriadPlugin.Test/{ => TestHttpClient}/TestPureGymRestApi.fs (100%) rename MyriadPlugin.Test/{ => TestHttpClient}/TestReturnTypes.fs (100%) rename MyriadPlugin.Test/{ => TestJsonParse}/TestJsonParse.fs (100%) rename MyriadPlugin.Test/{ => TestJsonParse}/TestPureGymJson.fs (100%) diff --git a/ConsumePlugin/GeneratedRestClient.fs b/ConsumePlugin/GeneratedRestClient.fs index 341824b..a457290 100644 --- a/ConsumePlugin/GeneratedRestClient.fs +++ b/ConsumePlugin/GeneratedRestClient.fs @@ -26,7 +26,12 @@ module PureGymApi = let! ct = Async.CancellationToken let uri = - System.Uri (client.BaseAddress, System.Uri ("v1/gyms/", System.UriKind.Relative)) + 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 ( @@ -52,7 +57,9 @@ module PureGymApi = let uri = System.Uri ( - client.BaseAddress, + (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.Web.HttpUtility.UrlEncode), @@ -83,7 +90,12 @@ module PureGymApi = let! ct = Async.CancellationToken let uri = - System.Uri (client.BaseAddress, System.Uri ("v1/member", System.UriKind.Relative)) + 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 ( @@ -109,7 +121,9 @@ module PureGymApi = let uri = System.Uri ( - client.BaseAddress, + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com" + | v -> v), System.Uri ( "v1/gyms/{gym_id}" .Replace ("{gym_id}", gymId.ToString () |> System.Web.HttpUtility.UrlEncode), @@ -140,7 +154,12 @@ module PureGymApi = let! ct = Async.CancellationToken let uri = - System.Uri (client.BaseAddress, System.Uri ("v1/member/activity", System.UriKind.Relative)) + 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 ( @@ -166,7 +185,9 @@ module PureGymApi = let uri = System.Uri ( - client.BaseAddress, + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com" + | v -> v), System.Uri ( ("/v2/gymSessions/member" + "?fromDate=" @@ -201,7 +222,9 @@ module PureGymApi = let uri = System.Uri ( - client.BaseAddress, + (match client.BaseAddress with + | null -> System.Uri "https://whatnot.com" + | v -> v), System.Uri ( "endpoint/{param}" .Replace ("{param}", parameter.ToString () |> System.Web.HttpUtility.UrlEncode), @@ -227,7 +250,12 @@ module PureGymApi = let! ct = Async.CancellationToken let uri = - System.Uri (client.BaseAddress, System.Uri ("endpoint", System.UriKind.Relative)) + 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 ( @@ -247,7 +275,12 @@ module PureGymApi = let! ct = Async.CancellationToken let uri = - System.Uri (client.BaseAddress, System.Uri ("endpoint", System.UriKind.Relative)) + 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 ( @@ -267,7 +300,12 @@ module PureGymApi = let! ct = Async.CancellationToken let uri = - System.Uri (client.BaseAddress, System.Uri ("endpoint", System.UriKind.Relative)) + 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 ( @@ -287,7 +325,12 @@ module PureGymApi = let! ct = Async.CancellationToken let uri = - System.Uri (client.BaseAddress, System.Uri ("endpoint", System.UriKind.Relative)) + 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 ( @@ -307,7 +350,12 @@ module PureGymApi = let! ct = Async.CancellationToken let uri = - System.Uri (client.BaseAddress, System.Uri ("endpoint", System.UriKind.Relative)) + 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 ( @@ -327,7 +375,12 @@ module PureGymApi = let! ct = Async.CancellationToken let uri = - System.Uri (client.BaseAddress, System.Uri ("endpoint", System.UriKind.Relative)) + 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 ( @@ -347,7 +400,12 @@ module PureGymApi = let! ct = Async.CancellationToken let uri = - System.Uri (client.BaseAddress, System.Uri ("endpoint", System.UriKind.Relative)) + 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 ( @@ -367,7 +425,12 @@ module PureGymApi = let! ct = Async.CancellationToken let uri = - System.Uri (client.BaseAddress, System.Uri ("endpoint", System.UriKind.Relative)) + 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 ( @@ -386,7 +449,12 @@ module PureGymApi = let! ct = Async.CancellationToken let uri = - System.Uri (client.BaseAddress, System.Uri ("endpoint", System.UriKind.Relative)) + 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 ( @@ -401,3 +469,55 @@ module PureGymApi = } |> (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 ApiWithoutBasePath = + /// Create a REST client. + let make (client : System.Net.Http.HttpClient) : IApiWithoutBasePath = + { new IApiWithoutBasePath 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 path was supplied on the type, and no BaseAddress was on the HttpClient." + ) + ) + | v -> v), + 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! node = response.Content.ReadAsStringAsync ct |> Async.AwaitTask + return node + } + |> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct)) + } diff --git a/ConsumePlugin/RestApiExample.fs b/ConsumePlugin/RestApiExample.fs index 7fdc677..dce03cd 100644 --- a/ConsumePlugin/RestApiExample.fs +++ b/ConsumePlugin/RestApiExample.fs @@ -9,6 +9,7 @@ open System.Net.Http open RestEase [] +[] type IPureGymApi = [] abstract GetGyms : ?ct : CancellationToken -> Task @@ -60,3 +61,8 @@ type IPureGymApi = [] abstract GetWithoutAnyReturnCode : ?ct : CancellationToken -> Task + +[] +type IApiWithoutBasePath = + [] + abstract GetPathParam : [] parameter : string * ?ct : CancellationToken -> Task diff --git a/MyriadPlugin.Test/HttpClient.fs b/MyriadPlugin.Test/HttpClient.fs index a978424..7951424 100644 --- a/MyriadPlugin.Test/HttpClient.fs +++ b/MyriadPlugin.Test/HttpClient.fs @@ -11,7 +11,11 @@ type HttpClientMock (result : HttpRequestMessage -> Async) [] module HttpClientMock = - let make (baseUrl : System.Uri) (handler : HttpRequestMessage -> Async) = + 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/MyriadPlugin.Test/MyriadPlugin.Test.fsproj b/MyriadPlugin.Test/MyriadPlugin.Test.fsproj index ac451fc..6b898d0 100644 --- a/MyriadPlugin.Test/MyriadPlugin.Test.fsproj +++ b/MyriadPlugin.Test/MyriadPlugin.Test.fsproj @@ -8,15 +8,16 @@ - - - + + + + + + + + - - - - diff --git a/MyriadPlugin.Test/TestAllowAnyStatusCode.fs b/MyriadPlugin.Test/TestHttpClient/TestAllowAnyStatusCode.fs similarity index 100% rename from MyriadPlugin.Test/TestAllowAnyStatusCode.fs rename to MyriadPlugin.Test/TestHttpClient/TestAllowAnyStatusCode.fs diff --git a/MyriadPlugin.Test/TestHttpClient/TestBasePath.fs b/MyriadPlugin.Test/TestHttpClient/TestBasePath.fs new file mode 100644 index 0000000..3104507 --- /dev/null +++ b/MyriadPlugin.Test/TestHttpClient/TestBasePath.fs @@ -0,0 +1,80 @@ +namespace MyriadPlugin.Test + +open System +open System.Net +open System.Net.Http +open NUnit.Framework +open PureGym +open FsUnitTyped + +[] +module TestBasePath = + [] + let ``Base path 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 + let api = PureGymApi.make client + + let observedUri = api.GetPathParam("param").Result + observedUri |> shouldEqual "https://whatnot.com/endpoint/param" + + [] + let ``Without a base path but with BaseAddress, 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 + let api = ApiWithoutBasePath.make client + + let observedUri = api.GetPathParam("param").Result + observedUri |> shouldEqual "https://baseaddress.com/endpoint/param" + + [] + let ``Without a base path, 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 + } + + use client = HttpClientMock.makeNoUri proc + let api = ApiWithoutBasePath.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 path was supplied on the type, and no BaseAddress was on the HttpClient. (Parameter 'BaseAddress')" diff --git a/MyriadPlugin.Test/TestPathParam.fs b/MyriadPlugin.Test/TestHttpClient/TestPathParam.fs similarity index 100% rename from MyriadPlugin.Test/TestPathParam.fs rename to MyriadPlugin.Test/TestHttpClient/TestPathParam.fs diff --git a/MyriadPlugin.Test/TestPureGymRestApi.fs b/MyriadPlugin.Test/TestHttpClient/TestPureGymRestApi.fs similarity index 100% rename from MyriadPlugin.Test/TestPureGymRestApi.fs rename to MyriadPlugin.Test/TestHttpClient/TestPureGymRestApi.fs diff --git a/MyriadPlugin.Test/TestReturnTypes.fs b/MyriadPlugin.Test/TestHttpClient/TestReturnTypes.fs similarity index 100% rename from MyriadPlugin.Test/TestReturnTypes.fs rename to MyriadPlugin.Test/TestHttpClient/TestReturnTypes.fs diff --git a/MyriadPlugin.Test/TestJsonParse.fs b/MyriadPlugin.Test/TestJsonParse/TestJsonParse.fs similarity index 100% rename from MyriadPlugin.Test/TestJsonParse.fs rename to MyriadPlugin.Test/TestJsonParse/TestJsonParse.fs diff --git a/MyriadPlugin.Test/TestPureGymJson.fs b/MyriadPlugin.Test/TestJsonParse/TestPureGymJson.fs similarity index 100% rename from MyriadPlugin.Test/TestPureGymJson.fs rename to MyriadPlugin.Test/TestJsonParse/TestPureGymJson.fs diff --git a/WoofWare.Myriad.Plugins/HttpClientGenerator.fs b/WoofWare.Myriad.Plugins/HttpClientGenerator.fs index 9116539..29e7fab 100644 --- a/WoofWare.Myriad.Plugins/HttpClientGenerator.fs +++ b/WoofWare.Myriad.Plugins/HttpClientGenerator.fs @@ -53,6 +53,7 @@ module internal HttpClientGenerator = Args : Parameter list Identifier : Ident EnsureSuccessHttpCode : bool + BasePath : SynExpr option } let httpMethodString (m : HttpMethod) : string = @@ -296,13 +297,55 @@ module internal HttpClientGenerator = let requestUri = let uriIdent = SynExpr.CreateLongIdent (SynLongIdent.Create [ "System" ; "Uri" ]) + let baseAddress = + SynExpr.CreateLongIdent (SynLongIdent.Create [ "client" ; "BaseAddress" ]) + + let baseAddress = + SynExpr.CreateMatch ( + baseAddress, + [ + SynMatchClause.Create ( + SynPat.CreateNull, + None, + match info.BasePath with + | None -> + SynExpr.CreateApp ( + SynExpr.CreateIdentString "raise", + SynExpr.CreateParen ( + SynExpr.CreateApp ( + SynExpr.CreateLongIdent ( + SynLongIdent.Create [ "System" ; "ArgumentNullException" ] + ), + SynExpr.CreateParenedTuple + [ + SynExpr.CreateApp ( + SynExpr.CreateIdentString "nameof", + SynExpr.CreateParen baseAddress + ) + SynExpr.CreateConstString + "No base path was supplied on the type, and no BaseAddress was on the HttpClient." + ] + ) + ) + ) + | Some expr -> SynExpr.CreateApp (uriIdent, expr) + ) + SynMatchClause.Create ( + SynPat.CreateNamed (Ident.Create "v"), + None, + SynExpr.CreateIdentString "v" + ) + ] + ) + |> SynExpr.CreateParen + SynExpr.App ( ExprAtomicFlag.Atomic, false, uriIdent, SynExpr.CreateParenedTuple [ - SynExpr.CreateLongIdent (SynLongIdent.Create [ "client" ; "BaseAddress" ]) + baseAddress SynExpr.CreateApp ( uriIdent, SynExpr.CreateParenedTuple @@ -551,15 +594,31 @@ module internal HttpClientGenerator = convertSigParam param :: extractTypes rest | _ -> failwithf "Didn't have alternating type-and-star in interface member definition: %+A" tupleType + let extractBasePath (attrs : SynAttributes) : SynExpr option = + attrs + |> List.tryPick (fun attr -> + attr.Attributes + |> List.tryPick (fun attr -> + match attr.TypeName.AsString with + | "BasePath" + | "RestEase.BasePath" + | "BasePathAttribute" + | "RestEase.BasePathAttribute" -> Some attr.ArgExpr + | _ -> None + ) + ) + let createModule (opens : SynOpenDeclTarget list) (ns : LongIdent) (interfaceType : SynTypeDefn) : SynModuleOrNamespace = - let (SynTypeDefn (SynComponentInfo (_, _, _, interfaceName, _, _, _, _), synTypeDefnRepr, _, _, _, _)) = + let (SynTypeDefn (SynComponentInfo (attrs, _, _, interfaceName, _, _, _, _), synTypeDefnRepr, _, _, _, _)) = interfaceType + let basePath = extractBasePath attrs + let members = match synTypeDefnRepr with | SynTypeDefnRepr.ObjectModel (_kind, members, _) -> @@ -640,6 +699,7 @@ module internal HttpClientGenerator = Args = args Identifier = ident EnsureSuccessHttpCode = shouldEnsureSuccess + BasePath = basePath } | _ -> failwithf "Unrecognised member definition: %+A" defn )