Respect BasePath attribute (#44)

This commit is contained in:
Patrick Stevens
2023-12-30 10:24:42 +00:00
committed by GitHub
parent 59be1f1806
commit 0d231c5200
12 changed files with 297 additions and 26 deletions

View File

@@ -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.
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
[<RequireQualifiedAccess>]
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))
}

View File

@@ -9,6 +9,7 @@ open System.Net.Http
open RestEase
[<WoofWare.Myriad.Plugins.HttpClient>]
[<BasePath "https://whatnot.com">]
type IPureGymApi =
[<Get "v1/gyms/">]
abstract GetGyms : ?ct : CancellationToken -> Task<Gym list>
@@ -60,3 +61,8 @@ type IPureGymApi =
[<Get "endpoint">]
abstract GetWithoutAnyReturnCode : ?ct : CancellationToken -> Task<HttpResponseMessage>
[<WoofWare.Myriad.Plugins.HttpClient>]
type IApiWithoutBasePath =
[<Get "endpoint/{param}">]
abstract GetPathParam : [<Path "param">] parameter : string * ?ct : CancellationToken -> Task<string>

View File

@@ -11,7 +11,11 @@ type HttpClientMock (result : HttpRequestMessage -> Async<HttpResponseMessage>)
[<RequireQualifiedAccess>]
module HttpClientMock =
let make (baseUrl : System.Uri) (handler : HttpRequestMessage -> Async<HttpResponseMessage>) =
let makeNoUri (handler : HttpRequestMessage -> Async<HttpResponseMessage>) =
let result = new HttpClientMock (handler)
result
let make (baseUrl : System.Uri) (handler : HttpRequestMessage -> Async<HttpResponseMessage>) =
let result = makeNoUri handler
result.BaseAddress <- baseUrl
result

View File

@@ -8,15 +8,16 @@
<ItemGroup>
<Compile Include="HttpClient.fs"/>
<Compile Include="TestPathParam.fs" />
<Compile Include="TestReturnTypes.fs" />
<Compile Include="TestAllowAnyStatusCode.fs" />
<Compile Include="PureGymDtos.fs"/>
<Compile Include="TestJsonParse\TestJsonParse.fs" />
<Compile Include="TestJsonParse\TestPureGymJson.fs" />
<Compile Include="TestHttpClient\TestPureGymRestApi.fs" />
<Compile Include="TestHttpClient\TestPathParam.fs" />
<Compile Include="TestHttpClient\TestReturnTypes.fs" />
<Compile Include="TestHttpClient\TestAllowAnyStatusCode.fs" />
<Compile Include="TestHttpClient\TestBasePath.fs" />
<Compile Include="TestSurface.fs"/>
<Compile Include="TestRemoveOptions.fs"/>
<Compile Include="TestJsonParse.fs"/>
<Compile Include="PureGymDtos.fs"/>
<Compile Include="TestPureGymJson.fs"/>
<Compile Include="TestPureGymRestApi.fs" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,80 @@
namespace MyriadPlugin.Test
open System
open System.Net
open System.Net.Http
open NUnit.Framework
open PureGym
open FsUnitTyped
[<TestFixture>]
module TestBasePath =
[<Test>]
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"
[<Test>]
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"
[<Test>]
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')"

View File

@@ -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
)