elm-pages 3.0.0-beta.8 → 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.
Files changed (164) hide show
  1. package/README.md +11 -2
  2. package/adapter/netlify.js +207 -0
  3. package/codegen/{elm-pages-codegen.js → elm-pages-codegen.cjs} +2730 -2938
  4. package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Pages-Review-DeadCodeEliminateData.elmi +0 -0
  5. package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Pages-Review-DeadCodeEliminateData.elmo +0 -0
  6. package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Pages-Review-DeadCodeEliminateDataTest.elmo +0 -0
  7. package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/d.dat +0 -0
  8. package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/i.dat +0 -0
  9. package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/o.dat +0 -0
  10. package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm.json +1 -1
  11. package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/Reporter.elm.js +1527 -422
  12. package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/Runner.elm.js +16840 -13653
  13. package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/node_runner.js +1 -1
  14. package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/node_supervisor.js +2 -2
  15. package/generator/dead-code-review/elm.json +9 -7
  16. package/generator/dead-code-review/src/Pages/Review/DeadCodeEliminateData.elm +59 -10
  17. package/generator/dead-code-review/tests/Pages/Review/DeadCodeEliminateDataTest.elm +52 -36
  18. package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Pages-Internal-RoutePattern.elmi +0 -0
  19. package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Pages-Internal-RoutePattern.elmo +0 -0
  20. package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Pages-Review-NoContractViolations.elmi +0 -0
  21. package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Pages-Review-NoContractViolations.elmo +0 -0
  22. package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/d.dat +0 -0
  23. package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/i.dat +0 -0
  24. package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/o.dat +0 -0
  25. package/generator/review/elm-stuff/tests-0.19.1/elm.json +1 -1
  26. package/generator/review/elm-stuff/tests-0.19.1/js/Reporter.elm.js +1527 -422
  27. package/generator/review/elm-stuff/tests-0.19.1/js/Runner.elm.js +25118 -21832
  28. package/generator/review/elm-stuff/tests-0.19.1/js/node_runner.js +1 -1
  29. package/generator/review/elm-stuff/tests-0.19.1/js/node_supervisor.js +2 -2
  30. package/generator/review/elm.json +10 -10
  31. package/generator/src/RouteBuilder.elm +93 -128
  32. package/generator/src/SharedTemplate.elm +8 -7
  33. package/generator/src/SiteConfig.elm +3 -2
  34. package/generator/src/basepath-middleware.js +3 -3
  35. package/generator/src/build.js +147 -63
  36. package/generator/src/cli.js +292 -88
  37. package/generator/src/codegen.js +29 -27
  38. package/generator/src/compatibility-key.js +3 -0
  39. package/generator/src/compile-elm.js +43 -26
  40. package/generator/src/config.js +2 -4
  41. package/generator/src/copy-dir.js +2 -2
  42. package/generator/src/dev-server.js +159 -92
  43. package/generator/src/dir-helpers.js +9 -26
  44. package/generator/src/elm-codegen.js +5 -4
  45. package/generator/src/elm-file-constants.js +2 -3
  46. package/generator/src/error-formatter.js +12 -11
  47. package/generator/src/file-helpers.js +3 -4
  48. package/generator/src/generate-template-module-connector.js +23 -23
  49. package/generator/src/init.js +9 -8
  50. package/generator/src/pre-render-html.js +10 -13
  51. package/generator/src/render-test.js +109 -0
  52. package/generator/src/render-worker.js +25 -28
  53. package/generator/src/render.js +321 -142
  54. package/generator/src/request-cache.js +265 -162
  55. package/generator/src/resolve-elm-module.js +64 -0
  56. package/generator/src/rewrite-client-elm-json.js +6 -5
  57. package/generator/src/rewrite-elm-json-help.js +56 -0
  58. package/generator/src/rewrite-elm-json.js +17 -7
  59. package/generator/src/route-codegen-helpers.js +16 -31
  60. package/generator/src/seo-renderer.js +12 -7
  61. package/generator/src/vite-utils.js +1 -2
  62. package/generator/static-code/elm-pages.js +10 -0
  63. package/generator/static-code/hmr.js +79 -13
  64. package/generator/template/app/Api.elm +3 -2
  65. package/generator/template/app/Effect.elm +155 -0
  66. package/generator/template/app/ErrorPage.elm +49 -6
  67. package/generator/template/app/Route/Blog/Slug_.elm +86 -0
  68. package/generator/template/app/Route/Greet.elm +107 -0
  69. package/generator/template/app/Route/Hello.elm +119 -0
  70. package/generator/template/app/Route/Index.elm +26 -25
  71. package/generator/template/app/Shared.elm +38 -39
  72. package/generator/template/app/Site.elm +4 -7
  73. package/generator/template/app/View.elm +9 -8
  74. package/generator/template/codegen/elm.codegen.json +18 -0
  75. package/generator/template/custom-backend-task.ts +3 -0
  76. package/generator/template/elm-pages.config.mjs +13 -0
  77. package/generator/template/elm-tooling.json +0 -3
  78. package/generator/template/elm.json +25 -20
  79. package/generator/template/index.ts +1 -2
  80. package/generator/template/netlify.toml +4 -1
  81. package/generator/template/package.json +10 -4
  82. package/generator/template/script/.elm-pages/compiled-ports/custom-backend-task.mjs +7 -0
  83. package/generator/template/script/custom-backend-task.ts +3 -0
  84. package/generator/template/script/elm.json +61 -0
  85. package/generator/template/script/src/AddRoute.elm +312 -0
  86. package/generator/template/script/src/Stars.elm +42 -0
  87. package/package.json +30 -27
  88. package/src/ApiRoute.elm +249 -85
  89. package/src/BackendTask/Custom.elm +325 -0
  90. package/src/BackendTask/Env.elm +90 -0
  91. package/src/{DataSource → BackendTask}/File.elm +171 -56
  92. package/src/{DataSource → BackendTask}/Glob.elm +136 -125
  93. package/src/BackendTask/Http.elm +679 -0
  94. package/src/{DataSource → BackendTask}/Internal/Glob.elm +1 -1
  95. package/src/BackendTask/Internal/Request.elm +69 -0
  96. package/src/BackendTask/Random.elm +79 -0
  97. package/src/BackendTask/Time.elm +47 -0
  98. package/src/BackendTask.elm +531 -0
  99. package/src/FatalError.elm +90 -0
  100. package/src/FormData.elm +21 -18
  101. package/src/Head/Seo.elm +4 -4
  102. package/src/Head.elm +237 -7
  103. package/src/Internal/ApiRoute.elm +7 -5
  104. package/src/Internal/Request.elm +84 -4
  105. package/src/PageServerResponse.elm +6 -1
  106. package/src/Pages/ConcurrentSubmission.elm +127 -0
  107. package/src/Pages/Form.elm +340 -0
  108. package/src/Pages/FormData.elm +19 -0
  109. package/src/Pages/GeneratorProgramConfig.elm +15 -0
  110. package/src/Pages/Internal/FatalError.elm +5 -0
  111. package/src/Pages/Internal/Msg.elm +93 -0
  112. package/src/Pages/Internal/NotFoundReason.elm +4 -4
  113. package/src/Pages/Internal/Platform/Cli.elm +629 -767
  114. package/src/Pages/Internal/Platform/CompatibilityKey.elm +6 -0
  115. package/src/Pages/Internal/Platform/Effect.elm +1 -2
  116. package/src/Pages/Internal/Platform/GeneratorApplication.elm +379 -0
  117. package/src/Pages/Internal/Platform/StaticResponses.elm +65 -276
  118. package/src/Pages/Internal/Platform/ToJsPayload.elm +6 -9
  119. package/src/Pages/Internal/Platform.elm +330 -203
  120. package/src/Pages/Internal/ResponseSketch.elm +2 -2
  121. package/src/Pages/Internal/Script.elm +17 -0
  122. package/src/Pages/Internal/StaticHttpBody.elm +35 -1
  123. package/src/Pages/Manifest.elm +52 -11
  124. package/src/Pages/Navigation.elm +85 -0
  125. package/src/Pages/PageUrl.elm +26 -12
  126. package/src/Pages/ProgramConfig.elm +32 -22
  127. package/src/Pages/Script.elm +166 -0
  128. package/src/Pages/SiteConfig.elm +3 -2
  129. package/src/Pages/StaticHttp/Request.elm +2 -2
  130. package/src/Pages/StaticHttpRequest.elm +23 -99
  131. package/src/Pages/Url.elm +3 -3
  132. package/src/PagesMsg.elm +88 -0
  133. package/src/QueryParams.elm +21 -172
  134. package/src/RenderRequest.elm +7 -7
  135. package/src/RequestsAndPending.elm +37 -20
  136. package/src/Result/Extra.elm +26 -0
  137. package/src/Scaffold/Form.elm +569 -0
  138. package/src/Scaffold/Route.elm +1431 -0
  139. package/src/Server/Request.elm +476 -1001
  140. package/src/Server/Response.elm +130 -36
  141. package/src/Server/Session.elm +181 -111
  142. package/src/Server/SetCookie.elm +80 -32
  143. package/src/Stub.elm +53 -0
  144. package/src/Test/Html/Internal/ElmHtml/ToString.elm +8 -9
  145. package/src/{Path.elm → UrlPath.elm} +33 -36
  146. package/generator/template/public/images/icon-png.png +0 -0
  147. package/src/DataSource/Env.elm +0 -38
  148. package/src/DataSource/Http.elm +0 -446
  149. package/src/DataSource/Internal/Request.elm +0 -20
  150. package/src/DataSource/Port.elm +0 -90
  151. package/src/DataSource.elm +0 -538
  152. package/src/Form/Field.elm +0 -717
  153. package/src/Form/FieldStatus.elm +0 -36
  154. package/src/Form/FieldView.elm +0 -417
  155. package/src/Form/FormData.elm +0 -22
  156. package/src/Form/Validation.elm +0 -391
  157. package/src/Form/Value.elm +0 -118
  158. package/src/Form.elm +0 -1683
  159. package/src/FormDecoder.elm +0 -102
  160. package/src/Pages/FormState.elm +0 -256
  161. package/src/Pages/Generate.elm +0 -1151
  162. package/src/Pages/Internal/Form.elm +0 -17
  163. package/src/Pages/Msg.elm +0 -79
  164. package/src/Pages/Transition.elm +0 -70
