elm-pages 3.0.0-beta.41 → 3.0.0-beta.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/codegen/elm-pages-codegen.cjs +47 -23
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/d.dat +0 -0
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/node_runner.js +1 -1
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/node_supervisor.js +1 -1
- package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/d.dat +0 -0
- package/generator/review/elm-stuff/tests-0.19.1/js/node_runner.js +1 -1
- package/generator/review/elm-stuff/tests-0.19.1/js/node_supervisor.js +1 -1
- package/generator/src/RouteBuilder.elm +12 -54
- package/generator/src/compatibility-key.js +2 -2
- package/package.json +2 -2
- package/src/ApiRoute.elm +3 -31
- package/src/FormData.elm +21 -1
- package/src/Internal/Request.elm +84 -4
- package/src/Pages/FormData.elm +2 -1
- package/src/Pages/Internal/Platform/Cli.elm +15 -2
- package/src/Pages/Internal/Platform/CompatibilityKey.elm +1 -1
- package/src/Pages/Navigation.elm +0 -2
- package/src/Pages/ProgramConfig.elm +3 -2
- package/src/Scaffold/Route.elm +70 -50
- package/src/Server/Request.elm +445 -952
- package/src/Server/Session.elm +140 -90
package/src/Server/Request.elm
CHANGED
|
@@ -1,126 +1,45 @@
|
|
|
1
1
|
module Server.Request exposing
|
|
2
|
-
(
|
|
3
|
-
,
|
|
2
|
+
( Request
|
|
3
|
+
, requestTime
|
|
4
|
+
, header, headers
|
|
5
|
+
, method, Method(..), methodToString
|
|
6
|
+
, body, jsonBody
|
|
4
7
|
, formData, formDataWithServerValidation
|
|
5
8
|
, rawFormData
|
|
6
|
-
,
|
|
7
|
-
,
|
|
8
|
-
,
|
|
9
|
-
,
|
|
10
|
-
, queryParam, expectQueryParam
|
|
11
|
-
, cookie, expectCookie
|
|
12
|
-
, expectHeader
|
|
13
|
-
, File, expectMultiPartFormPost
|
|
14
|
-
, expectBody
|
|
15
|
-
, map3, map4, map5, map6, map7, map8, map9
|
|
16
|
-
, Method(..), methodToString
|
|
17
|
-
, errorsToString, errorToString, getDecoder, ValidationError
|
|
9
|
+
, rawUrl
|
|
10
|
+
, queryParam, queryParams
|
|
11
|
+
, matchesContentType
|
|
12
|
+
, cookie, cookies
|
|
18
13
|
)
|
|
19
14
|
|
|
20
|
-
{-|
|
|
15
|
+
{-| Server-rendered Route modules and [server-rendered API Routes](ApiRoute#serverRender) give you access to a `Server.Request.Request` argument.
|
|
21
16
|
|
|
22
|
-
@docs
|
|
17
|
+
@docs Request
|
|
23
18
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
## Forms
|
|
28
|
-
|
|
29
|
-
@docs formData, formDataWithServerValidation
|
|
30
|
-
|
|
31
|
-
@docs rawFormData
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
## Direct Values
|
|
35
|
-
|
|
36
|
-
@docs method, rawBody, allCookies, rawHeaders, queryParams
|
|
37
|
-
|
|
38
|
-
@docs requestTime, optionalHeader, expectContentType, expectJsonBody
|
|
39
|
-
|
|
40
|
-
@docs acceptMethod, acceptContentTypes
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
## Transforming
|
|
44
|
-
|
|
45
|
-
@docs map, map2, oneOf, andMap, andThen
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
## Query Parameters
|
|
49
|
-
|
|
50
|
-
@docs queryParam, expectQueryParam
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
## Cookies
|
|
54
|
-
|
|
55
|
-
@docs cookie, expectCookie
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
## Headers
|
|
59
|
-
|
|
60
|
-
@docs expectHeader
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
## Multi-part forms and file uploads
|
|
64
|
-
|
|
65
|
-
@docs File, expectMultiPartFormPost
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
## Request Parsers That Can Fail
|
|
69
|
-
|
|
70
|
-
@docs expectBody
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
## Map Functions
|
|
74
|
-
|
|
75
|
-
@docs map3, map4, map5, map6, map7, map8, map9
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
## Method Type
|
|
79
|
-
|
|
80
|
-
@docs Method, methodToString
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
## Internals
|
|
84
|
-
|
|
85
|
-
@docs errorsToString, errorToString, getDecoder, ValidationError
|
|
86
|
-
|
|
87
|
-
-}
|
|
88
|
-
|
|
89
|
-
import BackendTask exposing (BackendTask)
|
|
90
|
-
import CookieParser
|
|
91
|
-
import Dict exposing (Dict)
|
|
92
|
-
import FatalError exposing (FatalError)
|
|
93
|
-
import Form
|
|
94
|
-
import Form.Handler
|
|
95
|
-
import Form.Validation as Validation
|
|
96
|
-
import FormData
|
|
97
|
-
import Internal.Request
|
|
98
|
-
import Json.Decode
|
|
99
|
-
import Json.Encode
|
|
100
|
-
import List.NonEmpty
|
|
101
|
-
import Pages.Form
|
|
102
|
-
import QueryParams
|
|
103
|
-
import Time
|
|
104
|
-
import Url
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
{-| A `Server.Request.Parser` lets you send a `Server.Response.Response` based on an incoming HTTP request. For example,
|
|
108
|
-
using a `Server.Request.Parser`, you could check a session cookie to decide whether to respond by rendering a page
|
|
19
|
+
For example, in a server-rendered route,
|
|
20
|
+
you could check a session cookie to decide whether to respond by rendering a page
|
|
109
21
|
for the logged-in user, or else respond with an HTTP redirect response (see the [`Server.Response` docs](Server-Response)).
|
|
110
22
|
|
|
111
23
|
You can access the incoming HTTP request's:
|
|
112
24
|
|
|
113
|
-
- Headers
|
|
114
|
-
- Cookies
|
|
25
|
+
- [Headers](#headers)
|
|
26
|
+
- [Cookies](#cookies)
|
|
115
27
|
- [`method`](#method)
|
|
116
|
-
-
|
|
28
|
+
- [`rawUrl`](#rawUrl)
|
|
117
29
|
- [`requestTime`](#requestTime) (as a `Time.Posix`)
|
|
118
30
|
|
|
31
|
+
There are also some high-level helpers that take the low-level Request data and let you parse it into Elm types:
|
|
32
|
+
|
|
33
|
+
- [`jsonBody`](#jsonBody)
|
|
34
|
+
- [Form Helpers](#forms)
|
|
35
|
+
- [URL query parameters](#queryParam)
|
|
36
|
+
- [Content Type](#content-type)
|
|
37
|
+
|
|
119
38
|
Note that this data is not available for pre-rendered pages or pre-rendered API Routes, only for server-rendered pages.
|
|
120
39
|
This is because when a page is pre-rendered, there _is_ no incoming HTTP request to respond to, it is rendered before a user
|
|
121
40
|
requests the page and then the pre-rendered page is served as a plain file (without running your Route Module).
|
|
122
41
|
|
|
123
|
-
That's why `RouteBuilder.preRender`
|
|
42
|
+
That's why `RouteBuilder.preRender` does not have a `Server.Request.Request` argument.
|
|
124
43
|
|
|
125
44
|
import BackendTask exposing (BackendTask)
|
|
126
45
|
import RouteBuilder exposing (StatelessRoute)
|
|
@@ -142,366 +61,68 @@ That's why `RouteBuilder.preRender` has `data : RouteParams -> BackendTask Data`
|
|
|
142
61
|
|> RouteBuilder.buildNoState { view = view }
|
|
143
62
|
|
|
144
63
|
A server-rendered Route Module _does_ have access to a user's incoming HTTP request because it runs every time the page
|
|
145
|
-
is loaded. That's why `data`
|
|
146
|
-
`RouteBuilder.serverRender` has `data : RouteParams -> Request
|
|
64
|
+
is loaded. That's why `data` has a `Server.Request.Request` argument in server-rendered Route Modules. Since you have an incoming HTTP request for server-rendered routes,
|
|
65
|
+
`RouteBuilder.serverRender` has `data : RouteParams -> Request -> BackendTask (Response Data)`. That means that you
|
|
147
66
|
can use the incoming HTTP request data to choose how to respond. For example, you could check for a dark-mode preference
|
|
148
67
|
cookie and render a light- or dark-themed page and render a different page.
|
|
149
68
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
`Request.Parser` means you can pull out
|
|
69
|
+
@docs requestTime
|
|
153
70
|
|
|
154
|
-
data from the request payload using a Server Request Parser.
|
|
155
|
-
|
|
156
|
-
import BackendTask exposing (BackendTask)
|
|
157
|
-
import RouteBuilder exposing (StatelessRoute)
|
|
158
|
-
import Server.Request as Request exposing (Request)
|
|
159
|
-
import Server.Response as Response exposing (Response)
|
|
160
|
-
|
|
161
|
-
type alias Data =
|
|
162
|
-
{}
|
|
163
|
-
|
|
164
|
-
data :
|
|
165
|
-
RouteParams
|
|
166
|
-
-> Request.Parser (BackendTask (Response Data))
|
|
167
|
-
data routeParams =
|
|
168
|
-
{}
|
|
169
|
-
|> Server.Response.render
|
|
170
|
-
|> BackendTask.succeed
|
|
171
|
-
|> Request.succeed
|
|
172
71
|
|
|
173
|
-
|
|
174
|
-
route =
|
|
175
|
-
RouteBuilder.serverRender
|
|
176
|
-
{ head = head
|
|
177
|
-
, data = data
|
|
178
|
-
}
|
|
179
|
-
|> RouteBuilder.buildNoState { view = view }
|
|
72
|
+
## Headers
|
|
180
73
|
|
|
181
|
-
|
|
182
|
-
type alias Parser decodesTo =
|
|
183
|
-
Internal.Request.Parser decodesTo ValidationError
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
oneOfInternal : List ValidationError -> List (Json.Decode.Decoder ( Result ValidationError decodesTo, List ValidationError )) -> Json.Decode.Decoder ( Result ValidationError decodesTo, List ValidationError )
|
|
187
|
-
oneOfInternal previousErrors optimizedDecoders =
|
|
188
|
-
-- elm-review: known-unoptimized-recursion
|
|
189
|
-
case optimizedDecoders of
|
|
190
|
-
[] ->
|
|
191
|
-
Json.Decode.succeed ( Err (OneOf previousErrors), [] )
|
|
192
|
-
|
|
193
|
-
[ single ] ->
|
|
194
|
-
single
|
|
195
|
-
|> Json.Decode.map
|
|
196
|
-
(\result ->
|
|
197
|
-
result
|
|
198
|
-
|> Tuple.mapFirst (Result.mapError (\error -> OneOf (previousErrors ++ [ error ])))
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
first :: rest ->
|
|
202
|
-
first
|
|
203
|
-
|> Json.Decode.andThen
|
|
204
|
-
(\( firstResult, firstErrors ) ->
|
|
205
|
-
case ( firstResult, firstErrors ) of
|
|
206
|
-
( Ok okFirstResult, [] ) ->
|
|
207
|
-
Json.Decode.succeed ( Ok okFirstResult, [] )
|
|
208
|
-
|
|
209
|
-
( Ok _, otherErrors ) ->
|
|
210
|
-
oneOfInternal (previousErrors ++ otherErrors) rest
|
|
211
|
-
|
|
212
|
-
( Err error, _ ) ->
|
|
213
|
-
case error of
|
|
214
|
-
OneOf errors ->
|
|
215
|
-
oneOfInternal (previousErrors ++ errors) rest
|
|
216
|
-
|
|
217
|
-
_ ->
|
|
218
|
-
oneOfInternal (previousErrors ++ [ error ]) rest
|
|
219
|
-
)
|
|
74
|
+
@docs header, headers
|
|
220
75
|
|
|
221
76
|
|
|
222
|
-
|
|
223
|
-
succeed : value -> Parser value
|
|
224
|
-
succeed value =
|
|
225
|
-
Internal.Request.Parser (Json.Decode.succeed ( Ok value, [] ))
|
|
77
|
+
## Method
|
|
226
78
|
|
|
79
|
+
@docs method, Method, methodToString
|
|
227
80
|
|
|
228
|
-
{-| TODO internal only
|
|
229
|
-
-}
|
|
230
|
-
getDecoder : Parser (BackendTask error response) -> Json.Decode.Decoder (Result ( ValidationError, List ValidationError ) (BackendTask error response))
|
|
231
|
-
getDecoder (Internal.Request.Parser decoder) =
|
|
232
|
-
decoder
|
|
233
|
-
|> Json.Decode.map
|
|
234
|
-
(\( result, validationErrors ) ->
|
|
235
|
-
case ( result, validationErrors ) of
|
|
236
|
-
( Ok value, [] ) ->
|
|
237
|
-
value
|
|
238
|
-
|> Ok
|
|
239
|
-
|
|
240
|
-
( Ok _, firstError :: rest ) ->
|
|
241
|
-
Err ( firstError, rest )
|
|
242
|
-
|
|
243
|
-
( Err fatalError, errors ) ->
|
|
244
|
-
Err ( fatalError, errors )
|
|
245
|
-
)
|
|
246
81
|
|
|
82
|
+
## Body
|
|
247
83
|
|
|
248
|
-
|
|
249
|
-
type ValidationError
|
|
250
|
-
= ValidationError String
|
|
251
|
-
| OneOf (List ValidationError)
|
|
252
|
-
| MissingQueryParam { missingParam : String, allQueryParams : String }
|
|
84
|
+
@docs body, jsonBody
|
|
253
85
|
|
|
254
86
|
|
|
255
|
-
|
|
256
|
-
errorsToString : ( ValidationError, List ValidationError ) -> String
|
|
257
|
-
errorsToString validationErrors =
|
|
258
|
-
validationErrors
|
|
259
|
-
|> List.NonEmpty.toList
|
|
260
|
-
|> List.map errorToString
|
|
261
|
-
|> String.join "\n"
|
|
87
|
+
## Forms
|
|
262
88
|
|
|
89
|
+
@docs formData, formDataWithServerValidation
|
|
263
90
|
|
|
264
|
-
|
|
265
|
-
-}
|
|
266
|
-
errorToString : ValidationError -> String
|
|
267
|
-
errorToString validationError_ =
|
|
268
|
-
-- elm-review: known-unoptimized-recursion
|
|
269
|
-
case validationError_ of
|
|
270
|
-
ValidationError message ->
|
|
271
|
-
message
|
|
272
|
-
|
|
273
|
-
OneOf validationErrors ->
|
|
274
|
-
"Server.Request.oneOf failed in the following "
|
|
275
|
-
++ String.fromInt (List.length validationErrors)
|
|
276
|
-
++ " ways:\n\n"
|
|
277
|
-
++ (validationErrors
|
|
278
|
-
|> List.indexedMap (\index error -> "(" ++ String.fromInt (index + 1) ++ ") " ++ errorToString error)
|
|
279
|
-
|> String.join "\n\n"
|
|
280
|
-
)
|
|
91
|
+
@docs rawFormData
|
|
281
92
|
|
|
282
|
-
MissingQueryParam record ->
|
|
283
|
-
"Missing query param \"" ++ record.missingParam ++ "\". Query string was `" ++ record.allQueryParams ++ "`"
|
|
284
93
|
|
|
94
|
+
## URL
|
|
285
95
|
|
|
286
|
-
|
|
287
|
-
map : (a -> b) -> Parser a -> Parser b
|
|
288
|
-
map mapFn (Internal.Request.Parser decoder) =
|
|
289
|
-
Internal.Request.Parser
|
|
290
|
-
(Json.Decode.map
|
|
291
|
-
(\( result, errors ) ->
|
|
292
|
-
( Result.map mapFn result, errors )
|
|
293
|
-
)
|
|
294
|
-
decoder
|
|
295
|
-
)
|
|
96
|
+
@docs rawUrl
|
|
296
97
|
|
|
98
|
+
@docs queryParam, queryParams
|
|
297
99
|
|
|
298
|
-
{-| -}
|
|
299
|
-
oneOf : List (Parser a) -> Parser a
|
|
300
|
-
oneOf serverRequests =
|
|
301
|
-
Internal.Request.Parser
|
|
302
|
-
(oneOfInternal []
|
|
303
|
-
(List.map
|
|
304
|
-
(\(Internal.Request.Parser decoder) -> decoder)
|
|
305
|
-
serverRequests
|
|
306
|
-
)
|
|
307
|
-
)
|
|
308
100
|
|
|
101
|
+
## Content Type
|
|
309
102
|
|
|
310
|
-
|
|
103
|
+
@docs matchesContentType
|
|
311
104
|
|
|
312
|
-
decoder : Decoder String
|
|
313
|
-
decoder =
|
|
314
|
-
succeed (String.repeat)
|
|
315
|
-
|> andMap (field "count" int)
|
|
316
|
-
|> andMap (field "val" string)
|
|
317
105
|
|
|
106
|
+
## Cookies
|
|
318
107
|
|
|
319
|
-
|
|
320
|
-
|> decodeString decoder
|
|
321
|
-
--> Success "hihihi"
|
|
108
|
+
@docs cookie, cookies
|
|
322
109
|
|
|
323
110
|
-}
|
|
324
|
-
andMap : Parser a -> Parser (a -> b) -> Parser b
|
|
325
|
-
andMap =
|
|
326
|
-
map2 (|>)
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
{-| -}
|
|
330
|
-
andThen : (a -> Parser b) -> Parser a -> Parser b
|
|
331
|
-
andThen toRequestB (Internal.Request.Parser requestA) =
|
|
332
|
-
Json.Decode.andThen
|
|
333
|
-
(\value ->
|
|
334
|
-
case value of
|
|
335
|
-
( Ok okValue, _ ) ->
|
|
336
|
-
okValue
|
|
337
|
-
|> toRequestB
|
|
338
|
-
|> unwrap
|
|
339
|
-
|
|
340
|
-
( Err error, errors ) ->
|
|
341
|
-
Json.Decode.succeed ( Err error, errors )
|
|
342
|
-
)
|
|
343
|
-
requestA
|
|
344
|
-
|> Internal.Request.Parser
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
unwrap : Parser a -> Json.Decode.Decoder ( Result ValidationError a, List ValidationError )
|
|
348
|
-
unwrap (Internal.Request.Parser decoder_) =
|
|
349
|
-
decoder_
|
|
350
|
-
|
|
351
111
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
{-| -}
|
|
368
|
-
map3 :
|
|
369
|
-
(value1 -> value2 -> value3 -> valueCombined)
|
|
370
|
-
-> Parser value1
|
|
371
|
-
-> Parser value2
|
|
372
|
-
-> Parser value3
|
|
373
|
-
-> Parser valueCombined
|
|
374
|
-
map3 combineFn request1 request2 request3 =
|
|
375
|
-
succeed combineFn
|
|
376
|
-
|> andMap request1
|
|
377
|
-
|> andMap request2
|
|
378
|
-
|> andMap request3
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
{-| -}
|
|
382
|
-
map4 :
|
|
383
|
-
(value1 -> value2 -> value3 -> value4 -> valueCombined)
|
|
384
|
-
-> Parser value1
|
|
385
|
-
-> Parser value2
|
|
386
|
-
-> Parser value3
|
|
387
|
-
-> Parser value4
|
|
388
|
-
-> Parser valueCombined
|
|
389
|
-
map4 combineFn request1 request2 request3 request4 =
|
|
390
|
-
succeed combineFn
|
|
391
|
-
|> andMap request1
|
|
392
|
-
|> andMap request2
|
|
393
|
-
|> andMap request3
|
|
394
|
-
|> andMap request4
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
{-| -}
|
|
398
|
-
map5 :
|
|
399
|
-
(value1 -> value2 -> value3 -> value4 -> value5 -> valueCombined)
|
|
400
|
-
-> Parser value1
|
|
401
|
-
-> Parser value2
|
|
402
|
-
-> Parser value3
|
|
403
|
-
-> Parser value4
|
|
404
|
-
-> Parser value5
|
|
405
|
-
-> Parser valueCombined
|
|
406
|
-
map5 combineFn request1 request2 request3 request4 request5 =
|
|
407
|
-
succeed combineFn
|
|
408
|
-
|> andMap request1
|
|
409
|
-
|> andMap request2
|
|
410
|
-
|> andMap request3
|
|
411
|
-
|> andMap request4
|
|
412
|
-
|> andMap request5
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
{-| -}
|
|
416
|
-
map6 :
|
|
417
|
-
(value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> valueCombined)
|
|
418
|
-
-> Parser value1
|
|
419
|
-
-> Parser value2
|
|
420
|
-
-> Parser value3
|
|
421
|
-
-> Parser value4
|
|
422
|
-
-> Parser value5
|
|
423
|
-
-> Parser value6
|
|
424
|
-
-> Parser valueCombined
|
|
425
|
-
map6 combineFn request1 request2 request3 request4 request5 request6 =
|
|
426
|
-
succeed combineFn
|
|
427
|
-
|> andMap request1
|
|
428
|
-
|> andMap request2
|
|
429
|
-
|> andMap request3
|
|
430
|
-
|> andMap request4
|
|
431
|
-
|> andMap request5
|
|
432
|
-
|> andMap request6
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
{-| -}
|
|
436
|
-
map7 :
|
|
437
|
-
(value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> value7 -> valueCombined)
|
|
438
|
-
-> Parser value1
|
|
439
|
-
-> Parser value2
|
|
440
|
-
-> Parser value3
|
|
441
|
-
-> Parser value4
|
|
442
|
-
-> Parser value5
|
|
443
|
-
-> Parser value6
|
|
444
|
-
-> Parser value7
|
|
445
|
-
-> Parser valueCombined
|
|
446
|
-
map7 combineFn request1 request2 request3 request4 request5 request6 request7 =
|
|
447
|
-
succeed combineFn
|
|
448
|
-
|> andMap request1
|
|
449
|
-
|> andMap request2
|
|
450
|
-
|> andMap request3
|
|
451
|
-
|> andMap request4
|
|
452
|
-
|> andMap request5
|
|
453
|
-
|> andMap request6
|
|
454
|
-
|> andMap request7
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
{-| -}
|
|
458
|
-
map8 :
|
|
459
|
-
(value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> value7 -> value8 -> valueCombined)
|
|
460
|
-
-> Parser value1
|
|
461
|
-
-> Parser value2
|
|
462
|
-
-> Parser value3
|
|
463
|
-
-> Parser value4
|
|
464
|
-
-> Parser value5
|
|
465
|
-
-> Parser value6
|
|
466
|
-
-> Parser value7
|
|
467
|
-
-> Parser value8
|
|
468
|
-
-> Parser valueCombined
|
|
469
|
-
map8 combineFn request1 request2 request3 request4 request5 request6 request7 request8 =
|
|
470
|
-
succeed combineFn
|
|
471
|
-
|> andMap request1
|
|
472
|
-
|> andMap request2
|
|
473
|
-
|> andMap request3
|
|
474
|
-
|> andMap request4
|
|
475
|
-
|> andMap request5
|
|
476
|
-
|> andMap request6
|
|
477
|
-
|> andMap request7
|
|
478
|
-
|> andMap request8
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
{-| -}
|
|
482
|
-
map9 :
|
|
483
|
-
(value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> value7 -> value8 -> value9 -> valueCombined)
|
|
484
|
-
-> Parser value1
|
|
485
|
-
-> Parser value2
|
|
486
|
-
-> Parser value3
|
|
487
|
-
-> Parser value4
|
|
488
|
-
-> Parser value5
|
|
489
|
-
-> Parser value6
|
|
490
|
-
-> Parser value7
|
|
491
|
-
-> Parser value8
|
|
492
|
-
-> Parser value9
|
|
493
|
-
-> Parser valueCombined
|
|
494
|
-
map9 combineFn request1 request2 request3 request4 request5 request6 request7 request8 request9 =
|
|
495
|
-
succeed combineFn
|
|
496
|
-
|> andMap request1
|
|
497
|
-
|> andMap request2
|
|
498
|
-
|> andMap request3
|
|
499
|
-
|> andMap request4
|
|
500
|
-
|> andMap request5
|
|
501
|
-
|> andMap request6
|
|
502
|
-
|> andMap request7
|
|
503
|
-
|> andMap request8
|
|
504
|
-
|> andMap request9
|
|
112
|
+
import BackendTask exposing (BackendTask)
|
|
113
|
+
import Dict exposing (Dict)
|
|
114
|
+
import FatalError exposing (FatalError)
|
|
115
|
+
import Form
|
|
116
|
+
import Form.Handler
|
|
117
|
+
import Form.Validation as Validation
|
|
118
|
+
import FormData
|
|
119
|
+
import Internal.Request
|
|
120
|
+
import Json.Decode
|
|
121
|
+
import List.NonEmpty
|
|
122
|
+
import Pages.Form
|
|
123
|
+
import QueryParams
|
|
124
|
+
import Time
|
|
125
|
+
import Url
|
|
505
126
|
|
|
506
127
|
|
|
507
128
|
optionalField : String -> Json.Decode.Decoder a -> Json.Decode.Decoder (Maybe a)
|
|
@@ -522,168 +143,58 @@ optionalField fieldName decoder_ =
|
|
|
522
143
|
|> Json.Decode.andThen finishDecoding
|
|
523
144
|
|
|
524
145
|
|
|
525
|
-
{-| Turn a Result into a Request. Useful with `andThen`. Turns `Err` into a skipped request handler (non-matching request),
|
|
526
|
-
and `Ok` values into a `succeed` (matching request).
|
|
527
|
-
-}
|
|
528
|
-
fromResult : Result String value -> Parser value
|
|
529
|
-
fromResult result =
|
|
530
|
-
case result of
|
|
531
|
-
Ok okValue ->
|
|
532
|
-
succeed okValue
|
|
533
|
-
|
|
534
|
-
Err error ->
|
|
535
|
-
skipInternal (ValidationError error)
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
jsonFromResult : Result String value -> Json.Decode.Decoder value
|
|
539
|
-
jsonFromResult result =
|
|
540
|
-
case result of
|
|
541
|
-
Ok okValue ->
|
|
542
|
-
Json.Decode.succeed okValue
|
|
543
|
-
|
|
544
|
-
Err error ->
|
|
545
|
-
Json.Decode.fail error
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
{-| -}
|
|
549
|
-
expectHeader : String -> Parser String
|
|
550
|
-
expectHeader headerName =
|
|
551
|
-
optionalField (headerName |> String.toLower) Json.Decode.string
|
|
552
|
-
|> Json.Decode.field "headers"
|
|
553
|
-
|> noErrors
|
|
554
|
-
|> Internal.Request.Parser
|
|
555
|
-
|> andThen
|
|
556
|
-
(\value ->
|
|
557
|
-
fromResult
|
|
558
|
-
(value |> Result.fromMaybe "Missing field headers")
|
|
559
|
-
)
|
|
560
|
-
|
|
561
|
-
|
|
562
146
|
{-| -}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|> noErrors
|
|
567
|
-
|> Internal.Request.Parser
|
|
147
|
+
headers : Request -> Dict String String
|
|
148
|
+
headers (Internal.Request.Request req) =
|
|
149
|
+
req.rawHeaders
|
|
568
150
|
|
|
569
151
|
|
|
570
|
-
{-|
|
|
571
|
-
|
|
572
|
-
requestTime
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|> noErrors
|
|
576
|
-
|> Internal.Request.Parser
|
|
152
|
+
{-| Get the `Time.Posix` when the incoming HTTP request was received.
|
|
153
|
+
-}
|
|
154
|
+
requestTime : Request -> Time.Posix
|
|
155
|
+
requestTime (Internal.Request.Request req) =
|
|
156
|
+
req.time
|
|
577
157
|
|
|
578
158
|
|
|
579
|
-
|
|
580
|
-
noErrors decoder =
|
|
581
|
-
decoder
|
|
582
|
-
|> Json.Decode.map (\value -> ( Ok value, [] ))
|
|
159
|
+
{-| The [HTTP request method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) of the incoming request.
|
|
583
160
|
|
|
161
|
+
Note that Route modules `data` is run for `GET` requests, and `action` is run for other request methods (including `POST`, `PUT`, `DELETE`).
|
|
162
|
+
So you don't need to check the `method` in your Route Module's `data` function, though you can choose to do so in its `action`.
|
|
584
163
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
optionalField ("Accept" |> String.toLower) Json.Decode.string
|
|
590
|
-
|> Json.Decode.field "headers"
|
|
591
|
-
|> Json.Decode.andThen
|
|
592
|
-
(\acceptHeader ->
|
|
593
|
-
if List.NonEmpty.fromCons accepted1 accepted |> List.NonEmpty.member (acceptHeader |> Maybe.withDefault "") then
|
|
594
|
-
decoder
|
|
595
|
-
|
|
596
|
-
else
|
|
597
|
-
decoder
|
|
598
|
-
|> appendError
|
|
599
|
-
(ValidationError
|
|
600
|
-
("Expected Accept header " ++ String.join ", " (accepted1 :: accepted) ++ " but was " ++ (acceptHeader |> Maybe.withDefault ""))
|
|
601
|
-
)
|
|
602
|
-
)
|
|
603
|
-
|> Internal.Request.Parser
|
|
164
|
+
-}
|
|
165
|
+
method : Request -> Method
|
|
166
|
+
method (Internal.Request.Request req) =
|
|
167
|
+
req.method |> methodFromString
|
|
604
168
|
|
|
605
169
|
|
|
606
|
-
{-|
|
|
607
|
-
acceptMethod : ( Method, List Method ) -> Parser value -> Parser value
|
|
608
|
-
acceptMethod ( accepted1, accepted ) (Internal.Request.Parser decoder) =
|
|
609
|
-
(Json.Decode.field "method" Json.Decode.string
|
|
610
|
-
|> Json.Decode.map methodFromString
|
|
611
|
-
|> Json.Decode.andThen
|
|
612
|
-
(\method_ ->
|
|
613
|
-
if (accepted1 :: accepted) |> List.member method_ then
|
|
614
|
-
decoder
|
|
615
|
-
|
|
616
|
-
else
|
|
617
|
-
decoder
|
|
618
|
-
|> appendError
|
|
619
|
-
(ValidationError
|
|
620
|
-
("Expected HTTP method " ++ String.join ", " ((accepted1 :: accepted) |> List.map methodToString) ++ " but was " ++ methodToString method_)
|
|
621
|
-
)
|
|
622
|
-
)
|
|
623
|
-
)
|
|
624
|
-
|> Internal.Request.Parser
|
|
170
|
+
{-| Get `Nothing` if the query param with the given name is missing, or `Just` the value if it is present.
|
|
625
171
|
|
|
172
|
+
If there are multiple query params with the same name, the first one is returned.
|
|
626
173
|
|
|
627
|
-
|
|
628
|
-
method : Parser Method
|
|
629
|
-
method =
|
|
630
|
-
Json.Decode.field "method" Json.Decode.string
|
|
631
|
-
|> Json.Decode.map methodFromString
|
|
632
|
-
|> noErrors
|
|
633
|
-
|> Internal.Request.Parser
|
|
174
|
+
queryParam "coupon"
|
|
634
175
|
|
|
176
|
+
-- url: http://example.com?coupon=abc
|
|
177
|
+
-- parses into: Just "abc"
|
|
635
178
|
|
|
636
|
-
|
|
637
|
-
appendError error decoder =
|
|
638
|
-
decoder
|
|
639
|
-
|> Json.Decode.map (Tuple.mapSecond (\errors -> error :: errors))
|
|
179
|
+
queryParam "coupon"
|
|
640
180
|
|
|
181
|
+
-- url: http://example.com?coupon=abc&coupon=xyz
|
|
182
|
+
-- parses into: Just "abc"
|
|
641
183
|
|
|
642
|
-
|
|
643
|
-
expectQueryParam : String -> Parser String
|
|
644
|
-
expectQueryParam name =
|
|
645
|
-
rawUrl
|
|
646
|
-
|> andThen
|
|
647
|
-
(\url_ ->
|
|
648
|
-
case url_ |> Url.fromString |> Maybe.andThen .query of
|
|
649
|
-
Just queryString ->
|
|
650
|
-
let
|
|
651
|
-
maybeParamValue : Maybe String
|
|
652
|
-
maybeParamValue =
|
|
653
|
-
queryString
|
|
654
|
-
|> QueryParams.fromString
|
|
655
|
-
|> Dict.get name
|
|
656
|
-
|> Maybe.andThen List.head
|
|
657
|
-
in
|
|
658
|
-
case maybeParamValue of
|
|
659
|
-
Just okParamValue ->
|
|
660
|
-
succeed okParamValue
|
|
661
|
-
|
|
662
|
-
Nothing ->
|
|
663
|
-
skipInternal
|
|
664
|
-
(MissingQueryParam
|
|
665
|
-
{ missingParam = name
|
|
666
|
-
, allQueryParams = queryString
|
|
667
|
-
}
|
|
668
|
-
)
|
|
184
|
+
queryParam "coupon"
|
|
669
185
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
)
|
|
186
|
+
-- url: http://example.com
|
|
187
|
+
-- parses into: Nothing
|
|
673
188
|
|
|
189
|
+
See also [`queryParams`](#queryParams), or [`rawUrl`](#rawUrl) if you need something more low-level.
|
|
674
190
|
|
|
675
|
-
|
|
676
|
-
queryParam : String ->
|
|
677
|
-
queryParam name =
|
|
678
|
-
rawUrl
|
|
679
|
-
|>
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|> Url.fromString
|
|
683
|
-
|> Maybe.andThen .query
|
|
684
|
-
|> Maybe.andThen (findFirstQueryParam name)
|
|
685
|
-
|> succeed
|
|
686
|
-
)
|
|
191
|
+
-}
|
|
192
|
+
queryParam : String -> Request -> Maybe String
|
|
193
|
+
queryParam name (Internal.Request.Request req) =
|
|
194
|
+
req.rawUrl
|
|
195
|
+
|> Url.fromString
|
|
196
|
+
|> Maybe.andThen .query
|
|
197
|
+
|> Maybe.andThen (findFirstQueryParam name)
|
|
687
198
|
|
|
688
199
|
|
|
689
200
|
findFirstQueryParam : String -> String -> Maybe String
|
|
@@ -694,188 +205,122 @@ findFirstQueryParam name queryString =
|
|
|
694
205
|
|> Maybe.andThen List.head
|
|
695
206
|
|
|
696
207
|
|
|
697
|
-
{-|
|
|
698
|
-
queryParams : Parser (Dict String (List String))
|
|
699
|
-
queryParams =
|
|
700
|
-
rawUrl
|
|
701
|
-
|> map
|
|
702
|
-
(\rawUrl_ ->
|
|
703
|
-
rawUrl_
|
|
704
|
-
|> Url.fromString
|
|
705
|
-
|> Maybe.andThen .query
|
|
706
|
-
|> Maybe.map QueryParams.fromString
|
|
707
|
-
|> Maybe.withDefault Dict.empty
|
|
708
|
-
)
|
|
709
|
-
|
|
208
|
+
{-| Gives all query params from the URL.
|
|
710
209
|
|
|
711
|
-
|
|
210
|
+
queryParam "coupon"
|
|
712
211
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
You could define a custom function like this
|
|
212
|
+
-- url: http://example.com?coupon=abc
|
|
213
|
+
-- parses into: Dict.fromList [("coupon", ["abc"])]
|
|
716
214
|
|
|
717
|
-
|
|
215
|
+
queryParam "coupon"
|
|
718
216
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
Request.expectBody
|
|
722
|
-
|> Request.andThen
|
|
723
|
-
(\bodyAsString ->
|
|
724
|
-
case runXmlDecoder xmlDecoder bodyAsString of
|
|
725
|
-
Ok decodedXml ->
|
|
726
|
-
Request.succeed decodedXml
|
|
727
|
-
|
|
728
|
-
Err error ->
|
|
729
|
-
Request.skip ("XML could not be decoded " ++ xmlErrorToString error)
|
|
730
|
-
)
|
|
731
|
-
|
|
732
|
-
Note that when we said `Request.skip`, remaining Request Parsers will run (for example if you use [`Server.Request.oneOf`](#oneOf)).
|
|
733
|
-
You could build this with different semantics if you wanted to handle _any_ valid XML body. This Request Parser will _not_
|
|
734
|
-
handle any valid XML body. It will only handle requests that can match the XmlDecoder that is passed in.
|
|
735
|
-
|
|
736
|
-
So when you define your `Server.Request.Parser`s, think carefully about whether you want to handle invalid cases and give an
|
|
737
|
-
error, or fall through to other Parsers. There's no universal right answer, it's just something to decide for your use case.
|
|
738
|
-
|
|
739
|
-
expectXmlBody : Request.Parser value
|
|
740
|
-
expectXmlBody =
|
|
741
|
-
Request.map2
|
|
742
|
-
acceptContentTypes
|
|
743
|
-
Request.expectBody
|
|
744
|
-
|> Request.andThen
|
|
745
|
-
(\bodyAsString ->
|
|
746
|
-
case runXmlDecoder xmlDecoder bodyAsString of
|
|
747
|
-
Ok decodedXml ->
|
|
748
|
-
Request.succeed decodedXml
|
|
749
|
-
|
|
750
|
-
Err error ->
|
|
751
|
-
Request.skip ("XML could not be decoded " ++ xmlErrorToString error)
|
|
752
|
-
)
|
|
217
|
+
-- url: http://example.com?coupon=abc&coupon=xyz
|
|
218
|
+
-- parses into: Dict.fromList [("coupon", ["abc", "xyz"])]
|
|
753
219
|
|
|
754
220
|
-}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
Internal.Request.Parser
|
|
763
|
-
(Json.Decode.succeed
|
|
764
|
-
( Err validationError_, [] )
|
|
765
|
-
)
|
|
221
|
+
queryParams : Request -> Dict String (List String)
|
|
222
|
+
queryParams (Internal.Request.Request req) =
|
|
223
|
+
req.rawUrl
|
|
224
|
+
|> Url.fromString
|
|
225
|
+
|> Maybe.andThen .query
|
|
226
|
+
|> Maybe.map QueryParams.fromString
|
|
227
|
+
|> Maybe.withDefault Dict.empty
|
|
766
228
|
|
|
767
229
|
|
|
768
|
-
{-|
|
|
769
|
-
rawUrl : Parser String
|
|
770
|
-
rawUrl =
|
|
771
|
-
Json.Decode.maybe
|
|
772
|
-
(Json.Decode.string
|
|
773
|
-
|> Json.Decode.field "rawUrl"
|
|
774
|
-
)
|
|
775
|
-
|> Json.Decode.map
|
|
776
|
-
(\url_ ->
|
|
777
|
-
case url_ of
|
|
778
|
-
Just justValue ->
|
|
779
|
-
( Ok justValue, [] )
|
|
780
|
-
|
|
781
|
-
Nothing ->
|
|
782
|
-
( Err (ValidationError "Internal error - expected rawUrl field but the adapter script didn't provide one."), [] )
|
|
783
|
-
)
|
|
784
|
-
|> Internal.Request.Parser
|
|
230
|
+
{-| The full URL of the incoming HTTP request, including the query params.
|
|
785
231
|
|
|
232
|
+
Note that the fragment is not included because this is client-only (not sent to the server).
|
|
786
233
|
|
|
787
|
-
|
|
788
|
-
optionalHeader : String -> Parser (Maybe String)
|
|
789
|
-
optionalHeader headerName =
|
|
790
|
-
optionalField (headerName |> String.toLower) Json.Decode.string
|
|
791
|
-
|> Json.Decode.field "headers"
|
|
792
|
-
|> noErrors
|
|
793
|
-
|> Internal.Request.Parser
|
|
234
|
+
rawUrl request
|
|
794
235
|
|
|
236
|
+
-- url: http://example.com?coupon=abc
|
|
237
|
+
-- parses into: "http://example.com?coupon=abc"
|
|
795
238
|
|
|
796
|
-
|
|
797
|
-
expectCookie : String -> Parser String
|
|
798
|
-
expectCookie name =
|
|
799
|
-
cookie name
|
|
800
|
-
|> andThen
|
|
801
|
-
(\maybeCookie ->
|
|
802
|
-
case maybeCookie of
|
|
803
|
-
Just justValue ->
|
|
804
|
-
succeed justValue
|
|
239
|
+
rawUrl request
|
|
805
240
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
)
|
|
241
|
+
-- url: https://example.com?coupon=abc&coupon=xyz
|
|
242
|
+
-- parses into: "https://example.com?coupon=abc&coupon=xyz"
|
|
809
243
|
|
|
244
|
+
-}
|
|
245
|
+
rawUrl : Request -> String
|
|
246
|
+
rawUrl (Internal.Request.Request req) =
|
|
247
|
+
req.rawUrl
|
|
810
248
|
|
|
811
|
-
{-| -}
|
|
812
|
-
cookie : String -> Parser (Maybe String)
|
|
813
|
-
cookie name =
|
|
814
|
-
allCookies
|
|
815
|
-
|> map (Dict.get name)
|
|
816
249
|
|
|
250
|
+
{-| Get a header from the request. The header name is case-insensitive.
|
|
817
251
|
|
|
818
|
-
|
|
819
|
-
allCookies : Parser (Dict String String)
|
|
820
|
-
allCookies =
|
|
821
|
-
Json.Decode.field "headers"
|
|
822
|
-
(optionalField "cookie"
|
|
823
|
-
Json.Decode.string
|
|
824
|
-
|> Json.Decode.map (Maybe.map CookieParser.parse)
|
|
825
|
-
)
|
|
826
|
-
|> Json.Decode.map (Maybe.withDefault Dict.empty)
|
|
827
|
-
|> noErrors
|
|
828
|
-
|> Internal.Request.Parser
|
|
252
|
+
Header: Accept-Language: en-US,en;q=0.5
|
|
829
253
|
|
|
254
|
+
request |> Request.header "Accept-Language"
|
|
255
|
+
-- Just "Accept-Language: en-US,en;q=0.5"
|
|
830
256
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
case value of
|
|
837
|
-
Just justValue ->
|
|
838
|
-
( Ok justValue, [] )
|
|
839
|
-
|
|
840
|
-
Nothing ->
|
|
841
|
-
( Err (ValidationError ("Missing form field '" ++ name ++ "'")), [] )
|
|
842
|
-
)
|
|
843
|
-
|> Internal.Request.Parser
|
|
844
|
-
|
|
257
|
+
-}
|
|
258
|
+
header : String -> Request -> Maybe String
|
|
259
|
+
header headerName (Internal.Request.Request req) =
|
|
260
|
+
req.rawHeaders
|
|
261
|
+
|> Dict.get (headerName |> String.toLower)
|
|
845
262
|
|
|
846
|
-
optionalFormField_ : String -> Parser (Maybe String)
|
|
847
|
-
optionalFormField_ name =
|
|
848
|
-
optionalField name Json.Decode.string
|
|
849
|
-
|> noErrors
|
|
850
|
-
|> Internal.Request.Parser
|
|
851
263
|
|
|
264
|
+
{-| Get a cookie from the request. For a more high-level API, see [`Server.Session`](Server-Session).
|
|
265
|
+
-}
|
|
266
|
+
cookie : String -> Request -> Maybe String
|
|
267
|
+
cookie name (Internal.Request.Request req) =
|
|
268
|
+
req.cookies
|
|
269
|
+
|> Dict.get name
|
|
852
270
|
|
|
853
|
-
{-| -}
|
|
854
|
-
type alias File =
|
|
855
|
-
{ name : String
|
|
856
|
-
, mimeType : String
|
|
857
|
-
, body : String
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
fileField_ : String -> Parser File
|
|
862
|
-
fileField_ name =
|
|
863
|
-
optionalField name
|
|
864
|
-
(Json.Decode.map3 File
|
|
865
|
-
(Json.Decode.field "filename" Json.Decode.string)
|
|
866
|
-
(Json.Decode.field "mimeType" Json.Decode.string)
|
|
867
|
-
(Json.Decode.field "body" Json.Decode.string)
|
|
868
|
-
)
|
|
869
|
-
|> Json.Decode.map
|
|
870
|
-
(\value ->
|
|
871
|
-
case value of
|
|
872
|
-
Just justValue ->
|
|
873
|
-
( Ok justValue, [] )
|
|
874
271
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
272
|
+
{-| Get all of the cookies from the incoming HTTP request. For a more high-level API, see [`Server.Session`](Server-Session).
|
|
273
|
+
-}
|
|
274
|
+
cookies : Request -> Dict String String
|
|
275
|
+
cookies (Internal.Request.Request req) =
|
|
276
|
+
req.cookies
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
--formField_ : String -> Parser String
|
|
281
|
+
--formField_ name =
|
|
282
|
+
-- optionalField name Json.Decode.string
|
|
283
|
+
-- |> Json.Decode.map
|
|
284
|
+
-- (\value ->
|
|
285
|
+
-- case value of
|
|
286
|
+
-- Just justValue ->
|
|
287
|
+
-- ( Ok justValue, [] )
|
|
288
|
+
--
|
|
289
|
+
-- Nothing ->
|
|
290
|
+
-- ( Err (ValidationError ("Missing form field '" ++ name ++ "'")), [] )
|
|
291
|
+
-- )
|
|
292
|
+
-- |> Internal.Request.Parser
|
|
293
|
+
--
|
|
294
|
+
--
|
|
295
|
+
--optionalFormField_ : String -> Parser (Maybe String)
|
|
296
|
+
--optionalFormField_ name =
|
|
297
|
+
-- optionalField name Json.Decode.string
|
|
298
|
+
-- |> noErrors
|
|
299
|
+
-- |> Internal.Request.Parser
|
|
300
|
+
--{-| -}
|
|
301
|
+
--type alias File =
|
|
302
|
+
-- { name : String
|
|
303
|
+
-- , mimeType : String
|
|
304
|
+
-- , body : String
|
|
305
|
+
-- }
|
|
306
|
+
--fileField_ : String -> Parser File
|
|
307
|
+
--fileField_ name =
|
|
308
|
+
-- optionalField name
|
|
309
|
+
-- (Json.Decode.map3 File
|
|
310
|
+
-- (Json.Decode.field "filename" Json.Decode.string)
|
|
311
|
+
-- (Json.Decode.field "mimeType" Json.Decode.string)
|
|
312
|
+
-- (Json.Decode.field "body" Json.Decode.string)
|
|
313
|
+
-- )
|
|
314
|
+
-- |> Json.Decode.map
|
|
315
|
+
-- (\value ->
|
|
316
|
+
-- case value of
|
|
317
|
+
-- Just justValue ->
|
|
318
|
+
-- ( Ok justValue, [] )
|
|
319
|
+
--
|
|
320
|
+
-- Nothing ->
|
|
321
|
+
-- ( Err (ValidationError ("Missing form field " ++ name)), [] )
|
|
322
|
+
-- )
|
|
323
|
+
-- |> Internal.Request.Parser
|
|
879
324
|
|
|
880
325
|
|
|
881
326
|
runForm : Validation.Validation error parsed kind constraints -> Form.Validated error parsed
|
|
@@ -893,60 +338,143 @@ runForm validation =
|
|
|
893
338
|
{-| -}
|
|
894
339
|
formDataWithServerValidation :
|
|
895
340
|
Pages.Form.Handler error combined
|
|
896
|
-
->
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
341
|
+
-> Request
|
|
342
|
+
-> Maybe (BackendTask FatalError (Result (Form.ServerResponse error) ( Form.ServerResponse error, combined )))
|
|
343
|
+
formDataWithServerValidation formParsers (Internal.Request.Request req) =
|
|
344
|
+
case req.body of
|
|
345
|
+
Nothing ->
|
|
346
|
+
Nothing
|
|
347
|
+
|
|
348
|
+
Just body_ ->
|
|
349
|
+
FormData.parseToList body_
|
|
350
|
+
|> (\rawFormData_ ->
|
|
351
|
+
case Form.Handler.run rawFormData_ formParsers of
|
|
352
|
+
Form.Valid decoded ->
|
|
353
|
+
decoded
|
|
354
|
+
|> BackendTask.map
|
|
355
|
+
(\clientValidated ->
|
|
356
|
+
case runForm clientValidated of
|
|
357
|
+
Form.Valid decodedFinal ->
|
|
358
|
+
Ok
|
|
359
|
+
( { persisted =
|
|
360
|
+
{ fields = Just rawFormData_
|
|
361
|
+
, clientSideErrors = Nothing
|
|
362
|
+
}
|
|
363
|
+
, serverSideErrors = Dict.empty
|
|
364
|
+
}
|
|
365
|
+
, decodedFinal
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
Form.Invalid _ errors2 ->
|
|
369
|
+
Err
|
|
370
|
+
{ persisted =
|
|
911
371
|
{ fields = Just rawFormData_
|
|
912
|
-
, clientSideErrors =
|
|
372
|
+
, clientSideErrors = Just errors2
|
|
913
373
|
}
|
|
914
|
-
|
|
915
|
-
}
|
|
916
|
-
, decodedFinal
|
|
917
|
-
)
|
|
918
|
-
|
|
919
|
-
Form.Invalid _ errors2 ->
|
|
920
|
-
Err
|
|
921
|
-
{ persisted =
|
|
922
|
-
{ fields = Just rawFormData_
|
|
923
|
-
, clientSideErrors = Just errors2
|
|
374
|
+
, serverSideErrors = Dict.empty
|
|
924
375
|
}
|
|
925
|
-
|
|
926
|
-
}
|
|
927
|
-
)
|
|
928
|
-
)
|
|
376
|
+
)
|
|
929
377
|
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
378
|
+
Form.Invalid _ errors ->
|
|
379
|
+
Err
|
|
380
|
+
{ persisted =
|
|
381
|
+
{ fields = Just rawFormData_
|
|
382
|
+
, clientSideErrors = Just errors
|
|
383
|
+
}
|
|
384
|
+
, serverSideErrors = Dict.empty
|
|
385
|
+
}
|
|
386
|
+
|> BackendTask.succeed
|
|
387
|
+
)
|
|
388
|
+
|> Just
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
{-| Takes a [`Form.Handler.Handler`](https://package.elm-lang.org/packages/dillonkearns/elm-form/latest/Form-Handler) and
|
|
392
|
+
parses the raw form data into a [`Form.Validated`](https://package.elm-lang.org/packages/dillonkearns/elm-form/latest/Form#Validated) value.
|
|
393
|
+
|
|
394
|
+
This is the standard pattern for dealing with form data in `elm-pages`. You can share your code for your [`Form`](https://package.elm-lang.org/packages/dillonkearns/elm-form/latest/Form#Form)
|
|
395
|
+
definitions between your client and server code, using this function to parse the raw form data into a `Form.Validated` value for the backend,
|
|
396
|
+
and [`Pages.Form`](Pages-Form) to render the `Form` on the client.
|
|
397
|
+
|
|
398
|
+
Since we are sharing the `Form` definition between frontend and backend, we get to re-use the same validation logic so we gain confidence that
|
|
399
|
+
the validation errors that the user sees on the client are protected on our backend, and vice versa.
|
|
400
|
+
|
|
401
|
+
import BackendTask
|
|
402
|
+
import Form
|
|
403
|
+
import Server.Request
|
|
404
|
+
|
|
405
|
+
type Action
|
|
406
|
+
= Delete
|
|
407
|
+
| CreateOrUpdate Post
|
|
408
|
+
|
|
409
|
+
formHandlers : Form.Handler.Handler String Action
|
|
410
|
+
formHandlers =
|
|
411
|
+
deleteForm
|
|
412
|
+
|> Form.Handler.init (\() -> Delete)
|
|
413
|
+
|> Form.Handler.with CreateOrUpdate createOrUpdateForm
|
|
414
|
+
|
|
415
|
+
deleteForm : Form.HtmlForm String () input msg
|
|
416
|
+
|
|
417
|
+
createOrUpdateForm : Form.HtmlForm String Post Post msg
|
|
418
|
+
|
|
419
|
+
action :
|
|
420
|
+
RouteParams
|
|
421
|
+
-> Server.Request.Parser (BackendTask.BackendTask FatalError.FatalError (Server.Response.Response ActionData ErrorPage.ErrorPage))
|
|
422
|
+
action routeParams =
|
|
423
|
+
Server.Request.map
|
|
424
|
+
(\( formResponse, parsedForm ) ->
|
|
425
|
+
case parsedForm of
|
|
426
|
+
Form.Valid Delete ->
|
|
427
|
+
deletePostBySlug routeParams.slug
|
|
428
|
+
|> BackendTask.map
|
|
429
|
+
(\() -> Route.redirectTo Route.Index)
|
|
430
|
+
|
|
431
|
+
Form.Valid (CreateOrUpdate post) ->
|
|
432
|
+
let
|
|
433
|
+
createPost : Bool
|
|
434
|
+
createPost =
|
|
435
|
+
okForm.slug == "new"
|
|
436
|
+
in
|
|
437
|
+
createOrUpdatePost post
|
|
438
|
+
|> BackendTask.map
|
|
439
|
+
(\() ->
|
|
440
|
+
Route.redirectTo
|
|
441
|
+
(Route.Admin__Slug_ { slug = okForm.slug })
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
Form.Invalid _ invalidForm ->
|
|
445
|
+
BackendTask.succeed
|
|
446
|
+
(Server.Response.render
|
|
447
|
+
{ errors = formResponse }
|
|
448
|
+
)
|
|
940
449
|
)
|
|
450
|
+
(Server.Request.formData formHandlers)
|
|
941
451
|
|
|
452
|
+
You can handle form submissions as either GET or POST requests. Note that for security reasons, it's important to performing mutations with care from GET requests,
|
|
453
|
+
since a GET request can be performed from an outside origin by embedding an image that points to the given URL. So a logout submission should be protected by
|
|
454
|
+
using `POST` to ensure that you can't log users out by embedding an image with a logout URL in it.
|
|
942
455
|
|
|
943
|
-
|
|
456
|
+
If the request has HTTP method `GET`, the form data will come from the query parameters.
|
|
457
|
+
|
|
458
|
+
If the request has the HTTP method `POST` _and_ the `Content-Type` is `application/x-www-form-urlencoded`, it will return the
|
|
459
|
+
decoded form data from the body of the request.
|
|
460
|
+
|
|
461
|
+
Otherwise, this `Parser` will not match.
|
|
462
|
+
|
|
463
|
+
Note that in server-rendered Route modules, your `data` function will handle `GET` requests (and will _not_ receive any `POST` requests),
|
|
464
|
+
while your `action` will receive POST (and other non-GET) requests.
|
|
465
|
+
|
|
466
|
+
By default, [`Form`]'s are rendered with a `POST` method, and you can configure them to submit `GET` requests using [`withGetMethod`](https://package.elm-lang.org/packages/dillonkearns/elm-form/latest/Form#withGetMethod).
|
|
467
|
+
So you will want to handle any `Form`'s rendered using `withGetMethod` in your Route's `data` function, or otherwise handle forms in `action`.
|
|
468
|
+
|
|
469
|
+
-}
|
|
944
470
|
formData :
|
|
945
471
|
Form.Handler.Handler error combined
|
|
946
|
-
->
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
472
|
+
-> Request
|
|
473
|
+
-> Maybe ( Form.ServerResponse error, Form.Validated error combined )
|
|
474
|
+
formData formParsers ((Internal.Request.Request req) as request) =
|
|
475
|
+
request
|
|
476
|
+
|> rawFormData
|
|
477
|
+
|> Maybe.map
|
|
950
478
|
(\rawFormData_ ->
|
|
951
479
|
case Form.Handler.run rawFormData_ formParsers of
|
|
952
480
|
(Form.Valid _) as validated ->
|
|
@@ -958,7 +486,6 @@ formData formParsers =
|
|
|
958
486
|
}
|
|
959
487
|
, validated
|
|
960
488
|
)
|
|
961
|
-
|> succeed
|
|
962
489
|
|
|
963
490
|
(Form.Invalid _ maybeErrors) as validated ->
|
|
964
491
|
( { persisted =
|
|
@@ -969,155 +496,109 @@ formData formParsers =
|
|
|
969
496
|
}
|
|
970
497
|
, validated
|
|
971
498
|
)
|
|
972
|
-
|> succeed
|
|
973
499
|
)
|
|
974
500
|
|
|
975
501
|
|
|
976
|
-
{-| -
|
|
977
|
-
rawFormData : Parser (List ( String, String ))
|
|
978
|
-
rawFormData =
|
|
979
|
-
-- TODO make an optional version
|
|
980
|
-
map4 (\parsedContentType a b c -> ( ( a, parsedContentType ), b, c ))
|
|
981
|
-
(rawContentType |> map (Maybe.map parseContentType))
|
|
982
|
-
(matchesContentType "application/x-www-form-urlencoded")
|
|
983
|
-
method
|
|
984
|
-
(rawBody |> map (Maybe.withDefault "")
|
|
985
|
-
-- TODO warn of empty body in case when field decoding fails?
|
|
986
|
-
)
|
|
987
|
-
|> andThen
|
|
988
|
-
(\( ( validContentType, parsedContentType ), validMethod, justBody ) ->
|
|
989
|
-
if validMethod == Get then
|
|
990
|
-
queryParams
|
|
991
|
-
|> map Dict.toList
|
|
992
|
-
|> map (List.map (Tuple.mapSecond (List.head >> Maybe.withDefault "")))
|
|
993
|
-
|
|
994
|
-
else if not ((validContentType |> Maybe.withDefault False) && validMethod == Post) then
|
|
995
|
-
Json.Decode.succeed
|
|
996
|
-
( Err
|
|
997
|
-
(ValidationError <|
|
|
998
|
-
case ( validContentType |> Maybe.withDefault False, validMethod == Post, parsedContentType ) of
|
|
999
|
-
( False, True, Just contentType_ ) ->
|
|
1000
|
-
"expectFormPost did not match - Was form POST but expected content-type `application/x-www-form-urlencoded` and instead got `" ++ contentType_ ++ "`"
|
|
1001
|
-
|
|
1002
|
-
( False, True, Nothing ) ->
|
|
1003
|
-
"expectFormPost did not match - Was form POST but expected content-type `application/x-www-form-urlencoded` but the request didn't have a content-type header"
|
|
1004
|
-
|
|
1005
|
-
_ ->
|
|
1006
|
-
"expectFormPost did not match - expected method POST, but the method was " ++ methodToString validMethod
|
|
1007
|
-
)
|
|
1008
|
-
, []
|
|
1009
|
-
)
|
|
1010
|
-
|> Internal.Request.Parser
|
|
502
|
+
{-| Get the raw key-value pairs from a form submission.
|
|
1011
503
|
|
|
1012
|
-
|
|
504
|
+
If the request has the HTTP method `GET`, it will return the query parameters.
|
|
505
|
+
|
|
506
|
+
If the request has the HTTP method `POST` _and_ the `Content-Type` is `application/x-www-form-urlencoded`, it will return the
|
|
507
|
+
decoded form data from the body of the request.
|
|
508
|
+
|
|
509
|
+
Otherwise, this `Parser` will not match.
|
|
510
|
+
|
|
511
|
+
Note that in server-rendered Route modules, your `data` function will handle `GET` requests (and will _not_ receive any `POST` requests),
|
|
512
|
+
while your `action` will receive POST (and other non-GET) requests.
|
|
513
|
+
|
|
514
|
+
By default, [`Form`]'s are rendered with a `POST` method, and you can configure them to submit `GET` requests using [`withGetMethod`](https://package.elm-lang.org/packages/dillonkearns/elm-form/latest/Form#withGetMethod).
|
|
515
|
+
So you will want to handle any `Form`'s rendered using `withGetMethod` in your Route's `data` function, or otherwise handle forms in `action`.
|
|
516
|
+
|
|
517
|
+
-}
|
|
518
|
+
rawFormData : Request -> Maybe (List ( String, String ))
|
|
519
|
+
rawFormData request =
|
|
520
|
+
if method request == Get then
|
|
521
|
+
request
|
|
522
|
+
|> queryParams
|
|
523
|
+
|> Dict.toList
|
|
524
|
+
|> List.map (Tuple.mapSecond (List.head >> Maybe.withDefault ""))
|
|
525
|
+
|> Just
|
|
526
|
+
|
|
527
|
+
else if (method request == Post) && (request |> matchesContentType "application/x-www-form-urlencoded") then
|
|
528
|
+
body request
|
|
529
|
+
|> Maybe.map
|
|
530
|
+
(\justBody ->
|
|
1013
531
|
justBody
|
|
1014
|
-
|> FormData.
|
|
1015
|
-
|
|
1016
|
-
|> andThen
|
|
1017
|
-
(\parsedForm ->
|
|
1018
|
-
let
|
|
1019
|
-
thing : Json.Encode.Value
|
|
1020
|
-
thing =
|
|
1021
|
-
parsedForm
|
|
1022
|
-
|> Dict.toList
|
|
1023
|
-
|> List.map
|
|
1024
|
-
(Tuple.mapSecond
|
|
1025
|
-
(\( first, _ ) ->
|
|
1026
|
-
Json.Encode.string first
|
|
1027
|
-
)
|
|
1028
|
-
)
|
|
1029
|
-
|> Json.Encode.object
|
|
1030
|
-
|
|
1031
|
-
innerDecoder : Json.Decode.Decoder ( Result ValidationError (List ( String, String )), List ValidationError )
|
|
1032
|
-
innerDecoder =
|
|
1033
|
-
Json.Decode.keyValuePairs Json.Decode.string
|
|
1034
|
-
|> noErrors
|
|
1035
|
-
in
|
|
1036
|
-
Json.Decode.decodeValue innerDecoder thing
|
|
1037
|
-
|> Result.mapError Json.Decode.errorToString
|
|
1038
|
-
|> jsonFromResult
|
|
1039
|
-
|> Internal.Request.Parser
|
|
1040
|
-
)
|
|
1041
|
-
)
|
|
532
|
+
|> FormData.parseToList
|
|
533
|
+
)
|
|
1042
534
|
|
|
535
|
+
else
|
|
536
|
+
Nothing
|
|
1043
537
|
|
|
1044
|
-
{-| -}
|
|
1045
|
-
expectMultiPartFormPost :
|
|
1046
|
-
({ field : String -> Parser String
|
|
1047
|
-
, optionalField : String -> Parser (Maybe String)
|
|
1048
|
-
, fileField : String -> Parser File
|
|
1049
|
-
}
|
|
1050
|
-
-> Parser decodedForm
|
|
1051
|
-
)
|
|
1052
|
-
-> Parser decodedForm
|
|
1053
|
-
expectMultiPartFormPost toForm =
|
|
1054
|
-
map2
|
|
1055
|
-
(\_ value ->
|
|
1056
|
-
value
|
|
1057
|
-
)
|
|
1058
|
-
(expectContentType "multipart/form-data")
|
|
1059
|
-
(toForm
|
|
1060
|
-
{ field = formField_
|
|
1061
|
-
, optionalField = optionalFormField_
|
|
1062
|
-
, fileField = fileField_
|
|
1063
|
-
}
|
|
1064
|
-
|> (\(Internal.Request.Parser decoder) -> decoder)
|
|
1065
|
-
-- @@@ TODO is it possible to do multipart form data parsing in pure Elm?
|
|
1066
|
-
|> Json.Decode.field "multiPartFormData"
|
|
1067
|
-
|> Internal.Request.Parser
|
|
1068
|
-
|> acceptMethod ( Post, [] )
|
|
1069
|
-
)
|
|
1070
538
|
|
|
1071
539
|
|
|
1072
|
-
{-| -}
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
540
|
+
--{-| -}
|
|
541
|
+
--expectMultiPartFormPost :
|
|
542
|
+
-- ({ field : String -> Parser String
|
|
543
|
+
-- , optionalField : String -> Parser (Maybe String)
|
|
544
|
+
-- , fileField : String -> Parser File
|
|
545
|
+
-- }
|
|
546
|
+
-- -> Parser decodedForm
|
|
547
|
+
-- )
|
|
548
|
+
-- -> Parser decodedForm
|
|
549
|
+
--expectMultiPartFormPost toForm =
|
|
550
|
+
-- map2
|
|
551
|
+
-- (\_ value ->
|
|
552
|
+
-- value
|
|
553
|
+
-- )
|
|
554
|
+
-- (expectContentType "multipart/form-data")
|
|
555
|
+
-- (toForm
|
|
556
|
+
-- { field = formField_
|
|
557
|
+
-- , optionalField = optionalFormField_
|
|
558
|
+
-- , fileField = fileField_
|
|
559
|
+
-- }
|
|
560
|
+
-- |> (\(Internal.Request.Parser decoder) -> decoder)
|
|
561
|
+
-- -- @@@ TODO is it possible to do multipart form data parsing in pure Elm?
|
|
562
|
+
-- |> Json.Decode.field "multiPartFormData"
|
|
563
|
+
-- |> Internal.Request.Parser
|
|
564
|
+
-- |> acceptMethod ( Post, [] )
|
|
565
|
+
-- )
|
|
1085
566
|
|
|
1086
|
-
Just contentType ->
|
|
1087
|
-
if (contentType |> parseContentType) == (expectedContentType |> parseContentType) then
|
|
1088
|
-
succeed ()
|
|
1089
567
|
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
568
|
+
rawContentType : Request -> Maybe String
|
|
569
|
+
rawContentType (Internal.Request.Request req) =
|
|
570
|
+
req.rawHeaders |> Dict.get "content-type"
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
{-| True if the `content-type` header is present AND matches the given argument.
|
|
1093
574
|
|
|
575
|
+
Examples:
|
|
1094
576
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|> noErrors
|
|
1099
|
-
|> Internal.Request.Parser
|
|
577
|
+
Content-Type: application/json; charset=utf-8
|
|
578
|
+
request |> matchesContentType "application/json"
|
|
579
|
+
-- True
|
|
1100
580
|
|
|
581
|
+
Content-Type: application/json
|
|
582
|
+
request |> matchesContentType "application/json"
|
|
583
|
+
-- True
|
|
1101
584
|
|
|
1102
|
-
|
|
1103
|
-
matchesContentType
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
585
|
+
Content-Type: application/json
|
|
586
|
+
request |> matchesContentType "application/xml"
|
|
587
|
+
-- False
|
|
588
|
+
|
|
589
|
+
-}
|
|
590
|
+
matchesContentType : String -> Request -> Bool
|
|
591
|
+
matchesContentType expectedContentType (Internal.Request.Request req) =
|
|
592
|
+
req.rawHeaders
|
|
593
|
+
|> Dict.get "content-type"
|
|
594
|
+
|> (\maybeContentType ->
|
|
1108
595
|
case maybeContentType of
|
|
1109
596
|
Nothing ->
|
|
1110
|
-
|
|
597
|
+
False
|
|
1111
598
|
|
|
1112
599
|
Just contentType ->
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
else
|
|
1117
|
-
Just False
|
|
1118
|
-
)
|
|
1119
|
-
|> noErrors
|
|
1120
|
-
|> Internal.Request.Parser
|
|
600
|
+
(contentType |> parseContentType) == (expectedContentType |> parseContentType)
|
|
601
|
+
)
|
|
1121
602
|
|
|
1122
603
|
|
|
1123
604
|
parseContentType : String -> String
|
|
@@ -1129,49 +610,41 @@ parseContentType contentTypeString =
|
|
|
1129
610
|
|> Maybe.withDefault contentTypeString
|
|
1130
611
|
|
|
1131
612
|
|
|
1132
|
-
{-| -
|
|
1133
|
-
|
|
1134
|
-
expectJsonBody jsonBodyDecoder =
|
|
1135
|
-
map2 (\_ secondValue -> secondValue)
|
|
1136
|
-
(expectContentType "application/json")
|
|
1137
|
-
(rawBody
|
|
1138
|
-
|> andThen
|
|
1139
|
-
(\rawBody_ ->
|
|
1140
|
-
(case rawBody_ of
|
|
1141
|
-
Just body_ ->
|
|
1142
|
-
Json.Decode.decodeString
|
|
1143
|
-
jsonBodyDecoder
|
|
1144
|
-
body_
|
|
1145
|
-
|> Result.mapError Json.Decode.errorToString
|
|
1146
|
-
|
|
1147
|
-
Nothing ->
|
|
1148
|
-
Err "Tried to parse JSON body but the request had no body."
|
|
1149
|
-
)
|
|
1150
|
-
|> fromResult
|
|
1151
|
-
)
|
|
1152
|
-
)
|
|
613
|
+
{-| If the request has a body and its `Content-Type` matches JSON, then
|
|
614
|
+
try running a JSON decoder on the body of the request. Otherwise, return `Nothing`.
|
|
1153
615
|
|
|
616
|
+
Example:
|
|
1154
617
|
|
|
1155
|
-
{
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
Json.Decode.field "
|
|
1159
|
-
|
|
1160
|
-
|
|
618
|
+
Body: { "name": "John" }
|
|
619
|
+
Headers:
|
|
620
|
+
Content-Type: application/json
|
|
621
|
+
request |> jsonBody (Json.Decode.field "name" Json.Decode.string)
|
|
622
|
+
-- Just (Ok "John")
|
|
623
|
+
|
|
624
|
+
Body: { "name": "John" }
|
|
625
|
+
No Headers
|
|
626
|
+
jsonBody (Json.Decode.field "name" Json.Decode.string) request
|
|
627
|
+
-- Nothing
|
|
1161
628
|
|
|
629
|
+
No Body
|
|
630
|
+
No Headers
|
|
631
|
+
jsonBody (Json.Decode.field "name" Json.Decode.string) request
|
|
632
|
+
-- Nothing
|
|
1162
633
|
|
|
1163
|
-
{-| Same as [`rawBody`](#rawBody), but will only match when a body is present in the HTTP request.
|
|
1164
634
|
-}
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
)
|
|
635
|
+
jsonBody : Json.Decode.Decoder value -> Request -> Maybe (Result Json.Decode.Error value)
|
|
636
|
+
jsonBody jsonBodyDecoder ((Internal.Request.Request req) as request) =
|
|
637
|
+
case ( req.body, request |> matchesContentType "application/json" ) of
|
|
638
|
+
( Just body_, True ) ->
|
|
639
|
+
Json.Decode.decodeString jsonBodyDecoder body_
|
|
640
|
+
|> Just
|
|
1172
641
|
|
|
642
|
+
_ ->
|
|
643
|
+
Nothing
|
|
1173
644
|
|
|
1174
|
-
|
|
645
|
+
|
|
646
|
+
{-| An [Incoming HTTP Request Method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods).
|
|
647
|
+
-}
|
|
1175
648
|
type Method
|
|
1176
649
|
= Connect
|
|
1177
650
|
| Delete
|
|
@@ -1219,7 +692,14 @@ methodFromString rawMethod =
|
|
|
1219
692
|
NonStandard rawMethod
|
|
1220
693
|
|
|
1221
694
|
|
|
1222
|
-
{-| Gets the HTTP Method as
|
|
695
|
+
{-| Gets the HTTP Method as an uppercase String.
|
|
696
|
+
|
|
697
|
+
Examples:
|
|
698
|
+
|
|
699
|
+
Get
|
|
700
|
+
|> methodToString
|
|
701
|
+
-- "GET"
|
|
702
|
+
|
|
1223
703
|
-}
|
|
1224
704
|
methodToString : Method -> String
|
|
1225
705
|
methodToString method_ =
|
|
@@ -1253,3 +733,16 @@ methodToString method_ =
|
|
|
1253
733
|
|
|
1254
734
|
NonStandard nonStandardMethod ->
|
|
1255
735
|
nonStandardMethod
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
{-| A value that lets you access data from the incoming HTTP request.
|
|
739
|
+
-}
|
|
740
|
+
type alias Request =
|
|
741
|
+
Internal.Request.Request
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
{-| The Request body, if present (or `Nothing` if there is no request body).
|
|
745
|
+
-}
|
|
746
|
+
body : Request -> Maybe String
|
|
747
|
+
body (Internal.Request.Request req) =
|
|
748
|
+
req.body
|