namespace WoofWare.Myriad.Plugins open System.Net.Http open Fantomas.FCS.Syntax open Fantomas.FCS.SyntaxTrivia open Fantomas.FCS.Xml open Myriad.Core type internal HttpClientGeneratorOutputSpec = { ExtensionMethods : bool } [] module internal HttpClientGenerator = open Fantomas.FCS.Text.Range open Myriad.Core.Ast [] type PathSpec = | Verbatim of string | MatchArgName type HttpAttribute = // TODO: Format parameter to these attrs | Query of string option | Path of PathSpec | Body type Parameter = { Attributes : HttpAttribute list IsOptional : bool Id : Ident option Type : SynType } [] type BodyParamMethods = | StringContent | StreamContent | ByteArrayContent | HttpContent | Serialise of SynType override this.ToString () = match this with | BodyParamMethods.Serialise _ -> "ToString" | BodyParamMethods.ByteArrayContent -> "ByteArrayContent" | BodyParamMethods.StringContent -> "StringContent" | BodyParamMethods.StreamContent -> "StreamContent" | BodyParamMethods.HttpContent -> "HttpContent" type MemberInfo = { /// E.g. HttpMethod.Get HttpMethod : HttpMethod /// E.g. SynExpr.Const "v1/gyms/{gym_id}/attendance" UrlTemplate : SynExpr TaskReturnType : SynType Args : Parameter list Identifier : Ident EnsureSuccessHttpCode : bool BaseAddress : SynExpr option BasePath : SynExpr option Accessibility : SynAccess option } let httpMethodString (m : HttpMethod) : string = if m = HttpMethod.Get then "Get" elif m = HttpMethod.Post then "Post" elif m = HttpMethod.Delete then "Delete" elif m = HttpMethod.Patch then "Post" elif m = HttpMethod.Options then "Options" elif m = HttpMethod.Head then "Head" elif m = HttpMethod.Put then "Put" elif m = HttpMethod.Trace then "Trace" else failwith $"Unrecognised method: %+A{m}" /// E.g. converts `[]` to (HttpMethod.Get, SynExpr.Const "blah") let extractHttpInformation (attrs : SynAttribute list) : HttpMethod * SynExpr = let matchingAttrs = attrs |> List.choose (fun attr -> match attr.TypeName.AsString with | "Get" | "GetAttribute" | "WoofWare.Myriad.Plugins.RestEase.Get" | "WoofWare.Myriad.Plugins.RestEase.GetAttribute" | "RestEase.Get" | "RestEase.GetAttribute" -> Some (HttpMethod.Get, attr.ArgExpr) | "Post" | "PostAttribute" | "WoofWare.Myriad.Plugins.RestEase.Post" | "WoofWare.Myriad.Plugins.RestEase.PostAttribute" | "RestEase.Post" | "RestEase.PostAttribute" -> Some (HttpMethod.Post, attr.ArgExpr) | "Put" | "PutAttribute" | "WoofWare.Myriad.Plugins.RestEase.Put" | "WoofWare.Myriad.Plugins.RestEase.PutAttribute" | "RestEase.Put" | "RestEase.PutAttribute" -> Some (HttpMethod.Put, attr.ArgExpr) | "Delete" | "DeleteAttribute" | "WoofWare.Myriad.Plugins.RestEase.Delete" | "WoofWare.Myriad.Plugins.RestEase.DeleteAttribute" | "RestEase.Delete" | "RestEase.DeleteAttribute" -> Some (HttpMethod.Delete, attr.ArgExpr) | "Head" | "HeadAttribute" | "WoofWare.Myriad.Plugins.RestEase.Head" | "WoofWare.Myriad.Plugins.RestEase.HeadAttribute" | "RestEase.Head" | "RestEase.HeadAttribute" -> Some (HttpMethod.Head, attr.ArgExpr) | "Options" | "OptionsAttribute" | "WoofWare.Myriad.Plugins.RestEase.Options" | "WoofWare.Myriad.Plugins.RestEase.OptionsAttribute" | "RestEase.Options" | "RestEase.OptionsAttribute" -> Some (HttpMethod.Options, attr.ArgExpr) | "Patch" | "PatchAttribute" | "WoofWare.Myriad.Plugins.RestEase.Patch" | "WoofWare.Myriad.Plugins.RestEase.PatchAttribute" | "RestEase.Patch" | "RestEase.PatchAttribute" -> Some (HttpMethod.Patch, attr.ArgExpr) | "Trace" | "TraceAttribute" | "WoofWare.Myriad.Plugins.RestEase.Trace" | "WoofWare.Myriad.Plugins.RestEase.TraceAttribute" | "RestEase.Trace" | "RestEase.TraceAttribute" -> Some (HttpMethod.Trace, attr.ArgExpr) | _ -> None ) match matchingAttrs with | [ (meth, arg) ] -> meth, arg | [] -> failwith "Required exactly one recognised RestEase attribute on member, but got none" | matchingAttrs -> 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" | "WoofWare.Myriad.Plugins.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 = attrs |> List.exists (fun attr -> match attr.TypeName.AsString with | "AllowAnyStatusCode" | "AllowAnyStatusCodeAttribute" | "RestEase.AllowAnyStatusCode" | "RestEase.AllowAnyStatusCodeAttribute" -> true | _ -> false ) /// 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 = SynValInfo.SynValInfo ( [ [ SynArgInfo.Empty ] [ for arg in info.Args do match arg.Id with | None -> yield SynArgInfo.CreateIdString (failwith "TODO: create an arg name") | Some id -> yield SynArgInfo.CreateId id ] ], SynArgInfo.Empty ) let valData = SynValData ( Some { IsInstance = true IsDispatchSlot = false IsOverrideOrExplicitImpl = true IsFinal = false GetterOrSetterIsCompilerGenerated = false MemberKind = SynMemberKind.Member }, valInfo, None ) let args = info.Args |> List.map (fun arg -> let argName = match arg.Id with | None -> failwith "TODO: create an arg name" | Some id -> id let argType = if arg.IsOptional then SynType.CreateApp ( SynType.CreateLongIdent (SynLongIdent.CreateString "option"), [ arg.Type ], isPostfix = true ) else arg.Type argName, SynPat.CreateTyped (SynPat.CreateNamed argName, argType) ) let cancellationTokenArg = match List.tryLast args with | None -> failwith $"expected an optional cancellation token as final arg in %s{info.Identifier.idText}" | Some (arg, _) -> arg let argPats = let args = args |> List.map snd SynPat.Tuple (false, args, List.replicate (args.Length - 1) range0, range0) |> SynPat.CreateParen |> List.singleton |> SynArgPats.Pats let headPat = let thisIdent = if variableHeaders.IsEmpty then "_" else "this" SynPat.LongIdent ( SynLongIdent.CreateFromLongIdent [ Ident.Create thisIdent ; info.Identifier ], None, None, argPats, None, range0 ) let requestUriTrailer = (info.UrlTemplate, info.Args) ||> List.fold (fun template arg -> (template, arg.Attributes) ||> List.fold (fun template attr -> match attr with | HttpAttribute.Path spec -> let varName = match arg.Id with | None -> failwith "TODO: anonymous args" | Some id -> id let substituteId = match spec with | PathSpec.Verbatim s -> s | PathSpec.MatchArgName -> varName.idText template |> SynExpr.callMethodArg "Replace" (SynExpr.CreateParenedTuple [ SynExpr.CreateConstString ("{" + substituteId + "}") SynExpr.callMethod "ToString" (SynExpr.CreateIdent varName) |> SynExpr.pipeThroughFunction ( SynExpr.CreateLongIdent ( SynLongIdent.Create [ "System" ; "Web" ; "HttpUtility" ; "UrlEncode" ] ) ) ]) | _ -> template ) ) /// List of (query-param-key, parameter-which-provides-value) let queryParams = info.Args |> List.collect (fun arg -> arg.Attributes |> List.choose (fun attr -> match attr with | Query None -> let name = match arg.Id with | None -> failwith "Expected a name for the argument we're trying to use as an anonymous query parameter" | Some name -> name.idText Some (name, arg) | Query (Some name) -> Some (name, arg) | _ -> None ) ) let requestUriTrailer = match queryParams with | [] -> requestUriTrailer | (firstKey, firstValue) :: queryParams -> let firstValueId = match firstValue.Id with | None -> failwith "Unable to get parameter variable name from anonymous parameter" | Some id -> id let urlSeparator = // apparent Myriad bug: `IndexOf '?'` gets formatted as `IndexOf ?` which is clearly wrong let questionMark = SynExpr.CreateParen ( SynExpr.CreateApp ( SynExpr.CreateIdentString "char", SynExpr.CreateConst (SynConst.Int32 63) ) ) let containsQuestion = info.UrlTemplate |> SynExpr.callMethodArg "IndexOf" questionMark |> SynExpr.greaterThanOrEqual (SynExpr.CreateConst (SynConst.Int32 0)) SynExpr.ifThenElse containsQuestion (SynExpr.CreateConst (SynConst.CreateString "?")) (SynExpr.CreateConst (SynConst.CreateString "&")) |> SynExpr.CreateParen let prefix = SynExpr.CreateIdent firstValueId |> SynExpr.toString firstValue.Type |> SynExpr.CreateParen |> SynExpr.pipeThroughFunction ( SynExpr.CreateLongIdent (SynLongIdent.Create [ "System" ; "Web" ; "HttpUtility" ; "UrlEncode" ]) ) |> SynExpr.CreateParen |> SynExpr.plus (SynExpr.plus urlSeparator (SynExpr.CreateConstString (firstKey + "="))) (prefix, queryParams) ||> List.fold (fun uri (paramKey, paramValue) -> let paramValueId = match paramValue.Id with | None -> failwith "Unable to get parameter variable name from anonymous parameter" | Some id -> id SynExpr.toString paramValue.Type (SynExpr.CreateIdent paramValueId) |> SynExpr.CreateParen |> SynExpr.pipeThroughFunction ( SynExpr.CreateLongIdent ( SynLongIdent.Create [ "System" ; "Web" ; "HttpUtility" ; "UrlEncode" ] ) ) |> SynExpr.CreateParen |> SynExpr.plus (SynExpr.plus uri (SynExpr.CreateConstString ("&" + paramKey + "="))) ) |> SynExpr.plus requestUriTrailer |> SynExpr.CreateParen 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.BaseAddress 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 address 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 [ baseAddress SynExpr.CreateApp ( uriIdent, SynExpr.CreateParenedTuple [ requestUriTrailer SynExpr.CreateLongIdent (SynLongIdent.Create [ "System" ; "UriKind" ; "Relative" ]) ] ) ], range0 ) let bodyParams = info.Args |> List.collect (fun arg -> arg.Attributes |> List.choose (fun attr -> match attr with | HttpAttribute.Body -> Some arg | _ -> None ) ) let bodyParam = match bodyParams with | [] -> None | [ x ] -> // TODO: body serialisation method let paramName = match x.Id with | None -> failwith "Anonymous [] parameter is unsupported" | Some id -> id match x.Type with | Stream -> Some (BodyParamMethods.StreamContent, paramName) | String -> Some (BodyParamMethods.StringContent, paramName) | ArrayType Byte -> Some (BodyParamMethods.ByteArrayContent, paramName) | HttpContent -> Some (BodyParamMethods.HttpContent, paramName) | ty -> Some (BodyParamMethods.Serialise ty, paramName) | _ -> failwith "You can only have at most one [] parameter on a method." let httpReqMessageConstructor = [ SynExpr.equals (SynExpr.CreateIdentString "Method") (SynExpr.CreateLongIdent ( SynLongIdent.Create [ "System" ; "Net" ; "Http" ; "HttpMethod" ; httpMethodString info.HttpMethod ] )) SynExpr.equals (SynExpr.CreateIdentString "RequestUri") (SynExpr.CreateIdentString "uri") ] |> SynExpr.CreateParenedTuple let returnExpr = match info.TaskReturnType with | HttpResponseMessage -> SynExpr.CreateIdentString "response" | String -> SynExpr.CreateIdentString "responseString" | Stream -> SynExpr.CreateIdentString "responseStream" | RestEaseResponseType contents -> let deserialiser = SynExpr.CreateLambda ( [ SynPat.CreateConst SynConst.Unit ], SynExpr.CreateParen ( JsonParseGenerator.parseNode None JsonParseGenerator.JsonParseOption.None contents (SynExpr.CreateIdentString "jsonNode") ) ) // new RestEase.Response (content : string, response : HttpResponseMessage, deserialiser : unit -> 'T) SynExpr.New ( false, SynType.App ( SynType.CreateLongIdent (SynLongIdent.Create [ "RestEase" ; "Response" ]), Some range0, [ SynType.Anon range0 ], [], Some range0, false, range0 ), SynExpr.CreateParenedTuple [ SynExpr.CreateIdentString "responseString" SynExpr.CreateIdentString "response" SynExpr.CreateParen deserialiser ], range0 ) | retType -> JsonParseGenerator.parseNode None JsonParseGenerator.JsonParseOption.None retType (SynExpr.CreateIdentString "jsonNode") let handleBodyParams = match bodyParam with | None -> [] | Some (bodyParamType, bodyParamName) -> match bodyParamType with | BodyParamMethods.StreamContent | BodyParamMethods.ByteArrayContent | BodyParamMethods.StringContent -> [ Let ( "queryParams", SynExpr.New ( false, SynType.CreateLongIdent ( SynLongIdent.Create [ "System" ; "Net" ; "Http" ; (bodyParamType : BodyParamMethods).ToString () ] ), SynExpr.CreateParen (SynExpr.CreateIdent bodyParamName), range0 ) ) Do ( SynExpr.LongIdentSet ( SynLongIdent.Create [ "httpMessage" ; "Content" ], SynExpr.CreateIdentString "queryParams", range0 ) ) ] | BodyParamMethods.HttpContent -> [ Do ( SynExpr.LongIdentSet ( SynLongIdent.Create [ "httpMessage" ; "Content" ], SynExpr.CreateIdent bodyParamName, range0 ) ) ] | BodyParamMethods.Serialise ty -> [ Let ( "queryParams", SynExpr.New ( false, SynType.CreateLongIdent ( SynLongIdent.Create [ "System" ; "Net" ; "Http" ; "StringContent" ] ), SynExpr.CreateParen ( SynExpr.CreateIdent bodyParamName |> SynExpr.pipeThroughFunction (JsonSerializeGenerator.serializeNode ty) |> SynExpr.pipeThroughFunction ( SynExpr.createLambda "node" (SynExpr.ifThenElse (SynExpr.CreateApp ( SynExpr.CreateIdentString "isNull", SynExpr.CreateIdentString "node" )) (SynExpr.CreateApp ( SynExpr.CreateLongIdent ( SynLongIdent.Create [ "node" ; "ToJsonString" ] ), SynExpr.CreateConst SynConst.Unit )) (SynExpr.CreateConst (SynConst.CreateString "null"))) ) ), range0 ) ) Do ( SynExpr.LongIdentSet ( SynLongIdent.Create [ "httpMessage" ; "Content" ], SynExpr.CreateIdent (Ident.Create "queryParams"), range0 ) ) ] let implementation = let responseString = LetBang ( "responseString", SynExpr.awaitTask ( SynExpr.CreateApp ( SynExpr.CreateLongIdent ( SynLongIdent.Create [ "response" ; "Content" ; "ReadAsStringAsync" ] ), SynExpr.CreateIdentString "ct" ) ) ) let responseStream = LetBang ( "responseStream", SynExpr.awaitTask ( SynExpr.CreateApp ( SynExpr.CreateLongIdent ( SynLongIdent.Create [ "response" ; "Content" ; "ReadAsStreamAsync" ] ), SynExpr.CreateIdentString "ct" ) ) ) let jsonNode = LetBang ( "jsonNode", SynExpr.awaitTask ( SynExpr.CreateApp ( SynExpr.CreateLongIdent ( SynLongIdent.Create [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonNode" ; "ParseAsync" ] ), SynExpr.CreateParenedTuple [ SynExpr.CreateIdentString "responseStream" SynExpr.equals (SynExpr.CreateIdentString "cancellationToken") (SynExpr.CreateIdentString "ct") ] ) ) ) 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 Let ("uri", requestUri) yield Use ( "httpMessage", SynExpr.New ( false, SynType.CreateLongIdent ( SynLongIdent.Create [ "System" ; "Net" ; "Http" ; "HttpRequestMessage" ] ), httpReqMessageConstructor, range0 ) ) yield! handleBodyParams yield! setVariableHeaders yield! setConstantHeaders yield LetBang ( "response", SynExpr.awaitTask ( SynExpr.CreateApp ( SynExpr.CreateLongIdent (SynLongIdent.Create [ "client" ; "SendAsync" ]), SynExpr.CreateParenedTuple [ SynExpr.CreateIdentString "httpMessage" ; SynExpr.CreateIdentString "ct" ] ) ) ) if info.EnsureSuccessHttpCode then yield Let ( "response", SynExpr.CreateApp ( SynExpr.CreateLongIdent (SynLongIdent.Create [ "response" ; "EnsureSuccessStatusCode" ]), SynExpr.CreateConst SynConst.Unit ) ) match info.TaskReturnType with | HttpResponseMessage -> () | RestEaseResponseType _ -> yield responseString yield responseStream yield jsonNode | String -> yield responseString | Stream -> yield responseStream | _ -> yield responseStream yield jsonNode ] |> SynExpr.createCompExpr "async" returnExpr |> SynExpr.startAsTask (SynLongIdent.CreateFromLongIdent [ cancellationTokenArg ]) SynMemberDefn.Member ( SynBinding.SynBinding ( info.Accessibility, SynBindingKind.Normal, false, false, [], PreXmlDoc.Empty, valData, headPat, None, implementation, range0, DebugPointAtBinding.Yes range0, SynExpr.synBindingTriviaZero true ), range0 ) let getHttpAttributes (attrs : SynAttribute list) : HttpAttribute list = attrs |> List.choose (fun attr -> match attr.TypeName.AsString with | "RestEase.Query" | "RestEase.QueryAttribute" | "WoofWare.Myriad.Plugins.RestEase.Query" | "WoofWare.Myriad.Plugins.RestEase.QueryAttribute" | "Query" | "QueryAttribute" -> match attr.ArgExpr with | SynExpr.Const (SynConst.Unit, _) -> Some (HttpAttribute.Query None) | SynExpr.Const (SynConst.String (s, SynStringKind.Regular, _), _) -> Some (HttpAttribute.Query (Some s)) | SynExpr.Const (a, _) -> failwith $"unrecognised constant arg to the Query attribute: %+A{a}" | _ -> None | "RestEase.Path" | "RestEase.PathAttribute" | "WoofWare.Myriad.Plugins.RestEase.Path" | "WoofWare.Myriad.Plugins.RestEase.PathAttribute" | "Path" | "PathAttribute" -> match attr.ArgExpr |> SynExpr.stripOptionalParen with | SynExpr.Const (SynConst.String (s, SynStringKind.Regular, _), _) -> Some (HttpAttribute.Path (PathSpec.Verbatim s)) | SynExpr.Const (SynConst.Unit, _) -> Some (HttpAttribute.Path PathSpec.MatchArgName) | SynExpr.Const (a, _) -> failwith $"unrecognised constant arg to the Path attribute: %+A{a}" | _ -> None | "RestEase.Body" | "RestEase.BodyAttribute" | "WoofWare.Myriad.Plugins.RestEase.Body" | "WoofWare.Myriad.Plugins.RestEase.BodyAttribute" | "Body" | "BodyAttribute" -> match attr.ArgExpr with | SynExpr.Const (SynConst.Unit, _) -> Some HttpAttribute.Body | SynExpr.Const (a, _) -> failwith $"unrecognised constant arg to the Body attribute: %+A{a}" | _ -> None | _ -> None ) let extractBasePath (attrs : SynAttribute list) : SynExpr option = attrs |> List.tryPick (fun attr -> match attr.TypeName.AsString with | "BasePath" | "RestEase.BasePath" | "WoofWare.Myriad.Plugins.RestEase.BasePath" | "BasePathAttribute" | "RestEase.BasePathAttribute" | "WoofWare.Myriad.Plugins.RestEase.BasePathAttribute" -> Some attr.ArgExpr | _ -> None ) let extractBaseAddress (attrs : SynAttribute list) : SynExpr option = attrs |> List.tryPick (fun attr -> match attr.TypeName.AsString with | "BaseAddress" | "RestEase.BaseAddress" | "WoofWare.Myriad.Plugins.RestEase.BaseAddress" | "BaseAddressAttribute" | "RestEase.BaseAddressAttribute" | "WoofWare.Myriad.Plugins.RestEase.BaseAddressAttribute" -> Some attr.ArgExpr | _ -> None ) let createModule (opens : SynOpenDeclTarget list) (ns : LongIdent) (interfaceType : SynTypeDefn, spec : HttpClientGeneratorOutputSpec) : SynModuleOrNamespace = let interfaceType = AstHelper.parseInterface interfaceType if not (List.isEmpty interfaceType.Inherits) then failwith "HttpClientGenerator does not support inheritance. Remove the `inherit` keyword if you want to use this generator." 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 [
], but got no args" | [ _ ] -> failwith "Expected constant header parameters to be of the form [
], but got only one arg" | _ -> failwith "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 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 |> List.map (fun mem -> let httpMethod, url = extractHttpInformation mem.Attributes let shouldEnsureSuccess = not (shouldAllowAnyStatusCode mem.Attributes) let returnType = match mem.ReturnType with | Task ty -> ty | a -> failwith $"Method must return a generic Task; returned %+A{a}" if mem.IsMutable then failwith $"mutable methods not supported (identifier: %+A{mem.Identifier})" if mem.IsInline then failwith $"inline methods not supported (identifier: %+A{mem.Identifier})" let args = match mem.Args with | [ args ] -> args.Args |> List.map (fun arg -> { Attributes = arg.Attributes |> getHttpAttributes IsOptional = arg.IsOptional Id = arg.Id Type = arg.Type } ) | [] -> failwith $"Expected %+A{mem.Identifier} to have tupled args, but it had no args." | _ -> failwith $"Expected %+A{mem.Identifier} to have tupled args, but it was curried: %+A{mem.Args}." { HttpMethod = httpMethod UrlTemplate = url TaskReturnType = returnType Args = args Identifier = mem.Identifier EnsureSuccessHttpCode = shouldEnsureSuccess BaseAddress = baseAddress BasePath = basePath 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 [ Ident.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 docString = (if spec.ExtensionMethods then "Extension methods" else "Module") |> sprintf " %s for constructing a REST client." |> PreXmlDoc.Create let interfaceImpl = SynExpr.ObjExpr ( SynType.LongIdent (SynLongIdent.CreateFromLongIdent interfaceType.Name), None, Some range0, [], members, [], range0, range0 ) let headerArgs = properties |> List.map (fun (_, pi) -> SynPat.CreateTyped ( SynPat.CreateNamed (Ident.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." |> PreXmlDoc.Create let functionName = Ident.Create "client" let valData = let memberFlags = if spec.ExtensionMethods then { SynMemberFlags.IsInstance = false SynMemberFlags.IsDispatchSlot = false SynMemberFlags.IsOverrideOrExplicitImpl = false SynMemberFlags.IsFinal = false SynMemberFlags.GetterOrSetterIsCompilerGenerated = false SynMemberFlags.MemberKind = SynMemberKind.Member } |> Some else None SynValData.SynValData ( memberFlags, SynValInfo.SynValInfo ([ [ SynArgInfo.SynArgInfo ([], false, Some functionName) ] ], SynArgInfo.Empty), None ) let pattern = SynPat.CreateLongIdent (SynLongIdent.CreateString "make", headerArgs @ [ clientCreationArg ]) let returnInfo = SynBindingReturnInfo.Create (SynType.LongIdent (SynLongIdent.CreateFromLongIdent interfaceType.Name)) let nameWithoutLeadingI = List.last interfaceType.Name |> _.idText |> fun s -> if s.StartsWith 'I' then s.[1..] else failwith $"Expected interface type to start with 'I', but was: %s{s}" let createFunc = if spec.ExtensionMethods then let binding = SynBinding.SynBinding ( None, SynBindingKind.Normal, false, false, [], xmlDoc, valData, pattern, Some returnInfo, interfaceImpl, range0, DebugPointAtBinding.NoneAtInvisible, { LeadingKeyword = SynLeadingKeyword.StaticMember (range0, range0) InlineKeyword = None EqualsRange = Some range0 } ) let mem = SynMemberDefn.Member (binding, range0) let containingType = SynTypeDefn.SynTypeDefn ( SynComponentInfo.Create ( [ Ident.Create nameWithoutLeadingI ], xmldoc = PreXmlDoc.Create " Extension methods for HTTP clients" ), SynTypeDefnRepr.ObjectModel (SynTypeDefnKind.Augmentation range0, [], range0), [ mem ], None, range0, { LeadingKeyword = SynTypeDefnLeadingKeyword.Type range0 EqualsRange = None WithKeyword = None } ) SynModuleDecl.Types ([ containingType ], range0) else SynBinding.SynBinding ( None, SynBindingKind.Normal, false, false, [], xmlDoc, valData, pattern, Some returnInfo, interfaceImpl, range0, DebugPointAtBinding.NoneAtLet, SynExpr.synBindingTriviaZero false ) |> List.singleton |> SynModuleDecl.CreateLet let moduleName : LongIdent = if spec.ExtensionMethods then [ Ident.Create (nameWithoutLeadingI + "HttpClientExtension") ] else [ Ident.Create nameWithoutLeadingI ] let attribs = if spec.ExtensionMethods then [ SynAttributeList.Create SynAttribute.autoOpen ] else [ SynAttributeList.Create SynAttribute.compilationRepresentation SynAttributeList.Create (SynAttribute.RequireQualifiedAccess ()) ] let modInfo = SynComponentInfo.Create ( moduleName, attributes = attribs, xmldoc = docString, access = interfaceType.Accessibility ) SynModuleOrNamespace.CreateNamespace ( ns, decls = [ for openStatement in opens do yield SynModuleDecl.CreateOpen openStatement yield SynModuleDecl.CreateNestedModule (modInfo, [ createFunc ]) ] ) /// Myriad generator that provides an HTTP client for an interface type using RestEase annotations. [] type HttpClientGenerator () = interface IMyriadGenerator with member _.ValidInputExtensions = [ ".fs" ] member _.Generate (context : GeneratorContext) = let ast, _ = Ast.fromFilename context.InputFilename |> Async.RunSynchronously |> Array.head let types = Ast.extractTypeDefn ast let opens = AstHelper.extractOpens ast let namespaceAndTypes = types |> List.choose (fun (ns, types) -> types |> List.choose (fun typeDef -> match Ast.getAttribute typeDef with | None -> None | Some attr -> let arg = match SynExpr.stripOptionalParen attr.ArgExpr with | SynExpr.Const (SynConst.Bool value, _) -> value | SynExpr.Const (SynConst.Unit, _) -> JsonParseAttribute.DefaultIsExtensionMethod | arg -> failwith $"Unrecognised argument %+A{arg} to [<%s{nameof HttpClientAttribute}>]. Literals are not supported. Use `true` or `false` (or unit) only." let spec = { ExtensionMethods = arg } Some (typeDef, spec) ) |> function | [] -> None | ty -> Some (ns, ty) ) let modules = namespaceAndTypes |> List.collect (fun (ns, types) -> types |> List.map (HttpClientGenerator.createModule opens ns)) Output.Ast modules