elm-pages 2.1.7 → 2.1.11
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/generator/review/elm.json +34 -0
- package/generator/review/src/ReviewConfig.elm +10 -0
- package/generator/src/basepath-middleware.js +15 -9
- package/generator/src/build.js +77 -4
- package/generator/src/cli.js +13 -9
- package/generator/src/compile-elm.js +43 -0
- package/generator/src/dev-server.js +63 -11
- package/generator/src/error-formatter.js +62 -9
- package/generator/src/generate-template-module-connector.js +17 -4
- package/generator/src/init.js +4 -0
- package/generator/src/pre-render-html.js +19 -12
- package/generator/src/render-worker.js +0 -1
- package/generator/src/render.js +1 -2
- package/generator/src/seo-renderer.js +21 -2
- package/generator/static-code/hmr.js +43 -6
- package/generator/template/elm.json +13 -5
- package/generator/template/package.json +3 -2
- package/package.json +14 -8
- package/src/ApiRoute.elm +178 -0
- package/src/AriaLiveAnnouncer.elm +36 -0
- package/src/BuildError.elm +60 -0
- package/src/DataSource/File.elm +288 -0
- package/src/DataSource/Glob.elm +1050 -0
- package/src/DataSource/Http.elm +467 -0
- package/src/DataSource/Internal/Glob.elm +74 -0
- package/src/DataSource/Port.elm +87 -0
- package/src/DataSource/ServerRequest.elm +60 -0
- package/src/DataSource.elm +801 -0
- package/src/Head/Seo.elm +516 -0
- package/src/Head/Twitter.elm +109 -0
- package/src/Head.elm +452 -0
- package/src/HtmlPrinter.elm +27 -0
- package/src/Internal/ApiRoute.elm +89 -0
- package/src/Internal/OptimizedDecoder.elm +18 -0
- package/src/KeepOrDiscard.elm +6 -0
- package/src/OptimizedDecoder/Pipeline.elm +335 -0
- package/src/OptimizedDecoder.elm +818 -0
- package/src/Pages/ContentCache.elm +248 -0
- package/src/Pages/Flags.elm +26 -0
- package/src/Pages/Http.elm +10 -0
- package/src/Pages/Internal/ApplicationType.elm +6 -0
- package/src/Pages/Internal/NotFoundReason.elm +256 -0
- package/src/Pages/Internal/Platform/Cli.elm +1015 -0
- package/src/Pages/Internal/Platform/Effect.elm +14 -0
- package/src/Pages/Internal/Platform/StaticResponses.elm +540 -0
- package/src/Pages/Internal/Platform/ToJsPayload.elm +138 -0
- package/src/Pages/Internal/Platform.elm +745 -0
- package/src/Pages/Internal/RoutePattern.elm +122 -0
- package/src/Pages/Internal/Router.elm +116 -0
- package/src/Pages/Internal/StaticHttpBody.elm +54 -0
- package/src/Pages/Internal/String.elm +39 -0
- package/src/Pages/Manifest/Category.elm +240 -0
- package/src/Pages/Manifest.elm +412 -0
- package/src/Pages/PageUrl.elm +38 -0
- package/src/Pages/ProgramConfig.elm +73 -0
- package/src/Pages/Review/NoContractViolations.elm +397 -0
- package/src/Pages/Secrets.elm +83 -0
- package/src/Pages/SiteConfig.elm +13 -0
- package/src/Pages/StaticHttp/Request.elm +42 -0
- package/src/Pages/StaticHttpRequest.elm +320 -0
- package/src/Pages/Url.elm +60 -0
- package/src/Path.elm +96 -0
- package/src/QueryParams.elm +216 -0
- package/src/RenderRequest.elm +163 -0
- package/src/RequestsAndPending.elm +20 -0
- package/src/Secrets.elm +111 -0
- package/src/SecretsDict.elm +45 -0
- package/src/StructuredData.elm +236 -0
- package/src/TerminalText.elm +242 -0
- package/src/Test/Html/Internal/ElmHtml/Constants.elm +53 -0
- package/src/Test/Html/Internal/ElmHtml/Helpers.elm +17 -0
- package/src/Test/Html/Internal/ElmHtml/InternalTypes.elm +529 -0
- package/src/Test/Html/Internal/ElmHtml/Markdown.elm +56 -0
- package/src/Test/Html/Internal/ElmHtml/ToString.elm +197 -0
- package/src/Test/Internal/KernelConstants.elm +34 -0
|
@@ -0,0 +1,801 @@
|
|
|
1
|
+
module DataSource exposing
|
|
2
|
+
( DataSource
|
|
3
|
+
, map, succeed, fail
|
|
4
|
+
, fromResult
|
|
5
|
+
, andThen, resolve, combine
|
|
6
|
+
, andMap
|
|
7
|
+
, map2, map3, map4, map5, map6, map7, map8, map9
|
|
8
|
+
, distill, validate, distillCodec, distillSerializeCodec
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
{-| In an `elm-pages` app, each page can define a value `data` which is a `DataSource` that will be resolved **before** `init` is called. That means it is also available
|
|
12
|
+
when the page's HTML is pre-rendered during the build step. You can also access the resolved data in `head` to use it for the page's SEO meta tags.
|
|
13
|
+
|
|
14
|
+
A `DataSource` lets you pull in data from:
|
|
15
|
+
|
|
16
|
+
- Local files ([`DataSource.File`](DataSource-File))
|
|
17
|
+
- HTTP requests ([`DataSource.Http`](DataSource-Http))
|
|
18
|
+
- Globs, i.e. listing out local files based on a pattern like `content/*.txt` ([`DataSource.Glob`](DataSource-Glob))
|
|
19
|
+
- Ports, i.e. getting JSON data from running custom NodeJS, similar to a port in a vanilla Elm app except run at build-time in NodeJS, rather than at run-time in the browser ([`DataSource.Port`](DataSource-Port))
|
|
20
|
+
- Hardcoded data (`DataSource.succeed "Hello!"`)
|
|
21
|
+
- Or any combination of the above, using `DataSource.map2`, `DataSource.andThen`, or other combining/continuing helpers from this module
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## Where Does DataSource Data Come From?
|
|
25
|
+
|
|
26
|
+
Data from a `DataSource` is resolved when you load a page in the `elm-pages` dev server, or when you run `elm-pages build`.
|
|
27
|
+
|
|
28
|
+
Because `elm-pages` hydrates into a full Elm single-page app, it does need the data in order to initialize the Elm app.
|
|
29
|
+
So why not just get the data the old-fashioned way, with `elm/http`, for example?
|
|
30
|
+
|
|
31
|
+
A few reasons:
|
|
32
|
+
|
|
33
|
+
1. DataSource's allow you to pull in data that you wouldn't normally be able to access from an Elm app, like local files, or listings of files in a folder. Not only that, but the dev server knows to automatically hot reload the data when the files it depends on change, so you can edit the files you used in your DataSource and see the page hot reload as you save!
|
|
34
|
+
2. Because `elm-pages` has a build step, you know that your `DataSource.Http` requests succeeded, your decoders succeeded, your custom DataSource validations succeeded, and everything went smoothly. If something went wrong, you get a build failure and can deal with the issues before the site goes live. That means your users won't see those errors, and as a developer you don't need to handle those error cases in your code! Think of it as "parse, don't validate", but for your entire build.
|
|
35
|
+
3. You don't have to worry about an API being down, or hitting it repeatedly. You can build in data and it will end up as JSON files served up with all the other assets of your site. If your CDN (static site host) is down, then the rest of your site is probably down anyway. If your site host is up, then so is all of your `DataSource` data. Also, it will be served up extremely quickly without needing to wait for any database queries to be performed, `andThen` requests to be resolved, etc., because all of that work and waiting was done at build-time!
|
|
36
|
+
4. You can pre-render pages, including the SEO meta tags, with all that rich, well-typed Elm data available! That's something you can't accomplish with a vanilla Elm app, and it's one of the main use cases for elm-pages.
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
## Mental Model
|
|
40
|
+
|
|
41
|
+
You can think of a DataSource as a declarative (not imperative) definition of data. It represents where to get the data from, and how to transform it (map, combine with other DataSources, etc.).
|
|
42
|
+
|
|
43
|
+
Even though an HTTP request is non-deterministic, you should think of it that way as much as possible with a DataSource because elm-pages will only perform a given DataSource.Http request once, and
|
|
44
|
+
it will share the result between any other DataSource.Http requests that have the exact same URL, Method, Body, and Headers.
|
|
45
|
+
|
|
46
|
+
So calling a function to increment a counter on a server through an HTTP request would not be a good fit for a `DataSource`. Let's imagine we have an HTTP endpoint that gives these stateful results when called repeatedly:
|
|
47
|
+
|
|
48
|
+
<https://my-api.example.com/increment-counter>
|
|
49
|
+
-> Returns 1
|
|
50
|
+
<https://my-api.example.com/increment-counter>
|
|
51
|
+
-> Returns 2
|
|
52
|
+
<https://my-api.example.com/increment-counter>
|
|
53
|
+
-> Returns 3
|
|
54
|
+
|
|
55
|
+
If we define a `DataSource` that hits that endpoint:
|
|
56
|
+
|
|
57
|
+
data =
|
|
58
|
+
DataSource.Http.get
|
|
59
|
+
(Secrets.succeed "https://my-api.example.com/increment-counter")
|
|
60
|
+
Decode.int
|
|
61
|
+
|
|
62
|
+
No matter how many places we use that `DataSource`, its response will be "locked in" (let's say the response was `3`, then every page would have the same value of `3` for that request).
|
|
63
|
+
|
|
64
|
+
So even though HTTP requests, JavaScript code, etc. can be non-deterministic, a `DataSource` always represents a single snapshot of a resource, and those values will be re-used as if they were a deterministic, declarative resource.
|
|
65
|
+
So it's best to use that mental model to avoid confusion.
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
## Basics
|
|
69
|
+
|
|
70
|
+
@docs DataSource
|
|
71
|
+
|
|
72
|
+
@docs map, succeed, fail
|
|
73
|
+
|
|
74
|
+
@docs fromResult
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
## Chaining Requests
|
|
78
|
+
|
|
79
|
+
@docs andThen, resolve, combine
|
|
80
|
+
|
|
81
|
+
@docs andMap
|
|
82
|
+
|
|
83
|
+
@docs map2, map3, map4, map5, map6, map7, map8, map9
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
## Optimizing Page Data
|
|
87
|
+
|
|
88
|
+
Distilling data lets you reduce the amount of data loaded on the client. You can also use it to perform computations at
|
|
89
|
+
build-time or server-request-time, store the result of the computation and then simply load that result on the client
|
|
90
|
+
without needing redo the computation again on the client.
|
|
91
|
+
|
|
92
|
+
@docs distill, validate, distillCodec, distillSerializeCodec
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
### Ensuring Unique Distill Keys
|
|
96
|
+
|
|
97
|
+
If you use the same string key for two different distilled values that have differing encoded JSON, then you
|
|
98
|
+
will get a build error (and an error in the dev server for that page). That means you can safely distill values
|
|
99
|
+
and let the build command tell you about these issues if they arise.
|
|
100
|
+
|
|
101
|
+
-}
|
|
102
|
+
|
|
103
|
+
import Codec
|
|
104
|
+
import Dict exposing (Dict)
|
|
105
|
+
import Dict.Extra
|
|
106
|
+
import Json.Decode as Decode
|
|
107
|
+
import Json.Encode as Encode
|
|
108
|
+
import KeepOrDiscard exposing (KeepOrDiscard)
|
|
109
|
+
import Pages.Internal.ApplicationType as ApplicationType exposing (ApplicationType)
|
|
110
|
+
import Pages.Internal.StaticHttpBody as Body
|
|
111
|
+
import Pages.Secrets
|
|
112
|
+
import Pages.StaticHttp.Request as HashRequest
|
|
113
|
+
import Pages.StaticHttpRequest exposing (RawRequest(..), WhatToDo)
|
|
114
|
+
import RequestsAndPending exposing (RequestsAndPending)
|
|
115
|
+
import Serialize
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
{-| A DataSource represents data that will be gathered at build time. Multiple `DataSource`s can be combined together using the `mapN` functions,
|
|
119
|
+
very similar to how you can manipulate values with Json Decoders in Elm.
|
|
120
|
+
-}
|
|
121
|
+
type alias DataSource value =
|
|
122
|
+
RawRequest value
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
{-| Transform a request into an arbitrary value. The same underlying HTTP requests will be performed during the build
|
|
126
|
+
step, but mapping allows you to change the resulting values by applying functions to the results.
|
|
127
|
+
|
|
128
|
+
A common use for this is to map your data into your elm-pages view:
|
|
129
|
+
|
|
130
|
+
import DataSource
|
|
131
|
+
import Json.Decode as Decode exposing (Decoder)
|
|
132
|
+
|
|
133
|
+
view =
|
|
134
|
+
DataSource.Http.get
|
|
135
|
+
(Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages")
|
|
136
|
+
(Decode.field "stargazers_count" Decode.int)
|
|
137
|
+
|> DataSource.map
|
|
138
|
+
(\stars ->
|
|
139
|
+
{ view =
|
|
140
|
+
\model viewForPage ->
|
|
141
|
+
{ title = "Current stars: " ++ String.fromInt stars
|
|
142
|
+
, body = Html.text <| "⭐️ " ++ String.fromInt stars
|
|
143
|
+
, head = []
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
-}
|
|
149
|
+
map : (a -> b) -> DataSource a -> DataSource b
|
|
150
|
+
map fn requestInfo =
|
|
151
|
+
-- elm-review: known-unoptimized-recursion
|
|
152
|
+
case requestInfo of
|
|
153
|
+
RequestError error ->
|
|
154
|
+
RequestError error
|
|
155
|
+
|
|
156
|
+
Request partiallyStripped ( urls, lookupFn ) ->
|
|
157
|
+
Request partiallyStripped
|
|
158
|
+
( urls
|
|
159
|
+
, \keepOrDiscard appType rawResponses ->
|
|
160
|
+
map fn (lookupFn keepOrDiscard appType rawResponses)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
ApiRoute stripped value ->
|
|
164
|
+
ApiRoute stripped (fn value)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
dontSaveData : DataSource a -> DataSource a
|
|
168
|
+
dontSaveData requestInfo =
|
|
169
|
+
case requestInfo of
|
|
170
|
+
RequestError _ ->
|
|
171
|
+
requestInfo
|
|
172
|
+
|
|
173
|
+
Request partiallyStripped ( urls, lookupFn ) ->
|
|
174
|
+
Request partiallyStripped
|
|
175
|
+
( urls
|
|
176
|
+
, \_ appType rawResponses ->
|
|
177
|
+
lookupFn KeepOrDiscard.Discard appType rawResponses
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
ApiRoute _ _ ->
|
|
181
|
+
requestInfo
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
{-| This is the low-level `distill` function. In most cases, you'll want to use `distill` with a `Codec` from either
|
|
185
|
+
[`miniBill/elm-codec`](https://package.elm-lang.org/packages/miniBill/elm-codec/latest/) or
|
|
186
|
+
[`MartinSStewart/elm-serialize`](https://package.elm-lang.org/packages/MartinSStewart/elm-serialize/latest/)
|
|
187
|
+
-}
|
|
188
|
+
distill :
|
|
189
|
+
String
|
|
190
|
+
-> (raw -> Encode.Value)
|
|
191
|
+
-> (Decode.Value -> Result String distilled)
|
|
192
|
+
-> DataSource raw
|
|
193
|
+
-> DataSource distilled
|
|
194
|
+
distill uniqueKey encode decode dataSource =
|
|
195
|
+
-- elm-review: known-unoptimized-recursion
|
|
196
|
+
case dataSource of
|
|
197
|
+
RequestError error ->
|
|
198
|
+
RequestError error
|
|
199
|
+
|
|
200
|
+
Request partiallyStripped ( urls, lookupFn ) ->
|
|
201
|
+
Request partiallyStripped
|
|
202
|
+
( urls
|
|
203
|
+
, \_ appType rawResponses ->
|
|
204
|
+
case appType of
|
|
205
|
+
ApplicationType.Browser ->
|
|
206
|
+
rawResponses
|
|
207
|
+
|> RequestsAndPending.get uniqueKey
|
|
208
|
+
|> (\maybeResponse ->
|
|
209
|
+
case maybeResponse of
|
|
210
|
+
Just rawResponse ->
|
|
211
|
+
rawResponse
|
|
212
|
+
|> Decode.decodeString Decode.value
|
|
213
|
+
|> Result.mapError Decode.errorToString
|
|
214
|
+
|> Result.andThen decode
|
|
215
|
+
|> Result.mapError Pages.StaticHttpRequest.DecoderError
|
|
216
|
+
|> Result.map (Tuple.pair Dict.empty)
|
|
217
|
+
|
|
218
|
+
Nothing ->
|
|
219
|
+
Err (Pages.StaticHttpRequest.MissingHttpResponse ("distill://" ++ uniqueKey) [])
|
|
220
|
+
)
|
|
221
|
+
|> toResult
|
|
222
|
+
|
|
223
|
+
ApplicationType.Cli ->
|
|
224
|
+
lookupFn KeepOrDiscard.Discard appType rawResponses
|
|
225
|
+
|> distill uniqueKey encode decode
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
ApiRoute strippedResponses value ->
|
|
229
|
+
Request
|
|
230
|
+
(strippedResponses
|
|
231
|
+
|> Dict.insert
|
|
232
|
+
-- TODO should this include a prefix? Probably.
|
|
233
|
+
uniqueKey
|
|
234
|
+
(Pages.StaticHttpRequest.DistilledResponse (encode value))
|
|
235
|
+
)
|
|
236
|
+
( []
|
|
237
|
+
, \_ _ _ ->
|
|
238
|
+
value
|
|
239
|
+
|> encode
|
|
240
|
+
|> decode
|
|
241
|
+
|> fromResult
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
{-| [`distill`](#distill) with a `Serialize.Codec` from [`MartinSStewart/elm-serialize`](https://package.elm-lang.org/packages/MartinSStewart/elm-serialize/latest).
|
|
246
|
+
|
|
247
|
+
import DataSource
|
|
248
|
+
import DataSource.Http
|
|
249
|
+
import Secrets
|
|
250
|
+
import Serialize
|
|
251
|
+
|
|
252
|
+
millionRandomSum : DataSource Int
|
|
253
|
+
millionRandomSum =
|
|
254
|
+
DataSource.Http.get
|
|
255
|
+
(Secrets.succeed "https://example.com/api/one-million-random-numbers.json")
|
|
256
|
+
(Decode.list Decode.int)
|
|
257
|
+
|> DataSource.map List.sum
|
|
258
|
+
-- all of this expensive computation and data will happen before it hits the client!
|
|
259
|
+
-- the user's browser simply loads up a single Int and runs an Int decoder to get it
|
|
260
|
+
|> DataSource.distillSerializeCodec "million-random-sum" Serialize.int
|
|
261
|
+
|
|
262
|
+
If we didn't distill the data here, then all million Ints would have to be loaded in order to load the page.
|
|
263
|
+
The reason the data for these `DataSource`s needs to be loaded is that `elm-pages` hydrates into an Elm app. If it
|
|
264
|
+
output only HTML then we could build the HTML and throw away the data. But we need to ensure that the hydrated Elm app
|
|
265
|
+
has all the data that a page depends on, even if it the HTML for the page is also pre-rendered.
|
|
266
|
+
|
|
267
|
+
Using a `Codec` makes it safer to distill data because you know it is reversible.
|
|
268
|
+
|
|
269
|
+
-}
|
|
270
|
+
distillSerializeCodec :
|
|
271
|
+
String
|
|
272
|
+
-> Serialize.Codec error value
|
|
273
|
+
-> DataSource value
|
|
274
|
+
-> DataSource value
|
|
275
|
+
distillSerializeCodec uniqueKey serializeCodec =
|
|
276
|
+
distill uniqueKey
|
|
277
|
+
(Serialize.encodeToJson serializeCodec)
|
|
278
|
+
(Serialize.decodeFromJson serializeCodec
|
|
279
|
+
>> Result.mapError
|
|
280
|
+
(\error ->
|
|
281
|
+
case error of
|
|
282
|
+
Serialize.DataCorrupted ->
|
|
283
|
+
"DataCorrupted"
|
|
284
|
+
|
|
285
|
+
Serialize.CustomError _ ->
|
|
286
|
+
"CustomError"
|
|
287
|
+
|
|
288
|
+
Serialize.SerializerOutOfDate ->
|
|
289
|
+
"SerializerOutOfDate"
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
{-| [`distill`](#distill) with a `Codec` from [`miniBill/elm-codec`](https://package.elm-lang.org/packages/miniBill/elm-codec/latest/).
|
|
295
|
+
|
|
296
|
+
import Codec
|
|
297
|
+
import DataSource
|
|
298
|
+
import DataSource.Http
|
|
299
|
+
import Secrets
|
|
300
|
+
|
|
301
|
+
millionRandomSum : DataSource Int
|
|
302
|
+
millionRandomSum =
|
|
303
|
+
DataSource.Http.get
|
|
304
|
+
(Secrets.succeed "https://example.com/api/one-million-random-numbers.json")
|
|
305
|
+
(Decode.list Decode.int)
|
|
306
|
+
|> DataSource.map List.sum
|
|
307
|
+
-- all of this expensive computation and data will happen before it hits the client!
|
|
308
|
+
-- the user's browser simply loads up a single Int and runs an Int decoder to get it
|
|
309
|
+
|> DataSource.distillCodec "million-random-sum" Codec.int
|
|
310
|
+
|
|
311
|
+
If we didn't distill the data here, then all million Ints would have to be loaded in order to load the page.
|
|
312
|
+
The reason the data for these `DataSource`s needs to be loaded is that `elm-pages` hydrates into an Elm app. If it
|
|
313
|
+
output only HTML then we could build the HTML and throw away the data. But we need to ensure that the hydrated Elm app
|
|
314
|
+
has all the data that a page depends on, even if it the HTML for the page is also pre-rendered.
|
|
315
|
+
|
|
316
|
+
Using a `Codec` makes it safer to distill data because you know it is reversible.
|
|
317
|
+
|
|
318
|
+
-}
|
|
319
|
+
distillCodec :
|
|
320
|
+
String
|
|
321
|
+
-> Codec.Codec value
|
|
322
|
+
-> DataSource value
|
|
323
|
+
-> DataSource value
|
|
324
|
+
distillCodec uniqueKey codec =
|
|
325
|
+
distill uniqueKey
|
|
326
|
+
(Codec.encodeToValue codec)
|
|
327
|
+
(Codec.decodeValue codec >> Result.mapError Decode.errorToString)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
toResult : Result Pages.StaticHttpRequest.Error ( Dict String WhatToDo, b ) -> RawRequest b
|
|
331
|
+
toResult result =
|
|
332
|
+
case result of
|
|
333
|
+
Err error ->
|
|
334
|
+
RequestError error
|
|
335
|
+
|
|
336
|
+
Ok ( stripped, okValue ) ->
|
|
337
|
+
ApiRoute stripped okValue
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
{-| -}
|
|
341
|
+
validate :
|
|
342
|
+
(unvalidated -> validated)
|
|
343
|
+
-> (unvalidated -> DataSource (Result String ()))
|
|
344
|
+
-> DataSource unvalidated
|
|
345
|
+
-> DataSource validated
|
|
346
|
+
validate markValidated validateDataSource unvalidatedDataSource =
|
|
347
|
+
unvalidatedDataSource
|
|
348
|
+
|> andThen
|
|
349
|
+
(\unvalidated ->
|
|
350
|
+
unvalidated
|
|
351
|
+
|> validateDataSource
|
|
352
|
+
|> andThen
|
|
353
|
+
(\result ->
|
|
354
|
+
case result of
|
|
355
|
+
Ok () ->
|
|
356
|
+
succeed <| markValidated unvalidated
|
|
357
|
+
|
|
358
|
+
Err error ->
|
|
359
|
+
fail error
|
|
360
|
+
)
|
|
361
|
+
|> dontSaveData
|
|
362
|
+
)
|
|
363
|
+
|> dontSaveData
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
{-| Helper to remove an inner layer of Request wrapping.
|
|
367
|
+
-}
|
|
368
|
+
resolve : DataSource (List (DataSource value)) -> DataSource (List value)
|
|
369
|
+
resolve =
|
|
370
|
+
andThen combine
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
{-| Turn a list of `StaticHttp.Request`s into a single one.
|
|
374
|
+
|
|
375
|
+
import DataSource
|
|
376
|
+
import Json.Decode as Decode exposing (Decoder)
|
|
377
|
+
|
|
378
|
+
type alias Pokemon =
|
|
379
|
+
{ name : String
|
|
380
|
+
, sprite : String
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
pokemonDetailRequest : StaticHttp.Request (List Pokemon)
|
|
384
|
+
pokemonDetailRequest =
|
|
385
|
+
StaticHttp.get
|
|
386
|
+
(Secrets.succeed "https://pokeapi.co/api/v2/pokemon/?limit=3")
|
|
387
|
+
(Decode.field "results"
|
|
388
|
+
(Decode.list
|
|
389
|
+
(Decode.map2 Tuple.pair
|
|
390
|
+
(Decode.field "name" Decode.string)
|
|
391
|
+
(Decode.field "url" Decode.string)
|
|
392
|
+
|> Decode.map
|
|
393
|
+
(\( name, url ) ->
|
|
394
|
+
StaticHttp.get (Secrets.succeed url)
|
|
395
|
+
(Decode.at
|
|
396
|
+
[ "sprites", "front_default" ]
|
|
397
|
+
Decode.string
|
|
398
|
+
|> Decode.map (Pokemon name)
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
)
|
|
402
|
+
)
|
|
403
|
+
)
|
|
404
|
+
|> StaticHttp.andThen StaticHttp.combine
|
|
405
|
+
|
|
406
|
+
-}
|
|
407
|
+
combine : List (DataSource value) -> DataSource (List value)
|
|
408
|
+
combine =
|
|
409
|
+
List.foldr (map2 (::)) (succeed [])
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
{-| Like map, but it takes in two `Request`s.
|
|
413
|
+
|
|
414
|
+
view siteMetadata page =
|
|
415
|
+
StaticHttp.map2
|
|
416
|
+
(\elmPagesStars elmMarkdownStars ->
|
|
417
|
+
{ view =
|
|
418
|
+
\model viewForPage ->
|
|
419
|
+
{ title = "Repo Stargazers"
|
|
420
|
+
, body = starsView elmPagesStars elmMarkdownStars
|
|
421
|
+
}
|
|
422
|
+
, head = head elmPagesStars elmMarkdownStars
|
|
423
|
+
}
|
|
424
|
+
)
|
|
425
|
+
(get
|
|
426
|
+
(Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages")
|
|
427
|
+
(Decode.field "stargazers_count" Decode.int)
|
|
428
|
+
)
|
|
429
|
+
(get
|
|
430
|
+
(Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-markdown")
|
|
431
|
+
(Decode.field "stargazers_count" Decode.int)
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
-}
|
|
435
|
+
map2 : (a -> b -> c) -> DataSource a -> DataSource b -> DataSource c
|
|
436
|
+
map2 fn request1 request2 =
|
|
437
|
+
-- elm-review: known-unoptimized-recursion
|
|
438
|
+
case ( request1, request2 ) of
|
|
439
|
+
( RequestError error, _ ) ->
|
|
440
|
+
RequestError error
|
|
441
|
+
|
|
442
|
+
( _, RequestError error ) ->
|
|
443
|
+
RequestError error
|
|
444
|
+
|
|
445
|
+
( Request newDict1 ( urls1, lookupFn1 ), Request newDict2 ( urls2, lookupFn2 ) ) ->
|
|
446
|
+
Request (combineReducedDicts newDict1 newDict2)
|
|
447
|
+
( urls1 ++ urls2
|
|
448
|
+
, \keepOrDiscard appType rawResponses ->
|
|
449
|
+
map2 fn
|
|
450
|
+
(lookupFn1 keepOrDiscard appType rawResponses)
|
|
451
|
+
(lookupFn2 keepOrDiscard appType rawResponses)
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
( Request dict1 ( urls1, lookupFn1 ), ApiRoute stripped2 value2 ) ->
|
|
455
|
+
Request dict1
|
|
456
|
+
( urls1
|
|
457
|
+
, \keepOrDiscard appType rawResponses ->
|
|
458
|
+
map2 fn
|
|
459
|
+
(lookupFn1 keepOrDiscard appType rawResponses)
|
|
460
|
+
(ApiRoute stripped2 value2)
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
( ApiRoute stripped2 value2, Request dict1 ( urls1, lookupFn1 ) ) ->
|
|
464
|
+
Request dict1
|
|
465
|
+
( urls1
|
|
466
|
+
, \keepOrDiscard appType rawResponses ->
|
|
467
|
+
map2 fn
|
|
468
|
+
(ApiRoute stripped2 value2)
|
|
469
|
+
(lookupFn1 keepOrDiscard appType rawResponses)
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
( ApiRoute stripped1 value1, ApiRoute stripped2 value2 ) ->
|
|
473
|
+
ApiRoute
|
|
474
|
+
(combineReducedDicts stripped1 stripped2)
|
|
475
|
+
(fn value1 value2)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
{-| Takes two dicts representing responses, some of which have been reduced, and picks the shorter of the two.
|
|
479
|
+
This is assuming that there are no duplicate URLs, so it can safely choose between either a raw or a reduced response.
|
|
480
|
+
It would not work correctly if it chose between two responses that were reduced with different `Json.Decode.Exploration.Decoder`s.
|
|
481
|
+
-}
|
|
482
|
+
combineReducedDicts : Dict String WhatToDo -> Dict String WhatToDo -> Dict String WhatToDo
|
|
483
|
+
combineReducedDicts dict1 dict2 =
|
|
484
|
+
if Dict.size dict1 > Dict.size dict2 then
|
|
485
|
+
uniqueInsertAll dict2 dict1
|
|
486
|
+
|
|
487
|
+
else
|
|
488
|
+
uniqueInsertAll dict1 dict2
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
uniqueInsertAll : Dict String WhatToDo -> Dict String WhatToDo -> Dict String WhatToDo
|
|
492
|
+
uniqueInsertAll dictToDedupeMerge startingDict =
|
|
493
|
+
Dict.foldl
|
|
494
|
+
(\key value acc -> Dict.Extra.insertDedupe (Pages.StaticHttpRequest.merge key) key value acc)
|
|
495
|
+
startingDict
|
|
496
|
+
dictToDedupeMerge
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
lookup : KeepOrDiscard -> ApplicationType -> DataSource value -> RequestsAndPending -> Result Pages.StaticHttpRequest.Error ( Dict String WhatToDo, value )
|
|
500
|
+
lookup =
|
|
501
|
+
lookupHelp Dict.empty
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
lookupHelp : Dict String WhatToDo -> KeepOrDiscard -> ApplicationType -> DataSource value -> RequestsAndPending -> Result Pages.StaticHttpRequest.Error ( Dict String WhatToDo, value )
|
|
505
|
+
lookupHelp strippedSoFar keepOrDiscard appType requestInfo rawResponses =
|
|
506
|
+
case requestInfo of
|
|
507
|
+
RequestError error ->
|
|
508
|
+
Err error
|
|
509
|
+
|
|
510
|
+
Request strippedResponses ( urls, lookupFn ) ->
|
|
511
|
+
lookupHelp (combineReducedDicts strippedResponses strippedSoFar)
|
|
512
|
+
keepOrDiscard
|
|
513
|
+
appType
|
|
514
|
+
(addUrls urls (lookupFn keepOrDiscard appType rawResponses))
|
|
515
|
+
rawResponses
|
|
516
|
+
|
|
517
|
+
ApiRoute stripped value ->
|
|
518
|
+
Ok ( combineReducedDicts stripped strippedSoFar, value )
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
addUrls : List (Pages.Secrets.Value HashRequest.Request) -> DataSource value -> DataSource value
|
|
522
|
+
addUrls urlsToAdd requestInfo =
|
|
523
|
+
case requestInfo of
|
|
524
|
+
RequestError error ->
|
|
525
|
+
RequestError error
|
|
526
|
+
|
|
527
|
+
Request stripped ( initialUrls, function ) ->
|
|
528
|
+
Request stripped ( initialUrls ++ urlsToAdd, function )
|
|
529
|
+
|
|
530
|
+
ApiRoute stripped value ->
|
|
531
|
+
ApiRoute stripped value
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
{-| The full details to perform a StaticHttp request.
|
|
535
|
+
-}
|
|
536
|
+
type alias RequestDetails =
|
|
537
|
+
{ url : String
|
|
538
|
+
, method : String
|
|
539
|
+
, headers : List ( String, String )
|
|
540
|
+
, body : Body.Body
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
lookupUrls : DataSource value -> List (Pages.Secrets.Value RequestDetails)
|
|
545
|
+
lookupUrls requestInfo =
|
|
546
|
+
case requestInfo of
|
|
547
|
+
RequestError _ ->
|
|
548
|
+
-- TODO should this have URLs passed through?
|
|
549
|
+
[]
|
|
550
|
+
|
|
551
|
+
Request _ ( urls, _ ) ->
|
|
552
|
+
urls
|
|
553
|
+
|
|
554
|
+
ApiRoute _ _ ->
|
|
555
|
+
[]
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
{-| Build off of the response from a previous `DataSource` request to build a follow-up request. You can use the data
|
|
559
|
+
from the previous response to build up the URL, headers, etc. that you send to the subsequent request.
|
|
560
|
+
|
|
561
|
+
import DataSource
|
|
562
|
+
import Json.Decode as Decode exposing (Decoder)
|
|
563
|
+
|
|
564
|
+
licenseData : DataSource String
|
|
565
|
+
licenseData =
|
|
566
|
+
DataSource.Http.get
|
|
567
|
+
(Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages")
|
|
568
|
+
(Decode.at [ "license", "url" ] Decode.string)
|
|
569
|
+
|> DataSource.andThen
|
|
570
|
+
(\licenseUrl ->
|
|
571
|
+
DataSource.Http.get (Secrets.succeed licenseUrl) (Decode.field "description" Decode.string)
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
-}
|
|
575
|
+
andThen : (a -> DataSource b) -> DataSource a -> DataSource b
|
|
576
|
+
andThen fn requestInfo =
|
|
577
|
+
-- TODO should this be non-empty Dict? Or should it be passed down some other way?
|
|
578
|
+
Request Dict.empty
|
|
579
|
+
( lookupUrls requestInfo
|
|
580
|
+
, \keepOrDiscard appType rawResponses ->
|
|
581
|
+
lookup
|
|
582
|
+
keepOrDiscard
|
|
583
|
+
appType
|
|
584
|
+
requestInfo
|
|
585
|
+
rawResponses
|
|
586
|
+
|> (\result ->
|
|
587
|
+
case result of
|
|
588
|
+
Err error ->
|
|
589
|
+
-- TODO should I pass through strippedResponses here?
|
|
590
|
+
--( strippedResponses, fn value )
|
|
591
|
+
RequestError error
|
|
592
|
+
|
|
593
|
+
Ok ( strippedResponses, value ) ->
|
|
594
|
+
case fn value of
|
|
595
|
+
Request dict ( values, function ) ->
|
|
596
|
+
Request (combineReducedDicts strippedResponses dict) ( values, function )
|
|
597
|
+
|
|
598
|
+
RequestError error ->
|
|
599
|
+
RequestError error
|
|
600
|
+
|
|
601
|
+
ApiRoute dict finalValue ->
|
|
602
|
+
ApiRoute (combineReducedDicts strippedResponses dict) finalValue
|
|
603
|
+
)
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
{-| A helper for combining `DataSource`s in pipelines.
|
|
608
|
+
-}
|
|
609
|
+
andMap : DataSource a -> DataSource (a -> b) -> DataSource b
|
|
610
|
+
andMap =
|
|
611
|
+
map2 (|>)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
{-| This is useful for prototyping with some hardcoded data, or for having a view that doesn't have any StaticHttp data.
|
|
615
|
+
|
|
616
|
+
import DataSource
|
|
617
|
+
|
|
618
|
+
view :
|
|
619
|
+
List ( PagePath, Metadata )
|
|
620
|
+
->
|
|
621
|
+
{ path : PagePath
|
|
622
|
+
, frontmatter : Metadata
|
|
623
|
+
}
|
|
624
|
+
->
|
|
625
|
+
StaticHttp.Request
|
|
626
|
+
{ view : Model -> View -> { title : String, body : Html Msg }
|
|
627
|
+
, head : List (Head.Tag Pages.PathKey)
|
|
628
|
+
}
|
|
629
|
+
view siteMetadata page =
|
|
630
|
+
StaticHttp.succeed
|
|
631
|
+
{ view =
|
|
632
|
+
\model viewForPage ->
|
|
633
|
+
mainView model viewForPage
|
|
634
|
+
, head = head page.frontmatter
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
-}
|
|
638
|
+
succeed : a -> DataSource a
|
|
639
|
+
succeed value =
|
|
640
|
+
ApiRoute Dict.empty value
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
{-| Stop the StaticHttp chain with the given error message. If you reach a `fail` in your request,
|
|
644
|
+
you will get a build error. Or in the dev server, you will see the error message in an overlay in your browser (and in
|
|
645
|
+
the terminal).
|
|
646
|
+
-}
|
|
647
|
+
fail : String -> DataSource a
|
|
648
|
+
fail errorMessage =
|
|
649
|
+
RequestError (Pages.StaticHttpRequest.UserCalledStaticHttpFail errorMessage)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
{-| Turn an Err into a DataSource failure.
|
|
653
|
+
-}
|
|
654
|
+
fromResult : Result String value -> DataSource value
|
|
655
|
+
fromResult result =
|
|
656
|
+
case result of
|
|
657
|
+
Ok okValue ->
|
|
658
|
+
succeed okValue
|
|
659
|
+
|
|
660
|
+
Err error ->
|
|
661
|
+
fail error
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
{-| -}
|
|
665
|
+
map3 :
|
|
666
|
+
(value1 -> value2 -> value3 -> valueCombined)
|
|
667
|
+
-> DataSource value1
|
|
668
|
+
-> DataSource value2
|
|
669
|
+
-> DataSource value3
|
|
670
|
+
-> DataSource valueCombined
|
|
671
|
+
map3 combineFn request1 request2 request3 =
|
|
672
|
+
succeed combineFn
|
|
673
|
+
|> map2 (|>) request1
|
|
674
|
+
|> map2 (|>) request2
|
|
675
|
+
|> map2 (|>) request3
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
{-| -}
|
|
679
|
+
map4 :
|
|
680
|
+
(value1 -> value2 -> value3 -> value4 -> valueCombined)
|
|
681
|
+
-> DataSource value1
|
|
682
|
+
-> DataSource value2
|
|
683
|
+
-> DataSource value3
|
|
684
|
+
-> DataSource value4
|
|
685
|
+
-> DataSource valueCombined
|
|
686
|
+
map4 combineFn request1 request2 request3 request4 =
|
|
687
|
+
succeed combineFn
|
|
688
|
+
|> map2 (|>) request1
|
|
689
|
+
|> map2 (|>) request2
|
|
690
|
+
|> map2 (|>) request3
|
|
691
|
+
|> map2 (|>) request4
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
{-| -}
|
|
695
|
+
map5 :
|
|
696
|
+
(value1 -> value2 -> value3 -> value4 -> value5 -> valueCombined)
|
|
697
|
+
-> DataSource value1
|
|
698
|
+
-> DataSource value2
|
|
699
|
+
-> DataSource value3
|
|
700
|
+
-> DataSource value4
|
|
701
|
+
-> DataSource value5
|
|
702
|
+
-> DataSource valueCombined
|
|
703
|
+
map5 combineFn request1 request2 request3 request4 request5 =
|
|
704
|
+
succeed combineFn
|
|
705
|
+
|> map2 (|>) request1
|
|
706
|
+
|> map2 (|>) request2
|
|
707
|
+
|> map2 (|>) request3
|
|
708
|
+
|> map2 (|>) request4
|
|
709
|
+
|> map2 (|>) request5
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
{-| -}
|
|
713
|
+
map6 :
|
|
714
|
+
(value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> valueCombined)
|
|
715
|
+
-> DataSource value1
|
|
716
|
+
-> DataSource value2
|
|
717
|
+
-> DataSource value3
|
|
718
|
+
-> DataSource value4
|
|
719
|
+
-> DataSource value5
|
|
720
|
+
-> DataSource value6
|
|
721
|
+
-> DataSource valueCombined
|
|
722
|
+
map6 combineFn request1 request2 request3 request4 request5 request6 =
|
|
723
|
+
succeed combineFn
|
|
724
|
+
|> map2 (|>) request1
|
|
725
|
+
|> map2 (|>) request2
|
|
726
|
+
|> map2 (|>) request3
|
|
727
|
+
|> map2 (|>) request4
|
|
728
|
+
|> map2 (|>) request5
|
|
729
|
+
|> map2 (|>) request6
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
{-| -}
|
|
733
|
+
map7 :
|
|
734
|
+
(value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> value7 -> valueCombined)
|
|
735
|
+
-> DataSource value1
|
|
736
|
+
-> DataSource value2
|
|
737
|
+
-> DataSource value3
|
|
738
|
+
-> DataSource value4
|
|
739
|
+
-> DataSource value5
|
|
740
|
+
-> DataSource value6
|
|
741
|
+
-> DataSource value7
|
|
742
|
+
-> DataSource valueCombined
|
|
743
|
+
map7 combineFn request1 request2 request3 request4 request5 request6 request7 =
|
|
744
|
+
succeed combineFn
|
|
745
|
+
|> map2 (|>) request1
|
|
746
|
+
|> map2 (|>) request2
|
|
747
|
+
|> map2 (|>) request3
|
|
748
|
+
|> map2 (|>) request4
|
|
749
|
+
|> map2 (|>) request5
|
|
750
|
+
|> map2 (|>) request6
|
|
751
|
+
|> map2 (|>) request7
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
{-| -}
|
|
755
|
+
map8 :
|
|
756
|
+
(value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> value7 -> value8 -> valueCombined)
|
|
757
|
+
-> DataSource value1
|
|
758
|
+
-> DataSource value2
|
|
759
|
+
-> DataSource value3
|
|
760
|
+
-> DataSource value4
|
|
761
|
+
-> DataSource value5
|
|
762
|
+
-> DataSource value6
|
|
763
|
+
-> DataSource value7
|
|
764
|
+
-> DataSource value8
|
|
765
|
+
-> DataSource valueCombined
|
|
766
|
+
map8 combineFn request1 request2 request3 request4 request5 request6 request7 request8 =
|
|
767
|
+
succeed combineFn
|
|
768
|
+
|> map2 (|>) request1
|
|
769
|
+
|> map2 (|>) request2
|
|
770
|
+
|> map2 (|>) request3
|
|
771
|
+
|> map2 (|>) request4
|
|
772
|
+
|> map2 (|>) request5
|
|
773
|
+
|> map2 (|>) request6
|
|
774
|
+
|> map2 (|>) request7
|
|
775
|
+
|> map2 (|>) request8
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
{-| -}
|
|
779
|
+
map9 :
|
|
780
|
+
(value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> value7 -> value8 -> value9 -> valueCombined)
|
|
781
|
+
-> DataSource value1
|
|
782
|
+
-> DataSource value2
|
|
783
|
+
-> DataSource value3
|
|
784
|
+
-> DataSource value4
|
|
785
|
+
-> DataSource value5
|
|
786
|
+
-> DataSource value6
|
|
787
|
+
-> DataSource value7
|
|
788
|
+
-> DataSource value8
|
|
789
|
+
-> DataSource value9
|
|
790
|
+
-> DataSource valueCombined
|
|
791
|
+
map9 combineFn request1 request2 request3 request4 request5 request6 request7 request8 request9 =
|
|
792
|
+
succeed combineFn
|
|
793
|
+
|> map2 (|>) request1
|
|
794
|
+
|> map2 (|>) request2
|
|
795
|
+
|> map2 (|>) request3
|
|
796
|
+
|> map2 (|>) request4
|
|
797
|
+
|> map2 (|>) request5
|
|
798
|
+
|> map2 (|>) request6
|
|
799
|
+
|> map2 (|>) request7
|
|
800
|
+
|> map2 (|>) request8
|
|
801
|
+
|> map2 (|>) request9
|