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