@thomasfosterau/effect-parse 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +550 -2
  3. package/dist/Ascii.d.ts +392 -0
  4. package/dist/Ascii.d.ts.map +1 -0
  5. package/dist/Ascii.js +610 -0
  6. package/dist/Ascii.js.map +1 -0
  7. package/dist/Binary.d.ts +153 -0
  8. package/dist/Binary.d.ts.map +1 -0
  9. package/dist/Binary.js +171 -0
  10. package/dist/Binary.js.map +1 -0
  11. package/dist/Bits.d.ts +60 -0
  12. package/dist/Bits.d.ts.map +1 -0
  13. package/dist/Bits.js +144 -0
  14. package/dist/Bits.js.map +1 -0
  15. package/dist/Encoding.d.ts +116 -0
  16. package/dist/Encoding.d.ts.map +1 -0
  17. package/dist/Encoding.js +85 -0
  18. package/dist/Encoding.js.map +1 -0
  19. package/dist/Expression.d.ts +124 -0
  20. package/dist/Expression.d.ts.map +1 -0
  21. package/dist/Expression.js +139 -0
  22. package/dist/Expression.js.map +1 -0
  23. package/dist/Input.d.ts +301 -0
  24. package/dist/Input.d.ts.map +1 -0
  25. package/dist/Input.js +312 -0
  26. package/dist/Input.js.map +1 -0
  27. package/dist/Nom.d.ts +518 -0
  28. package/dist/Nom.d.ts.map +1 -0
  29. package/dist/Nom.js +519 -0
  30. package/dist/Nom.js.map +1 -0
  31. package/dist/ParseError.d.ts +120 -0
  32. package/dist/ParseError.d.ts.map +1 -0
  33. package/dist/ParseError.js +301 -0
  34. package/dist/ParseError.js.map +1 -0
  35. package/dist/Parser.d.ts +1312 -0
  36. package/dist/Parser.d.ts.map +1 -0
  37. package/dist/Parser.js +1936 -0
  38. package/dist/Parser.js.map +1 -0
  39. package/dist/ParserSchema.d.ts +31 -0
  40. package/dist/ParserSchema.d.ts.map +1 -0
  41. package/dist/ParserSchema.js +56 -0
  42. package/dist/ParserSchema.js.map +1 -0
  43. package/dist/ParserStream.d.ts +127 -0
  44. package/dist/ParserStream.d.ts.map +1 -0
  45. package/dist/ParserStream.js +270 -0
  46. package/dist/ParserStream.js.map +1 -0
  47. package/dist/Text.d.ts +330 -0
  48. package/dist/Text.d.ts.map +1 -0
  49. package/dist/Text.js +529 -0
  50. package/dist/Text.js.map +1 -0
  51. package/dist/Token.d.ts +225 -0
  52. package/dist/Token.d.ts.map +1 -0
  53. package/dist/Token.js +294 -0
  54. package/dist/Token.js.map +1 -0
  55. package/dist/index.d.ts +17 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +17 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/internal/compiler/closure.d.ts +25 -0
  60. package/dist/internal/compiler/closure.d.ts.map +1 -0
  61. package/dist/internal/compiler/closure.js +1668 -0
  62. package/dist/internal/compiler/closure.js.map +1 -0
  63. package/dist/internal/compiler/entries.d.ts +19 -0
  64. package/dist/internal/compiler/entries.d.ts.map +1 -0
  65. package/dist/internal/compiler/entries.js +32 -0
  66. package/dist/internal/compiler/entries.js.map +1 -0
  67. package/dist/internal/compiler/first.d.ts +33 -0
  68. package/dist/internal/compiler/first.d.ts.map +1 -0
  69. package/dist/internal/compiler/first.js +235 -0
  70. package/dist/internal/compiler/first.js.map +1 -0
  71. package/dist/internal/compiler/jit.d.ts +20 -0
  72. package/dist/internal/compiler/jit.d.ts.map +1 -0
  73. package/dist/internal/compiler/jit.js +1030 -0
  74. package/dist/internal/compiler/jit.js.map +1 -0
  75. package/dist/internal/compiler/registry.d.ts +34 -0
  76. package/dist/internal/compiler/registry.d.ts.map +1 -0
  77. package/dist/internal/compiler/registry.js +46 -0
  78. package/dist/internal/compiler/registry.js.map +1 -0
  79. package/dist/internal/compiler/sets.d.ts +29 -0
  80. package/dist/internal/compiler/sets.d.ts.map +1 -0
  81. package/dist/internal/compiler/sets.js +85 -0
  82. package/dist/internal/compiler/sets.js.map +1 -0
  83. package/dist/internal/entries.d.ts +27 -0
  84. package/dist/internal/entries.d.ts.map +1 -0
  85. package/dist/internal/entries.js +42 -0
  86. package/dist/internal/entries.js.map +1 -0
  87. package/dist/internal/node.d.ts +251 -0
  88. package/dist/internal/node.d.ts.map +1 -0
  89. package/dist/internal/node.js +73 -0
  90. package/dist/internal/node.js.map +1 -0
  91. package/dist/internal/numbers.d.ts +41 -0
  92. package/dist/internal/numbers.d.ts.map +1 -0
  93. package/dist/internal/numbers.js +302 -0
  94. package/dist/internal/numbers.js.map +1 -0
  95. package/dist/internal/resume.d.ts +39 -0
  96. package/dist/internal/resume.d.ts.map +1 -0
  97. package/dist/internal/resume.js +37 -0
  98. package/dist/internal/resume.js.map +1 -0
  99. package/dist/internal/text.d.ts +72 -0
  100. package/dist/internal/text.d.ts.map +1 -0
  101. package/dist/internal/text.js +391 -0
  102. package/dist/internal/text.js.map +1 -0
  103. package/dist/internal/tokens.d.ts +93 -0
  104. package/dist/internal/tokens.d.ts.map +1 -0
  105. package/dist/internal/tokens.js +400 -0
  106. package/dist/internal/tokens.js.map +1 -0
  107. package/package.json +115 -3
  108. package/src/Ascii.ts +741 -0
  109. package/src/Binary.ts +228 -0
  110. package/src/Bits.ts +192 -0
  111. package/src/Encoding.ts +128 -0
  112. package/src/Expression.ts +245 -0
  113. package/src/Input.ts +429 -0
  114. package/src/Nom.ts +642 -0
  115. package/src/ParseError.ts +378 -0
  116. package/src/Parser.ts +2763 -0
  117. package/src/ParserSchema.ts +87 -0
  118. package/src/ParserStream.ts +390 -0
  119. package/src/Text.ts +757 -0
  120. package/src/Token.ts +416 -0
  121. package/src/index.ts +16 -0
  122. package/src/internal/compiler/closure.ts +1583 -0
  123. package/src/internal/compiler/entries.ts +36 -0
  124. package/src/internal/compiler/first.ts +252 -0
  125. package/src/internal/compiler/jit.ts +1091 -0
  126. package/src/internal/compiler/registry.ts +79 -0
  127. package/src/internal/compiler/sets.ts +100 -0
  128. package/src/internal/entries.ts +51 -0
  129. package/src/internal/node.ts +207 -0
  130. package/src/internal/numbers.ts +291 -0
  131. package/src/internal/resume.ts +55 -0
  132. package/src/internal/text.ts +411 -0
  133. package/src/internal/tokens.ts +409 -0
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Building Effect `Schema`s from parsers.
3
+ *
4
+ * A parser over strings becomes a schema that decodes a `string` into the
5
+ * parser's output. Parse errors become schema issues carrying the rendered
6
+ * `ParseError` message. Encoding needs a printer, supplied as `encode`;
7
+ * without one, encoding is forbidden.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { Schema } from "effect"
12
+ * import { Ascii, Parser, ParserSchema, Token } from "@thomasfosterau/effect-parse"
13
+ *
14
+ * const Version = ParserSchema.make(
15
+ * Parser.struct({
16
+ * major: Ascii.decUint,
17
+ * minor: Parser.preceded(Token.literal("."), Ascii.decUint)
18
+ * }),
19
+ * { encode: ({ major, minor }) => `${major}.${minor}` }
20
+ * )
21
+ *
22
+ * Schema.decodeUnknownSync(Version)("1.2") // { major: 1, minor: 2 }
23
+ * Schema.encodeSync(Version)({ major: 3, minor: 4 }) // "3.4"
24
+ * ```
25
+ *
26
+ * @since 0.1.0
27
+ */
28
+ import * as Effect from "effect/Effect"
29
+ import * as Result from "effect/Result"
30
+ import * as Schema from "effect/Schema"
31
+ import * as SchemaIssue from "effect/SchemaIssue"
32
+ import * as SchemaTransformation from "effect/SchemaTransformation"
33
+ import type { Parser } from "./Parser.js"
34
+ import * as P from "./Parser.js"
35
+
36
+ /**
37
+ * Options for {@link make}.
38
+ *
39
+ * @since 0.1.0
40
+ * @category models
41
+ */
42
+ export interface Options<A> {
43
+ /** Prints a value back to a string. Without it, encoding fails. */
44
+ readonly encode?: ((value: A) => string) | undefined
45
+ /** A description of the decoded type, used by schema tooling. */
46
+ readonly description?: string | undefined
47
+ }
48
+
49
+ /**
50
+ * A schema decoding strings with a parser.
51
+ *
52
+ * @since 0.1.0
53
+ * @category models
54
+ */
55
+ export interface ParserSchema<A> extends Schema.decodeTo<Schema.declare<A>, Schema.String> {}
56
+
57
+ /**
58
+ * Creates a schema that decodes a string with `parser`, which must consume
59
+ * the whole string.
60
+ *
61
+ * @since 0.1.0
62
+ * @category constructors
63
+ */
64
+ export const make = <A, E>(parser: Parser<A, E, never, string>, options?: Options<A>): ParserSchema<A> => {
65
+ const target = Schema.declare<A>(
66
+ (_u): _u is A => true,
67
+ options?.description !== undefined ? { description: options.description } : undefined
68
+ )
69
+ const encode = options?.encode
70
+ return Schema.String.pipe(
71
+ Schema.decodeTo(
72
+ target,
73
+ SchemaTransformation.transformEffect<A, string>({
74
+ decode: (input, parseOptions) => {
75
+ const result = P.parse(parser, input)
76
+ return Result.isSuccess(result)
77
+ ? Effect.succeed(result.success)
78
+ : Effect.fail(new SchemaIssue.InvalidValue({ message: result.failure.message }, input, parseOptions))
79
+ },
80
+ encode: (value, parseOptions) =>
81
+ encode !== undefined
82
+ ? Effect.succeed(encode(value))
83
+ : Effect.fail(new SchemaIssue.Forbidden({ message: "Encoding is not supported" }, value, parseOptions))
84
+ })
85
+ )
86
+ )
87
+ }
@@ -0,0 +1,390 @@
1
+ /**
2
+ * Running parsers over Effect `Stream`s of input chunks.
3
+ *
4
+ * This is where nom's and winnow's *partial* (streaming) parsing meets
5
+ * Effect: chunks are buffered and the parser runs in partial mode, so a
6
+ * value split across chunk boundaries is reported as `Incomplete` and
7
+ * retried once more data arrives. When the stream ends, the leftover input
8
+ * is parsed in complete mode.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { Effect, Stream } from "effect"
13
+ * import { Ascii, Parser, ParserStream, Token } from "@thomasfosterau/effect-parse"
14
+ *
15
+ * const line = Parser.terminated(Ascii.decInt, Token.literal("\n"))
16
+ *
17
+ * const program = Stream.make("1\n2", "3\n", "4\n").pipe(
18
+ * ParserStream.parseMany(line),
19
+ * Stream.runCollect
20
+ * )
21
+ * // Effect.runSync(program) => [1, 23, 4]
22
+ * ```
23
+ *
24
+ * @since 0.1.0
25
+ */
26
+ import type * as Context from "effect/Context"
27
+ import * as Data from "effect/Data"
28
+ import * as Effect from "effect/Effect"
29
+ import { dual } from "effect/Function"
30
+ import * as Stream from "effect/Stream"
31
+ import type { BinaryInput, Input, Source } from "./Input.js"
32
+ import { Fail, Failure, fromSource, State } from "./Input.js"
33
+ import * as ParseError from "./ParseError.js"
34
+ import * as Compiler from "./internal/compiler/registry.js"
35
+ import type * as Resume from "./internal/resume.js"
36
+ import type { Parser } from "./Parser.js"
37
+
38
+ /**
39
+ * Input kinds that can be streamed in chunks.
40
+ *
41
+ * @since 0.1.0
42
+ * @category models
43
+ */
44
+ export type Chunkable = string | Uint8Array | ReadonlyArray<unknown>
45
+
46
+ const concat = <I extends Chunkable>(a: I, b: I): I => {
47
+ if (typeof a === "string") return (a + (b as string)) as I
48
+ if (a instanceof Uint8Array) {
49
+ if (a.length === 0) return b
50
+ const out = new Uint8Array(a.length + (b as Uint8Array).length)
51
+ out.set(a)
52
+ out.set(b as Uint8Array, a.length)
53
+ return out as I
54
+ }
55
+ return [...a, ...(b as ReadonlyArray<unknown>)] as unknown as I
56
+ }
57
+
58
+ const sliceFrom = <I extends Chunkable>(input: I, offset: number): I =>
59
+ (typeof input === "string" ? input.slice(offset) : input.slice(offset)) as I
60
+
61
+ const noProgress = (input: Input, offset: number) =>
62
+ ParseError.fromFailure<never>(
63
+ input,
64
+ new Failure(offset, "Assert", [], [], false, undefined, "`parseMany` parsers must always consume", 0),
65
+ "Cut"
66
+ )
67
+
68
+ /**
69
+ * Options for {@link parseMany}.
70
+ *
71
+ * @since 0.1.0
72
+ * @category models
73
+ */
74
+ export interface ParseManyOptions<I> {
75
+ /** The empty input, used to initialise the buffer (inferred from the first chunk if omitted). */
76
+ readonly empty?: Source<I> | undefined
77
+ }
78
+
79
+ /**
80
+ * Transforms a stream of input chunks into a stream of parsed values by
81
+ * applying `parser` repeatedly. Each application must consume input.
82
+ *
83
+ * Services required by the parser are taken from the stream's context.
84
+ * Error offsets are relative to the internal buffer of unconsumed input,
85
+ * which is also the `input` of the reported `ParseError`.
86
+ *
87
+ * @since 0.1.0
88
+ * @category combinators
89
+ */
90
+ export const parseMany: {
91
+ <A, E, R, I extends Chunkable>(
92
+ parser: Parser<A, E, R, I>,
93
+ options?: ParseManyOptions<I>
94
+ ): <E0, R0>(self: Stream.Stream<Source<I>, E0, R0>) => Stream.Stream<A, E0 | ParseError.ParseError<E>, R0 | R>
95
+ <I extends Chunkable, E0, R0, A, E, R>(
96
+ self: Stream.Stream<Source<NoInfer<I>>, E0, R0>,
97
+ parser: Parser<A, E, R, I>,
98
+ options?: ParseManyOptions<I>
99
+ ): Stream.Stream<A, E0 | ParseError.ParseError<E>, R0 | R>
100
+ } = dual((args) => Stream.isStream(args[0]), <I extends Chunkable, E0, R0, A, E, R>(
101
+ self: Stream.Stream<Source<I>, E0, R0>,
102
+ parser: Parser<A, E, R, I>,
103
+ options?: ParseManyOptions<I>
104
+ ): Stream.Stream<A, E0 | ParseError.ParseError<E>, R0 | R> =>
105
+ Stream.suspend(() => {
106
+ const step = parser.step
107
+ let buffer: I | undefined = options?.empty === undefined ? undefined : fromSource<I>(options.empty)
108
+
109
+ const drain = (
110
+ context: any,
111
+ partial: boolean
112
+ ): Effect.Effect<Array<A>, ParseError.ParseError<E>> => {
113
+ const out: Array<A> = []
114
+ if (buffer === undefined) return Effect.succeed(out)
115
+ const input = buffer
116
+ let offset = 0
117
+ while (offset < input.length) {
118
+ const state = new State(input, { partial, context, offset })
119
+ const a = step(state)
120
+ if (a === Fail) {
121
+ if (partial && state.mode === "Incomplete") break
122
+ return Effect.fail(ParseError.fromFailure<E>(input, state.reportedFailure(), state.mode, state.encoding))
123
+ }
124
+ if (state.pos === offset) return Effect.fail(noProgress(input, offset))
125
+ out.push(a)
126
+ offset = state.pos
127
+ }
128
+ buffer = sliceFrom(input, offset)
129
+ return Effect.succeed(out)
130
+ }
131
+
132
+ const feed = (source: Source<I>) =>
133
+ Effect.flatMap(Effect.context<R>(), (context) =>
134
+ Effect.suspend(() => {
135
+ const chunk = fromSource<I>(source)
136
+ buffer = buffer === undefined ? chunk : concat(buffer, chunk)
137
+ return drain(context, true)
138
+ }))
139
+
140
+ const finish = Effect.flatMap(Effect.context<R>(), (context) => Effect.suspend(() => drain(context, false)))
141
+
142
+ return self.pipe(
143
+ Stream.mapEffect(feed),
144
+ Stream.flattenIterable,
145
+ Stream.concat(Stream.unwrap(Effect.map(finish, Stream.fromIterable)))
146
+ )
147
+ }))
148
+
149
+ /**
150
+ * Parses a stream of chunks incrementally, as one input. Chunks are parsed
151
+ * as they arrive, in partial mode; when the parser runs out of input, its
152
+ * repetitions keep checkpoints (see `internal/resume.ts`), so the next
153
+ * attempt carries on from where the last one stopped instead of starting
154
+ * over.
155
+ */
156
+ class Incremental<A, E, I extends Chunkable> {
157
+ private kind: 0 | 1 | 2 | undefined
158
+ private text = ""
159
+ private bytes = new Uint8Array(0)
160
+ private items: Array<unknown> = []
161
+ private length = 0
162
+ private readonly resume: Resume.Checkpoints = new Map()
163
+ /** The input length at the last attempt, and where that attempt ran out of input. */
164
+ private attempted = 0
165
+ private restart = 0
166
+ /** Set once an attempt settles: a success (with its end offset) or a definite failure. */
167
+ private settled: { readonly value: A; readonly pos: number } | "failed" | undefined
168
+
169
+ constructor(
170
+ private readonly parser: Parser<A, E, any, I>,
171
+ private readonly context: Context.Context<never>,
172
+ empty: I | undefined
173
+ ) {
174
+ if (empty !== undefined) this.append(empty)
175
+ }
176
+
177
+ private append(chunk: I): void {
178
+ if (typeof chunk === "string") {
179
+ this.kind ??= 0
180
+ this.text += chunk
181
+ this.length += chunk.length
182
+ } else if (chunk instanceof Uint8Array) {
183
+ this.kind ??= 1
184
+ const needed = this.length + chunk.length
185
+ if (needed > this.bytes.length) {
186
+ // Grow geometrically, so appending costs amortized constant time per byte
187
+ const grown = new Uint8Array(Math.max(needed, this.bytes.length * 2, 4096))
188
+ grown.set(this.bytes.subarray(0, this.length))
189
+ this.bytes = grown
190
+ }
191
+ this.bytes.set(chunk, this.length)
192
+ this.length = needed
193
+ } else {
194
+ this.kind ??= 2
195
+ for (let i = 0; i < chunk.length; i++) this.items.push(chunk[i])
196
+ this.length = this.items.length
197
+ }
198
+ }
199
+
200
+ private input(): I {
201
+ switch (this.kind) {
202
+ case 1:
203
+ return this.bytes.subarray(0, this.length) as I
204
+ case 2:
205
+ // Items are only appended between attempts, after each state has read its `end`
206
+ return this.items as unknown as I
207
+ default:
208
+ return this.text as I
209
+ }
210
+ }
211
+
212
+ push(chunk: I): void {
213
+ if (chunk.length === 0) return
214
+ this.append(chunk)
215
+ if (this.settled !== undefined) return
216
+ // An attempt re-reads what the last one could not finish, from `restart`;
217
+ // waiting until at least half as much new input has arrived keeps the
218
+ // total work linear in the input. Strings are also copied when they are
219
+ // flattened, so they wait for a fraction of the whole input too.
220
+ const added = this.length - this.attempted
221
+ if (added < (this.attempted - this.restart) / 2) return
222
+ if (this.kind === 0 && added < this.attempted / 8) return
223
+ this.attempt()
224
+ }
225
+
226
+ /** The step attempts run: the parser's resumable compiled code if it was compiled, else the interpreter. */
227
+ private step(state: State<I>): unknown {
228
+ return (Compiler.stepFor(this.parser, state, true) ?? this.parser.step)(state)
229
+ }
230
+
231
+ private attempt(): void {
232
+ const state = new State(this.input(), { partial: true, context: this.context })
233
+ state.resume = this.resume
234
+ const a = this.step(state)
235
+ this.attempted = this.length
236
+ if (a !== Fail) {
237
+ this.settled = { value: a as A, pos: state.pos }
238
+ } else if (state.mode === "Incomplete") {
239
+ this.restart = state.failure.offset
240
+ } else {
241
+ this.settled = "failed"
242
+ }
243
+ }
244
+
245
+ finish(): Effect.Effect<A, ParseError.ParseError<E>> {
246
+ const settled = this.settled
247
+ if (settled !== undefined && settled !== "failed") {
248
+ if (settled.pos === this.length) return Effect.succeed(settled.value)
249
+ } else if (settled === undefined) {
250
+ // The last attempt, in complete mode, resumes from the checkpoints too
251
+ const state = new State(this.input(), { context: this.context })
252
+ state.resume = this.resume
253
+ const a = this.step(state)
254
+ if (a !== Fail && state.pos === state.end) return Effect.succeed(a as A)
255
+ }
256
+ // The error comes from an ordinary parse of the whole input, so it is the
257
+ // same as if the input had been parsed in one go
258
+ const all = this.input()
259
+ const state = new State(all, { context: this.context })
260
+ const a = this.parser.step(state)
261
+ if (a !== Fail && state.pos === state.end) return Effect.succeed(a)
262
+ if (a !== Fail) state.backtrack("Eof", ["end of input"])
263
+ return Effect.fail(ParseError.fromFailure<E>(all, state.reportedFailure(), state.mode, state.encoding))
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Parses an entire stream of chunks as a single complete input.
269
+ *
270
+ * Parsing does not wait for the stream to end: each chunk is parsed as it
271
+ * arrives, in partial mode, and when the parser runs out of input its
272
+ * repetitions (`repeat`, `separated`, `fold`, `escaped`, ...) remember how
273
+ * far they got, so that the next attempt resumes there rather than starting
274
+ * over. A large document, such as the body of a `fetch` response, is
275
+ * therefore parsed while it downloads, and is ready soon after its last
276
+ * byte. Use {@link parseReadable} or {@link parseResponse} for web streams.
277
+ *
278
+ * The result and any `ParseError` are those of parsing the whole input at
279
+ * once: errors are reported once the stream has ended, by an ordinary
280
+ * interpreted parse of the whole input. A parser compiled with
281
+ * `Parser.compile` streams compiled too (with a build that handles partial
282
+ * input), which is several times faster. As with compiled parsers,
283
+ * callbacks should be pure: the elements parsed before a checkpoint are
284
+ * not parsed again, and on failure everything is parsed again.
285
+ *
286
+ * @since 0.1.0
287
+ * @category combinators
288
+ */
289
+ export const parseAll: {
290
+ <A, E, R, I extends Chunkable>(
291
+ parser: Parser<A, E, R, I>,
292
+ options?: ParseManyOptions<I>
293
+ ): <E0, R0>(self: Stream.Stream<Source<I>, E0, R0>) => Effect.Effect<A, E0 | ParseError.ParseError<E>, R0 | R>
294
+ <I extends Chunkable, E0, R0, A, E, R>(
295
+ self: Stream.Stream<Source<NoInfer<I>>, E0, R0>,
296
+ parser: Parser<A, E, R, I>,
297
+ options?: ParseManyOptions<I>
298
+ ): Effect.Effect<A, E0 | ParseError.ParseError<E>, R0 | R>
299
+ } = dual((args) => Stream.isStream(args[0]), <I extends Chunkable, E0, R0, A, E, R>(
300
+ self: Stream.Stream<Source<I>, E0, R0>,
301
+ parser: Parser<A, E, R, I>,
302
+ options?: ParseManyOptions<I>
303
+ ): Effect.Effect<A, E0 | ParseError.ParseError<E>, R0 | R> =>
304
+ Effect.flatMap(Effect.context<R>(), (context) =>
305
+ Effect.suspend(() => {
306
+ const empty = options?.empty === undefined ? undefined : fromSource<I>(options.empty)
307
+ const incremental = new Incremental(parser, context as Context.Context<never>, empty)
308
+ return Effect.flatMap(
309
+ Stream.runForEach(self, (chunk) => Effect.sync(() => incremental.push(fromSource<I>(chunk)))),
310
+ () => Effect.suspend(() => incremental.finish())
311
+ )
312
+ })))
313
+
314
+ /**
315
+ * An error reading a `ReadableStream` given to {@link parseReadable} or
316
+ * {@link parseResponse}.
317
+ *
318
+ * @since 0.1.0
319
+ * @category errors
320
+ */
321
+ export class ReadError extends Data.TaggedError("ReadError")<{ readonly cause: unknown }> {}
322
+
323
+ /**
324
+ * Parses a web `ReadableStream` of bytes, such as the body of a `fetch`
325
+ * response, incrementally (see {@link parseAll}): the bytes are parsed as
326
+ * they arrive, without waiting for the end of the stream or decoding them
327
+ * first. Use a byte parser, e.g. one built from `Text.utf8`.
328
+ *
329
+ * @example
330
+ * ```ts
331
+ * import { Effect } from "effect"
332
+ * import { Parser, ParserStream, Text } from "@thomasfosterau/effect-parse"
333
+ *
334
+ * const csvLine = Parser.separated(Text.utf8.takeTill(",\n"), Text.utf8.literal(","))
335
+ * const csv = Parser.separated(csvLine, Text.utf8.literal("\n"))
336
+ *
337
+ * const response = await fetch("https://example.com/data.csv")
338
+ * const rows = await Effect.runPromise(ParserStream.parseReadable(response.body!, csv))
339
+ * ```
340
+ *
341
+ * @since 0.1.0
342
+ * @category combinators
343
+ */
344
+ export const parseReadable: {
345
+ <A, E, R>(
346
+ parser: Parser<A, E, R, Uint8Array>
347
+ ): (self: ReadableStream<BinaryInput>) => Effect.Effect<A, ParseError.ParseError<E> | ReadError, R>
348
+ <A, E, R>(
349
+ self: ReadableStream<BinaryInput>,
350
+ parser: Parser<A, E, R, Uint8Array>
351
+ ): Effect.Effect<A, ParseError.ParseError<E> | ReadError, R>
352
+ } = dual(2, <A, E, R>(
353
+ self: ReadableStream<BinaryInput>,
354
+ parser: Parser<A, E, R, Uint8Array>
355
+ ): Effect.Effect<A, ParseError.ParseError<E> | ReadError, R> =>
356
+ parseAll(
357
+ Stream.fromReadableStream({ evaluate: () => self, onError: (cause) => new ReadError({ cause }) }),
358
+ parser,
359
+ { empty: new Uint8Array(0) }
360
+ ))
361
+
362
+ /**
363
+ * Parses the body of a `fetch` `Response` incrementally, as it downloads
364
+ * (see {@link parseReadable}). A response without a body is parsed as empty
365
+ * input. The status is not checked.
366
+ *
367
+ * @example
368
+ * ```ts
369
+ * import { Effect } from "effect"
370
+ * import { Parser, ParserStream, Text } from "@thomasfosterau/effect-parse"
371
+ *
372
+ * const lines = Parser.separated(Text.utf8.tillLineEnding, Text.utf8.lineEnding)
373
+ *
374
+ * const response = await fetch("https://example.com/log.txt")
375
+ * const value = await Effect.runPromise(ParserStream.parseResponse(response, lines))
376
+ * ```
377
+ *
378
+ * @since 0.1.0
379
+ * @category combinators
380
+ */
381
+ export const parseResponse: {
382
+ <A, E, R>(
383
+ parser: Parser<A, E, R, Uint8Array>
384
+ ): (self: Response) => Effect.Effect<A, ParseError.ParseError<E> | ReadError, R>
385
+ <A, E, R>(self: Response, parser: Parser<A, E, R, Uint8Array>): Effect.Effect<A, ParseError.ParseError<E> | ReadError, R>
386
+ } = dual(2, <A, E, R>(
387
+ self: Response,
388
+ parser: Parser<A, E, R, Uint8Array>
389
+ ): Effect.Effect<A, ParseError.ParseError<E> | ReadError, R> =>
390
+ parseReadable(self.body ?? new ReadableStream<Uint8Array>({ start: (c) => c.close() }), parser))