elm-pages 3.0.12 → 3.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -26
- package/codegen/elm-pages-codegen.cjs +13695 -11461
- package/generator/src/build.js +15 -5
- package/generator/src/cli.js +3 -5
- package/generator/src/compatibility-key.js +2 -2
- package/generator/src/dev-server.js +3 -0
- package/generator/src/render.js +681 -50
- package/generator/src/request-cache.js +13 -6
- package/generator/src/spinnies/index.js +200 -0
- package/generator/src/spinnies/utils.js +123 -0
- package/generator/src/validate-stream.js +25 -0
- package/package.json +4 -2
- package/src/BackendTask/Custom.elm +38 -0
- package/src/BackendTask/Do.elm +233 -0
- package/src/BackendTask/File.elm +24 -9
- package/src/BackendTask/Glob.elm +208 -25
- package/src/BackendTask/Http.elm +32 -21
- package/src/BackendTask/Internal/Glob.elm +16 -4
- package/src/BackendTask/Stream.elm +1179 -0
- package/src/BackendTask.elm +214 -7
- package/src/Pages/Internal/Platform/CompatibilityKey.elm +1 -1
- package/src/Pages/Internal/Platform.elm +11 -2
- package/src/Pages/Script/Spinner.elm +505 -0
- package/src/Pages/Script.elm +199 -2
- package/src/Pages/StaticHttp/Request.elm +7 -0
- package/src/RequestsAndPending.elm +1 -1
- package/src/Scaffold/Form.elm +2 -3
- package/src/TerminalText.elm +8 -0
- package/src/Vendored/Result/Extra.elm +75 -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/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 +0 -1
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/Reporter.elm.js +0 -7900
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/Runner.elm.js +0 -28657
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/node_runner.js +0 -110
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/node_supervisor.js +0 -187
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/package.json +0 -1
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/src/Reporter.elm +0 -26
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/src/Runner.elm +0 -62
- 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 +0 -1
- package/generator/review/elm-stuff/tests-0.19.1/js/Reporter.elm.js +0 -7900
- package/generator/review/elm-stuff/tests-0.19.1/js/Runner.elm.js +0 -30511
- package/generator/review/elm-stuff/tests-0.19.1/js/node_runner.js +0 -110
- package/generator/review/elm-stuff/tests-0.19.1/js/node_supervisor.js +0 -187
- package/generator/review/elm-stuff/tests-0.19.1/js/package.json +0 -1
- package/generator/review/elm-stuff/tests-0.19.1/src/Reporter.elm +0 -26
- package/generator/review/elm-stuff/tests-0.19.1/src/Runner.elm +0 -62
- package/src/Result/Extra.elm +0 -26
|
@@ -0,0 +1,1179 @@
|
|
|
1
|
+
module BackendTask.Stream exposing
|
|
2
|
+
( Stream
|
|
3
|
+
, pipe
|
|
4
|
+
, fileRead, fileWrite, fromString, http, httpWithInput, stdin, stdout, stderr
|
|
5
|
+
, read, readJson, readMetadata, run
|
|
6
|
+
, Error(..)
|
|
7
|
+
, command
|
|
8
|
+
, commandWithOptions
|
|
9
|
+
, StderrOutput(..)
|
|
10
|
+
, CommandOptions, defaultCommandOptions, allowNon0Status, withOutput, withTimeout
|
|
11
|
+
, gzip, unzip
|
|
12
|
+
, customRead, customWrite, customDuplex
|
|
13
|
+
, customReadWithMeta, customTransformWithMeta, customWriteWithMeta
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
{-| A `Stream` represents a flow of data through a pipeline.
|
|
17
|
+
|
|
18
|
+
It is typically
|
|
19
|
+
|
|
20
|
+
- An input source, or Readable Stream (`Stream { read : (), write : Never }`)
|
|
21
|
+
- An output destination, or Writable Stream (`Stream { read : Never, write : () }`)
|
|
22
|
+
- And (optionally) a series of transformations in between, or Duplex Streams (`Stream { read : (), write : () }`)
|
|
23
|
+
|
|
24
|
+
For example, you could have a stream that
|
|
25
|
+
|
|
26
|
+
- Reads from a file [`fileRead`](#fileRead)
|
|
27
|
+
- Unzips the contents [`unzip`](#unzip)
|
|
28
|
+
- Runs a shell command on the contents [`command`](#command)
|
|
29
|
+
- And writes the result to a network connection [`httpWithInput`](#httpWithInput)
|
|
30
|
+
|
|
31
|
+
For example,
|
|
32
|
+
|
|
33
|
+
import BackendTask.Stream as Stream exposing (Stream)
|
|
34
|
+
|
|
35
|
+
example =
|
|
36
|
+
Stream.fileRead "data.txt"
|
|
37
|
+
|> Stream.unzip
|
|
38
|
+
|> Stream.command "wc" [ "-l" ]
|
|
39
|
+
|> Stream.httpWithInput
|
|
40
|
+
{ url = "http://example.com"
|
|
41
|
+
, method = "POST"
|
|
42
|
+
, headers = []
|
|
43
|
+
, retries = Nothing
|
|
44
|
+
, timeoutInMs = Nothing
|
|
45
|
+
}
|
|
46
|
+
|> Stream.run
|
|
47
|
+
|
|
48
|
+
End example
|
|
49
|
+
|
|
50
|
+
@docs Stream
|
|
51
|
+
|
|
52
|
+
@docs pipe
|
|
53
|
+
|
|
54
|
+
@docs fileRead, fileWrite, fromString, http, httpWithInput, stdin, stdout, stderr
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
## Running Streams
|
|
58
|
+
|
|
59
|
+
@docs read, readJson, readMetadata, run
|
|
60
|
+
|
|
61
|
+
@docs Error
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
## Shell Commands
|
|
65
|
+
|
|
66
|
+
Note that the commands do not execute through a shell but rather directly executes a child process. That means that
|
|
67
|
+
special shell syntax will have no effect, but instead will be interpreted as literal characters in arguments to the command.
|
|
68
|
+
|
|
69
|
+
So instead of `grep error < log.txt`, you would use
|
|
70
|
+
|
|
71
|
+
module GrepErrors exposing (run)
|
|
72
|
+
|
|
73
|
+
import BackendTask
|
|
74
|
+
import BackendTask.Stream as Stream
|
|
75
|
+
import Pages.Script as Script exposing (Script)
|
|
76
|
+
|
|
77
|
+
run : Script
|
|
78
|
+
run =
|
|
79
|
+
Script.withoutCliOptions
|
|
80
|
+
(Stream.fileRead "log.txt"
|
|
81
|
+
|> Stream.pipe (Stream.command "grep" [ "error" ])
|
|
82
|
+
|> Stream.stdout
|
|
83
|
+
|> Stream.run
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
@docs command
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
## Command Options
|
|
90
|
+
|
|
91
|
+
@docs commandWithOptions
|
|
92
|
+
|
|
93
|
+
@docs StderrOutput
|
|
94
|
+
|
|
95
|
+
@docs CommandOptions, defaultCommandOptions, allowNon0Status, withOutput, withTimeout
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
## Command Output Strategies
|
|
99
|
+
|
|
100
|
+
There are 3 things that effect the output behavior of a command:
|
|
101
|
+
|
|
102
|
+
- The verbosity of the `BackendTask` context ([`BackendTask.quiet`](BackendTask#quiet))
|
|
103
|
+
- Whether the `Stream` output is ignored ([`Stream.run`](#run)), or read ([`Stream.read`](#read))
|
|
104
|
+
- [`withOutput`](#withOutput) (allows you to use stdout, stderr, or both)
|
|
105
|
+
|
|
106
|
+
With `BackendTask.quiet`, the output of the command will not print as it runs, but you still read it in Elm if you read the `Stream` (instead of using [`Stream.run`](#run)).
|
|
107
|
+
|
|
108
|
+
There are 3 ways to handle the output of a command:
|
|
109
|
+
|
|
110
|
+
1. Read the output but don't print
|
|
111
|
+
2. Print the output but don't read
|
|
112
|
+
3. Ignore the output
|
|
113
|
+
|
|
114
|
+
To read the output (1), use [`Stream.read`](#read) or [`Stream.readJson`](#readJson). This will give you the output as a String or JSON object.
|
|
115
|
+
Regardless of whether you use `BackendTask.quiet`, the output will be read and returned to Elm.
|
|
116
|
+
|
|
117
|
+
To let the output from the command natively print to the console (2), use [`Stream.run`](#run) without setting `BackendTask.quiet`. Based on
|
|
118
|
+
the command's `withOutput` configuration, either stderr, stdout, or both will print to the console. The native output will
|
|
119
|
+
sometimes be treated more like running the command directly in the terminal, for example `elm make` will print progress
|
|
120
|
+
messages which will be cleared and updated in place.
|
|
121
|
+
|
|
122
|
+
To ignore the output (3), use [`Stream.run`](#run) with `BackendTask.quiet`. This will run the command without printing anything to the console.
|
|
123
|
+
You can also use [`Stream.read`](#read) and ignore the captured output, but this is less efficient than using `BackendTask.quiet` with `Stream.run`.
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
## Compression Helpers
|
|
127
|
+
|
|
128
|
+
module CompressionDemo exposing (run)
|
|
129
|
+
|
|
130
|
+
import BackendTask
|
|
131
|
+
import BackendTask.Stream as Stream
|
|
132
|
+
import Pages.Script as Script exposing (Script)
|
|
133
|
+
|
|
134
|
+
run : Script
|
|
135
|
+
run =
|
|
136
|
+
Script.withoutCliOptions
|
|
137
|
+
(Stream.fileRead "elm.json"
|
|
138
|
+
|> Stream.pipe Stream.gzip
|
|
139
|
+
|> Stream.pipe (Stream.fileWrite "elm.json.gz")
|
|
140
|
+
|> Stream.run
|
|
141
|
+
|> BackendTask.andThen
|
|
142
|
+
(\_ ->
|
|
143
|
+
Stream.fileRead "elm.json.gz"
|
|
144
|
+
|> Stream.pipe Stream.unzip
|
|
145
|
+
|> Stream.pipe Stream.stdout
|
|
146
|
+
|> Stream.run
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
@docs gzip, unzip
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
## Custom Streams
|
|
154
|
+
|
|
155
|
+
[`BackendTask.Custom`](BackendTask-Custom) lets you define custom `BackendTask`s from async NodeJS functions in your `custom-backend-task` file.
|
|
156
|
+
Similarly, you can define custom streams with async functions in your `custom-backend-task` file, returning native NodeJS Streams, and optionally functions to extract metadata.
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
import { Writable, Transform, Readable } from "node:stream";
|
|
160
|
+
|
|
161
|
+
export async function upperCaseStream(input, { cwd, env, quiet }) {
|
|
162
|
+
return {
|
|
163
|
+
metadata: () => "Hi! I'm metadata from upperCaseStream!",
|
|
164
|
+
stream: new Transform({
|
|
165
|
+
transform(chunk, encoding, callback) {
|
|
166
|
+
callback(null, chunk.toString().toUpperCase());
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function customReadStream(input) {
|
|
173
|
+
return new Readable({
|
|
174
|
+
read(size) {
|
|
175
|
+
this.push("Hello from customReadStream!");
|
|
176
|
+
this.push(null);
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function customWriteStream(input, { cwd, env, quiet }) {
|
|
182
|
+
return {
|
|
183
|
+
stream: new Writable({
|
|
184
|
+
write(chunk, encoding, callback) {
|
|
185
|
+
console.error("...received chunk...");
|
|
186
|
+
console.log(chunk.toString());
|
|
187
|
+
callback();
|
|
188
|
+
},
|
|
189
|
+
}),
|
|
190
|
+
metadata: () => {
|
|
191
|
+
return "Hi! I'm metadata from customWriteStream!";
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
module CustomStreamDemo exposing (run)
|
|
198
|
+
|
|
199
|
+
import BackendTask
|
|
200
|
+
import BackendTask.Stream as Stream
|
|
201
|
+
import Pages.Script as Script exposing (Script)
|
|
202
|
+
|
|
203
|
+
run : Script
|
|
204
|
+
run =
|
|
205
|
+
Script.withoutCliOptions
|
|
206
|
+
(Stream.customRead "customReadStream" Encode.null
|
|
207
|
+
|> Stream.pipe (Stream.customDuplex "upperCaseStream" Encode.null)
|
|
208
|
+
|> Stream.pipe (Stream.customWrite "customWriteStream" Encode.null)
|
|
209
|
+
|> Stream.run
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
To extract the metadata from the custom stream, you can use the `...WithMeta` functions:
|
|
213
|
+
|
|
214
|
+
module CustomStreamDemoWithMeta exposing (run)
|
|
215
|
+
|
|
216
|
+
import BackendTask
|
|
217
|
+
import BackendTask.Stream as Stream
|
|
218
|
+
import Pages.Script as Script exposing (Script)
|
|
219
|
+
|
|
220
|
+
run : Script
|
|
221
|
+
run =
|
|
222
|
+
Script.withoutCliOptions
|
|
223
|
+
(Stream.customReadWithMeta "customReadStream" Encode.null Decode.succeed
|
|
224
|
+
|> Stream.pipe (Stream.customTransformWithMeta "upperCaseStream" Encode.null Decode.succeed)
|
|
225
|
+
|> Stream.readMetadata
|
|
226
|
+
|> BackendTask.allowFatal
|
|
227
|
+
|> BackendTask.andThen
|
|
228
|
+
(\metadata ->
|
|
229
|
+
Script.log ("Metadata: " ++ metadata)
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
--> Script.log "Metadata: Hi! I'm metadata from upperCaseStream!"
|
|
233
|
+
|
|
234
|
+
@docs customRead, customWrite, customDuplex
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
### With Metadata Decoders
|
|
238
|
+
|
|
239
|
+
@docs customReadWithMeta, customTransformWithMeta, customWriteWithMeta
|
|
240
|
+
|
|
241
|
+
-}
|
|
242
|
+
|
|
243
|
+
import BackendTask exposing (BackendTask)
|
|
244
|
+
import BackendTask.Http exposing (Body)
|
|
245
|
+
import BackendTask.Internal.Request
|
|
246
|
+
import Base64
|
|
247
|
+
import FatalError exposing (FatalError)
|
|
248
|
+
import Json.Decode as Decode exposing (Decoder)
|
|
249
|
+
import Json.Encode as Encode
|
|
250
|
+
import Pages.Internal.StaticHttpBody
|
|
251
|
+
import RequestsAndPending
|
|
252
|
+
import TerminalText
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
{-| Once you've defined a `Stream`, it can be turned into a `BackendTask` that will run it (and optionally read its output and metadata).
|
|
256
|
+
-}
|
|
257
|
+
type Stream error metadata kind
|
|
258
|
+
= Stream ( String, Decoder (Result (Recoverable error) metadata) ) (List StreamPart)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
type alias Recoverable error =
|
|
262
|
+
{ fatal : FatalError, recoverable : error }
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
mapRecoverable : Maybe body -> { a | fatal : b, recoverable : c } -> { fatal : b, recoverable : Error c body }
|
|
266
|
+
mapRecoverable maybeBody { fatal, recoverable } =
|
|
267
|
+
{ fatal = fatal
|
|
268
|
+
, recoverable = CustomError recoverable maybeBody
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
type StreamPart
|
|
273
|
+
= StreamPart String (List ( String, Encode.Value ))
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
single : ( String, Decoder (Result (Recoverable error) metadata) ) -> String -> List ( String, Encode.Value ) -> Stream error metadata kind
|
|
277
|
+
single decoder inner1 inner2 =
|
|
278
|
+
Stream decoder [ StreamPart inner1 inner2 ]
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
unit : ( String, Decoder (Result (Recoverable ()) ()) )
|
|
282
|
+
unit =
|
|
283
|
+
( "unit", Decode.succeed (Ok ()) )
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
{-| The `stdin` from the process. When you execute an `elm-pages` script, this will be the value that is piped in to it. For example, given this script module:
|
|
287
|
+
|
|
288
|
+
module CountLines exposing (run)
|
|
289
|
+
|
|
290
|
+
import BackendTask
|
|
291
|
+
import BackendTask.Stream as Stream
|
|
292
|
+
import Pages.Script as Script exposing (Script)
|
|
293
|
+
|
|
294
|
+
run : Script
|
|
295
|
+
run =
|
|
296
|
+
Script.withoutCliOptions
|
|
297
|
+
(Stream.stdin
|
|
298
|
+
|> Stream.read
|
|
299
|
+
|> BackendTask.allowFatal
|
|
300
|
+
|> BackendTask.andThen
|
|
301
|
+
(\{ body } ->
|
|
302
|
+
body
|
|
303
|
+
|> String.lines
|
|
304
|
+
|> List.length
|
|
305
|
+
|> String.fromInt
|
|
306
|
+
|> Script.log
|
|
307
|
+
)
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
If you run the script without any stdin, it will wait until stdin is closed.
|
|
311
|
+
|
|
312
|
+
```shell
|
|
313
|
+
elm-pages run script/src/CountLines.elm
|
|
314
|
+
# pressing ctrl-d (or your platform-specific way of closing stdin) will print the number of lines in the input
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Or you can pipe to it and it will read that input:
|
|
318
|
+
|
|
319
|
+
```shell
|
|
320
|
+
ls | elm-pages run script/src/CountLines.elm
|
|
321
|
+
# prints the number of files in the current directory
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
-}
|
|
325
|
+
stdin : Stream () () { read : (), write : Never }
|
|
326
|
+
stdin =
|
|
327
|
+
single unit "stdin" []
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
{-| Streaming through to stdout can be a convenient way to print a pipeline directly without going through to Elm.
|
|
331
|
+
|
|
332
|
+
module UnzipFile exposing (run)
|
|
333
|
+
|
|
334
|
+
import BackendTask
|
|
335
|
+
import BackendTask.Stream as Stream
|
|
336
|
+
import Pages.Script as Script exposing (Script)
|
|
337
|
+
|
|
338
|
+
run : Script
|
|
339
|
+
run =
|
|
340
|
+
Script.withoutCliOptions
|
|
341
|
+
(Stream.fileRead "data.gzip.txt"
|
|
342
|
+
|> Stream.pipe Stream.unzip
|
|
343
|
+
|> Stream.pipe Stream.stdout
|
|
344
|
+
|> Stream.run
|
|
345
|
+
|> BackendTask.allowFatal
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
-}
|
|
349
|
+
stdout : Stream () () { read : Never, write : () }
|
|
350
|
+
stdout =
|
|
351
|
+
single unit "stdout" []
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
{-| Similar to [`stdout`](#stdout), but writes to `stderr` instead.
|
|
355
|
+
-}
|
|
356
|
+
stderr : Stream () () { read : Never, write : () }
|
|
357
|
+
stderr =
|
|
358
|
+
single unit "stderr" []
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
{-| Open a file's contents as a Stream.
|
|
362
|
+
|
|
363
|
+
module ReadFile exposing (run)
|
|
364
|
+
|
|
365
|
+
import BackendTask
|
|
366
|
+
import BackendTask.Stream as Stream
|
|
367
|
+
import Pages.Script as Script exposing (Script)
|
|
368
|
+
|
|
369
|
+
run : Script
|
|
370
|
+
run =
|
|
371
|
+
Script.withoutCliOptions
|
|
372
|
+
(Stream.fileRead "elm.json"
|
|
373
|
+
|> Stream.readJson (Decode.field "source-directories" (Decode.list Decode.string))
|
|
374
|
+
|> BackendTask.allowFatal
|
|
375
|
+
|> BackendTask.andThen
|
|
376
|
+
(\{ body } ->
|
|
377
|
+
Script.log
|
|
378
|
+
("The source directories are: "
|
|
379
|
+
++ String.join ", " body
|
|
380
|
+
)
|
|
381
|
+
)
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
If you want to read a file but don't need to use any of the other Stream functions, you can use [`BackendTask.File.read`](BackendTask-File#rawFile) instead.
|
|
385
|
+
|
|
386
|
+
-}
|
|
387
|
+
fileRead : String -> Stream () () { read : (), write : Never }
|
|
388
|
+
fileRead path =
|
|
389
|
+
-- TODO revisit the error type instead of ()?
|
|
390
|
+
single unit "fileRead" [ ( "path", Encode.string path ) ]
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
{-| Write a Stream to a file.
|
|
394
|
+
|
|
395
|
+
module WriteFile exposing (run)
|
|
396
|
+
|
|
397
|
+
import BackendTask
|
|
398
|
+
import BackendTask.Stream as Stream
|
|
399
|
+
import Pages.Script as Script exposing (Script)
|
|
400
|
+
|
|
401
|
+
run : Script
|
|
402
|
+
run =
|
|
403
|
+
Script.withoutCliOptions
|
|
404
|
+
(Stream.fileRead "logs.txt"
|
|
405
|
+
|> Stream.pipe (Stream.command "grep" [ "error" ])
|
|
406
|
+
|> Stream.pipe (Stream.fileWrite "errors.txt")
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
-}
|
|
410
|
+
fileWrite : String -> Stream () () { read : Never, write : () }
|
|
411
|
+
fileWrite path =
|
|
412
|
+
single unit "fileWrite" [ ( "path", Encode.string path ) ]
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
{-| Calls an async function from your `custom-backend-task` definitions and uses the NodeJS `ReadableStream` it returns.
|
|
416
|
+
-}
|
|
417
|
+
customRead : String -> Encode.Value -> Stream () () { read : (), write : Never }
|
|
418
|
+
customRead name input =
|
|
419
|
+
single unit
|
|
420
|
+
"customRead"
|
|
421
|
+
[ ( "portName", Encode.string name )
|
|
422
|
+
, ( "input", input )
|
|
423
|
+
]
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
{-| Calls an async function from your `custom-backend-task` definitions and uses the NodeJS `WritableStream` it returns.
|
|
427
|
+
-}
|
|
428
|
+
customWrite : String -> Encode.Value -> Stream () () { read : Never, write : () }
|
|
429
|
+
customWrite name input =
|
|
430
|
+
single unit
|
|
431
|
+
"customWrite"
|
|
432
|
+
[ ( "portName", Encode.string name )
|
|
433
|
+
, ( "input", input )
|
|
434
|
+
]
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
{-| Calls an async function from your `custom-backend-task` definitions and uses the NodeJS `DuplexStream` it returns.
|
|
438
|
+
-}
|
|
439
|
+
customReadWithMeta :
|
|
440
|
+
String
|
|
441
|
+
-> Encode.Value
|
|
442
|
+
-> Decoder (Result { fatal : FatalError, recoverable : error } metadata)
|
|
443
|
+
-> Stream error metadata { read : (), write : Never }
|
|
444
|
+
customReadWithMeta name input decoder =
|
|
445
|
+
single ( "", decoder )
|
|
446
|
+
"customRead"
|
|
447
|
+
[ ( "portName", Encode.string name )
|
|
448
|
+
, ( "input", input )
|
|
449
|
+
]
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
{-| Calls an async function from your `custom-backend-task` definitions and uses the NodeJS `WritableStream` and metadata function it returns.
|
|
453
|
+
-}
|
|
454
|
+
customWriteWithMeta :
|
|
455
|
+
String
|
|
456
|
+
-> Encode.Value
|
|
457
|
+
-> Decoder (Result { fatal : FatalError, recoverable : error } metadata)
|
|
458
|
+
-> Stream error metadata { read : Never, write : () }
|
|
459
|
+
customWriteWithMeta name input decoder =
|
|
460
|
+
single ( "", decoder )
|
|
461
|
+
"customWrite"
|
|
462
|
+
[ ( "portName", Encode.string name )
|
|
463
|
+
, ( "input", input )
|
|
464
|
+
]
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
{-| Calls an async function from your `custom-backend-task` definitions and uses the NodeJS `DuplexStream` and metadata function it returns.
|
|
468
|
+
-}
|
|
469
|
+
customTransformWithMeta :
|
|
470
|
+
String
|
|
471
|
+
-> Encode.Value
|
|
472
|
+
-> Decoder (Result { fatal : FatalError, recoverable : error } metadata)
|
|
473
|
+
-> Stream error metadata { read : (), write : () }
|
|
474
|
+
customTransformWithMeta name input decoder =
|
|
475
|
+
single ( "", decoder )
|
|
476
|
+
"customDuplex"
|
|
477
|
+
[ ( "portName", Encode.string name )
|
|
478
|
+
, ( "input", input )
|
|
479
|
+
]
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
{-| Calls an async function from your `custom-backend-task` definitions and uses the NodeJS `DuplexStream` it returns.
|
|
483
|
+
-}
|
|
484
|
+
customDuplex : String -> Encode.Value -> Stream () () { read : (), write : () }
|
|
485
|
+
customDuplex name input =
|
|
486
|
+
single unit
|
|
487
|
+
"customDuplex"
|
|
488
|
+
[ ( "portName", Encode.string name )
|
|
489
|
+
, ( "input", input )
|
|
490
|
+
]
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
{-| Transforms the input with gzip compression.
|
|
494
|
+
|
|
495
|
+
Under the hood this builds a Stream using Node's [`zlib.createGzip`](https://nodejs.org/api/zlib.html#zlibcreategzipoptions).
|
|
496
|
+
|
|
497
|
+
-}
|
|
498
|
+
gzip : Stream () () { read : (), write : () }
|
|
499
|
+
gzip =
|
|
500
|
+
single unit "gzip" []
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
{-| Transforms the input by auto-detecting the header and decompressing either a Gzip- or Deflate-compressed stream.
|
|
504
|
+
|
|
505
|
+
Under the hood, this builds a Stream using Node's [`zlib.createUnzip`](https://nodejs.org/api/zlib.html#zlibcreateunzip).
|
|
506
|
+
|
|
507
|
+
-}
|
|
508
|
+
unzip : Stream () () { read : (), write : () }
|
|
509
|
+
unzip =
|
|
510
|
+
single unit "unzip" []
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
{-| Streams the data from the input stream as the body of the HTTP request. The HTTP response body becomes the output stream.
|
|
514
|
+
-}
|
|
515
|
+
httpWithInput :
|
|
516
|
+
{ url : String
|
|
517
|
+
, method : String
|
|
518
|
+
, headers : List ( String, String )
|
|
519
|
+
, retries : Maybe Int
|
|
520
|
+
, timeoutInMs : Maybe Int
|
|
521
|
+
}
|
|
522
|
+
-> Stream BackendTask.Http.Error BackendTask.Http.Metadata { read : (), write : () }
|
|
523
|
+
httpWithInput string =
|
|
524
|
+
-- Pages.Internal.StaticHttpBody
|
|
525
|
+
single httpMetadataDecoder
|
|
526
|
+
"httpWrite"
|
|
527
|
+
[ ( "url", Encode.string string.url )
|
|
528
|
+
, ( "method", Encode.string string.method )
|
|
529
|
+
, ( "headers", Encode.list (\( key, value ) -> Encode.object [ ( "key", Encode.string key ), ( "value", Encode.string value ) ]) string.headers )
|
|
530
|
+
, ( "retries", nullable Encode.int string.retries )
|
|
531
|
+
, ( "timeoutInMs", nullable Encode.int string.timeoutInMs )
|
|
532
|
+
]
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
{-| Uses a regular HTTP request body (not a `Stream`). Streams the HTTP response body.
|
|
536
|
+
|
|
537
|
+
If you want to pass a stream as the request body, use [`httpWithInput`](#httpWithInput) instead.
|
|
538
|
+
|
|
539
|
+
If you don't need to stream the response body, you can use the functions from [`BackendTask.Http`](BackendTask-Http) instead.
|
|
540
|
+
|
|
541
|
+
-}
|
|
542
|
+
http :
|
|
543
|
+
{ url : String
|
|
544
|
+
, method : String
|
|
545
|
+
, headers : List ( String, String )
|
|
546
|
+
, body : Body
|
|
547
|
+
, retries : Maybe Int
|
|
548
|
+
, timeoutInMs : Maybe Int
|
|
549
|
+
}
|
|
550
|
+
-> Stream BackendTask.Http.Error BackendTask.Http.Metadata { read : (), write : Never }
|
|
551
|
+
http request_ =
|
|
552
|
+
single httpMetadataDecoder
|
|
553
|
+
"httpWrite"
|
|
554
|
+
[ ( "url", Encode.string request_.url )
|
|
555
|
+
, ( "method", Encode.string request_.method )
|
|
556
|
+
, ( "headers", Encode.list (\( key, value ) -> Encode.object [ ( "key", Encode.string key ), ( "value", Encode.string value ) ]) request_.headers )
|
|
557
|
+
, ( "body", Pages.Internal.StaticHttpBody.encode request_.body )
|
|
558
|
+
, ( "retries", nullable Encode.int request_.retries )
|
|
559
|
+
, ( "timeoutInMs", nullable Encode.int request_.timeoutInMs )
|
|
560
|
+
]
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
httpMetadataDecoder : ( String, Decoder (Result (Recoverable BackendTask.Http.Error) BackendTask.Http.Metadata) )
|
|
564
|
+
httpMetadataDecoder =
|
|
565
|
+
( "http"
|
|
566
|
+
, RequestsAndPending.responseDecoder
|
|
567
|
+
|> Decode.map
|
|
568
|
+
(\thing ->
|
|
569
|
+
toBadResponse (Just thing) RequestsAndPending.WhateverBody
|
|
570
|
+
|> Maybe.map
|
|
571
|
+
(\httpError ->
|
|
572
|
+
FatalError.recoverable
|
|
573
|
+
(errorToString httpError)
|
|
574
|
+
httpError
|
|
575
|
+
|> Err
|
|
576
|
+
)
|
|
577
|
+
|> Maybe.withDefault (Ok thing)
|
|
578
|
+
)
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
{-| You can build up a pipeline of streams by using the `pipe` function.
|
|
583
|
+
|
|
584
|
+
The stream you are piping to must be writable (`{ write : () }`),
|
|
585
|
+
and the stream you are piping from must be readable (`{ read : () }`).
|
|
586
|
+
|
|
587
|
+
module HelloWorld exposing (run)
|
|
588
|
+
|
|
589
|
+
import BackendTask
|
|
590
|
+
import BackendTask.Stream as Stream
|
|
591
|
+
import Pages.Script as Script exposing (Script)
|
|
592
|
+
|
|
593
|
+
run : Script
|
|
594
|
+
run =
|
|
595
|
+
Script.withoutCliOptions
|
|
596
|
+
(Stream.fromString "Hello, World!"
|
|
597
|
+
|> Stream.stdout
|
|
598
|
+
|> Stream.run
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
-}
|
|
602
|
+
pipe :
|
|
603
|
+
Stream errorTo metaTo { read : toReadable, write : () }
|
|
604
|
+
-> Stream errorFrom metaFrom { read : (), write : fromWriteable }
|
|
605
|
+
-> Stream errorTo metaTo { read : toReadable, write : fromWriteable }
|
|
606
|
+
pipe (Stream decoderTo to) (Stream _ from) =
|
|
607
|
+
Stream decoderTo (from ++ to)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
{-| Gives a `BackendTask` to execute the `Stream`, ignoring its body and metadata.
|
|
611
|
+
|
|
612
|
+
This is useful if you only want the side-effect from the `Stream` and don't need to programmatically use its
|
|
613
|
+
output. For example, if the end result you want is:
|
|
614
|
+
|
|
615
|
+
- Printing to the console
|
|
616
|
+
- Writing to a file
|
|
617
|
+
- Making an HTTP request
|
|
618
|
+
|
|
619
|
+
If you need to read the output of the `Stream`, use [`read`](#read), [`readJson`](#readJson), or [`readMetadata`](#readMetadata) instead.
|
|
620
|
+
|
|
621
|
+
-}
|
|
622
|
+
run : Stream error metadata kind -> BackendTask FatalError ()
|
|
623
|
+
run stream =
|
|
624
|
+
-- TODO give access to recoverable error here instead of just FatalError
|
|
625
|
+
BackendTask.Internal.Request.request
|
|
626
|
+
{ name = "stream"
|
|
627
|
+
, body = BackendTask.Http.jsonBody (pipelineEncoder stream "none")
|
|
628
|
+
, expect =
|
|
629
|
+
BackendTask.Http.expectJson
|
|
630
|
+
(Decode.oneOf
|
|
631
|
+
[ Decode.field "error" Decode.string
|
|
632
|
+
|> Decode.andThen
|
|
633
|
+
(\error ->
|
|
634
|
+
Decode.succeed
|
|
635
|
+
(Err
|
|
636
|
+
(FatalError.recoverable
|
|
637
|
+
{ title = "Stream Error"
|
|
638
|
+
, body = error
|
|
639
|
+
}
|
|
640
|
+
(StreamError error)
|
|
641
|
+
)
|
|
642
|
+
)
|
|
643
|
+
)
|
|
644
|
+
, Decode.succeed (Ok ())
|
|
645
|
+
]
|
|
646
|
+
)
|
|
647
|
+
}
|
|
648
|
+
|> BackendTask.andThen BackendTask.fromResult
|
|
649
|
+
|> BackendTask.allowFatal
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
pipelineEncoder : Stream error metadata kind -> String -> Encode.Value
|
|
653
|
+
pipelineEncoder (Stream _ parts) kind =
|
|
654
|
+
Encode.object
|
|
655
|
+
[ ( "kind", Encode.string kind )
|
|
656
|
+
, ( "parts"
|
|
657
|
+
, Encode.list
|
|
658
|
+
(\(StreamPart name data) ->
|
|
659
|
+
Encode.object (( "name", Encode.string name ) :: data)
|
|
660
|
+
)
|
|
661
|
+
parts
|
|
662
|
+
)
|
|
663
|
+
]
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
{-| A handy way to turn either a hardcoded String, or any other value from Elm into a Stream.
|
|
667
|
+
|
|
668
|
+
module HelloWorld exposing (run)
|
|
669
|
+
|
|
670
|
+
import BackendTask
|
|
671
|
+
import BackendTask.Stream as Stream
|
|
672
|
+
import Pages.Script as Script exposing (Script)
|
|
673
|
+
|
|
674
|
+
run : Script
|
|
675
|
+
run =
|
|
676
|
+
Script.withoutCliOptions
|
|
677
|
+
(Stream.fromString "Hello, World!"
|
|
678
|
+
|> Stream.stdout
|
|
679
|
+
|> Stream.run
|
|
680
|
+
|> BackendTask.allowFatal
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
A more programmatic use of `fromString` to use the result of a previous `BackendTask` to a `Stream`:
|
|
684
|
+
|
|
685
|
+
module HelloWorld exposing (run)
|
|
686
|
+
|
|
687
|
+
import BackendTask
|
|
688
|
+
import BackendTask.Stream as Stream
|
|
689
|
+
import Pages.Script as Script exposing (Script)
|
|
690
|
+
|
|
691
|
+
run : Script
|
|
692
|
+
run =
|
|
693
|
+
Script.withoutCliOptions
|
|
694
|
+
(Glob.fromString "src/**/*.elm"
|
|
695
|
+
|> BackendTask.andThen
|
|
696
|
+
(\elmFiles ->
|
|
697
|
+
elmFiles
|
|
698
|
+
|> String.join ", "
|
|
699
|
+
|> Stream.fromString
|
|
700
|
+
|> Stream.pipe Stream.stdout
|
|
701
|
+
|> Stream.run
|
|
702
|
+
)
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
-}
|
|
706
|
+
fromString : String -> Stream () () { read : (), write : Never }
|
|
707
|
+
fromString string =
|
|
708
|
+
single unit "fromString" [ ( "string", Encode.string string ) ]
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
{-| Running or reading a `Stream` can give one of two kinds of error:
|
|
712
|
+
|
|
713
|
+
- `StreamError String` - when something in the middle of the stream fails
|
|
714
|
+
- `CustomError error body` - when the `Stream` fails with a custom error
|
|
715
|
+
|
|
716
|
+
A `CustomError` can only come from the final part of the stream.
|
|
717
|
+
|
|
718
|
+
You can define your own custom errors by decoding metadata to an `Err` in the `...WithMeta` helpers.
|
|
719
|
+
|
|
720
|
+
-}
|
|
721
|
+
type Error error body
|
|
722
|
+
= StreamError String
|
|
723
|
+
| CustomError error (Maybe body)
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
{-| Read the body of the `Stream` as text.
|
|
727
|
+
-}
|
|
728
|
+
read :
|
|
729
|
+
Stream error metadata { read : (), write : write }
|
|
730
|
+
-> BackendTask { fatal : FatalError, recoverable : Error error String } { metadata : metadata, body : String }
|
|
731
|
+
read ((Stream ( _, decoder ) _) as stream) =
|
|
732
|
+
BackendTask.Internal.Request.request
|
|
733
|
+
{ name = "stream"
|
|
734
|
+
|
|
735
|
+
-- TODO pass in `decoderName` to pipelineEncoder
|
|
736
|
+
, body = BackendTask.Http.jsonBody (pipelineEncoder stream "text")
|
|
737
|
+
, expect =
|
|
738
|
+
BackendTask.Http.expectJson
|
|
739
|
+
(decodeLog
|
|
740
|
+
(Decode.oneOf
|
|
741
|
+
[ Decode.field "error" Decode.string
|
|
742
|
+
|> Decode.andThen
|
|
743
|
+
(\error ->
|
|
744
|
+
Decode.succeed
|
|
745
|
+
(Err
|
|
746
|
+
(FatalError.recoverable
|
|
747
|
+
{ title = "Stream Error"
|
|
748
|
+
, body = error
|
|
749
|
+
}
|
|
750
|
+
(StreamError error)
|
|
751
|
+
)
|
|
752
|
+
)
|
|
753
|
+
)
|
|
754
|
+
, decodeLog (Decode.field "metadata" decoder)
|
|
755
|
+
|> Decode.andThen
|
|
756
|
+
(\result ->
|
|
757
|
+
case result of
|
|
758
|
+
Ok metadata ->
|
|
759
|
+
Decode.map
|
|
760
|
+
(\body ->
|
|
761
|
+
Ok
|
|
762
|
+
{ metadata = metadata
|
|
763
|
+
, body = body
|
|
764
|
+
}
|
|
765
|
+
)
|
|
766
|
+
(Decode.field "body" Decode.string)
|
|
767
|
+
|
|
768
|
+
Err error ->
|
|
769
|
+
Decode.field "body" Decode.string
|
|
770
|
+
|> Decode.maybe
|
|
771
|
+
|> Decode.map
|
|
772
|
+
(\body ->
|
|
773
|
+
error |> mapRecoverable body |> Err
|
|
774
|
+
)
|
|
775
|
+
)
|
|
776
|
+
, Decode.succeed
|
|
777
|
+
(Err
|
|
778
|
+
(FatalError.recoverable
|
|
779
|
+
{ title = "Stream Error", body = "No metadata" }
|
|
780
|
+
(StreamError "No metadata")
|
|
781
|
+
)
|
|
782
|
+
)
|
|
783
|
+
]
|
|
784
|
+
)
|
|
785
|
+
)
|
|
786
|
+
}
|
|
787
|
+
|> BackendTask.andThen BackendTask.fromResult
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
{-| Ignore the body of the `Stream`, while capturing the metadata from the final part of the Stream.
|
|
791
|
+
-}
|
|
792
|
+
readMetadata :
|
|
793
|
+
Stream error metadata { read : read, write : write }
|
|
794
|
+
-> BackendTask { fatal : FatalError, recoverable : Error error String } metadata
|
|
795
|
+
readMetadata ((Stream ( _, decoder ) _) as stream) =
|
|
796
|
+
BackendTask.Internal.Request.request
|
|
797
|
+
{ name = "stream"
|
|
798
|
+
|
|
799
|
+
-- TODO pass in `decoderName` to pipelineEncoder
|
|
800
|
+
, body = BackendTask.Http.jsonBody (pipelineEncoder stream "none")
|
|
801
|
+
, expect =
|
|
802
|
+
BackendTask.Http.expectJson
|
|
803
|
+
(decodeLog
|
|
804
|
+
(Decode.oneOf
|
|
805
|
+
[ Decode.field "error" Decode.string
|
|
806
|
+
|> Decode.andThen
|
|
807
|
+
(\error ->
|
|
808
|
+
Decode.succeed
|
|
809
|
+
(Err
|
|
810
|
+
(FatalError.recoverable
|
|
811
|
+
{ title = "Stream Error"
|
|
812
|
+
, body = error
|
|
813
|
+
}
|
|
814
|
+
(StreamError error)
|
|
815
|
+
)
|
|
816
|
+
)
|
|
817
|
+
)
|
|
818
|
+
, decodeLog (Decode.field "metadata" decoder)
|
|
819
|
+
|> Decode.map
|
|
820
|
+
(\result ->
|
|
821
|
+
case result of
|
|
822
|
+
Ok metadata ->
|
|
823
|
+
Ok metadata
|
|
824
|
+
|
|
825
|
+
Err error ->
|
|
826
|
+
error |> mapRecoverable Nothing |> Err
|
|
827
|
+
)
|
|
828
|
+
, Decode.succeed
|
|
829
|
+
(Err
|
|
830
|
+
(FatalError.recoverable
|
|
831
|
+
{ title = "Stream Error", body = "No metadata" }
|
|
832
|
+
(StreamError "No metadata")
|
|
833
|
+
)
|
|
834
|
+
)
|
|
835
|
+
]
|
|
836
|
+
)
|
|
837
|
+
)
|
|
838
|
+
}
|
|
839
|
+
|> BackendTask.andThen BackendTask.fromResult
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
decodeLog : Decoder a -> Decoder a
|
|
843
|
+
decodeLog decoder =
|
|
844
|
+
Decode.value
|
|
845
|
+
|> Decode.andThen
|
|
846
|
+
(\_ ->
|
|
847
|
+
--let
|
|
848
|
+
-- _ =
|
|
849
|
+
-- Debug.log "VALUE" (Encode.encode 2 value)
|
|
850
|
+
--in
|
|
851
|
+
decoder
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
{-| Read the body of the `Stream` as JSON.
|
|
856
|
+
|
|
857
|
+
module ReadJson exposing (run)
|
|
858
|
+
|
|
859
|
+
import BackendTask
|
|
860
|
+
import BackendTask.Stream as Stream
|
|
861
|
+
import Json.Decode as Decode
|
|
862
|
+
import Pages.Script as Script exposing (Script)
|
|
863
|
+
|
|
864
|
+
run : Script
|
|
865
|
+
run =
|
|
866
|
+
Script.withoutCliOptions
|
|
867
|
+
(Stream.fileRead "data.json"
|
|
868
|
+
|> Stream.readJson (Decode.field "name" Decode.string)
|
|
869
|
+
|> BackendTask.allowFatal
|
|
870
|
+
|> BackendTask.andThen
|
|
871
|
+
(\{ body } ->
|
|
872
|
+
Script.log ("The name is: " ++ body)
|
|
873
|
+
)
|
|
874
|
+
)
|
|
875
|
+
|
|
876
|
+
-}
|
|
877
|
+
readJson :
|
|
878
|
+
Decoder value
|
|
879
|
+
-> Stream error metadata { read : (), write : write }
|
|
880
|
+
-> BackendTask { fatal : FatalError, recoverable : Error error value } { metadata : metadata, body : value }
|
|
881
|
+
readJson decoder ((Stream ( _, metadataDecoder ) _) as stream) =
|
|
882
|
+
BackendTask.Internal.Request.request
|
|
883
|
+
{ name = "stream"
|
|
884
|
+
, body = BackendTask.Http.jsonBody (pipelineEncoder stream "json")
|
|
885
|
+
, expect =
|
|
886
|
+
BackendTask.Http.expectJson
|
|
887
|
+
(Decode.field "metadata" metadataDecoder
|
|
888
|
+
|> Decode.andThen
|
|
889
|
+
(\result1 ->
|
|
890
|
+
let
|
|
891
|
+
bodyResult : Decoder (Result Decode.Error value)
|
|
892
|
+
bodyResult =
|
|
893
|
+
Decode.field "body" Decode.value
|
|
894
|
+
|> Decode.map
|
|
895
|
+
(\bodyValue ->
|
|
896
|
+
Decode.decodeValue decoder bodyValue
|
|
897
|
+
)
|
|
898
|
+
in
|
|
899
|
+
bodyResult
|
|
900
|
+
|> Decode.map
|
|
901
|
+
(\result ->
|
|
902
|
+
case result1 of
|
|
903
|
+
Ok metadata ->
|
|
904
|
+
case result of
|
|
905
|
+
Ok body ->
|
|
906
|
+
Ok
|
|
907
|
+
{ metadata = metadata
|
|
908
|
+
, body = body
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
Err decoderError ->
|
|
912
|
+
FatalError.recoverable
|
|
913
|
+
{ title = "Failed to decode body"
|
|
914
|
+
, body = "Failed to decode body"
|
|
915
|
+
}
|
|
916
|
+
(StreamError (Decode.errorToString decoderError))
|
|
917
|
+
|> Err
|
|
918
|
+
|
|
919
|
+
Err error ->
|
|
920
|
+
error
|
|
921
|
+
|> mapRecoverable (Result.toMaybe result)
|
|
922
|
+
|> Err
|
|
923
|
+
)
|
|
924
|
+
)
|
|
925
|
+
)
|
|
926
|
+
}
|
|
927
|
+
|> BackendTask.andThen BackendTask.fromResult
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
{-| Run a command (or `child_process`). The command's output becomes the body of the `Stream`.
|
|
931
|
+
-}
|
|
932
|
+
command : String -> List String -> Stream Int () { read : read, write : write }
|
|
933
|
+
command command_ args_ =
|
|
934
|
+
commandWithOptions defaultCommandOptions command_ args_
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
commandDecoder : Bool -> ( String, Decoder (Result (Recoverable Int) ()) )
|
|
938
|
+
commandDecoder allowNon0 =
|
|
939
|
+
( "command"
|
|
940
|
+
, commandOutputDecoder
|
|
941
|
+
|> Decode.map
|
|
942
|
+
(\exitCode ->
|
|
943
|
+
if exitCode == 0 || allowNon0 || True then
|
|
944
|
+
Ok ()
|
|
945
|
+
|
|
946
|
+
else
|
|
947
|
+
Err
|
|
948
|
+
(FatalError.recoverable
|
|
949
|
+
{ title = "Command Failed"
|
|
950
|
+
, body = "Command failed with exit code " ++ String.fromInt exitCode
|
|
951
|
+
}
|
|
952
|
+
exitCode
|
|
953
|
+
)
|
|
954
|
+
)
|
|
955
|
+
)
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
-- on error, give CommandOutput as well
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
{-| Pass in custom [`CommandOptions`](#CommandOptions) to configure the behavior of the command.
|
|
963
|
+
|
|
964
|
+
For example, `grep` will return a non-zero status code if it doesn't find any matches. To ignore the non-zero status code and proceed with
|
|
965
|
+
empty output, you can use `allowNon0Status`.
|
|
966
|
+
|
|
967
|
+
module GrepErrors exposing (run)
|
|
968
|
+
|
|
969
|
+
import BackendTask
|
|
970
|
+
import BackendTask.Stream as Stream
|
|
971
|
+
import Pages.Script as Script exposing (Script)
|
|
972
|
+
|
|
973
|
+
run : Script
|
|
974
|
+
run =
|
|
975
|
+
Script.withoutCliOptions
|
|
976
|
+
(Stream.fileRead "log.txt"
|
|
977
|
+
|> Stream.pipe
|
|
978
|
+
(Stream.commandWithOptions
|
|
979
|
+
(Stream.defaultCommandOptions |> Stream.allowNon0Status)
|
|
980
|
+
"grep"
|
|
981
|
+
[ "error" ]
|
|
982
|
+
)
|
|
983
|
+
|> Stream.pipe Stream.stdout
|
|
984
|
+
|> Stream.run
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
-}
|
|
988
|
+
commandWithOptions : CommandOptions -> String -> List String -> Stream Int () { read : read, write : write }
|
|
989
|
+
commandWithOptions (CommandOptions options) command_ args_ =
|
|
990
|
+
single (commandDecoder options.allowNon0Status)
|
|
991
|
+
"command"
|
|
992
|
+
[ ( "command", Encode.string command_ )
|
|
993
|
+
, ( "args", Encode.list Encode.string args_ )
|
|
994
|
+
, ( "allowNon0Status", Encode.bool options.allowNon0Status )
|
|
995
|
+
, ( "output", encodeChannel options.output )
|
|
996
|
+
, ( "timeoutInMs", nullable Encode.int options.timeoutInMs )
|
|
997
|
+
]
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
nullable : (a -> Encode.Value) -> Maybe a -> Encode.Value
|
|
1001
|
+
nullable encoder maybeValue =
|
|
1002
|
+
case maybeValue of
|
|
1003
|
+
Just value ->
|
|
1004
|
+
encoder value
|
|
1005
|
+
|
|
1006
|
+
Nothing ->
|
|
1007
|
+
Encode.null
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
{-| Configuration for [`commandWithOptions`](#commandWithOptions).
|
|
1011
|
+
-}
|
|
1012
|
+
type CommandOptions
|
|
1013
|
+
= CommandOptions CommandOptions_
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
type alias CommandOptions_ =
|
|
1017
|
+
{ output : StderrOutput
|
|
1018
|
+
, allowNon0Status : Bool
|
|
1019
|
+
, timeoutInMs : Maybe Int
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
{-| The default options that are used for [`command`](#command). Used to build up `CommandOptions`
|
|
1024
|
+
to pass in to [`commandWithOptions`](#commandWithOptions).
|
|
1025
|
+
-}
|
|
1026
|
+
defaultCommandOptions : CommandOptions
|
|
1027
|
+
defaultCommandOptions =
|
|
1028
|
+
CommandOptions
|
|
1029
|
+
{ output = PrintStderr
|
|
1030
|
+
, allowNon0Status = False
|
|
1031
|
+
, timeoutInMs = Nothing
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
{-| Configure the [`StderrOutput`](#StderrOutput) behavior.
|
|
1036
|
+
-}
|
|
1037
|
+
withOutput : StderrOutput -> CommandOptions -> CommandOptions
|
|
1038
|
+
withOutput output (CommandOptions cmd) =
|
|
1039
|
+
CommandOptions { cmd | output = output }
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
{-| By default, the `Stream` will halt with an error if a command returns a non-zero status code.
|
|
1043
|
+
|
|
1044
|
+
With `allowNon0Status`, the stream will continue without an error if the command returns a non-zero status code.
|
|
1045
|
+
|
|
1046
|
+
-}
|
|
1047
|
+
allowNon0Status : CommandOptions -> CommandOptions
|
|
1048
|
+
allowNon0Status (CommandOptions cmd) =
|
|
1049
|
+
CommandOptions { cmd | allowNon0Status = True }
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
{-| By default, commands do not have a timeout. This will set the timeout, in milliseconds, for the given command. If that duration is exceeded,
|
|
1053
|
+
the `Stream` will fail with an error.
|
|
1054
|
+
-}
|
|
1055
|
+
withTimeout : Int -> CommandOptions -> CommandOptions
|
|
1056
|
+
withTimeout timeoutMs (CommandOptions cmd) =
|
|
1057
|
+
CommandOptions { cmd | timeoutInMs = Just timeoutMs }
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
encodeChannel : StderrOutput -> Encode.Value
|
|
1061
|
+
encodeChannel output =
|
|
1062
|
+
Encode.string
|
|
1063
|
+
(case output of
|
|
1064
|
+
IgnoreStderr ->
|
|
1065
|
+
"Ignore"
|
|
1066
|
+
|
|
1067
|
+
PrintStderr ->
|
|
1068
|
+
"Print"
|
|
1069
|
+
|
|
1070
|
+
MergeStderrAndStdout ->
|
|
1071
|
+
"MergeWithStdout"
|
|
1072
|
+
|
|
1073
|
+
StderrInsteadOfStdout ->
|
|
1074
|
+
"InsteadOfStdout"
|
|
1075
|
+
)
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
commandOutputDecoder : Decoder Int
|
|
1079
|
+
commandOutputDecoder =
|
|
1080
|
+
Decode.field "exitCode" Decode.int
|
|
1081
|
+
|
|
1082
|
+
|
|
1083
|
+
{-| The output configuration for [`withOutput`](#withOutput). The default is `PrintStderr`.
|
|
1084
|
+
|
|
1085
|
+
- `PrintStderr` - Print (but do not pass along) the `stderr` output of the command. Only `stdout` will be passed along as the body of the stream.
|
|
1086
|
+
- `IgnoreStderr` - Ignore the `stderr` output of the command, only include `stdout`
|
|
1087
|
+
- `MergeStderrAndStdout` - Both `stderr` and `stdout` will be passed along as the body of the stream.
|
|
1088
|
+
- `StderrInsteadOfStdout` - Only `stderr` will be passed along as the body of the stream. `stdout` will be ignored.
|
|
1089
|
+
|
|
1090
|
+
-}
|
|
1091
|
+
type StderrOutput
|
|
1092
|
+
= PrintStderr
|
|
1093
|
+
| IgnoreStderr
|
|
1094
|
+
| MergeStderrAndStdout
|
|
1095
|
+
| StderrInsteadOfStdout
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
toBadResponse : Maybe BackendTask.Http.Metadata -> RequestsAndPending.ResponseBody -> Maybe BackendTask.Http.Error
|
|
1099
|
+
toBadResponse maybeResponse body =
|
|
1100
|
+
case maybeResponse of
|
|
1101
|
+
Just response ->
|
|
1102
|
+
if not (response.statusCode >= 200 && response.statusCode < 300) then
|
|
1103
|
+
case body of
|
|
1104
|
+
RequestsAndPending.StringBody s ->
|
|
1105
|
+
BackendTask.Http.BadStatus
|
|
1106
|
+
{ url = response.url
|
|
1107
|
+
, statusCode = response.statusCode
|
|
1108
|
+
, statusText = response.statusText
|
|
1109
|
+
, headers = response.headers
|
|
1110
|
+
}
|
|
1111
|
+
s
|
|
1112
|
+
|> Just
|
|
1113
|
+
|
|
1114
|
+
RequestsAndPending.BytesBody bytes ->
|
|
1115
|
+
BackendTask.Http.BadStatus
|
|
1116
|
+
{ url = response.url
|
|
1117
|
+
, statusCode = response.statusCode
|
|
1118
|
+
, statusText = response.statusText
|
|
1119
|
+
, headers = response.headers
|
|
1120
|
+
}
|
|
1121
|
+
(Base64.fromBytes bytes |> Maybe.withDefault "")
|
|
1122
|
+
|> Just
|
|
1123
|
+
|
|
1124
|
+
RequestsAndPending.JsonBody value ->
|
|
1125
|
+
BackendTask.Http.BadStatus
|
|
1126
|
+
{ url = response.url
|
|
1127
|
+
, statusCode = response.statusCode
|
|
1128
|
+
, statusText = response.statusText
|
|
1129
|
+
, headers = response.headers
|
|
1130
|
+
}
|
|
1131
|
+
(Encode.encode 0 value)
|
|
1132
|
+
|> Just
|
|
1133
|
+
|
|
1134
|
+
RequestsAndPending.WhateverBody ->
|
|
1135
|
+
BackendTask.Http.BadStatus
|
|
1136
|
+
{ url = response.url
|
|
1137
|
+
, statusCode = response.statusCode
|
|
1138
|
+
, statusText = response.statusText
|
|
1139
|
+
, headers = response.headers
|
|
1140
|
+
}
|
|
1141
|
+
""
|
|
1142
|
+
|> Just
|
|
1143
|
+
|
|
1144
|
+
else
|
|
1145
|
+
Nothing
|
|
1146
|
+
|
|
1147
|
+
Nothing ->
|
|
1148
|
+
Nothing
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
errorToString : BackendTask.Http.Error -> { title : String, body : String }
|
|
1152
|
+
errorToString error =
|
|
1153
|
+
{ title = "HTTP Error"
|
|
1154
|
+
, body =
|
|
1155
|
+
(case error of
|
|
1156
|
+
BackendTask.Http.BadUrl string ->
|
|
1157
|
+
[ TerminalText.text ("BadUrl " ++ string)
|
|
1158
|
+
]
|
|
1159
|
+
|
|
1160
|
+
BackendTask.Http.Timeout ->
|
|
1161
|
+
[ TerminalText.text "Timeout"
|
|
1162
|
+
]
|
|
1163
|
+
|
|
1164
|
+
BackendTask.Http.NetworkError ->
|
|
1165
|
+
[ TerminalText.text "NetworkError"
|
|
1166
|
+
]
|
|
1167
|
+
|
|
1168
|
+
BackendTask.Http.BadStatus metadata _ ->
|
|
1169
|
+
[ TerminalText.text "BadStatus: "
|
|
1170
|
+
, TerminalText.red (String.fromInt metadata.statusCode)
|
|
1171
|
+
, TerminalText.text (" " ++ metadata.statusText)
|
|
1172
|
+
]
|
|
1173
|
+
|
|
1174
|
+
BackendTask.Http.BadBody _ string ->
|
|
1175
|
+
[ TerminalText.text ("BadBody: " ++ string)
|
|
1176
|
+
]
|
|
1177
|
+
)
|
|
1178
|
+
|> TerminalText.toString
|
|
1179
|
+
}
|