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.
@@ -1,126 +1,45 @@
1
1
  module Server.Request exposing
2
- ( Parser
3
- , succeed, fromResult, skip
2
+ ( Request
3
+ , requestTime
4
+ , header, headers
5
+ , method, Method(..), methodToString
6
+ , body, jsonBody
4
7
  , formData, formDataWithServerValidation
5
8
  , rawFormData
6
- , method, rawBody, allCookies, rawHeaders, queryParams
7
- , requestTime, optionalHeader, expectContentType, expectJsonBody
8
- , acceptMethod, acceptContentTypes
9
- , map, map2, oneOf, andMap, andThen
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 Parser
17
+ @docs Request
23
18
 
24
- @docs succeed, fromResult, skip
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
- - URL query parameters
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` has `data : RouteParams -> BackendTask Data`:
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` is a `Request.Parser` in server-rendered Route Modules. Since you have an incoming HTTP request for server-rendered routes,
146
- `RouteBuilder.serverRender` has `data : RouteParams -> Request.Parser (BackendTask (Response Data))`. That means that you
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
- That's a mouthful, so let's unpack what it means.
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
- route : StatelessRoute RouteParams Data ActionData
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
- {-| TODO internal only
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
- {-| Decode an argument and provide it to a function in a decoder.
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
- """ { "val": "hi", "count": 3 } """
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
- map2 : (a -> b -> c) -> Parser a -> Parser b -> Parser c
354
- map2 f (Internal.Request.Parser jdA) (Internal.Request.Parser jdB) =
355
- Internal.Request.Parser
356
- (Json.Decode.map2
357
- (\( result1, errors1 ) ( result2, errors2 ) ->
358
- ( Result.map2 f result1 result2
359
- , errors1 ++ errors2
360
- )
361
- )
362
- jdA
363
- jdB
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
- rawHeaders : Parser (Dict String String)
564
- rawHeaders =
565
- Json.Decode.field "headers" (Json.Decode.dict Json.Decode.string)
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
- requestTime : Parser Time.Posix
572
- requestTime =
573
- Json.Decode.field "requestTime"
574
- (Json.Decode.int |> Json.Decode.map Time.millisToPosix)
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
- noErrors : Json.Decode.Decoder value -> Json.Decode.Decoder ( Result ValidationError value, List ValidationError )
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
- acceptContentTypes : ( String, List String ) -> Parser value -> Parser value
587
- acceptContentTypes ( accepted1, accepted ) (Internal.Request.Parser decoder) =
588
- -- TODO this should parse content-types so it doesn't need to be an exact match (support `; q=...`, etc.)
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
- appendError : ValidationError -> Json.Decode.Decoder ( value, List ValidationError ) -> Json.Decode.Decoder ( value, List ValidationError )
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
- Nothing ->
671
- skipInternal (ValidationError ("Expected query param \"" ++ name ++ "\", but there were no query params."))
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 -> Parser (Maybe String)
677
- queryParam name =
678
- rawUrl
679
- |> andThen
680
- (\url_ ->
681
- url_
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
- {-| This is a Request.Parser that will never match an HTTP request. Similar to `Json.Decode.fail`.
210
+ queryParam "coupon"
712
211
 
713
- Why would you want it to always fail? It's helpful for building custom `Server.Request.Parser`. For example, let's say
714
- you wanted to define a custom `Server.Request.Parser` to use an XML Decoding package on the request body.
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
- import Server.Request as Request
215
+ queryParam "coupon"
718
216
 
719
- expectXmlBody : XmlDecoder value -> Request.Parser value
720
- expectXmlBody xmlDecoder =
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
- skip : String -> Parser value
756
- skip errorMessage =
757
- skipInternal (ValidationError errorMessage)
758
-
759
-
760
- skipInternal : ValidationError -> Parser value
761
- skipInternal validationError_ =
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
- Nothing ->
807
- skipInternal (ValidationError ("Missing cookie " ++ name))
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
- formField_ : String -> Parser String
832
- formField_ name =
833
- optionalField name Json.Decode.string
834
- |> Json.Decode.map
835
- (\value ->
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
- Nothing ->
876
- ( Err (ValidationError ("Missing form field " ++ name)), [] )
877
- )
878
- |> Internal.Request.Parser
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
- -> Parser (BackendTask FatalError (Result (Form.ServerResponse error) ( Form.ServerResponse error, combined )))
897
- formDataWithServerValidation formParsers =
898
- rawFormData
899
- |> andThen
900
- (\rawFormData_ ->
901
- case Form.Handler.run rawFormData_ formParsers of
902
- Form.Valid decoded ->
903
- succeed
904
- (decoded
905
- |> BackendTask.map
906
- (\clientValidated ->
907
- case runForm clientValidated of
908
- Form.Valid decodedFinal ->
909
- Ok
910
- ( { persisted =
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 = Nothing
372
+ , clientSideErrors = Just errors2
913
373
  }
914
- , serverSideErrors = Dict.empty
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
- , serverSideErrors = Dict.empty
926
- }
927
- )
928
- )
376
+ )
929
377
 
930
- Form.Invalid _ errors ->
931
- Err
932
- { persisted =
933
- { fields = Just rawFormData_
934
- , clientSideErrors = Just errors
935
- }
936
- , serverSideErrors = Dict.empty
937
- }
938
- |> BackendTask.succeed
939
- |> succeed
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
- -> Parser ( Form.ServerResponse error, Form.Validated error combined )
947
- formData formParsers =
948
- rawFormData
949
- |> andThen
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
- else
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.parse
1015
- |> succeed
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
- expectContentType : String -> Parser ()
1074
- expectContentType expectedContentType =
1075
- optionalField "content-type" Json.Decode.string
1076
- |> Json.Decode.field "headers"
1077
- |> noErrors
1078
- |> Internal.Request.Parser
1079
- |> andThen
1080
- (\maybeContentType ->
1081
- case maybeContentType of
1082
- Nothing ->
1083
- skipInternal <|
1084
- ValidationError ("Expected content-type `" ++ expectedContentType ++ "` but there was no content-type header.")
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
- else
1091
- skipInternal <| ValidationError ("Expected content-type to be " ++ expectedContentType ++ " but it was " ++ contentType)
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
- rawContentType : Parser (Maybe String)
1096
- rawContentType =
1097
- optionalField ("content-type" |> String.toLower) Json.Decode.string
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
- matchesContentType : String -> Parser (Maybe Bool)
1103
- matchesContentType expectedContentType =
1104
- optionalField ("content-type" |> String.toLower) Json.Decode.string
1105
- |> Json.Decode.field "headers"
1106
- |> Json.Decode.map
1107
- (\maybeContentType ->
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
- Nothing
597
+ False
1111
598
 
1112
599
  Just contentType ->
1113
- if (contentType |> parseContentType) == (expectedContentType |> parseContentType) then
1114
- Just True
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
- expectJsonBody : Json.Decode.Decoder value -> Parser value
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
- rawBody : Parser (Maybe String)
1157
- rawBody =
1158
- Json.Decode.field "body" (Json.Decode.nullable Json.Decode.string)
1159
- |> noErrors
1160
- |> Internal.Request.Parser
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
- expectBody : Parser String
1166
- expectBody =
1167
- rawBody
1168
- |> andThen
1169
- (Result.fromMaybe "Expected body but none was present."
1170
- >> fromResult
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 a String, like 'GET', 'PUT', etc.
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