mirror of
https://github.com/Smaug123/WoofWare.Myriad
synced 2025-10-06 12:38:40 +00:00
Implement RestEase variable headers (#76)
This commit is contained in:
@@ -1047,3 +1047,66 @@ module ApiWithBasePathAndAddress =
|
|||||||
}
|
}
|
||||||
|> (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 ApiWithHeaders =
|
||||||
|
/// 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.
|
||||||
|
let 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.Web.HttpUtility.UrlEncode),
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
@@ -125,3 +125,15 @@ type IApiWithBasePath =
|
|||||||
type IApiWithBasePathAndAddress =
|
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>
|
||||||
|
|
||||||
|
[<WoofWare.Myriad.Plugins.HttpClient>]
|
||||||
|
[<Header("Header-Name", "Header-Value")>]
|
||||||
|
type IApiWithHeaders =
|
||||||
|
[<Header "X-Foo">]
|
||||||
|
abstract SomeHeader : string
|
||||||
|
|
||||||
|
[<Header "Authorization">]
|
||||||
|
abstract SomeOtherHeader : int
|
||||||
|
|
||||||
|
[<Get "endpoint/{param}">]
|
||||||
|
abstract GetPathParam : [<Path "param">] parameter : string * ?ct : CancellationToken -> Task<string>
|
||||||
|
@@ -258,6 +258,11 @@ module PureGymApi =
|
|||||||
|
|
||||||
The motivating example is again ahead-of-time compilation: we wish to avoid the reflection which RestEase does.
|
The motivating example is again ahead-of-time compilation: we wish to avoid the reflection which RestEase does.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Variable and constant header values are supported:
|
||||||
|
see [the definition of `IApiWithHeaders`](./ConsumePlugin/RestApiExample.fs).
|
||||||
|
|
||||||
### Limitations
|
### Limitations
|
||||||
|
|
||||||
RestEase is complex, and handles a lot of different stuff.
|
RestEase is complex, and handles a lot of different stuff.
|
||||||
@@ -270,7 +275,6 @@ RestEase is complex, and handles a lot of different stuff.
|
|||||||
all body parameters must be types which have a suitable `toJsonNode : 'a -> JsonNode` method.
|
all body parameters must be types which have a suitable `toJsonNode : 'a -> JsonNode` method.
|
||||||
* 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.
|
|
||||||
* Anonymous parameters are currently forbidden.
|
* Anonymous parameters are currently forbidden.
|
||||||
|
|
||||||
There are also some design decisions:
|
There are also some design decisions:
|
||||||
|
@@ -0,0 +1,108 @@
|
|||||||
|
namespace WoofWare.Myriad.Plugins.Test
|
||||||
|
|
||||||
|
open System
|
||||||
|
open System.Net
|
||||||
|
open System.Net.Http
|
||||||
|
open System.Threading
|
||||||
|
open NUnit.Framework
|
||||||
|
open FsUnitTyped
|
||||||
|
open PureGym
|
||||||
|
|
||||||
|
[<TestFixture>]
|
||||||
|
module TestVariableHeader =
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
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 = ApiWithHeaders.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" ; "X-Foo: 11" |]
|
||||||
|
|
||||||
|
someHeaderCount.Value |> shouldEqual 11
|
||||||
|
someOtherHeaderCount.Value |> shouldEqual -99
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
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 = ApiWithHeaders.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" ; "X-Foo: 11" |]
|
||||||
|
|
||||||
|
api.GetPathParam("param").Result.Split "\n"
|
||||||
|
|> Array.sort
|
||||||
|
|> shouldEqual [| "Authorization: -98" ; "Header-Name: Header-Value" ; "X-Foo: 12" |]
|
||||||
|
|
||||||
|
someHeaderCount.Value |> shouldEqual 12
|
||||||
|
someOtherHeaderCount.Value |> shouldEqual -98
|
@@ -19,6 +19,7 @@
|
|||||||
<Compile Include="TestHttpClient\TestBasePath.fs" />
|
<Compile Include="TestHttpClient\TestBasePath.fs" />
|
||||||
<Compile Include="TestHttpClient\TestBodyParam.fs" />
|
<Compile Include="TestHttpClient\TestBodyParam.fs" />
|
||||||
<Compile Include="TestHttpClient\TestVaultClient.fs" />
|
<Compile Include="TestHttpClient\TestVaultClient.fs" />
|
||||||
|
<Compile Include="TestHttpClient\TestVariableHeader.fs" />
|
||||||
<Compile Include="TestMockGenerator\TestMockGenerator.fs" />
|
<Compile Include="TestMockGenerator\TestMockGenerator.fs" />
|
||||||
<Compile Include="TestRemoveOptions.fs"/>
|
<Compile Include="TestRemoveOptions.fs"/>
|
||||||
<Compile Include="TestSurface.fs"/>
|
<Compile Include="TestSurface.fs"/>
|
||||||
|
@@ -33,11 +33,29 @@ type internal MemberInfo =
|
|||||||
IsMutable : bool
|
IsMutable : bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
type internal PropertyAccessors =
|
||||||
|
| Get
|
||||||
|
| Set
|
||||||
|
| GetSet
|
||||||
|
|
||||||
|
type internal PropertyInfo =
|
||||||
|
{
|
||||||
|
Type : SynType
|
||||||
|
Accessibility : SynAccess option
|
||||||
|
Attributes : SynAttribute list
|
||||||
|
XmlDoc : PreXmlDoc option
|
||||||
|
Accessors : PropertyAccessors
|
||||||
|
IsInline : bool
|
||||||
|
Identifier : Ident
|
||||||
|
}
|
||||||
|
|
||||||
type internal InterfaceType =
|
type internal InterfaceType =
|
||||||
{
|
{
|
||||||
Attributes : SynAttribute list
|
Attributes : SynAttribute list
|
||||||
Name : LongIdent
|
Name : LongIdent
|
||||||
Members : MemberInfo list
|
Members : MemberInfo list
|
||||||
|
Properties : PropertyInfo list
|
||||||
Generics : SynTyparDecls option
|
Generics : SynTyparDecls option
|
||||||
Accessibility : SynAccess option
|
Accessibility : SynAccess option
|
||||||
}
|
}
|
||||||
@@ -230,6 +248,108 @@ module internal AstHelper =
|
|||||||
((toFun (List.map fst inputArgs) inputRet), hasParen) :: args, ret
|
((toFun (List.map fst inputArgs) inputRet), hasParen) :: args, ret
|
||||||
| _ -> [], ty
|
| _ -> [], ty
|
||||||
|
|
||||||
|
let private parseMember (slotSig : SynValSig) (flags : SynMemberFlags) : Choice<MemberInfo, PropertyInfo> =
|
||||||
|
if not flags.IsInstance then
|
||||||
|
failwith "member was not an instance member"
|
||||||
|
|
||||||
|
let propertyAccessors =
|
||||||
|
match flags.MemberKind with
|
||||||
|
| SynMemberKind.Member -> None
|
||||||
|
| SynMemberKind.PropertyGet -> Some PropertyAccessors.Get
|
||||||
|
| SynMemberKind.PropertySet -> Some PropertyAccessors.Set
|
||||||
|
| SynMemberKind.PropertyGetSet -> Some PropertyAccessors.GetSet
|
||||||
|
| kind -> failwithf "Unrecognised member kind: %+A" kind
|
||||||
|
|
||||||
|
match slotSig with
|
||||||
|
| SynValSig (attrs,
|
||||||
|
SynIdent.SynIdent (ident, _),
|
||||||
|
_typeParams,
|
||||||
|
synType,
|
||||||
|
_arity,
|
||||||
|
isInline,
|
||||||
|
isMutable,
|
||||||
|
xmlDoc,
|
||||||
|
accessibility,
|
||||||
|
synExpr,
|
||||||
|
_,
|
||||||
|
_) ->
|
||||||
|
|
||||||
|
match synExpr with
|
||||||
|
| Some _ -> failwith "literal members are not supported"
|
||||||
|
| None -> ()
|
||||||
|
|
||||||
|
let attrs = attrs |> List.collect _.Attributes
|
||||||
|
|
||||||
|
let args, ret = getType synType
|
||||||
|
|
||||||
|
let args =
|
||||||
|
args
|
||||||
|
|> List.map (fun (args, hasParen) ->
|
||||||
|
match args with
|
||||||
|
| SynType.Tuple (false, path, _) -> extractTupledTypes path
|
||||||
|
| SynType.SignatureParameter _ ->
|
||||||
|
let arg, hasParen = convertSigParam args
|
||||||
|
|
||||||
|
{
|
||||||
|
HasParen = hasParen
|
||||||
|
Args = [ arg ]
|
||||||
|
}
|
||||||
|
| SynType.LongIdent (SynLongIdent (ident, _, _)) ->
|
||||||
|
{
|
||||||
|
HasParen = false
|
||||||
|
Args =
|
||||||
|
{
|
||||||
|
Attributes = []
|
||||||
|
IsOptional = false
|
||||||
|
Id = None
|
||||||
|
Type = SynType.CreateLongIdent (SynLongIdent.CreateFromLongIdent ident)
|
||||||
|
}
|
||||||
|
|> List.singleton
|
||||||
|
}
|
||||||
|
| SynType.Var (typar, _) ->
|
||||||
|
{
|
||||||
|
HasParen = false
|
||||||
|
Args =
|
||||||
|
{
|
||||||
|
Attributes = []
|
||||||
|
IsOptional = false
|
||||||
|
Id = None
|
||||||
|
Type = SynType.Var (typar, range0)
|
||||||
|
}
|
||||||
|
|> List.singleton
|
||||||
|
}
|
||||||
|
| _ -> failwith $"Unrecognised args in interface method declaration: %+A{args}"
|
||||||
|
|> fun ty ->
|
||||||
|
{ ty with
|
||||||
|
HasParen = ty.HasParen || hasParen
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
match propertyAccessors with
|
||||||
|
| None ->
|
||||||
|
{
|
||||||
|
ReturnType = ret
|
||||||
|
Args = args
|
||||||
|
Identifier = ident
|
||||||
|
Attributes = attrs
|
||||||
|
XmlDoc = Some xmlDoc
|
||||||
|
Accessibility = accessibility
|
||||||
|
IsInline = isInline
|
||||||
|
IsMutable = isMutable
|
||||||
|
}
|
||||||
|
|> Choice1Of2
|
||||||
|
| Some accessors ->
|
||||||
|
{
|
||||||
|
Type = ret
|
||||||
|
Accessibility = accessibility
|
||||||
|
Attributes = attrs
|
||||||
|
XmlDoc = Some xmlDoc
|
||||||
|
Accessors = accessors
|
||||||
|
IsInline = isInline
|
||||||
|
Identifier = ident
|
||||||
|
}
|
||||||
|
|> Choice2Of2
|
||||||
|
|
||||||
/// Assumes that the input type is an ObjectModel, i.e. a `type Foo = member ...`
|
/// Assumes that the input type is an ObjectModel, i.e. a `type Foo = member ...`
|
||||||
let parseInterface (interfaceType : SynTypeDefn) : InterfaceType =
|
let parseInterface (interfaceType : SynTypeDefn) : InterfaceType =
|
||||||
let (SynTypeDefn (SynComponentInfo (attrs, typars, _, interfaceName, _, _, accessibility, _),
|
let (SynTypeDefn (SynComponentInfo (attrs, typars, _, interfaceName, _, _, accessibility, _),
|
||||||
@@ -242,104 +362,21 @@ module internal AstHelper =
|
|||||||
|
|
||||||
let attrs = attrs |> List.collect (fun s -> s.Attributes)
|
let attrs = attrs |> List.collect (fun s -> s.Attributes)
|
||||||
|
|
||||||
let members =
|
let members, properties =
|
||||||
match synTypeDefnRepr with
|
match synTypeDefnRepr with
|
||||||
| SynTypeDefnRepr.ObjectModel (_kind, members, _) ->
|
| SynTypeDefnRepr.ObjectModel (_kind, members, _) ->
|
||||||
members
|
members
|
||||||
|> List.map (fun defn ->
|
|> List.map (fun defn ->
|
||||||
match defn with
|
match defn with
|
||||||
| SynMemberDefn.AbstractSlot (slotSig, flags, _, _) ->
|
| SynMemberDefn.AbstractSlot (slotSig, flags, _, _) -> parseMember slotSig flags
|
||||||
match flags.MemberKind with
|
|
||||||
| SynMemberKind.Member -> ()
|
|
||||||
| kind -> failwithf "Unrecognised member kind: %+A" kind
|
|
||||||
|
|
||||||
if not flags.IsInstance then
|
|
||||||
failwith "member was not an instance member"
|
|
||||||
|
|
||||||
match slotSig with
|
|
||||||
| SynValSig (attrs,
|
|
||||||
SynIdent.SynIdent (ident, _),
|
|
||||||
_typeParams,
|
|
||||||
synType,
|
|
||||||
arity,
|
|
||||||
isInline,
|
|
||||||
isMutable,
|
|
||||||
xmlDoc,
|
|
||||||
accessibility,
|
|
||||||
synExpr,
|
|
||||||
_,
|
|
||||||
_) ->
|
|
||||||
|
|
||||||
match synExpr with
|
|
||||||
| Some _ -> failwith "literal members are not supported"
|
|
||||||
| None -> ()
|
|
||||||
|
|
||||||
let attrs = attrs |> List.collect (fun attr -> attr.Attributes)
|
|
||||||
|
|
||||||
let args, ret = getType synType
|
|
||||||
|
|
||||||
let args =
|
|
||||||
args
|
|
||||||
|> List.map (fun (args, hasParen) ->
|
|
||||||
match args with
|
|
||||||
| SynType.Tuple (false, path, _) -> extractTupledTypes path
|
|
||||||
| SynType.SignatureParameter _ ->
|
|
||||||
let arg, hasParen = convertSigParam args
|
|
||||||
|
|
||||||
{
|
|
||||||
HasParen = hasParen
|
|
||||||
Args = [ arg ]
|
|
||||||
}
|
|
||||||
| SynType.LongIdent (SynLongIdent (ident, _, _)) ->
|
|
||||||
{
|
|
||||||
HasParen = false
|
|
||||||
Args =
|
|
||||||
{
|
|
||||||
Attributes = []
|
|
||||||
IsOptional = false
|
|
||||||
Id = None
|
|
||||||
Type =
|
|
||||||
SynType.CreateLongIdent (
|
|
||||||
SynLongIdent.CreateFromLongIdent ident
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|> List.singleton
|
|
||||||
}
|
|
||||||
| SynType.Var (typar, _) ->
|
|
||||||
{
|
|
||||||
HasParen = false
|
|
||||||
Args =
|
|
||||||
{
|
|
||||||
Attributes = []
|
|
||||||
IsOptional = false
|
|
||||||
Id = None
|
|
||||||
Type = SynType.Var (typar, range0)
|
|
||||||
}
|
|
||||||
|> List.singleton
|
|
||||||
}
|
|
||||||
| _ -> failwith $"Unrecognised args in interface method declaration: %+A{args}"
|
|
||||||
|> fun ty ->
|
|
||||||
{ ty with
|
|
||||||
HasParen = ty.HasParen || hasParen
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
{
|
|
||||||
ReturnType = ret
|
|
||||||
Args = args
|
|
||||||
Identifier = ident
|
|
||||||
Attributes = attrs
|
|
||||||
XmlDoc = Some xmlDoc
|
|
||||||
Accessibility = accessibility
|
|
||||||
IsInline = isInline
|
|
||||||
IsMutable = isMutable
|
|
||||||
}
|
|
||||||
| _ -> failwith $"Unrecognised member definition: %+A{defn}"
|
| _ -> failwith $"Unrecognised member definition: %+A{defn}"
|
||||||
)
|
)
|
||||||
| _ -> failwith $"Unrecognised SynTypeDefnRepr for an interface type: %+A{synTypeDefnRepr}"
|
| _ -> failwith $"Unrecognised SynTypeDefnRepr for an interface type: %+A{synTypeDefnRepr}"
|
||||||
|
|> List.partitionChoice
|
||||||
|
|
||||||
{
|
{
|
||||||
Members = members
|
Members = members
|
||||||
|
Properties = properties
|
||||||
Name = interfaceName
|
Name = interfaceName
|
||||||
Attributes = attrs
|
Attributes = attrs
|
||||||
Generics = typars
|
Generics = typars
|
||||||
|
@@ -2,7 +2,9 @@ namespace WoofWare.Myriad.Plugins
|
|||||||
|
|
||||||
open System
|
open System
|
||||||
open System.Net.Http
|
open System.Net.Http
|
||||||
|
open System.Text
|
||||||
open Fantomas.FCS.Syntax
|
open Fantomas.FCS.Syntax
|
||||||
|
open Fantomas.FCS.SyntaxTrivia
|
||||||
open Fantomas.FCS.Xml
|
open Fantomas.FCS.Xml
|
||||||
open Myriad.Core
|
open Myriad.Core
|
||||||
|
|
||||||
@@ -125,6 +127,20 @@ module internal HttpClientGenerator =
|
|||||||
| matchingAttrs ->
|
| matchingAttrs ->
|
||||||
failwith $"Required exactly one recognised RestEase attribute on member, but got %i{matchingAttrs.Length}"
|
failwith $"Required exactly one recognised RestEase attribute on member, 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.AsString with
|
||||||
|
| "Header"
|
||||||
|
| "RestEase.Header" ->
|
||||||
|
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 =
|
let shouldAllowAnyStatusCode (attrs : SynAttribute list) : bool =
|
||||||
attrs
|
attrs
|
||||||
|> List.exists (fun attr ->
|
|> List.exists (fun attr ->
|
||||||
@@ -136,7 +152,14 @@ module internal HttpClientGenerator =
|
|||||||
| _ -> false
|
| _ -> false
|
||||||
)
|
)
|
||||||
|
|
||||||
let constructMember (info : MemberInfo) : SynMemberDefn =
|
/// 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 valInfo =
|
let valInfo =
|
||||||
SynValInfo.SynValInfo (
|
SynValInfo.SynValInfo (
|
||||||
[
|
[
|
||||||
@@ -194,8 +217,10 @@ module internal HttpClientGenerator =
|
|||||||
|> SynArgPats.Pats
|
|> SynArgPats.Pats
|
||||||
|
|
||||||
let headPat =
|
let headPat =
|
||||||
|
let thisIdent = if variableHeaders.IsEmpty then "_" else "this"
|
||||||
|
|
||||||
SynPat.LongIdent (
|
SynPat.LongIdent (
|
||||||
SynLongIdent.CreateFromLongIdent [ Ident.Create "_" ; info.Identifier ],
|
SynLongIdent.CreateFromLongIdent [ Ident.Create thisIdent ; info.Identifier ],
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
argPats,
|
argPats,
|
||||||
@@ -561,6 +586,38 @@ module internal HttpClientGenerator =
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let setVariableHeaders =
|
||||||
|
variableHeaders
|
||||||
|
|> List.map (fun (headerName, callToGetValue) ->
|
||||||
|
Do (
|
||||||
|
SynExpr.CreateApp (
|
||||||
|
SynExpr.CreateLongIdent (SynLongIdent.Create [ "httpMessage" ; "Headers" ; "Add" ]),
|
||||||
|
SynExpr.CreateParenedTuple
|
||||||
|
[
|
||||||
|
headerName
|
||||||
|
SynExpr.CreateApp (
|
||||||
|
SynExpr.CreateLongIdent (
|
||||||
|
SynLongIdent.CreateFromLongIdent
|
||||||
|
[ Ident.Create "this" ; callToGetValue ; Ident.Create "ToString" ]
|
||||||
|
),
|
||||||
|
SynExpr.CreateConst SynConst.Unit
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
let setConstantHeaders =
|
||||||
|
constantHeaders
|
||||||
|
|> List.map (fun (headerName, headerValue) ->
|
||||||
|
Do (
|
||||||
|
SynExpr.CreateApp (
|
||||||
|
SynExpr.CreateLongIdent (SynLongIdent.Create [ "httpMessage" ; "Headers" ; "Add" ]),
|
||||||
|
SynExpr.CreateParenedTuple [ headerName ; headerValue ]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
[
|
[
|
||||||
yield LetBang ("ct", SynExpr.CreateLongIdent (SynLongIdent.Create [ "Async" ; "CancellationToken" ]))
|
yield LetBang ("ct", SynExpr.CreateLongIdent (SynLongIdent.Create [ "Async" ; "CancellationToken" ]))
|
||||||
yield Let ("uri", requestUri)
|
yield Let ("uri", requestUri)
|
||||||
@@ -579,6 +636,9 @@ module internal HttpClientGenerator =
|
|||||||
|
|
||||||
yield! handleBodyParams
|
yield! handleBodyParams
|
||||||
|
|
||||||
|
yield! setVariableHeaders
|
||||||
|
yield! setConstantHeaders
|
||||||
|
|
||||||
yield
|
yield
|
||||||
LetBang (
|
LetBang (
|
||||||
"response",
|
"response",
|
||||||
@@ -682,6 +742,12 @@ module internal HttpClientGenerator =
|
|||||||
| _ -> None
|
| _ -> None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let lowerFirstLetter (x : Ident) : Ident =
|
||||||
|
let result = StringBuilder x.idText.Length
|
||||||
|
result.Append (Char.ToLowerInvariant x.idText.[0]) |> ignore
|
||||||
|
result.Append x.idText.[1..] |> ignore
|
||||||
|
Ident.Create ((result : StringBuilder).ToString ())
|
||||||
|
|
||||||
let createModule
|
let createModule
|
||||||
(opens : SynOpenDeclTarget list)
|
(opens : SynOpenDeclTarget list)
|
||||||
(ns : LongIdent)
|
(ns : LongIdent)
|
||||||
@@ -690,10 +756,48 @@ module internal HttpClientGenerator =
|
|||||||
=
|
=
|
||||||
let interfaceType = AstHelper.parseInterface interfaceType
|
let interfaceType = AstHelper.parseInterface interfaceType
|
||||||
|
|
||||||
|
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 [<Header (key, value)>], but got no args"
|
||||||
|
| [ _ ] ->
|
||||||
|
failwith
|
||||||
|
"Expected constant header parameters to be of the form [<Header (key, value)>], but got only one arg"
|
||||||
|
| _ ->
|
||||||
|
failwith
|
||||||
|
"Expected constant header parameters to be of the form [<Header (key, value)>], but got more than two args"
|
||||||
|
)
|
||||||
|
|
||||||
let baseAddress = extractBaseAddress interfaceType.Attributes
|
let baseAddress = extractBaseAddress interfaceType.Attributes
|
||||||
let basePath = extractBasePath interfaceType.Attributes
|
let basePath = extractBasePath interfaceType.Attributes
|
||||||
|
|
||||||
let members =
|
let properties =
|
||||||
|
interfaceType.Properties
|
||||||
|
|> List.map (fun pi ->
|
||||||
|
let headerInfo =
|
||||||
|
match extractHeaderInformation pi.Attributes with
|
||||||
|
| [ [ x ] ] -> x
|
||||||
|
| [ xs ] ->
|
||||||
|
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
|
interfaceType.Members
|
||||||
|> List.map (fun mem ->
|
|> List.map (fun mem ->
|
||||||
let httpMethod, url = extractHttpInformation mem.Attributes
|
let httpMethod, url = extractHttpInformation mem.Attributes
|
||||||
@@ -740,8 +844,57 @@ module internal HttpClientGenerator =
|
|||||||
Accessibility = mem.Accessibility
|
Accessibility = mem.Accessibility
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|> List.map (constructMember constantHeaders properties)
|
||||||
|
|
||||||
|
let propertyMembers =
|
||||||
|
properties
|
||||||
|
|> List.map (fun (_, pi) ->
|
||||||
|
SynMemberDefn.Member (
|
||||||
|
SynBinding.SynBinding (
|
||||||
|
pi.Accessibility,
|
||||||
|
SynBindingKind.Normal,
|
||||||
|
pi.IsInline,
|
||||||
|
false,
|
||||||
|
[],
|
||||||
|
PreXmlDoc.Empty,
|
||||||
|
SynValData.SynValData (
|
||||||
|
Some
|
||||||
|
{
|
||||||
|
IsInstance = true
|
||||||
|
IsDispatchSlot = false
|
||||||
|
IsOverrideOrExplicitImpl = true
|
||||||
|
IsFinal = false
|
||||||
|
GetterOrSetterIsCompilerGenerated = false
|
||||||
|
MemberKind = SynMemberKind.Member
|
||||||
|
},
|
||||||
|
SynValInfo.SynValInfo ([ [ SynArgInfo.Empty ] ; [] ], SynArgInfo.Empty),
|
||||||
|
None
|
||||||
|
),
|
||||||
|
SynPat.CreateLongIdent (
|
||||||
|
SynLongIdent.CreateFromLongIdent [ Ident.Create "_" ; pi.Identifier ],
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
Some (SynBindingReturnInfo.Create pi.Type),
|
||||||
|
SynExpr.CreateApp (
|
||||||
|
SynExpr.CreateLongIdent (
|
||||||
|
SynLongIdent.CreateFromLongIdent [ lowerFirstLetter pi.Identifier ]
|
||||||
|
),
|
||||||
|
SynExpr.CreateConst SynConst.Unit
|
||||||
|
),
|
||||||
|
range0,
|
||||||
|
DebugPointAtBinding.Yes range0,
|
||||||
|
{
|
||||||
|
LeadingKeyword = SynLeadingKeyword.Member range0
|
||||||
|
InlineKeyword = if pi.IsInline then Some range0 else None
|
||||||
|
EqualsRange = Some range0
|
||||||
|
}
|
||||||
|
),
|
||||||
|
range0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
let members = propertyMembers @ nonPropertyMembers
|
||||||
|
|
||||||
let constructed = members |> List.map constructMember
|
|
||||||
let docString = PreXmlDoc.Create " Module for constructing a REST client."
|
let docString = PreXmlDoc.Create " Module for constructing a REST client."
|
||||||
|
|
||||||
let interfaceImpl =
|
let interfaceImpl =
|
||||||
@@ -750,12 +903,35 @@ module internal HttpClientGenerator =
|
|||||||
None,
|
None,
|
||||||
Some range0,
|
Some range0,
|
||||||
[],
|
[],
|
||||||
constructed,
|
members,
|
||||||
[],
|
[],
|
||||||
range0,
|
range0,
|
||||||
range0
|
range0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let headerArgs =
|
||||||
|
properties
|
||||||
|
|> List.map (fun (_, pi) ->
|
||||||
|
SynPat.CreateTyped (
|
||||||
|
SynPat.CreateNamed (lowerFirstLetter pi.Identifier),
|
||||||
|
SynType.CreateFun (SynType.CreateLongIdent "unit", pi.Type)
|
||||||
|
)
|
||||||
|
|> SynPat.CreateParen
|
||||||
|
)
|
||||||
|
|
||||||
|
let clientCreationArg =
|
||||||
|
SynPat.CreateTyped (
|
||||||
|
SynPat.CreateNamed (Ident.Create "client"),
|
||||||
|
SynType.CreateLongIdent (SynLongIdent.Create [ "System" ; "Net" ; "Http" ; "HttpClient" ])
|
||||||
|
)
|
||||||
|
|> SynPat.CreateParen
|
||||||
|
|
||||||
|
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."
|
||||||
|
|
||||||
let createFunc =
|
let createFunc =
|
||||||
SynBinding.SynBinding (
|
SynBinding.SynBinding (
|
||||||
None,
|
None,
|
||||||
@@ -763,7 +939,7 @@ module internal HttpClientGenerator =
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
[],
|
[],
|
||||||
PreXmlDoc.Create " Create a REST client.",
|
PreXmlDoc.Create xmlDoc,
|
||||||
SynValData.SynValData (
|
SynValData.SynValData (
|
||||||
None,
|
None,
|
||||||
SynValInfo.SynValInfo (
|
SynValInfo.SynValInfo (
|
||||||
@@ -772,19 +948,7 @@ module internal HttpClientGenerator =
|
|||||||
),
|
),
|
||||||
None
|
None
|
||||||
),
|
),
|
||||||
SynPat.CreateLongIdent (
|
SynPat.CreateLongIdent (SynLongIdent.CreateString "make", headerArgs @ [ clientCreationArg ]),
|
||||||
SynLongIdent.CreateString "make",
|
|
||||||
[
|
|
||||||
SynPat.CreateParen (
|
|
||||||
SynPat.CreateTyped (
|
|
||||||
SynPat.CreateNamed (Ident.Create "client"),
|
|
||||||
SynType.CreateLongIdent (
|
|
||||||
SynLongIdent.Create [ "System" ; "Net" ; "Http" ; "HttpClient" ]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
),
|
|
||||||
Some (
|
Some (
|
||||||
SynBindingReturnInfo.Create (
|
SynBindingReturnInfo.Create (
|
||||||
SynType.LongIdent (SynLongIdent.CreateFromLongIdent interfaceType.Name)
|
SynType.LongIdent (SynLongIdent.CreateFromLongIdent interfaceType.Name)
|
||||||
@@ -800,7 +964,7 @@ module internal HttpClientGenerator =
|
|||||||
|
|
||||||
let moduleName : LongIdent =
|
let moduleName : LongIdent =
|
||||||
List.last interfaceType.Name
|
List.last interfaceType.Name
|
||||||
|> fun ident -> ident.idText
|
|> _.idText
|
||||||
|> fun s ->
|
|> fun s ->
|
||||||
if s.StartsWith 'I' then
|
if s.StartsWith 'I' then
|
||||||
s.[1..]
|
s.[1..]
|
||||||
|
14
WoofWare.Myriad.Plugins/List.fs
Normal file
14
WoofWare.Myriad.Plugins/List.fs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace WoofWare.Myriad.Plugins
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module private List =
|
||||||
|
let partitionChoice<'a, 'b> (xs : Choice<'a, 'b> list) : 'a list * 'b list =
|
||||||
|
let xs, ys =
|
||||||
|
(([], []), xs)
|
||||||
|
||> List.fold (fun (xs, ys) v ->
|
||||||
|
match v with
|
||||||
|
| Choice1Of2 x -> x :: xs, ys
|
||||||
|
| Choice2Of2 y -> xs, y :: ys
|
||||||
|
)
|
||||||
|
|
||||||
|
List.rev xs, List.rev ys
|
@@ -24,6 +24,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Compile Include="List.fs" />
|
||||||
<Compile Include="AstHelper.fs"/>
|
<Compile Include="AstHelper.fs"/>
|
||||||
<Compile Include="SynExpr.fs"/>
|
<Compile Include="SynExpr.fs"/>
|
||||||
<Compile Include="SynType.fs" />
|
<Compile Include="SynType.fs" />
|
||||||
|
Reference in New Issue
Block a user