-
Notifications
You must be signed in to change notification settings - Fork 76
Expand file tree
/
Copy pathMultipartRequest.fs
More file actions
190 lines (180 loc) · 8.8 KB
/
MultipartRequest.fs
File metadata and controls
190 lines (180 loc) · 8.8 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
namespace FSharp.Data.GraphQL.Samples.StarWarsApi
open System
open System.Collections
open System.Collections.Generic
open System.Text.Json
open System.Text.Json.Serialization
open Microsoft.AspNetCore.WebUtilities
open FSharp.Data.GraphQL
open FSharp.Data.GraphQL.Ast
[<Struct>]
type GraphQLMultipartSection =
| Form of Form : FormMultipartSection
| File of File : FileMultipartSection
static member FromSection (section : MultipartSection) =
match section with
| null -> ValueNone
| _ ->
match section.AsFormDataSection () with
| null ->
match section.AsFileSection () with
| null -> ValueNone
| x -> ValueSome (File x)
| x -> ValueSome (Form x)
member x.Name =
match x with
| Form x -> x.Name
| File x -> x.Name
/// <summary> A GrahpQL request using multipart request specification. </summary>
/// <remarks> For more information, see https://github.com/jaydenseric/graphql-multipart-request-spec. </remarks>
[<Struct>]
type MultipartRequest =
/// Contains the list of operations of this request.
/// If the request is not batched, then the single operation will be inside this list as a singleton.
{
Operations : GQLRequestContent list
}
/// Contains tools for working with GraphQL multipart requests.
module MultipartRequest =
let private parseOperations
(jsonOptions : JsonSerializerOptions)
(operations : GQLRequestContent list)
(map : IDictionary<string, string>)
(files : IDictionary<string, File>)
=
let mapOperation (operationIndex : int voption) (operation : GQLRequestContent) =
let findFile (varName : string) (varValue : JsonElement) = seq {
let tryPickMultipleFilesFromMap (length : int) (varName : string) =
Seq.init length (fun ix ->
match map.TryGetValue $"%s{varName}.%i{ix}" with
| (true, v) -> Some v
| _ -> None)
|> Seq.map (fun key ->
key
|> Option.map (fun key ->
match files.TryGetValue (key) with
| (true, v) -> Some v
| _ -> None)
|> Option.flatten)
|> List.ofSeq
let pickMultipleFilesFromMap (length : int) (varName : string) =
Seq.init length (fun ix -> map.[$"%s{varName}.%i{ix}"])
|> Seq.map (fun key -> files.[key])
|> List.ofSeq
let tryPickSingleFileFromMap varName =
let found =
map
|> Seq.choose (fun kvp -> if kvp.Key = varName then Some files.[kvp.Value] else None)
|> List.ofSeq
match found with
| [ x ] -> Some x
| _ -> None
let pickSingleFileFromMap varName =
map
|> Seq.choose (fun kvp -> if kvp.Key = varName then Some files.[kvp.Value] else None)
|> Seq.exactlyOne
let pickFileRequestFromMap (request : UploadRequest) varName : UploadRequest = {
Single = pickSingleFileFromMap $"%s{varName}.single"
Multiple = pickMultipleFilesFromMap request.Multiple.Length $"%s{varName}.multiple"
NullableMultiple =
request.NullableMultiple
|> Option.map (fun x -> pickMultipleFilesFromMap x.Length $"%s{varName}.nullableMultiple")
NullableMultipleNullable =
request.NullableMultipleNullable
|> Option.map (fun x -> tryPickMultipleFilesFromMap x.Length $"%s{varName}.nullableMultipleNullable")
}
let rec isUpload (t : InputType) =
match t with
| NamedType tname -> tname = "Upload" || tname = "UploadRequest"
| ListType t
| NonNullType t -> isUpload t
let ast = Parser.parse operation.Query
let varDefs =
ast.Definitions
|> List.choose (function
| OperationDefinition def -> Some def.VariableDefinitions
| _ -> None)
|> List.collect id
let varDef = varDefs |> List.find (fun x -> x.VariableName = varName)
if isUpload varDef.Type then
match varValue.ValueKind with
| JsonValueKind.Object ->
let request = varValue.Deserialize<UploadRequest> (jsonOptions)
let varName =
match operationIndex with
| ValueSome operationIndex -> $"%i{operationIndex}.variables.%s{varName}"
| ValueNone -> $"variables.%s{varName}"
yield pickFileRequestFromMap request varName
| JsonValueKind.Array ->
let files =
varValue.EnumerateArray ()
|> Seq.mapi (fun valueIndex _ ->
let varName =
match operationIndex with
| ValueSome operationIndex -> $"%i{operationIndex}.variables.%s{varName}.%i{valueIndex}"
| ValueNone -> $"variables.%s{varName}.%i{valueIndex}"
tryPickSingleFileFromMap varName)
|> Seq.choose id
yield! files
| _ ->
let varName =
match operationIndex with
| ValueSome operationIndex -> $"%i{operationIndex}.variables.%s{varName}"
| ValueNone -> $"variables.%s{varName}"
match tryPickSingleFileFromMap varName with
| Some File -> yield file
| None -> ()
}
{
operation with
Variables =
operation.Variables
|> Skippable.map (Map.map (fun k v -> findFile k v))
}
match operations with
| [ operation ] -> [ mapOperation ValueNone operation ]
| operations ->
operations
|> List.mapi (fun ix operation -> mapOperation (ValueSome ix) operation)
/// Reads a GraphQL multipart request from a MultipartReader.
let read (jsonOptions : JsonSerializerOptions) cancellationToken (reader : MultipartReader) = task {
let mutable section : GraphQLMultipartSection voption = ValueNone
let readNextSection () = task {
let! next = reader.ReadNextSectionAsync cancellationToken
section <- GraphQLMultipartSection.FromSection (next)
}
let mutable operations : string = null
let mutable map : IDictionary<string, string> = null
let files = Dictionary<string, File> ()
do! readNextSection ()
while not section.IsNone do
match section.Value with
| Form section ->
let! value = section.GetValueAsync ()
match section.Name with
| "operations" -> operations <- value
| "map" ->
map <-
JsonSerializer.Deserialize<Map<string, string list>> (value, jsonOptions)
|> Seq.map (fun kvp -> kvp.Value.Head, kvp.Key)
|> Map.ofSeq
| _ -> failwithf $"""Error reading multipart request. Unexpected section name "%s{section.Name}"."""
| File section ->
let stream = new System.IO.MemoryStream (4096)
do! section.FileStream.CopyToAsync (stream, cancellationToken)
stream.Position <- 0L
let value = {
Name = section.FileName
ContentType = section.Section.ContentType
Content = stream
}
files.Add (section.Name, value)
do! readNextSection ()
let operations =
let jsonElement = (JsonDocument.Parse operations).RootElement
match jsonElement.ValueKind with
| JsonValueKind.Array -> jsonElement.Deserialize<GQLRequestContent list> (jsonOptions)
| JsonValueKind.Object -> [ jsonElement.Deserialize<GQLRequestContent> (jsonOptions) ]
| _ -> failwith "Unexpected operations value."
return { Operations = parseOperations jsonOptions operations map files }
}