-
Notifications
You must be signed in to change notification settings - Fork 60
add file upload and download endpoints to the test controller #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
17b0aa6
1324288
c8aabc7
a6e2400
4bcdb94
8bc7a82
e23ae83
a80dc24
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,12 @@ open FSharp.Data | |
| open Microsoft.FSharp.Quotations | ||
| open Microsoft.FSharp.Quotations.ExprShape | ||
| open System.Text.RegularExpressions | ||
| open System.IO | ||
|
|
||
| type BodyType = | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. modeling the body content like this made it easier to switch between formdata and multipart form data when we get parameters lists that have normal form data and files |
||
| | BodyForm of Expr<(string*string) seq> | ||
| | BodyObject of Expr<obj> | ||
| | BodyMultipart of Expr<MultipartItem seq> | ||
|
|
||
| /// Object for compiling operations. | ||
| type OperationCompiler (schema:SwaggerObject, defCompiler:DefinitionCompiler) = | ||
|
|
@@ -27,20 +33,23 @@ type OperationCompiler (schema:SwaggerObject, defCompiler:DefinitionCompiler) = | |
| let paramName = niceCamelName x.Name | ||
| let paramType = defCompiler.CompileTy methodName paramName x.Type x.Required | ||
| if x.Required then ProvidedParameter(paramName, paramType) | ||
| else | ||
| else | ||
| let paramDefaultValue = defCompiler.GetDefaultValue x.Type | ||
| ProvidedParameter(paramName, paramType, false, paramDefaultValue) | ||
| ] | ||
| let retTy = | ||
| let retTy, isReturnFile = | ||
| let okResponse = // BUG : wrong selector | ||
| op.Responses |> Array.tryFind (fun (code, resp) -> | ||
| (code.IsSome && (code.Value = 200 || code.Value = 201)) || code.IsNone) | ||
| match okResponse with | ||
| | Some (_,resp) -> | ||
| match resp.Schema with | ||
| | None -> typeof<unit> | ||
| | Some ty -> defCompiler.CompileTy methodName "Response" ty true | ||
| | None -> typeof<unit> | ||
| | None -> typeof<unit>, false | ||
| | Some ty when ty = SchemaObject.File -> | ||
| typeof<IO.Stream>, true | ||
| | Some ty -> | ||
| defCompiler.CompileTy methodName "Response" ty true, false | ||
| | None -> typeof<unit>, false | ||
|
|
||
| let m = ProvidedMethod(methodName, parameters, retTy) | ||
| if not <| String.IsNullOrEmpty(op.Summary) | ||
|
|
@@ -62,13 +71,13 @@ type OperationCompiler (schema:SwaggerObject, defCompiler:DefinitionCompiler) = | |
| // Fit headers into quotation | ||
| let headers = | ||
| let jsonConsumable = op.Consumes |> Seq.exists (fun mt -> mt="application/json") | ||
| <@@ | ||
| <@ | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. generally got away from the untyped expression API as much as possible in this PR. |
||
| let headersArr = (%%headers:(string*string)[]) | ||
| let ctHeaderExist = headersArr |> Array.exists (fun (h,_)->h="Content-Type") | ||
| if not(ctHeaderExist) && jsonConsumable | ||
| then Array.append [|"Content-Type","application/json"|] headersArr | ||
| else headersArr | ||
| @@> | ||
| @> | ||
| //let headerPairs = | ||
| // seq { | ||
| // yield! headers | ||
|
|
@@ -99,36 +108,52 @@ type OperationCompiler (schema:SwaggerObject, defCompiler:DefinitionCompiler) = | |
| let obj = Expr.Coerce(exp, typeof<obj>) | ||
| <@ (%%obj : obj).ToString() @> | ||
|
|
||
| let rec corceQueryString name expr = | ||
| let rec coerceQueryString name expr = | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tiny typo here |
||
| let obj = Expr.Coerce(expr, typeof<obj>) | ||
| <@ let o = (%%obj : obj) | ||
| SwaggerProvider.Internal.RuntimeHelpers.toQueryParams name o @> | ||
|
|
||
| let replacePathTemplate path name (exp : Expr) = | ||
| let template = "{" + name + "}" | ||
| <@@ Regex.Replace(%%path, template, string (%%exp : string)) @@> | ||
| let replacePathTemplate path name (exp: Expr<string>) = | ||
| let template = sprintf "{%s}" name | ||
| <@ Regex.Replace(%path, template, %exp) @> | ||
|
|
||
| let createStream (stringData: string) = | ||
| stringData |> Text.Encoding.UTF8.GetBytes |> MemoryStream :> Stream | ||
|
|
||
| let addPayload load (param : ParameterObject) (exp : Expr) = | ||
| let name = param.Name | ||
| let var = coerceString param.Type param.CollectionFormat exp | ||
| // delay the string coercion so that if it's a stream we don't tostring it. | ||
| 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)]) @@>) | ||
| | Some (BodyForm f) -> | ||
| Some (BodyForm <@ Seq.append %f [name, %var()] @>) | ||
| | Some (BodyForm f) when param.Type = SchemaObject.File -> | ||
| // have to convert existing form items to streams when we hit a file so that multipart upload can occur | ||
| let prevs = <@ %f |> Seq.map (fun (k,v) -> MultipartItem(k, k, createStream(v))) @> | ||
| let wrapper = Expr.Coerce (exp, typeof<FileWrapper>) |> Expr.Cast<FileWrapper> | ||
| Some (BodyMultipart <@ Seq.append %prevs [ MultipartItem(name, (%wrapper).fileName, (%wrapper).data) ] @>) | ||
| | Some (BodyMultipart f) -> | ||
| Some (BodyMultipart <@ Seq.append %f [ MultipartItem(name, name, createStream(%var())) ] @>) | ||
| | None when param.Type = SchemaObject.File -> | ||
| let wrapper = Expr.Coerce (exp, typeof<FileWrapper>) |> Expr.Cast<FileWrapper> | ||
| Some(BodyMultipart <@ Seq.singleton (MultipartItem(name, (%wrapper).fileName, (%wrapper).data)) @>) | ||
| | None -> | ||
| match param.In with | ||
| | Body -> | ||
| Some (BodyObject (Expr.Coerce (exp, typeof<obj>) |> Expr.Cast<obj>)) | ||
| | _ -> | ||
| Some (BodyForm <@ (seq [name, (%var() : string)]) @>) | ||
| | _ -> failwith ("Can only contain one payload") | ||
|
|
||
| let addQuery quer name (exp : Expr) = | ||
| let listValues = corceQueryString name exp | ||
| <@@ List.append | ||
| (%%quer : (string*string) list) | ||
| (%listValues : (string*string) list) @@> | ||
| let listValues = coerceQueryString name exp | ||
| <@ List.append %quer %listValues @> | ||
|
|
||
| let addHeader head name (exp : Expr) = | ||
| <@@ Array.append (%%head : (string*string) []) ([|name, (%%exp : string)|]) @@> | ||
| let addHeader head name (exp : Expr<string>) = | ||
| <@ Array.append %head ([| name, %exp |]) @> | ||
|
|
||
| // Partitions arguments based on their locations | ||
| let (path, payload, queries, heads) = | ||
| let (path, payload, queries: Expr<(string*string) list>, heads) = | ||
| let mPath = op.Path | ||
| parameters | ||
| |> List.fold ( | ||
|
|
@@ -137,54 +162,68 @@ type OperationCompiler (schema:SwaggerObject, defCompiler:DefinitionCompiler) = | |
| let value = coerceString param.Type param.CollectionFormat exp | ||
| match param.In with | ||
| | Path -> (replacePathTemplate path name value, load, quer, head) | ||
| | FormData | ||
| | Body -> (path, addPayload load param exp, quer, head) | ||
| | FormData | Body -> (path, addPayload load param exp, quer, head) | ||
| | Query -> (path, load, addQuery quer name exp, head) | ||
| | Header -> (path, load, quer, addHeader head name value) | ||
| ) | ||
| (<@@ mPath @@>, None, <@@ ([] : (string*string) list) @@>, headers) | ||
| (<@ mPath @>, None, <@ ([] : (string*string) list) @>, headers) | ||
|
|
||
|
|
||
| let address = <@@ SwaggerProvider.Internal.RuntimeHelpers.combineUrl %basePath (%%path :string ) @@> | ||
| let address = <@ SwaggerProvider.Internal.RuntimeHelpers.combineUrl %basePath %path @> | ||
| let restCall = op.Type.ToString() | ||
|
|
||
| let customizeHttpRequest = | ||
| <@@ let customizeCall = (%%customizeHttpRequest : Net.HttpWebRequest -> Net.HttpWebRequest) | ||
| fun (request:Net.HttpWebRequest) -> | ||
| <@ let customizeCall = %%customizeHttpRequest | ||
| fun (request:Net.HttpWebRequest) -> | ||
| if restCall = "Post" | ||
| then request.ContentLength <- 0L | ||
| customizeCall request @@> | ||
| customizeCall request @> | ||
|
|
||
| // Make HTTP call | ||
| let result = | ||
| match payload with | ||
| | None -> | ||
| <@@ Http.RequestString(%%address, | ||
| <@ Http.RequestStream(%address, | ||
| httpMethod = restCall, | ||
| headers = %heads, | ||
| query = %queries, | ||
| customizeHttpRequest = %customizeHttpRequest) @> | ||
| | Some (BodyMultipart parts) -> | ||
| <@ Http.RequestStream(%address, | ||
| httpMethod = restCall, | ||
| headers = (%%heads : array<string*string>), | ||
| query = (%%queries : (string * string) list), | ||
| customizeHttpRequest = (%%customizeHttpRequest : Net.HttpWebRequest -> Net.HttpWebRequest)) @@> | ||
| | Some (FormData, b) -> | ||
| <@@ Http.RequestString(%%address, | ||
| body = HttpRequestBody.Multipart(string (Guid.NewGuid()), %parts), | ||
| headers = %heads, | ||
| query = %queries, | ||
| customizeHttpRequest = %customizeHttpRequest) @> | ||
| | Some (BodyForm formData) -> | ||
| <@ Http.RequestStream(%address, | ||
| httpMethod = restCall, | ||
| headers = (%%heads : array<string*string>), | ||
| body = HttpRequestBody.FormValues (%%b : seq<string * string>), | ||
| query = (%%queries : (string * string) list), | ||
| customizeHttpRequest = (%%customizeHttpRequest : Net.HttpWebRequest -> Net.HttpWebRequest)) @@> | ||
| | Some (Body, b) -> | ||
| <@@ let body = SwaggerProvider.Internal.RuntimeHelpers.serialize (%%b : obj) | ||
| Http.RequestString(%%address, | ||
| headers = %heads, | ||
| body = HttpRequestBody.FormValues %formData, | ||
| query = %queries, | ||
| customizeHttpRequest = %customizeHttpRequest) @> | ||
| | Some (BodyObject b) -> | ||
| <@ let body = SwaggerProvider.Internal.RuntimeHelpers.serialize %b | ||
| Http.RequestStream(%address, | ||
| httpMethod = restCall, | ||
| headers = (%%heads : array<string*string>), | ||
| headers = %heads, | ||
| body = HttpRequestBody.TextRequest body, | ||
| query = (%%queries : (string * string) list), | ||
| customizeHttpRequest = (%%customizeHttpRequest : Net.HttpWebRequest -> Net.HttpWebRequest)) | ||
| @@> | ||
| | Some (x, _) -> failwith ("Payload should not be able to have type: " + string x) | ||
|
|
||
| query = %queries, | ||
| customizeHttpRequest = %customizeHttpRequest) @> | ||
| // Return deserialized object | ||
| let value = <@@ SwaggerProvider.Internal.RuntimeHelpers.deserialize | ||
| (%%result : string) retTy @@> | ||
|
|
||
| let value = | ||
| <@@ | ||
| let stream = (%result).ResponseStream | ||
| if isReturnFile | ||
| then box stream | ||
| else | ||
| let reader = new StreamReader(stream) | ||
| let body = reader.ReadToEnd() | ||
| let ret = box <| SwaggerProvider.Internal.RuntimeHelpers.deserialize body retTy | ||
| reader.Close() | ||
| stream.Close() | ||
| ret @@> | ||
| Expr.Coerce(value, retTy) | ||
|
|
||
| m | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| // Place your settings in this file to overwrite default and user settings. | ||
| { | ||
| fsharp | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,9 @@ | |
| open SwaggerProvider | ||
| open FSharp.Data | ||
| open Expecto | ||
| open SwaggerProvider.Internal.Compilers | ||
| open System.IO | ||
| open System | ||
|
|
||
| type PetStore = SwaggerProvider<"http://petstore.swagger.io/v2/swagger.json", "Content-Type=application/json"> | ||
| let store = PetStore() | ||
|
|
@@ -51,4 +54,11 @@ let petStoreTests = | |
| Expect.equal pet.Category pet2.Category "same Category" | ||
| Expect.equal pet.Status pet2.Status "same Status" | ||
| Expect.notEqual pet pet2 "different objects" | ||
|
|
||
| testCase "file Upload" <| fun _ -> | ||
| try | ||
| let result = store.UploadFile(6L, "thing", new FileWrapper("thing.jpg", new MemoryStream(Text.Encoding.UTF8.GetBytes("hello!")))) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FileWrapper is a leaky type, I'd like to not have to expose it. Thoughts? |
||
| printfn "%A" result | ||
| with | ||
| | exn -> () | ||
| ] | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to make this a tuple of these two pieces of data but that failed for some reason.