elm-pages 2.1.11 → 3.0.0-beta.1
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/codegen/elm-pages-codegen.js +38507 -0
- 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.elmi +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/Reporter.elmi +0 -0
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Reporter.elmo +0 -0
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Runner.elmi +0 -0
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Runner.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/{template/public/style.css → dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/lock} +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 -0
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/Reporter.elm.js +6795 -0
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/Runner.elm.js +25651 -0
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/node_runner.js +110 -0
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/node_supervisor.js +187 -0
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/package.json +1 -0
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/src/Reporter.elm +26 -0
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/src/Runner.elm +62 -0
- package/generator/dead-code-review/elm.json +35 -0
- package/generator/dead-code-review/src/Pages/Review/DeadCodeEliminateData.elm +181 -0
- package/generator/dead-code-review/src/ReviewConfig.elm +9 -0
- package/generator/dead-code-review/tests/Pages/Review/DeadCodeEliminateDataTest.elm +455 -0
- 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/Pages-Review-NoContractViolationsTest.elmi +0 -0
- package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Pages-Review-NoContractViolationsTest.elmo +0 -0
- package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Reporter.elmi +0 -0
- package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Reporter.elmo +0 -0
- package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Runner.elmi +0 -0
- package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Runner.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/lock +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 -0
- package/generator/review/elm-stuff/tests-0.19.1/js/Reporter.elm.js +6795 -0
- package/generator/review/elm-stuff/tests-0.19.1/js/Runner.elm.js +27617 -0
- package/generator/review/elm-stuff/tests-0.19.1/js/node_runner.js +110 -0
- package/generator/review/elm-stuff/tests-0.19.1/js/node_supervisor.js +187 -0
- package/generator/review/elm-stuff/tests-0.19.1/js/package.json +1 -0
- package/generator/review/elm-stuff/tests-0.19.1/src/Reporter.elm +26 -0
- package/generator/review/elm-stuff/tests-0.19.1/src/Runner.elm +62 -0
- package/generator/review/elm.json +13 -4
- package/{src → generator/review/src}/Pages/Review/NoContractViolations.elm +148 -148
- package/generator/review/tests/Pages/Review/NoContractViolationsTest.elm +331 -0
- package/generator/src/RouteBuilder.elm +420 -0
- package/generator/src/SharedTemplate.elm +4 -5
- package/generator/src/SiteConfig.elm +3 -9
- package/generator/src/build.js +308 -95
- package/generator/src/cli.js +103 -8
- package/generator/src/codegen.js +192 -35
- package/generator/src/compile-elm.js +183 -31
- package/generator/src/dev-server.js +353 -96
- package/generator/src/elm-application.json +3 -1
- package/generator/src/elm-codegen.js +34 -0
- package/generator/src/elm-file-constants.js +2 -0
- package/generator/src/error-formatter.js +20 -1
- package/generator/src/generate-template-module-connector.js +125 -927
- package/generator/src/hello.ts +5 -0
- package/generator/src/pre-render-html.js +58 -104
- package/generator/src/render-worker.js +27 -13
- package/generator/src/render.js +252 -197
- package/generator/src/request-cache-fs.js +18 -0
- package/generator/src/request-cache.js +128 -56
- package/generator/src/rewrite-client-elm-json.js +49 -0
- package/generator/src/route-codegen-helpers.js +62 -1
- package/generator/static-code/dev-style.css +22 -0
- package/generator/static-code/elm-pages.js +43 -39
- package/generator/static-code/hmr.js +98 -88
- package/generator/template/app/Api.elm +25 -0
- package/generator/template/app/ErrorPage.elm +38 -0
- package/generator/template/app/Route/Index.elm +87 -0
- package/generator/template/{src → app}/Shared.elm +34 -13
- package/generator/template/app/Site.elm +19 -0
- package/generator/template/{src → app}/View.elm +0 -0
- package/generator/template/elm-pages.config.mjs +5 -0
- package/generator/template/elm.json +1 -0
- package/generator/template/{public/index.js → index.ts} +7 -3
- package/generator/template/package.json +4 -4
- package/generator/template/public/favicon.ico +0 -0
- package/generator/template/public/images/icon-png.png +0 -0
- package/generator/template/src/.gitkeep +0 -0
- package/generator/template/style.css +4 -0
- package/package.json +33 -23
- package/src/ApiRoute.elm +176 -43
- package/src/BuildError.elm +10 -1
- package/src/CookieParser.elm +84 -0
- package/src/DataSource/Env.elm +38 -0
- package/src/DataSource/File.elm +27 -16
- package/src/DataSource/Glob.elm +126 -80
- package/src/DataSource/Http.elm +283 -304
- package/src/DataSource/Internal/Glob.elm +5 -21
- package/src/DataSource/Internal/Request.elm +25 -0
- package/src/DataSource/Port.elm +17 -14
- package/src/DataSource.elm +55 -318
- package/src/Form/Field.elm +717 -0
- package/src/Form/FieldStatus.elm +36 -0
- package/src/Form/FieldView.elm +417 -0
- package/src/Form/FormData.elm +22 -0
- package/src/Form/Validation.elm +391 -0
- package/src/Form/Value.elm +118 -0
- package/src/Form.elm +1683 -0
- package/src/FormData.elm +58 -0
- package/src/FormDecoder.elm +102 -0
- package/src/Head/Seo.elm +12 -4
- package/src/Head.elm +12 -2
- package/src/HtmlPrinter.elm +1 -1
- package/src/Internal/ApiRoute.elm +17 -4
- package/src/Internal/Request.elm +7 -0
- package/src/PageServerResponse.elm +68 -0
- package/src/Pages/ContentCache.elm +1 -229
- package/src/Pages/Fetcher.elm +58 -0
- package/src/Pages/FormState.elm +256 -0
- package/src/Pages/Generate.elm +800 -0
- package/src/Pages/Internal/Form.elm +17 -0
- package/src/Pages/Internal/NotFoundReason.elm +3 -55
- package/src/Pages/Internal/Platform/Cli.elm +777 -579
- package/src/Pages/Internal/Platform/Effect.elm +5 -5
- package/src/Pages/Internal/Platform/StaticResponses.elm +178 -394
- package/src/Pages/Internal/Platform/ToJsPayload.elm +24 -23
- package/src/Pages/Internal/Platform.elm +1244 -504
- package/src/Pages/Internal/ResponseSketch.elm +19 -0
- package/src/Pages/Internal/RoutePattern.elm +596 -45
- package/src/Pages/Manifest.elm +26 -0
- package/src/Pages/Msg.elm +79 -0
- package/src/Pages/ProgramConfig.elm +67 -14
- package/src/Pages/SiteConfig.elm +3 -6
- package/src/Pages/StaticHttp/Request.elm +4 -2
- package/src/Pages/StaticHttpRequest.elm +50 -215
- package/src/Pages/Transition.elm +70 -0
- package/src/Path.elm +1 -0
- package/src/Pattern.elm +98 -0
- package/src/RenderRequest.elm +2 -2
- package/src/RequestsAndPending.elm +111 -9
- package/src/Server/Request.elm +1253 -0
- package/src/Server/Response.elm +292 -0
- package/src/Server/Session.elm +316 -0
- package/src/Server/SetCookie.elm +169 -0
- package/src/TerminalText.elm +1 -1
- package/src/Test/Html/Internal/ElmHtml/Markdown.elm +0 -1
- package/src/Test/Html/Internal/ElmHtml/ToString.elm +1 -1
- package/generator/src/Page.elm +0 -359
- package/generator/src/codegen-template-module.js +0 -183
- package/generator/src/elm-pages-js-minified.js +0 -1
- package/generator/template/src/Api.elm +0 -14
- package/generator/template/src/Page/Index.elm +0 -69
- package/generator/template/src/Site.elm +0 -41
- package/src/DataSource/ServerRequest.elm +0 -60
- package/src/Internal/OptimizedDecoder.elm +0 -18
- package/src/KeepOrDiscard.elm +0 -6
- package/src/OptimizedDecoder/Pipeline.elm +0 -335
- package/src/OptimizedDecoder.elm +0 -818
- package/src/Pages/Internal/ApplicationType.elm +0 -6
- package/src/Pages/Secrets.elm +0 -83
- package/src/Secrets.elm +0 -111
- package/src/SecretsDict.elm +0 -45
|
@@ -0,0 +1,1253 @@
|
|
|
1
|
+
module Server.Request exposing
|
|
2
|
+
( Parser
|
|
3
|
+
, succeed, fromResult, skip
|
|
4
|
+
, formData, formDataWithServerValidation
|
|
5
|
+
, 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
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
{-|
|
|
21
|
+
|
|
22
|
+
@docs Parser
|
|
23
|
+
|
|
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
|
|
107
|
+
for the logged-in user, or else respond with an HTTP redirect response (see the [`Server.Response` docs](Server-Response)).
|
|
108
|
+
|
|
109
|
+
You can access the incoming HTTP request's:
|
|
110
|
+
|
|
111
|
+
- Headers
|
|
112
|
+
- Cookies
|
|
113
|
+
- [`method`](#method)
|
|
114
|
+
- URL query parameters
|
|
115
|
+
- [`requestTime`](#requestTime) (as a `Time.Posix`)
|
|
116
|
+
|
|
117
|
+
Note that this data is not available for pre-rendered pages or pre-rendered API Routes, only for server-rendered pages.
|
|
118
|
+
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
|
+
requests the page and then the pre-rendered page is served as a plain file (without running your Route Module).
|
|
120
|
+
|
|
121
|
+
That's why `RouteBuilder.preRender` has `data : RouteParams -> DataSource Data`:
|
|
122
|
+
|
|
123
|
+
import DataSource exposing (DataSource)
|
|
124
|
+
import RouteBuilder exposing (StatelessRoute)
|
|
125
|
+
|
|
126
|
+
type alias Data =
|
|
127
|
+
{}
|
|
128
|
+
|
|
129
|
+
data : RouteParams -> DataSource Data
|
|
130
|
+
data routeParams =
|
|
131
|
+
DataSource.succeed Data
|
|
132
|
+
|
|
133
|
+
route : StatelessRoute RouteParams Data ActionData
|
|
134
|
+
route =
|
|
135
|
+
RouteBuilder.preRender
|
|
136
|
+
{ data = data
|
|
137
|
+
, head = head
|
|
138
|
+
, pages = pages
|
|
139
|
+
}
|
|
140
|
+
|> RouteBuilder.buildNoState { view = view }
|
|
141
|
+
|
|
142
|
+
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
|
|
145
|
+
can use the incoming HTTP request data to choose how to respond. For example, you could check for a dark-mode preference
|
|
146
|
+
cookie and render a light- or dark-themed page and render a different page.
|
|
147
|
+
|
|
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
|
+
{}
|
|
161
|
+
|
|
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
|
+
|
|
171
|
+
route : StatelessRoute RouteParams Data ActionData
|
|
172
|
+
route =
|
|
173
|
+
RouteBuilder.serverRender
|
|
174
|
+
{ head = head
|
|
175
|
+
, data = data
|
|
176
|
+
}
|
|
177
|
+
|> RouteBuilder.buildNoState { view = view }
|
|
178
|
+
|
|
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
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
{-| -}
|
|
221
|
+
succeed : value -> Parser value
|
|
222
|
+
succeed value =
|
|
223
|
+
Internal.Request.Parser (Json.Decode.succeed ( Ok value, [] ))
|
|
224
|
+
|
|
225
|
+
|
|
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
|
+
|
|
245
|
+
|
|
246
|
+
{-| -}
|
|
247
|
+
type ValidationError
|
|
248
|
+
= ValidationError String
|
|
249
|
+
| OneOf (List ValidationError)
|
|
250
|
+
| MissingQueryParam { missingParam : String, allQueryParams : String }
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
{-| -}
|
|
254
|
+
errorsToString : ( ValidationError, List ValidationError ) -> String
|
|
255
|
+
errorsToString validationErrors =
|
|
256
|
+
validationErrors
|
|
257
|
+
|> List.NonEmpty.toList
|
|
258
|
+
|> List.map errorToString
|
|
259
|
+
|> String.join "\n"
|
|
260
|
+
|
|
261
|
+
|
|
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
|
+
)
|
|
279
|
+
|
|
280
|
+
MissingQueryParam record ->
|
|
281
|
+
"Missing query param \"" ++ record.missingParam ++ "\". Query string was `" ++ record.allQueryParams ++ "`"
|
|
282
|
+
|
|
283
|
+
|
|
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
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
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
|
+
|
|
307
|
+
|
|
308
|
+
{-| Decode an argument and provide it to a function in a decoder.
|
|
309
|
+
|
|
310
|
+
decoder : Decoder String
|
|
311
|
+
decoder =
|
|
312
|
+
succeed (String.repeat)
|
|
313
|
+
|> andMap (field "count" int)
|
|
314
|
+
|> andMap (field "val" string)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
""" { "val": "hi", "count": 3 } """
|
|
318
|
+
|> decodeString decoder
|
|
319
|
+
--> Success "hihihi"
|
|
320
|
+
|
|
321
|
+
-}
|
|
322
|
+
andMap : Parser a -> Parser (a -> b) -> Parser b
|
|
323
|
+
andMap =
|
|
324
|
+
map2 (|>)
|
|
325
|
+
|
|
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
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
{-| -}
|
|
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
|
|
477
|
+
|
|
478
|
+
|
|
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).
|
|
525
|
+
-}
|
|
526
|
+
fromResult : Result String value -> Parser value
|
|
527
|
+
fromResult result =
|
|
528
|
+
case result of
|
|
529
|
+
Ok okValue ->
|
|
530
|
+
succeed okValue
|
|
531
|
+
|
|
532
|
+
Err error ->
|
|
533
|
+
skipInternal (ValidationError error)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
jsonFromResult : Result String value -> Json.Decode.Decoder value
|
|
537
|
+
jsonFromResult result =
|
|
538
|
+
case result of
|
|
539
|
+
Ok okValue ->
|
|
540
|
+
Json.Decode.succeed okValue
|
|
541
|
+
|
|
542
|
+
Err error ->
|
|
543
|
+
Json.Decode.fail error
|
|
544
|
+
|
|
545
|
+
|
|
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
|
+
)
|
|
558
|
+
|
|
559
|
+
|
|
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
|
|
566
|
+
|
|
567
|
+
|
|
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
|
|
575
|
+
|
|
576
|
+
|
|
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, [] ))
|
|
581
|
+
|
|
582
|
+
|
|
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
|
|
602
|
+
|
|
603
|
+
|
|
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
|
+
|
|
624
|
+
|
|
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
|
+
|
|
633
|
+
|
|
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))
|
|
638
|
+
|
|
639
|
+
|
|
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
|
+
)
|
|
668
|
+
|
|
669
|
+
Nothing ->
|
|
670
|
+
skipInternal (ValidationError ("Expected query param \"" ++ name ++ "\", but there were no query params."))
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
|
|
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
|
+
|
|
687
|
+
|
|
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
|
|
695
|
+
|
|
696
|
+
|
|
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
|
+
)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
{-| This is a Request.Parser that will never match an HTTP request. Similar to `Json.Decode.fail`.
|
|
713
|
+
|
|
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
|
|
717
|
+
|
|
718
|
+
import Server.Request as Request
|
|
719
|
+
|
|
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
|
|
728
|
+
|
|
729
|
+
Err error ->
|
|
730
|
+
Request.skip ("XML could not be decoded " ++ xmlErrorToString error)
|
|
731
|
+
)
|
|
732
|
+
|
|
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
|
+
)
|
|
754
|
+
|
|
755
|
+
-}
|
|
756
|
+
skip : String -> Parser value
|
|
757
|
+
skip errorMessage =
|
|
758
|
+
skipInternal (ValidationError errorMessage)
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
skipInternal : ValidationError -> Parser value
|
|
762
|
+
skipInternal validationError_ =
|
|
763
|
+
Internal.Request.Parser
|
|
764
|
+
(Json.Decode.succeed
|
|
765
|
+
( Err validationError_, [] )
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
|
|
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
|
+
|
|
782
|
+
Nothing ->
|
|
783
|
+
( Err (ValidationError "Internal error - expected rawUrl field but the adapter script didn't provide one."), [] )
|
|
784
|
+
)
|
|
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
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
{-| -}
|
|
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
|
|
806
|
+
|
|
807
|
+
Nothing ->
|
|
808
|
+
skipInternal (ValidationError ("Missing cookie " ++ name))
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
{-| -}
|
|
813
|
+
cookie : String -> Parser (Maybe String)
|
|
814
|
+
cookie name =
|
|
815
|
+
allCookies
|
|
816
|
+
|> map (Dict.get name)
|
|
817
|
+
|
|
818
|
+
|
|
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
|
|
830
|
+
|
|
831
|
+
|
|
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, [] )
|
|
840
|
+
|
|
841
|
+
Nothing ->
|
|
842
|
+
( Err (ValidationError ("Missing form field '" ++ name ++ "'")), [] )
|
|
843
|
+
)
|
|
844
|
+
|> Internal.Request.Parser
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
optionalFormField_ : String -> Parser (Maybe String)
|
|
848
|
+
optionalFormField_ name =
|
|
849
|
+
optionalField name Json.Decode.string
|
|
850
|
+
|> noErrors
|
|
851
|
+
|> Internal.Request.Parser
|
|
852
|
+
|
|
853
|
+
|
|
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, [] )
|
|
875
|
+
|
|
876
|
+
Nothing ->
|
|
877
|
+
( Err (ValidationError ("Missing form field " ++ name)), [] )
|
|
878
|
+
)
|
|
879
|
+
|> Internal.Request.Parser
|
|
880
|
+
|
|
881
|
+
|
|
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
|
+
)
|
|
925
|
+
|
|
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
|
+
)
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
{-| -}
|
|
943
|
+
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
|
|
949
|
+
(\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
|
+
, []
|
|
1007
|
+
)
|
|
1008
|
+
|> Internal.Request.Parser
|
|
1009
|
+
|
|
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
|
+
)
|
|
1039
|
+
)
|
|
1040
|
+
|
|
1041
|
+
|
|
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
|
+
)
|
|
1068
|
+
|
|
1069
|
+
|
|
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.")
|
|
1083
|
+
|
|
1084
|
+
Just contentType ->
|
|
1085
|
+
if (contentType |> parseContentType) == (expectedContentType |> parseContentType) then
|
|
1086
|
+
succeed ()
|
|
1087
|
+
|
|
1088
|
+
else
|
|
1089
|
+
skipInternal <| ValidationError ("Expected content-type to be " ++ expectedContentType ++ " but it was " ++ contentType)
|
|
1090
|
+
)
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
rawContentType : Parser (Maybe String)
|
|
1094
|
+
rawContentType =
|
|
1095
|
+
optionalField ("content-type" |> String.toLower) Json.Decode.string
|
|
1096
|
+
|> noErrors
|
|
1097
|
+
|> Internal.Request.Parser
|
|
1098
|
+
|
|
1099
|
+
|
|
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 ->
|
|
1106
|
+
case maybeContentType of
|
|
1107
|
+
Nothing ->
|
|
1108
|
+
Nothing
|
|
1109
|
+
|
|
1110
|
+
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
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
parseContentType : String -> String
|
|
1122
|
+
parseContentType contentTypeString =
|
|
1123
|
+
contentTypeString
|
|
1124
|
+
|> String.split ";"
|
|
1125
|
+
|> List.head
|
|
1126
|
+
|> Maybe.map String.trim
|
|
1127
|
+
|> Maybe.withDefault contentTypeString
|
|
1128
|
+
|
|
1129
|
+
|
|
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
|
+
)
|
|
1151
|
+
|
|
1152
|
+
|
|
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
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
{-| Same as [`rawBody`](#rawBody), but will only match when a body is present in the HTTP request.
|
|
1162
|
+
-}
|
|
1163
|
+
expectBody : Parser String
|
|
1164
|
+
expectBody =
|
|
1165
|
+
rawBody
|
|
1166
|
+
|> andThen
|
|
1167
|
+
(Result.fromMaybe "Expected body but none was present."
|
|
1168
|
+
>> fromResult
|
|
1169
|
+
)
|
|
1170
|
+
|
|
1171
|
+
|
|
1172
|
+
{-| -}
|
|
1173
|
+
type Method
|
|
1174
|
+
= Connect
|
|
1175
|
+
| Delete
|
|
1176
|
+
| Get
|
|
1177
|
+
| Head
|
|
1178
|
+
| Options
|
|
1179
|
+
| Patch
|
|
1180
|
+
| Post
|
|
1181
|
+
| Put
|
|
1182
|
+
| Trace
|
|
1183
|
+
| NonStandard String
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
methodFromString : String -> Method
|
|
1187
|
+
methodFromString rawMethod =
|
|
1188
|
+
case rawMethod |> String.toLower of
|
|
1189
|
+
"connect" ->
|
|
1190
|
+
Connect
|
|
1191
|
+
|
|
1192
|
+
"delete" ->
|
|
1193
|
+
Delete
|
|
1194
|
+
|
|
1195
|
+
"get" ->
|
|
1196
|
+
Get
|
|
1197
|
+
|
|
1198
|
+
"head" ->
|
|
1199
|
+
Head
|
|
1200
|
+
|
|
1201
|
+
"options" ->
|
|
1202
|
+
Options
|
|
1203
|
+
|
|
1204
|
+
"patch" ->
|
|
1205
|
+
Patch
|
|
1206
|
+
|
|
1207
|
+
"post" ->
|
|
1208
|
+
Post
|
|
1209
|
+
|
|
1210
|
+
"put" ->
|
|
1211
|
+
Put
|
|
1212
|
+
|
|
1213
|
+
"trace" ->
|
|
1214
|
+
Trace
|
|
1215
|
+
|
|
1216
|
+
_ ->
|
|
1217
|
+
NonStandard rawMethod
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
{-| Gets the HTTP Method as a String, like 'GET', 'PUT', etc.
|
|
1221
|
+
-}
|
|
1222
|
+
methodToString : Method -> String
|
|
1223
|
+
methodToString method_ =
|
|
1224
|
+
case method_ of
|
|
1225
|
+
Connect ->
|
|
1226
|
+
"CONNECT"
|
|
1227
|
+
|
|
1228
|
+
Delete ->
|
|
1229
|
+
"DELETE"
|
|
1230
|
+
|
|
1231
|
+
Get ->
|
|
1232
|
+
"GET"
|
|
1233
|
+
|
|
1234
|
+
Head ->
|
|
1235
|
+
"HEAD"
|
|
1236
|
+
|
|
1237
|
+
Options ->
|
|
1238
|
+
"OPTIONS"
|
|
1239
|
+
|
|
1240
|
+
Patch ->
|
|
1241
|
+
"PATCH"
|
|
1242
|
+
|
|
1243
|
+
Post ->
|
|
1244
|
+
"POST"
|
|
1245
|
+
|
|
1246
|
+
Put ->
|
|
1247
|
+
"PUT"
|
|
1248
|
+
|
|
1249
|
+
Trace ->
|
|
1250
|
+
"TRACE"
|
|
1251
|
+
|
|
1252
|
+
NonStandard nonStandardMethod ->
|
|
1253
|
+
nonStandardMethod
|