mirror of
https://github.com/Smaug123/WoofWare.Myriad
synced 2025-10-10 06:28:40 +00:00
Fix BaseAddress semantics (#45)
This commit is contained in:
@@ -482,10 +482,10 @@ open RestEase
|
|||||||
/// Module for constructing a REST client.
|
/// Module for constructing a REST client.
|
||||||
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
|
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module ApiWithoutBasePath =
|
module ApiWithoutBaseAddress =
|
||||||
/// Create a REST client.
|
/// Create a REST client.
|
||||||
let make (client : System.Net.Http.HttpClient) : IApiWithoutBasePath =
|
let make (client : System.Net.Http.HttpClient) : IApiWithoutBaseAddress =
|
||||||
{ new IApiWithoutBasePath with
|
{ new IApiWithoutBaseAddress with
|
||||||
member _.GetPathParam (parameter : string, ct : CancellationToken option) =
|
member _.GetPathParam (parameter : string, ct : CancellationToken option) =
|
||||||
async {
|
async {
|
||||||
let! ct = Async.CancellationToken
|
let! ct = Async.CancellationToken
|
||||||
@@ -497,7 +497,7 @@ module ApiWithoutBasePath =
|
|||||||
raise (
|
raise (
|
||||||
System.ArgumentNullException (
|
System.ArgumentNullException (
|
||||||
nameof (client.BaseAddress),
|
nameof (client.BaseAddress),
|
||||||
"No base path was supplied on the type, and no BaseAddress was on the HttpClient."
|
"No base address was supplied on the type, and no BaseAddress was on the HttpClient."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
| v -> v),
|
| v -> v),
|
||||||
@@ -521,3 +521,101 @@ module ApiWithoutBasePath =
|
|||||||
}
|
}
|
||||||
|> (fun a -> Async.StartAsTask (a, ?cancellationToken = ct))
|
|> (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 ApiWithBasePath =
|
||||||
|
/// Create a REST client.
|
||||||
|
let make (client : System.Net.Http.HttpClient) : IApiWithBasePath =
|
||||||
|
{ new IApiWithBasePath 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.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))
|
||||||
|
}
|
||||||
|
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 ApiWithBasePathAndAddress =
|
||||||
|
/// Create a REST client.
|
||||||
|
let 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 (
|
||||||
|
(match client.BaseAddress with
|
||||||
|
| null -> System.Uri "https://whatnot.com"
|
||||||
|
| 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))
|
||||||
|
}
|
||||||
|
@@ -9,7 +9,7 @@ open System.Net.Http
|
|||||||
open RestEase
|
open RestEase
|
||||||
|
|
||||||
[<WoofWare.Myriad.Plugins.HttpClient>]
|
[<WoofWare.Myriad.Plugins.HttpClient>]
|
||||||
[<BasePath "https://whatnot.com">]
|
[<BaseAddress "https://whatnot.com">]
|
||||||
type IPureGymApi =
|
type IPureGymApi =
|
||||||
[<Get "v1/gyms/">]
|
[<Get "v1/gyms/">]
|
||||||
abstract GetGyms : ?ct : CancellationToken -> Task<Gym list>
|
abstract GetGyms : ?ct : CancellationToken -> Task<Gym list>
|
||||||
@@ -63,6 +63,21 @@ type IPureGymApi =
|
|||||||
abstract GetWithoutAnyReturnCode : ?ct : CancellationToken -> Task<HttpResponseMessage>
|
abstract GetWithoutAnyReturnCode : ?ct : CancellationToken -> Task<HttpResponseMessage>
|
||||||
|
|
||||||
[<WoofWare.Myriad.Plugins.HttpClient>]
|
[<WoofWare.Myriad.Plugins.HttpClient>]
|
||||||
type IApiWithoutBasePath =
|
type IApiWithoutBaseAddress =
|
||||||
|
[<Get "endpoint/{param}">]
|
||||||
|
abstract GetPathParam : [<Path "param">] parameter : string * ?ct : CancellationToken -> Task<string>
|
||||||
|
|
||||||
|
// TODO: implement BasePath support
|
||||||
|
|
||||||
|
[<WoofWare.Myriad.Plugins.HttpClient>]
|
||||||
|
[<BasePath "foo">]
|
||||||
|
type IApiWithBasePath =
|
||||||
|
[<Get "endpoint/{param}">]
|
||||||
|
abstract GetPathParam : [<Path "param">] parameter : string * ?ct : CancellationToken -> Task<string>
|
||||||
|
|
||||||
|
[<WoofWare.Myriad.Plugins.HttpClient>]
|
||||||
|
[<BaseAddress "https://whatnot.com">]
|
||||||
|
[<BasePath "foo">]
|
||||||
|
type IApiWithBasePathAndAddress =
|
||||||
[<Get "endpoint/{param}">]
|
[<Get "endpoint/{param}">]
|
||||||
abstract GetPathParam : [<Path "param">] parameter : string * ?ct : CancellationToken -> Task<string>
|
abstract GetPathParam : [<Path "param">] parameter : string * ?ct : CancellationToken -> Task<string>
|
||||||
|
@@ -10,7 +10,7 @@ open FsUnitTyped
|
|||||||
[<TestFixture>]
|
[<TestFixture>]
|
||||||
module TestBasePath =
|
module TestBasePath =
|
||||||
[<Test>]
|
[<Test>]
|
||||||
let ``Base path is respected`` () =
|
let ``Base address is respected`` () =
|
||||||
let proc (message : HttpRequestMessage) : HttpResponseMessage Async =
|
let proc (message : HttpRequestMessage) : HttpResponseMessage Async =
|
||||||
async {
|
async {
|
||||||
message.Method |> shouldEqual HttpMethod.Get
|
message.Method |> shouldEqual HttpMethod.Get
|
||||||
@@ -27,7 +27,7 @@ module TestBasePath =
|
|||||||
observedUri |> shouldEqual "https://whatnot.com/endpoint/param"
|
observedUri |> shouldEqual "https://whatnot.com/endpoint/param"
|
||||||
|
|
||||||
[<Test>]
|
[<Test>]
|
||||||
let ``Without a base path but with BaseAddress, request goes through`` () =
|
let ``Without a base address attr but with BaseAddress on client, request goes through`` () =
|
||||||
let proc (message : HttpRequestMessage) : HttpResponseMessage Async =
|
let proc (message : HttpRequestMessage) : HttpResponseMessage Async =
|
||||||
async {
|
async {
|
||||||
message.Method |> shouldEqual HttpMethod.Get
|
message.Method |> shouldEqual HttpMethod.Get
|
||||||
@@ -38,13 +38,13 @@ module TestBasePath =
|
|||||||
}
|
}
|
||||||
|
|
||||||
use client = HttpClientMock.make (System.Uri "https://baseaddress.com") proc
|
use client = HttpClientMock.make (System.Uri "https://baseaddress.com") proc
|
||||||
let api = ApiWithoutBasePath.make client
|
let api = ApiWithoutBaseAddress.make client
|
||||||
|
|
||||||
let observedUri = api.GetPathParam("param").Result
|
let observedUri = api.GetPathParam("param").Result
|
||||||
observedUri |> shouldEqual "https://baseaddress.com/endpoint/param"
|
observedUri |> shouldEqual "https://baseaddress.com/endpoint/param"
|
||||||
|
|
||||||
[<Test>]
|
[<Test>]
|
||||||
let ``Without a base path, request throws`` () =
|
let ``Without a base address attr or BaseAddress on client, request throws`` () =
|
||||||
let proc (message : HttpRequestMessage) : HttpResponseMessage Async =
|
let proc (message : HttpRequestMessage) : HttpResponseMessage Async =
|
||||||
async {
|
async {
|
||||||
message.Method |> shouldEqual HttpMethod.Get
|
message.Method |> shouldEqual HttpMethod.Get
|
||||||
@@ -55,7 +55,7 @@ module TestBasePath =
|
|||||||
}
|
}
|
||||||
|
|
||||||
use client = HttpClientMock.makeNoUri proc
|
use client = HttpClientMock.makeNoUri proc
|
||||||
let api = ApiWithoutBasePath.make client
|
let api = ApiWithoutBaseAddress.make client
|
||||||
|
|
||||||
let observedExc =
|
let observedExc =
|
||||||
async {
|
async {
|
||||||
@@ -77,4 +77,4 @@ module TestBasePath =
|
|||||||
|
|
||||||
observedExc.Message
|
observedExc.Message
|
||||||
|> shouldEqual
|
|> shouldEqual
|
||||||
"No base path was supplied on the type, and no BaseAddress was on the HttpClient. (Parameter 'BaseAddress')"
|
"No base address was supplied on the type, and no BaseAddress was on the HttpClient. (Parameter 'BaseAddress')"
|
||||||
|
@@ -210,14 +210,12 @@ RestEase is complex, and handles a lot of different stuff.
|
|||||||
* Deserialisation follows the same logic as the `JsonParse` generator,
|
* Deserialisation follows the same logic as the `JsonParse` generator,
|
||||||
and it generally assumes you're using types which `JsonParse` is applied to.
|
and it generally assumes you're using types which `JsonParse` is applied to.
|
||||||
* Headers are not yet supported.
|
* Headers are not yet supported.
|
||||||
* You have to specify the `BaseAddress` on the input client yourself, and you can't have the same client talking to a
|
|
||||||
different `BaseAddress` this way unless you manually set it before making any different request.
|
|
||||||
* I haven't yet worked out how to integrate this with a mocked HTTP client; you can always mock up an `HttpClient`,
|
|
||||||
but I prefer to use a mock which defines a single member `SendAsync`.
|
|
||||||
* Anonymous parameters are currently forbidden.
|
* Anonymous parameters are currently forbidden.
|
||||||
|
|
||||||
|
There are also some design decisions:
|
||||||
|
|
||||||
* Every function must take an optional `CancellationToken` (which is good practice anyway);
|
* Every function must take an optional `CancellationToken` (which is good practice anyway);
|
||||||
so arguments are forced to be tupled.
|
so arguments are forced to be tupled.
|
||||||
This is a won't-fix for as long as F# requires tupled arguments if any of the args are optional.
|
|
||||||
|
|
||||||
# Detailed examples
|
# Detailed examples
|
||||||
|
|
||||||
|
@@ -53,6 +53,7 @@ module internal HttpClientGenerator =
|
|||||||
Args : Parameter list
|
Args : Parameter list
|
||||||
Identifier : Ident
|
Identifier : Ident
|
||||||
EnsureSuccessHttpCode : bool
|
EnsureSuccessHttpCode : bool
|
||||||
|
BaseAddress : SynExpr option
|
||||||
BasePath : SynExpr option
|
BasePath : SynExpr option
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,7 +308,7 @@ module internal HttpClientGenerator =
|
|||||||
SynMatchClause.Create (
|
SynMatchClause.Create (
|
||||||
SynPat.CreateNull,
|
SynPat.CreateNull,
|
||||||
None,
|
None,
|
||||||
match info.BasePath with
|
match info.BaseAddress with
|
||||||
| None ->
|
| None ->
|
||||||
SynExpr.CreateApp (
|
SynExpr.CreateApp (
|
||||||
SynExpr.CreateIdentString "raise",
|
SynExpr.CreateIdentString "raise",
|
||||||
@@ -323,7 +324,7 @@ module internal HttpClientGenerator =
|
|||||||
SynExpr.CreateParen baseAddress
|
SynExpr.CreateParen baseAddress
|
||||||
)
|
)
|
||||||
SynExpr.CreateConstString
|
SynExpr.CreateConstString
|
||||||
"No base path was supplied on the type, and no BaseAddress was on the HttpClient."
|
"No base address was supplied on the type, and no BaseAddress was on the HttpClient."
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -608,6 +609,20 @@ module internal HttpClientGenerator =
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let extractBaseAddress (attrs : SynAttributes) : SynExpr option =
|
||||||
|
attrs
|
||||||
|
|> List.tryPick (fun attr ->
|
||||||
|
attr.Attributes
|
||||||
|
|> List.tryPick (fun attr ->
|
||||||
|
match attr.TypeName.AsString with
|
||||||
|
| "BaseAddress"
|
||||||
|
| "RestEase.BaseAddress"
|
||||||
|
| "BaseAddressAttribute"
|
||||||
|
| "RestEase.BaseAddressAttribute" -> Some attr.ArgExpr
|
||||||
|
| _ -> None
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
let createModule
|
let createModule
|
||||||
(opens : SynOpenDeclTarget list)
|
(opens : SynOpenDeclTarget list)
|
||||||
(ns : LongIdent)
|
(ns : LongIdent)
|
||||||
@@ -617,6 +632,7 @@ module internal HttpClientGenerator =
|
|||||||
let (SynTypeDefn (SynComponentInfo (attrs, _, _, interfaceName, _, _, _, _), synTypeDefnRepr, _, _, _, _)) =
|
let (SynTypeDefn (SynComponentInfo (attrs, _, _, interfaceName, _, _, _, _), synTypeDefnRepr, _, _, _, _)) =
|
||||||
interfaceType
|
interfaceType
|
||||||
|
|
||||||
|
let baseAddress = extractBaseAddress attrs
|
||||||
let basePath = extractBasePath attrs
|
let basePath = extractBasePath attrs
|
||||||
|
|
||||||
let members =
|
let members =
|
||||||
@@ -699,6 +715,7 @@ module internal HttpClientGenerator =
|
|||||||
Args = args
|
Args = args
|
||||||
Identifier = ident
|
Identifier = ident
|
||||||
EnsureSuccessHttpCode = shouldEnsureSuccess
|
EnsureSuccessHttpCode = shouldEnsureSuccess
|
||||||
|
BaseAddress = baseAddress
|
||||||
BasePath = basePath
|
BasePath = basePath
|
||||||
}
|
}
|
||||||
| _ -> failwithf "Unrecognised member definition: %+A" defn
|
| _ -> failwithf "Unrecognised member definition: %+A" defn
|
||||||
|
Reference in New Issue
Block a user