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