@@ -1,134 +1,55 @@
1
1
  module Server.Request exposing
2
- ( Parser
3
- , succeed, fromResult, skip
2
+ ( Request
3
+ , requestTime
4
+ , header, headers
5
+ , method, Method(..), methodToString
6
+ , body, jsonBody
4
7
  , formData, formDataWithServerValidation
5
8
  , rawFormData
6
- , method, rawBody, allCookies, rawHeaders, queryParams
7
- , requestTime, optionalHeader, expectContentType, expectJsonBody
8
- , acceptMethod, acceptContentTypes
9
- , map, map2, oneOf, andMap, andThen
10
- , queryParam, expectQueryParam
11
- , cookie, expectCookie
12
- , expectHeader
13
- , File, expectMultiPartFormPost
14
- , expectBody
15
- , map3, map4, map5, map6, map7, map8, map9
16
- , Method(..), methodToString
17
- , errorsToString, errorToString, getDecoder, ValidationError
9
+ , rawUrl
10
+ , queryParam, queryParams
11
+ , matchesContentType
12
+ , cookie, cookies
18
13
  )
19
14
 
20
- {-|
15
+ {-| Server-rendered Route modules and [server-rendered API Routes](ApiRoute#serverRender) give you access to a `Server.Request.Request` argument.
21
16
 
22
- @docs Parser
17
+ @docs Request
23
18
 
24
- @docs succeed, fromResult, skip
25
-
26
-
27
- ## Forms
28
-
29
- @docs formData, formDataWithServerValidation
30
-
31
- @docs rawFormData
32
-
33
-
34
- ## Direct Values
35
-
36
- @docs method, rawBody, allCookies, rawHeaders, queryParams
37
-
38
- @docs requestTime, optionalHeader, expectContentType, expectJsonBody
39
-
40
- @docs acceptMethod, acceptContentTypes
41
-
42
-
43
- ## Transforming
44
-
45
- @docs map, map2, oneOf, andMap, andThen
46
-
47
-
48
- ## Query Parameters
49
-
50
- @docs queryParam, expectQueryParam
51
-
52
-
53
- ## Cookies
54
-
55
- @docs cookie, expectCookie
56
-
57
-
58
- ## Headers
59
-
60
- @docs expectHeader
61
-
62
-
63
- ## Multi-part forms and file uploads
64
-
65
- @docs File, expectMultiPartFormPost
66
-
67
-
68
- ## Request Parsers That Can Fail
69
-
70
- @docs expectBody
71
-
72
-
73
- ## Map Functions
74
-
75
- @docs map3, map4, map5, map6, map7, map8, map9
76
-
77
-
78
- ## Method Type
79
-
80
- @docs Method, methodToString
81
-
82
-
83
- ## Internals
84
-
85
- @docs errorsToString, errorToString, getDecoder, ValidationError
86
-
87
- -}
88
-
89
- import 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
- - URL query parameters
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` has `data : RouteParams -> DataSource Data`:
42
+ That's why `RouteBuilder.preRender` does not have a `Server.Request.Request` argument.
122
43
 
123
- import DataSource exposing (DataSource)
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 -> DataSource Data
50
+ data : RouteParams -> BackendTask Data
130
51
  data routeParams =
131
- DataSource.succeed Data
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` is a `Request.Parser` in server-rendered Route Modules. Since you have an incoming HTTP request for server-rendered routes,
144
- `RouteBuilder.serverRender` has `data : RouteParams -> Request.Parser (DataSource (Response Data))`. That means that you
64
+ is loaded. That's why `data` has a `Server.Request.Request` argument in server-rendered Route Modules. Since you have an incoming HTTP request for server-rendered routes,
65
+ `RouteBuilder.serverRender` has `data : RouteParams -> Request -> BackendTask (Response Data)`. That means that you
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
- That's a mouthful, so let's unpack what it means.
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
- route : StatelessRoute RouteParams Data ActionData
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
- {-| TODO internal only
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
- {-| Decode an argument and provide it to a function in a decoder.
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
- """ { "val": "hi", "count": 3 } """
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
- andThen : (a -> Parser b) -> Parser a -> Parser b
329
- andThen toRequestB (Internal.Request.Parser requestA) =
330
- Json.Decode.andThen
331
- (\value ->
332
- case value of
333
- ( Ok okValue, _ ) ->
334
- okValue
335
- |> toRequestB
336
- |> unwrap
337
-
338
- ( Err error, errors ) ->
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
- map8 :
457
- (value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> value7 -> value8 -> valueCombined)
458
- -> Parser value1
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
- fromResult : Result String value -> Parser value
527
- fromResult result =
528
- case result of
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
- jsonFromResult : Result String value -> Json.Decode.Decoder value
537
- jsonFromResult result =
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
- Err error ->
543
- Json.Decode.fail error
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
- noErrors : Json.Decode.Decoder value -> Json.Decode.Decoder ( Result ValidationError value, List ValidationError )
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
- appendError : ValidationError -> Json.Decode.Decoder ( value, List ValidationError ) -> Json.Decode.Decoder ( value, List ValidationError )
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
- Nothing ->
670
- skipInternal (ValidationError ("Expected query param \"" ++ name ++ "\", but there were no query params."))
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
- findFirstQueryParam : String -> String -> Maybe String
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
- queryParams : Parser (Dict String (List String))
699
- queryParams =
700
- rawUrl
701
- |> map
702
- (\rawUrl_ ->
703
- rawUrl_
704
- |> Url.fromString
705
- |> Maybe.andThen .query
706
- |> Maybe.map QueryParams.fromString
707
- |> Maybe.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
- {-| This is a Request.Parser that will never match an HTTP request. Similar to `Json.Decode.fail`.
222
+ -- url: https://example.com?coupon=abc&coupon=xyz
223
+ -- parses into: "https://example.com?coupon=abc&coupon=xyz"
713
224
 
714
- Why would you want it to always fail? It's helpful for building custom `Server.Request.Parser`. For example, let's say
715
- you wanted to define a custom `Server.Request.Parser` to use an XML Decoding package on the request body.
716
- You could define a custom function like this
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
- expectXmlBody : XmlDecoder value -> Request.Parser value
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
- Err error ->
730
- Request.skip ("XML could not be decoded " ++ xmlErrorToString error)
731
- )
233
+ Header: Accept-Language: en-US,en;q=0.5
732
234
 
733
- Note that when we said `Request.skip`, remaining Request Parsers will run (for example if you use [`Server.Request.oneOf`](#oneOf)).
734
- You could build this with different semantics if you wanted to handle _any_ valid XML body. This Request Parser will _not_
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
- skip : String -> Parser value
757
- skip errorMessage =
758
- skipInternal (ValidationError errorMessage)
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
- skipInternal : ValidationError -> Parser value
762
- skipInternal validationError_ =
763
- Internal.Request.Parser
764
- (Json.Decode.succeed
765
- ( Err validationError_, [] )
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
- Nothing ->
783
- ( Err (ValidationError "Internal error - expected rawUrl field but the adapter script didn't provide one."), [] )
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
- |> Internal.Request.Parser
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
- expectCookie : String -> Parser String
799
- expectCookie name =
800
- cookie name
801
- |> andThen
802
- (\maybeCookie ->
803
- case maybeCookie of
804
- Just justValue ->
805
- succeed justValue
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
- Nothing ->
808
- skipInternal (ValidationError ("Missing cookie " ++ name))
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
- cookie : String -> Parser (Maybe String)
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
- allCookies : Parser (Dict String String)
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
- formField_ : String -> Parser String
833
- formField_ name =
834
- optionalField name Json.Decode.string
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
- Nothing ->
842
- ( Err (ValidationError ("Missing form field '" ++ name ++ "'")), [] )
843
- )
844
- |> Internal.Request.Parser
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
- optionalFormField_ : String -> Parser (Maybe String)
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
- type alias File =
856
- { name : String
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
- Nothing ->
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
- ( _, maybeErrors ) ->
927
- Err
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.ServerForms error combined
945
- -> Parser (Result { fields : List ( String, String ), errors : Dict String (List error) } combined)
946
- formData formParsers =
947
- rawFormData
948
- |> andThen
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
- let
951
- ( maybeDecoded, errors ) =
952
- Form.runOneOfServerSide
953
- rawFormData_
954
- formParsers
955
- in
956
- case ( maybeDecoded, errors |> Dict.toList |> List.filter (\( _, value ) -> value |> List.isEmpty |> not) |> List.NonEmpty.fromList ) of
957
- ( Just decoded, Nothing ) ->
958
- Ok decoded
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
- else
1011
- justBody
1012
- |> FormData.parse
1013
- |> succeed
1014
- |> andThen
1015
- (\parsedForm ->
1016
- let
1017
- thing : Json.Encode.Value
1018
- thing =
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
- expectContentType : String -> Parser ()
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
- Just contentType ->
1085
- if (contentType |> parseContentType) == (expectedContentType |> parseContentType) then
1086
- succeed ()
494
+ Otherwise, this `Parser` will not match.
1087
495
 
1088
- else
1089
- skipInternal <| ValidationError ("Expected content-type to be " ++ expectedContentType ++ " but it was " ++ contentType)
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
- rawContentType : Parser (Maybe String)
1094
- rawContentType =
1095
- optionalField ("content-type" |> String.toLower) Json.Decode.string
1096
- |> noErrors
1097
- |> Internal.Request.Parser
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
- matchesContentType : String -> Parser (Maybe Bool)
1101
- matchesContentType expectedContentType =
1102
- optionalField ("content-type" |> String.toLower) Json.Decode.string
1103
- |> Json.Decode.field "headers"
1104
- |> Json.Decode.map
1105
- (\maybeContentType ->
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
- Nothing
577
+ False
1109
578
 
1110
579
  Just contentType ->
1111
- if (contentType |> parseContentType) == (expectedContentType |> parseContentType) then
1112
- Just True
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
- expectJsonBody : Json.Decode.Decoder value -> Parser value
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
- rawBody : Parser (Maybe String)
1155
- rawBody =
1156
- Json.Decode.field "body" (Json.Decode.nullable Json.Decode.string)
1157
- |> noErrors
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
- expectBody : Parser String
1164
- expectBody =
1165
- rawBody
1166
- |> andThen
1167
- (Result.fromMaybe "Expected body but none was present."
1168
- >> fromResult
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 a String, like 'GET', 'PUT', etc.
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