-
Notifications
You must be signed in to change notification settings - Fork 59
Expand file tree
/
Copy pathOperationCompiler.fs
More file actions
390 lines (323 loc) · 18.1 KB
/
OperationCompiler.fs
File metadata and controls
390 lines (323 loc) · 18.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
namespace SwaggerProvider.Internal.v2.Compilers
open ProviderImplementation.ProvidedTypes
open FSharp.Data.Runtime.NameUtils
open SwaggerProvider.Internal.v2.Parser.Schema
open Swagger.Internal
open System
open System.Net.Http
open System.Text.Json
open System.Text.RegularExpressions
open Microsoft.FSharp.Quotations
open Microsoft.FSharp.Quotations.ExprShape
open SwaggerProvider.Internal
open Swagger
/// Object for compiling operations.
type OperationCompiler(schema: SwaggerObject, defCompiler: DefinitionCompiler, ignoreControllerPrefix, ignoreOperationId, asAsync: bool) =
let compileOperation (methodName: string) (op: OperationObject) =
if String.IsNullOrWhiteSpace methodName then
failwithf $"Operation name could not be empty. See '%s{op.Path}/%A{op.Type}'"
let parameters =
/// handles deduping Swagger parameter names if the same parameter name
/// appears in multiple locations in a given operation definition.
let uniqueParamName existing (current: ParameterObject) =
let name = niceCamelName current.Name
if Set.contains name existing then
Set.add current.UnambiguousName existing, current.UnambiguousName
else
Set.add name existing, name
let required, optional = op.Parameters |> Array.partition(_.Required)
Array.append required optional
|> Array.fold
(fun (names, parameters) current ->
let names, paramName = uniqueParamName names current
let paramType =
defCompiler.CompileTy methodName paramName current.Type current.Required
let providedParam =
if current.Required then
ProvidedParameter(paramName, paramType)
else
let paramDefaultValue = defCompiler.GetDefaultValue paramType
ProvidedParameter(paramName, paramType, false, paramDefaultValue)
(names, providedParam :: parameters))
(Set.empty, [])
|> snd
// because we built up our list in reverse order with the fold,
// reverse it again so that all required properties come first
|> List.rev
// Append an optional CancellationToken parameter last (after all OpenAPI params).
// Using a UniqueNameGenerator avoids collisions with existing parameter names.
let ctArgIndex = List.length parameters
let parameters =
let scope = UniqueNameGenerator()
parameters |> List.iter(fun p -> scope.MakeUnique p.Name |> ignore)
let ctName = scope.MakeUnique "cancellationToken"
parameters
@ [ ProvidedParameter(ctName, typeof<Threading.CancellationToken>, false, null) ]
// find the inner type value
let retTy =
let okResponse =
op.Responses
|> Array.tryFind(fun (code, _) -> code.IsSome && code.Value = 200)
|> Option.orElseWith(fun () ->
op.Responses
|> Array.tryFind(fun (code, _) -> code.IsSome && code.Value >= 201 && code.Value < 300))
|> Option.orElseWith(fun () -> op.Responses |> Array.tryFind(fun (code, _) -> code.IsNone))
match okResponse with
| Some(_, resp) ->
match resp.Schema with
| None -> None
| Some ty -> Some <| defCompiler.CompileTy methodName "Response" ty true
| None -> None
let overallReturnType =
ProvidedTypeBuilder.MakeGenericType(
(if asAsync then
typedefof<Async<unit>>
else
typedefof<System.Threading.Tasks.Task<unit>>),
[ defaultArg retTy typeof<unit> ]
)
let m =
ProvidedMethod(
methodName,
parameters,
overallReturnType,
invokeCode =
fun args ->
let this =
Expr.Coerce(args[0], typeof<ProvidedApiClientBase>)
|> Expr.Cast<ProvidedApiClientBase>
let httpMethod = op.Type.ToString()
let basePath = schema.BasePath
let headers =
let jsonConsumable =
op.Consumes |> Seq.exists(fun mt -> mt = MediaTypes.ApplicationJson)
let jsonProducible =
op.Produces |> Seq.exists(fun mt -> mt = MediaTypes.ApplicationJson)
<@
[| if jsonProducible then
"Accept", MediaTypes.ApplicationJson
if jsonConsumable then
"Content-Type", MediaTypes.ApplicationJson |]
@>
// Extract CancellationToken (appended at ctArgIndex) and separate from OpenAPI params.
let allArgs = List.tail args // skip `this` param
let ct = List.item ctArgIndex allArgs |> Expr.Cast<Threading.CancellationToken>
// Locates parameters matching the arguments (excluding CT arg)
let parameters =
allArgs
|> List.indexed
|> List.choose(fun (i, arg) -> if i = ctArgIndex then None else Some arg)
|> List.map (function
| ShapeVar sVar as expr ->
let param =
op.Parameters
|> Array.find(fun x ->
// pain point: we have to make sure that the set of names we search for here are the same as the set of names generated when we make `parameters` above
let baseName = niceCamelName x.Name
baseName = sVar.Name || x.UnambiguousName = sVar.Name)
param, expr
| _ -> failwithf $"Function '%s{methodName}' does not support functions as arguments.")
// Makes argument a string // TODO: Make body an exception
let coerceString defType (format: CollectionFormat) exp =
let obj = Expr.Coerce(exp, typeof<obj>) |> Expr.Cast<obj>
<@ let x = (%obj) in RuntimeHelpers.toParam x @>
let rec coerceQueryString name expr =
let obj = Expr.Coerce(expr, typeof<obj>)
<@ let o = (%%obj: obj) in RuntimeHelpers.toQueryParams name o (%this) @>
let replacePathTemplate (path: Expr<string>) (name: string) (value: Expr<string>) =
let pattern = $"{{%s{name}}}"
// Escape $ in the replacement to avoid regex back-reference interpretation ($0, $& etc.)
let escaped = <@ (%value).Replace("$", "$$") @>
<@ Regex.Replace(%path, pattern, %escaped) @>
let addPayload load (param: ParameterObject) (exp: Expr) =
let name = param.Name
let var = coerceString param.Type param.CollectionFormat exp
match load with
| Some(FormData, b) -> Some(FormData, <@@ Seq.append %%b [ name, (%var: string) ] @@>)
| None ->
match param.In with
| Body -> Some(Body, Expr.Coerce(exp, typeof<obj>))
| _ -> Some(FormData, <@@ (seq [ name, (%var: string) ]) @@>)
| _ -> failwith("Can only contain one payload")
let addQuery (quer: Expr<(string * string) list>) name (exp: Expr) =
let listValues = coerceQueryString name exp
<@ List.append %quer %listValues @>
let addHeader (heads: Expr<(string * string)[]>) name (value: Expr<string>) =
<@ Array.append %heads [| name, %value |] @>
// Partitions arguments based on their locations
let path, payload, queries, heads =
let mPath = op.Path
parameters
|> List.fold
(fun (path, load, quer, head) (param: ParameterObject, exp) ->
let name = param.Name
match param.In with
| Path ->
let value = coerceString param.Type param.CollectionFormat exp
(replacePathTemplate path name value, load, quer, head)
| FormData
| Body -> (path, addPayload load param exp, quer, head)
| Query -> (path, load, addQuery quer name exp, head)
| Header ->
let value = coerceString param.Type param.CollectionFormat exp
(path, load, quer, addHeader head name value))
(<@ mPath @>, None, <@ [] @>, headers)
let address = <@ RuntimeHelpers.combineUrl basePath %path @>
let innerReturnType = defaultArg retTy null
let httpRequestMessage =
<@ RuntimeHelpers.createHttpRequest httpMethod %address %queries @>
let httpRequestMessageWithPayload =
match payload with
| None -> httpRequestMessage
| Some(FormData, b) ->
<@
let data = (%%b: seq<string * string>) |> Seq.map(fun (k, v) -> (k, box v))
let content = RuntimeHelpers.toFormUrlEncodedContent data
let msg = %httpRequestMessage
msg.Content <- content
msg
@>
| Some(Body, b) ->
<@
let valueStr = (%this).Serialize(%%b: obj)
let content = RuntimeHelpers.toStringContent(valueStr)
let msg = %httpRequestMessage
msg.Content <- content
msg
@>
| Some(x, _) -> failwith("Payload should not be able to have type: " + string x)
let action =
<@
let msg = %httpRequestMessageWithPayload
RuntimeHelpers.fillHeaders msg %heads
(%this).CallAsync(msg, [||], [||], %ct)
@>
let responseObj =
<@
let x = %action
task {
let! response = x
let! content = response.ReadAsStringAsync()
return (%this).Deserialize(content, innerReturnType)
}
@>
let responseUnit =
<@
let x = %action
task {
let! _ = x
return ()
}
@>
let awaitTask t =
<@ Async.AwaitTask(%t) @>
// if we're an async method, then we can just return the above, coerced to the overallReturnType.
// if we're not async, then run that^ through Async.RunSynchronously before doing the coercion.
match asAsync, retTy with
| false, Some t -> Expr.Coerce(<@ RuntimeHelpers.taskCast t %responseObj @>, overallReturnType)
| false, None -> responseUnit.Raw
| true, Some t -> Expr.Coerce(<@ RuntimeHelpers.asyncCast t %(awaitTask responseObj) @>, overallReturnType)
| true, None -> (awaitTask responseUnit).Raw
)
if not <| String.IsNullOrEmpty(op.Summary) then
m.AddXmlDoc(op.Summary) // TODO: Use description of parameters in docs
if op.Deprecated then
m.AddObsoleteAttribute("Operation is deprecated", false)
m
static member GetMethodNameCandidate (op: OperationObject) skipLength ignoreOperationId =
if ignoreOperationId || String.IsNullOrWhiteSpace(op.OperationId) then
let _, pathParts =
(op.Path.Split([| '/' |], StringSplitOptions.RemoveEmptyEntries), (false, []))
||> Array.foldBack(fun x (nextIsArg, pathParts) ->
if x.StartsWith("{") then
(true, pathParts)
else
(false, (if nextIsArg then singularize x else x) :: pathParts))
String.Join("_", op.Type.ToString() :: pathParts)
else
op.OperationId.Substring(skipLength)
|> nicePascalName
member _.CompileProvidedClients(ns: NamespaceAbstraction) =
let defaultHost =
let protocol =
match schema.Schemes with
| [||] -> "http" // Should use the scheme used to access the Swagger definition itself.
| array -> array[0]
$"%s{protocol}://%s{schema.Host}"
let baseTy = Some typeof<ProvidedApiClientBase>
let baseCtor = baseTy.Value.GetConstructors().[0]
List.ofArray schema.Paths
|> List.groupBy(fun x ->
if ignoreControllerPrefix then
String.Empty //
else
let ind = x.OperationId.IndexOf("_")
if ind <= 0 then
String.Empty
else
x.OperationId.Substring(0, ind))
|> List.iter(fun (clientName, operations) ->
let tyName = ns.ReserveUniqueName clientName "Client"
let ty =
ProvidedTypeDefinition(tyName, baseTy, isErased = false, isSealed = false, hideObjectMethods = true)
ns.RegisterType(tyName, ty)
if not <| String.IsNullOrEmpty clientName then
ty.AddXmlDoc $"Client for '%s{clientName}_*' operations"
[ ProvidedConstructor(
[ ProvidedParameter("httpClient", typeof<HttpClient>)
ProvidedParameter("options", typeof<JsonSerializerOptions>) ],
invokeCode =
(fun args ->
match args with
| [] -> failwith "Generated constructors should always pass the instance as the first argument!"
| _ -> <@@ () @@>),
BaseConstructorCall = fun args -> (baseCtor, args)
)
ProvidedConstructor(
[ ProvidedParameter("httpClient", typeof<HttpClient>) ],
invokeCode =
(fun args ->
match args with
| [] -> failwith "Generated constructors should always pass the instance as the first argument!"
| _ -> <@@ () @@>),
BaseConstructorCall =
fun args ->
let args' = args @ [ <@@ null @@> ]
(baseCtor, args')
)
ProvidedConstructor(
[ ProvidedParameter("options", typeof<JsonSerializerOptions>) ],
invokeCode = (fun _ -> <@@ () @@>),
BaseConstructorCall =
fun args ->
let httpClient = <@ RuntimeHelpers.getDefaultHttpClient defaultHost @> :> Expr
let args' =
match args with
| [ instance; options ] -> [ instance; httpClient; options ]
| _ -> failwithf $"unexpected arguments received %A{args}"
(baseCtor, args')
)
ProvidedConstructor(
[],
invokeCode = (fun _ -> <@@ () @@>),
BaseConstructorCall =
fun args ->
let httpClient = <@ RuntimeHelpers.getDefaultHttpClient defaultHost @> :> Expr
let args' =
match args with
| [ instance ] -> [ instance; httpClient; <@@ null @@> ]
| _ -> failwithf $"unexpected arguments received %A{args}"
(baseCtor, args')
) ]
|> ty.AddMembers
let methodNameScope = UniqueNameGenerator()
operations
|> List.map(fun op ->
let skipLength =
if String.IsNullOrEmpty clientName then
0
else
clientName.Length + 1
let name = OperationCompiler.GetMethodNameCandidate op skipLength ignoreOperationId
compileOperation (methodNameScope.MakeUnique name) op)
|> ty.AddMembers)