@thomasfosterau/effect-parse 0.0.0-stage → 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +550 -2
- package/dist/Ascii.d.ts +392 -0
- package/dist/Ascii.d.ts.map +1 -0
- package/dist/Ascii.js +610 -0
- package/dist/Ascii.js.map +1 -0
- package/dist/Binary.d.ts +153 -0
- package/dist/Binary.d.ts.map +1 -0
- package/dist/Binary.js +171 -0
- package/dist/Binary.js.map +1 -0
- package/dist/Bits.d.ts +60 -0
- package/dist/Bits.d.ts.map +1 -0
- package/dist/Bits.js +144 -0
- package/dist/Bits.js.map +1 -0
- package/dist/Encoding.d.ts +116 -0
- package/dist/Encoding.d.ts.map +1 -0
- package/dist/Encoding.js +85 -0
- package/dist/Encoding.js.map +1 -0
- package/dist/Expression.d.ts +124 -0
- package/dist/Expression.d.ts.map +1 -0
- package/dist/Expression.js +139 -0
- package/dist/Expression.js.map +1 -0
- package/dist/Input.d.ts +301 -0
- package/dist/Input.d.ts.map +1 -0
- package/dist/Input.js +312 -0
- package/dist/Input.js.map +1 -0
- package/dist/Nom.d.ts +518 -0
- package/dist/Nom.d.ts.map +1 -0
- package/dist/Nom.js +519 -0
- package/dist/Nom.js.map +1 -0
- package/dist/ParseError.d.ts +120 -0
- package/dist/ParseError.d.ts.map +1 -0
- package/dist/ParseError.js +301 -0
- package/dist/ParseError.js.map +1 -0
- package/dist/Parser.d.ts +1312 -0
- package/dist/Parser.d.ts.map +1 -0
- package/dist/Parser.js +1936 -0
- package/dist/Parser.js.map +1 -0
- package/dist/ParserSchema.d.ts +31 -0
- package/dist/ParserSchema.d.ts.map +1 -0
- package/dist/ParserSchema.js +56 -0
- package/dist/ParserSchema.js.map +1 -0
- package/dist/ParserStream.d.ts +127 -0
- package/dist/ParserStream.d.ts.map +1 -0
- package/dist/ParserStream.js +270 -0
- package/dist/ParserStream.js.map +1 -0
- package/dist/Text.d.ts +330 -0
- package/dist/Text.d.ts.map +1 -0
- package/dist/Text.js +529 -0
- package/dist/Text.js.map +1 -0
- package/dist/Token.d.ts +225 -0
- package/dist/Token.d.ts.map +1 -0
- package/dist/Token.js +294 -0
- package/dist/Token.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/compiler/closure.d.ts +25 -0
- package/dist/internal/compiler/closure.d.ts.map +1 -0
- package/dist/internal/compiler/closure.js +1668 -0
- package/dist/internal/compiler/closure.js.map +1 -0
- package/dist/internal/compiler/entries.d.ts +19 -0
- package/dist/internal/compiler/entries.d.ts.map +1 -0
- package/dist/internal/compiler/entries.js +32 -0
- package/dist/internal/compiler/entries.js.map +1 -0
- package/dist/internal/compiler/first.d.ts +33 -0
- package/dist/internal/compiler/first.d.ts.map +1 -0
- package/dist/internal/compiler/first.js +235 -0
- package/dist/internal/compiler/first.js.map +1 -0
- package/dist/internal/compiler/jit.d.ts +20 -0
- package/dist/internal/compiler/jit.d.ts.map +1 -0
- package/dist/internal/compiler/jit.js +1030 -0
- package/dist/internal/compiler/jit.js.map +1 -0
- package/dist/internal/compiler/registry.d.ts +34 -0
- package/dist/internal/compiler/registry.d.ts.map +1 -0
- package/dist/internal/compiler/registry.js +46 -0
- package/dist/internal/compiler/registry.js.map +1 -0
- package/dist/internal/compiler/sets.d.ts +29 -0
- package/dist/internal/compiler/sets.d.ts.map +1 -0
- package/dist/internal/compiler/sets.js +85 -0
- package/dist/internal/compiler/sets.js.map +1 -0
- package/dist/internal/entries.d.ts +27 -0
- package/dist/internal/entries.d.ts.map +1 -0
- package/dist/internal/entries.js +42 -0
- package/dist/internal/entries.js.map +1 -0
- package/dist/internal/node.d.ts +251 -0
- package/dist/internal/node.d.ts.map +1 -0
- package/dist/internal/node.js +73 -0
- package/dist/internal/node.js.map +1 -0
- package/dist/internal/numbers.d.ts +41 -0
- package/dist/internal/numbers.d.ts.map +1 -0
- package/dist/internal/numbers.js +302 -0
- package/dist/internal/numbers.js.map +1 -0
- package/dist/internal/resume.d.ts +39 -0
- package/dist/internal/resume.d.ts.map +1 -0
- package/dist/internal/resume.js +37 -0
- package/dist/internal/resume.js.map +1 -0
- package/dist/internal/text.d.ts +72 -0
- package/dist/internal/text.d.ts.map +1 -0
- package/dist/internal/text.js +391 -0
- package/dist/internal/text.js.map +1 -0
- package/dist/internal/tokens.d.ts +93 -0
- package/dist/internal/tokens.d.ts.map +1 -0
- package/dist/internal/tokens.js +400 -0
- package/dist/internal/tokens.js.map +1 -0
- package/package.json +115 -3
- package/src/Ascii.ts +741 -0
- package/src/Binary.ts +228 -0
- package/src/Bits.ts +192 -0
- package/src/Encoding.ts +128 -0
- package/src/Expression.ts +245 -0
- package/src/Input.ts +429 -0
- package/src/Nom.ts +642 -0
- package/src/ParseError.ts +378 -0
- package/src/Parser.ts +2763 -0
- package/src/ParserSchema.ts +87 -0
- package/src/ParserStream.ts +390 -0
- package/src/Text.ts +757 -0
- package/src/Token.ts +416 -0
- package/src/index.ts +16 -0
- package/src/internal/compiler/closure.ts +1583 -0
- package/src/internal/compiler/entries.ts +36 -0
- package/src/internal/compiler/first.ts +252 -0
- package/src/internal/compiler/jit.ts +1091 -0
- package/src/internal/compiler/registry.ts +79 -0
- package/src/internal/compiler/sets.ts +100 -0
- package/src/internal/entries.ts +51 -0
- package/src/internal/node.ts +207 -0
- package/src/internal/numbers.ts +291 -0
- package/src/internal/resume.ts +55 -0
- package/src/internal/text.ts +411 -0
- 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))
|