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.
- package/README.md +1 -1
- package/codegen/elm-pages-codegen.cjs +251 -285
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/d.dat +0 -0
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/node_runner.js +1 -1
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/node_supervisor.js +1 -1
- package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/d.dat +0 -0
- package/generator/review/elm-stuff/tests-0.19.1/js/node_runner.js +1 -1
- package/generator/review/elm-stuff/tests-0.19.1/js/node_supervisor.js +1 -1
- package/generator/src/RouteBuilder.elm +19 -60
- package/generator/src/SharedTemplate.elm +5 -5
- package/generator/src/compatibility-key.js +2 -2
- package/package.json +2 -2
- package/src/ApiRoute.elm +3 -31
- package/src/BackendTask.elm +18 -24
- package/src/FormData.elm +21 -1
- package/src/Head/Seo.elm +4 -4
- package/src/Internal/Request.elm +84 -4
- package/src/Pages/ConcurrentSubmission.elm +127 -0
- package/src/Pages/Form.elm +151 -40
- package/src/Pages/FormData.elm +19 -0
- package/src/Pages/Internal/NotFoundReason.elm +4 -4
- package/src/Pages/Internal/Platform/Cli.elm +30 -17
- package/src/Pages/Internal/Platform/CompatibilityKey.elm +1 -1
- package/src/Pages/Internal/Platform.elm +39 -38
- package/src/Pages/Internal/ResponseSketch.elm +2 -2
- package/src/Pages/Manifest.elm +23 -7
- package/src/Pages/Navigation.elm +85 -0
- package/src/Pages/PageUrl.elm +3 -3
- package/src/Pages/ProgramConfig.elm +13 -11
- package/src/Pages/Script.elm +64 -7
- package/src/Pages/Url.elm +3 -3
- package/src/PagesMsg.elm +9 -3
- package/src/RenderRequest.elm +7 -7
- package/src/Scaffold/Form.elm +28 -5
- package/src/Scaffold/Route.elm +82 -53
- package/src/Server/Request.elm +446 -952
- package/src/Server/Session.elm +141 -91
- package/src/Server/SetCookie.elm +71 -31
- package/src/{Path.elm → UrlPath.elm} +21 -21
- package/src/Pages/Transition.elm +0 -79
package/src/Server/Request.elm
CHANGED
|
@@ -1,125 +1,45 @@
|
|
|
1
1
|
module Server.Request exposing
|
|
2
|
-
(
|
|
3
|
-
,
|
|
2
|
+
( Request
|
|
3
|
+
, requestTime
|
|
4
|
+
, header, headers
|
|
5
|
+
, method, Method(..), methodToString
|
|
6
|
+
, body, jsonBody
|
|
4
7
|
, formData, formDataWithServerValidation
|
|
5
8
|
, rawFormData
|
|
6
|
-
,
|
|
7
|
-
,
|
|
8
|
-
,
|
|
9
|
-
,
|
|
10
|
-
, queryParam, expectQueryParam
|
|
11
|
-
, cookie, expectCookie
|
|
12
|
-
, expectHeader
|
|
13
|
-
, File, expectMultiPartFormPost
|
|
14
|
-
, expectBody
|
|
15
|
-
, map3, map4, map5, map6, map7, map8, map9
|
|
16
|
-
, Method(..), methodToString
|
|
17
|
-
, errorsToString, errorToString, getDecoder, ValidationError
|
|
9
|
+
, rawUrl
|
|
10
|
+
, queryParam, queryParams
|
|
11
|
+
, matchesContentType
|
|
12
|
+
, cookie, cookies
|
|
18
13
|
)
|
|
19
14
|
|
|
20
|
-
{-|
|
|
15
|
+
{-| Server-rendered Route modules and [server-rendered API Routes](ApiRoute#serverRender) give you access to a `Server.Request.Request` argument.
|
|
21
16
|
|
|
22
|
-
@docs
|
|
17
|
+
@docs Request
|
|
23
18
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
## Forms
|
|
28
|
-
|
|
29
|
-
@docs formData, formDataWithServerValidation
|
|
30
|
-
|
|
31
|
-
@docs rawFormData
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
## Direct Values
|
|
35
|
-
|
|
36
|
-
@docs method, rawBody, allCookies, rawHeaders, queryParams
|
|
37
|
-
|
|
38
|
-
@docs requestTime, optionalHeader, expectContentType, expectJsonBody
|
|
39
|
-
|
|
40
|
-
@docs acceptMethod, acceptContentTypes
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
## Transforming
|
|
44
|
-
|
|
45
|
-
@docs map, map2, oneOf, andMap, andThen
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
## Query Parameters
|
|
49
|
-
|
|
50
|
-
@docs queryParam, expectQueryParam
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
## Cookies
|
|
54
|
-
|
|
55
|
-
@docs cookie, expectCookie
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
## Headers
|
|
59
|
-
|
|
60
|
-
@docs expectHeader
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
## Multi-part forms and file uploads
|
|
64
|
-
|
|
65
|
-
@docs File, expectMultiPartFormPost
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
## Request Parsers That Can Fail
|
|
69
|
-
|
|
70
|
-
@docs expectBody
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
## Map Functions
|
|
74
|
-
|
|
75
|
-
@docs map3, map4, map5, map6, map7, map8, map9
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
## Method Type
|
|
79
|
-
|
|
80
|
-
@docs Method, methodToString
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
## Internals
|
|
84
|
-
|
|
85
|
-
@docs errorsToString, errorToString, getDecoder, ValidationError
|
|
86
|
-
|
|
87
|
-
-}
|
|
88
|
-
|
|
89
|
-
import BackendTask exposing (BackendTask)
|
|
90
|
-
import CookieParser
|
|
91
|
-
import Dict exposing (Dict)
|
|
92
|
-
import FatalError exposing (FatalError)
|
|
93
|
-
import Form
|
|
94
|
-
import Form.Handler
|
|
95
|
-
import Form.Validation as Validation
|
|
96
|
-
import FormData
|
|
97
|
-
import Internal.Request
|
|
98
|
-
import Json.Decode
|
|
99
|
-
import Json.Encode
|
|
100
|
-
import List.NonEmpty
|
|
101
|
-
import 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
|
-
-
|
|
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`
|
|
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`
|
|
145
|
-
`RouteBuilder.serverRender` has `data : RouteParams -> Request
|
|
64
|
+
is loaded. That's why `data` has a `Server.Request.Request` argument in server-rendered Route Modules. Since you have an incoming HTTP request for server-rendered routes,
|
|
65
|
+
`RouteBuilder.serverRender` has `data : RouteParams -> Request -> BackendTask (Response Data)`. That means that you
|
|
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
|
-
|
|
69
|
+
@docs requestTime
|
|
150
70
|
|
|
151
|
-
`Request.Parser` means you can pull out
|
|
152
71
|
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
571
|
-
requestTime
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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
|
-
|
|
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
|
-
|
|
670
|
-
|
|
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 ->
|
|
676
|
-
queryParam name =
|
|
677
|
-
rawUrl
|
|
678
|
-
|>
|
|
679
|
-
|
|
680
|
-
|
|
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
|
-
|
|
212
|
+
-- url: http://example.com?coupon=abc
|
|
213
|
+
-- parses into: Dict.fromList [("coupon", ["abc"])]
|
|
711
214
|
|
|
712
|
-
|
|
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
|
-
|
|
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
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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
|
-
|
|
831
|
-
|
|
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
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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
|
|
895
|
-
->
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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 =
|
|
372
|
+
, clientSideErrors = Just errors2
|
|
912
373
|
}
|
|
913
|
-
|
|
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
|
-
|
|
925
|
-
}
|
|
926
|
-
)
|
|
927
|
-
)
|
|
376
|
+
)
|
|
928
377
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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
|
-
->
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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
|
-
|
|
977
|
-
|
|
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
|
-
|
|
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.
|
|
1014
|
-
|
|
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
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
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
|
-
|
|
1090
|
-
|
|
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
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
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
|
-
|
|
1102
|
-
matchesContentType
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
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
|
-
|
|
597
|
+
False
|
|
1110
598
|
|
|
1111
599
|
Just contentType ->
|
|
1112
|
-
|
|
1113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1156
|
-
|
|
1157
|
-
Json.Decode.field "
|
|
1158
|
-
|
|
1159
|
-
|
|
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
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
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
|
|
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
|