@thomasfosterau/effect-parse 0.1.0 → 0.3.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/README.md +291 -231
- package/dist/Ascii.d.ts +32 -53
- package/dist/Ascii.d.ts.map +1 -1
- package/dist/Ascii.js +576 -571
- package/dist/Ascii.js.map +1 -1
- package/dist/Binary.d.ts +6 -29
- package/dist/Binary.d.ts.map +1 -1
- package/dist/Binary.js +129 -116
- package/dist/Binary.js.map +1 -1
- package/dist/Bits.d.ts +4 -2
- package/dist/Bits.d.ts.map +1 -1
- package/dist/Bits.js +121 -123
- package/dist/Bits.js.map +1 -1
- package/dist/Compiler.d.ts +39 -0
- package/dist/Compiler.d.ts.map +1 -0
- package/dist/Compiler.js +48 -0
- package/dist/Compiler.js.map +1 -0
- package/dist/Encoding.d.ts +29 -46
- package/dist/Encoding.d.ts.map +1 -1
- package/dist/Encoding.js +76 -75
- package/dist/Encoding.js.map +1 -1
- package/dist/Expression.d.ts +15 -49
- package/dist/Expression.d.ts.map +1 -1
- package/dist/Expression.js +151 -132
- package/dist/Expression.js.map +1 -1
- package/dist/Input.d.ts +251 -177
- package/dist/Input.d.ts.map +1 -1
- package/dist/Input.js +409 -299
- package/dist/Input.js.map +1 -1
- package/dist/Nom.d.ts +45 -85
- package/dist/Nom.d.ts.map +1 -1
- package/dist/Nom.js +423 -453
- package/dist/Nom.js.map +1 -1
- package/dist/ParseError.d.ts +54 -49
- package/dist/ParseError.d.ts.map +1 -1
- package/dist/ParseError.js +232 -275
- package/dist/ParseError.js.map +1 -1
- package/dist/Parser.d.ts +345 -313
- package/dist/Parser.d.ts.map +1 -1
- package/dist/Parser.js +2034 -1802
- package/dist/Parser.js.map +1 -1
- package/dist/ParserSchema.d.ts +206 -7
- package/dist/ParserSchema.d.ts.map +1 -1
- package/dist/ParserSchema.js +678 -49
- package/dist/ParserSchema.js.map +1 -1
- package/dist/ParserStream.d.ts +26 -20
- package/dist/ParserStream.d.ts.map +1 -1
- package/dist/ParserStream.js +267 -255
- package/dist/ParserStream.js.map +1 -1
- package/dist/Syntax.d.ts +401 -0
- package/dist/Syntax.d.ts.map +1 -0
- package/dist/Syntax.js +717 -0
- package/dist/Syntax.js.map +1 -0
- package/dist/Text.d.ts +98 -134
- package/dist/Text.d.ts.map +1 -1
- package/dist/Text.js +534 -496
- package/dist/Text.js.map +1 -1
- package/dist/Token.d.ts +60 -42
- package/dist/Token.d.ts.map +1 -1
- package/dist/Token.js +331 -265
- package/dist/Token.js.map +1 -1
- package/dist/index.d.ts +16 -17
- package/dist/index.js +16 -17
- package/dist/internal/compiler/closure.d.ts +14 -16
- package/dist/internal/compiler/closure.d.ts.map +1 -1
- package/dist/internal/compiler/closure.js +1458 -1637
- package/dist/internal/compiler/closure.js.map +1 -1
- package/dist/internal/compiler/entries.d.ts +11 -11
- package/dist/internal/compiler/entries.d.ts.map +1 -1
- package/dist/internal/compiler/entries.js +36 -26
- package/dist/internal/compiler/entries.js.map +1 -1
- package/dist/internal/compiler/first.d.ts +19 -19
- package/dist/internal/compiler/first.d.ts.map +1 -1
- package/dist/internal/compiler/first.js +251 -227
- package/dist/internal/compiler/first.js.map +1 -1
- package/dist/internal/compiler/jit.d.ts +6 -17
- package/dist/internal/compiler/jit.d.ts.map +1 -1
- package/dist/internal/compiler/jit.js +860 -988
- package/dist/internal/compiler/jit.js.map +1 -1
- package/dist/internal/compiler/registry.d.ts +20 -21
- package/dist/internal/compiler/registry.d.ts.map +1 -1
- package/dist/internal/compiler/registry.js +21 -41
- package/dist/internal/compiler/registry.js.map +1 -1
- package/dist/internal/compiler/sets.d.ts +9 -13
- package/dist/internal/compiler/sets.d.ts.map +1 -1
- package/dist/internal/compiler/sets.js +76 -73
- package/dist/internal/compiler/sets.js.map +1 -1
- package/dist/internal/entries.d.ts +3 -3
- package/dist/internal/entries.d.ts.map +1 -1
- package/dist/internal/entries.js +38 -30
- package/dist/internal/entries.js.map +1 -1
- package/dist/internal/node.d.ts +177 -173
- package/dist/internal/node.d.ts.map +1 -1
- package/dist/internal/node.js +77 -66
- package/dist/internal/node.js.map +1 -1
- package/dist/internal/numbers.d.ts +7 -8
- package/dist/internal/numbers.d.ts.map +1 -1
- package/dist/internal/numbers.js +246 -276
- package/dist/internal/numbers.js.map +1 -1
- package/dist/internal/resume.d.ts +6 -24
- package/dist/internal/resume.d.ts.map +1 -1
- package/dist/internal/resume.js +30 -29
- package/dist/internal/resume.js.map +1 -1
- package/dist/internal/run.d.ts +15 -0
- package/dist/internal/run.d.ts.map +1 -0
- package/dist/internal/run.js +36 -0
- package/dist/internal/run.js.map +1 -0
- package/dist/internal/stack.d.ts +16 -0
- package/dist/internal/stack.d.ts.map +1 -0
- package/dist/internal/stack.js +36 -0
- package/dist/internal/stack.js.map +1 -0
- package/dist/internal/syntax.d.ts +147 -0
- package/dist/internal/syntax.d.ts.map +1 -0
- package/dist/internal/syntax.js +309 -0
- package/dist/internal/syntax.js.map +1 -0
- package/dist/internal/text.d.ts +10 -2
- package/dist/internal/text.d.ts.map +1 -1
- package/dist/internal/text.js +295 -345
- package/dist/internal/text.js.map +1 -1
- package/dist/internal/ties.d.ts +30 -0
- package/dist/internal/ties.d.ts.map +1 -0
- package/dist/internal/ties.js +67 -0
- package/dist/internal/ties.js.map +1 -0
- package/dist/internal/tokens.d.ts +25 -30
- package/dist/internal/tokens.d.ts.map +1 -1
- package/dist/internal/tokens.js +369 -363
- package/dist/internal/tokens.js.map +1 -1
- package/package.json +42 -28
- package/src/Ascii.ts +278 -187
- package/src/Binary.ts +150 -44
- package/src/Bits.ts +6 -3
- package/src/Compiler.ts +98 -0
- package/src/Expression.ts +24 -16
- package/src/Input.ts +210 -35
- package/src/Nom.ts +4 -3
- package/src/ParseError.ts +49 -52
- package/src/Parser.ts +1199 -799
- package/src/ParserSchema.ts +982 -23
- package/src/ParserStream.ts +104 -80
- package/src/Syntax.ts +900 -0
- package/src/Text.ts +272 -187
- package/src/Token.ts +180 -87
- package/src/index.ts +2 -0
- package/src/internal/compiler/closure.ts +121 -88
- package/src/internal/compiler/entries.ts +21 -8
- package/src/internal/compiler/first.ts +17 -15
- package/src/internal/compiler/jit.ts +360 -157
- package/src/internal/compiler/registry.ts +12 -33
- package/src/internal/compiler/sets.ts +17 -11
- package/src/internal/entries.ts +4 -2
- package/src/internal/node.ts +51 -37
- package/src/internal/numbers.ts +22 -20
- package/src/internal/resume.ts +8 -4
- package/src/internal/run.ts +41 -0
- package/src/internal/stack.ts +41 -0
- package/src/internal/syntax.ts +426 -0
- package/src/internal/text.ts +32 -25
- package/src/internal/ties.ts +83 -0
- package/src/internal/tokens.ts +96 -51
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
package/dist/ParserSchema.js
CHANGED
|
@@ -1,56 +1,685 @@
|
|
|
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";
|
|
1
|
+
import * as Predicate from "effect/Predicate";
|
|
29
2
|
import * as Result from "effect/Result";
|
|
3
|
+
import * as P from "./Parser.js";
|
|
4
|
+
import { describeLiteral } from "./internal/tokens.js";
|
|
5
|
+
import * as Token from "./Token.js";
|
|
6
|
+
import * as Option from "effect/Option";
|
|
7
|
+
import * as Ascii from "./Ascii.js";
|
|
8
|
+
import * as Effect from "effect/Effect";
|
|
9
|
+
import { dual } from "effect/Function";
|
|
10
|
+
import * as Arr from "effect/Array";
|
|
11
|
+
import * as Match from "effect/Match";
|
|
12
|
+
import * as MutableHashMap from "effect/MutableHashMap";
|
|
13
|
+
import * as Order from "effect/Order";
|
|
14
|
+
import * as Record from "effect/Record";
|
|
30
15
|
import * as Schema from "effect/Schema";
|
|
16
|
+
import * as SchemaAST from "effect/SchemaAST";
|
|
31
17
|
import * as SchemaIssue from "effect/SchemaIssue";
|
|
32
18
|
import * as SchemaTransformation from "effect/SchemaTransformation";
|
|
33
|
-
|
|
19
|
+
//#region src/ParserSchema.ts
|
|
20
|
+
/**
|
|
21
|
+
* Effect `Schema`s and parsers, in both directions.
|
|
22
|
+
*
|
|
23
|
+
* {@link make} turns a parser over strings into a schema that decodes a
|
|
24
|
+
* `string` into the parser's output. Parse errors become schema issues
|
|
25
|
+
* carrying the rendered `ParseError` message. Encoding needs a printer,
|
|
26
|
+
* supplied as `encode`; without one, encoding is forbidden.
|
|
27
|
+
*
|
|
28
|
+
* {@link toParser} goes the other way: it derives a parser from a schema,
|
|
29
|
+
* guided by annotations ({@link annotate}) that say how each part is
|
|
30
|
+
* written. {@link toPrinter} derives the printer that writes that text, and
|
|
31
|
+
* {@link fromSchema} wraps both as a schema.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* import { Schema } from "effect"
|
|
36
|
+
* import { Ascii, Parser, ParserSchema, Token } from "@thomasfosterau/effect-parse"
|
|
37
|
+
*
|
|
38
|
+
* const Version = ParserSchema.make(
|
|
39
|
+
* Parser.struct({
|
|
40
|
+
* major: Ascii.decUint,
|
|
41
|
+
* minor: Parser.preceded(Token.literal("."), Ascii.decUint)
|
|
42
|
+
* }),
|
|
43
|
+
* { encode: ({ major, minor }) => `${major}.${minor}` }
|
|
44
|
+
* )
|
|
45
|
+
*
|
|
46
|
+
* Schema.decodeUnknownSync(Version)("1.2") // { major: 1, minor: 2 }
|
|
47
|
+
* Schema.encodeSync(Version)({ major: 3, minor: 4 }) // "3.4"
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* @since 0.1.0
|
|
51
|
+
*/
|
|
52
|
+
/**
|
|
53
|
+
* The key of the annotation that says how a schema is written as text
|
|
54
|
+
* ({@link Annotation}), namespaced to this package so it cannot collide with
|
|
55
|
+
* another library's. {@link annotate} sets it.
|
|
56
|
+
*
|
|
57
|
+
* @since 0.3.0
|
|
58
|
+
* @category annotations
|
|
59
|
+
*/
|
|
60
|
+
const annotationKey = "@thomasfosterau/effect-parse/ParserSchema";
|
|
61
|
+
const encodeWith = (encode) => (value) => Result.succeed(encode(value));
|
|
62
|
+
const decodeWith = (target, parser, encode) => Schema.String.pipe(Schema.decodeTo(target, SchemaTransformation.transformEffect({
|
|
63
|
+
decode: (input, parseOptions) => {
|
|
64
|
+
const result = P.parse(parser, input);
|
|
65
|
+
return Result.isSuccess(result) ? Effect.succeed(result.success) : Effect.fail(new SchemaIssue.InvalidValue({ message: result.failure.message }, input, parseOptions));
|
|
66
|
+
},
|
|
67
|
+
encode: (value, parseOptions) => {
|
|
68
|
+
if (encode === void 0) return Effect.fail(new SchemaIssue.Forbidden({ message: "Encoding is not supported" }, value, parseOptions));
|
|
69
|
+
const result = encode(value);
|
|
70
|
+
return Result.isSuccess(result) ? Effect.succeed(result.success) : Effect.fail(result.failure);
|
|
71
|
+
}
|
|
72
|
+
})));
|
|
73
|
+
/**
|
|
74
|
+
* Creates a schema that decodes a string with `parser`, which must consume
|
|
75
|
+
* the whole string.
|
|
76
|
+
*
|
|
77
|
+
* @since 0.1.0
|
|
78
|
+
* @category constructors
|
|
79
|
+
*/
|
|
80
|
+
const make = (parser, options) => decodeWith(Schema.declare((_u) => true, options?.description !== void 0 ? { description: options.description } : void 0), parser, options?.encode === void 0 ? void 0 : encodeWith(options.encode));
|
|
81
|
+
/**
|
|
82
|
+
* Annotates a schema with how it is written as text, for {@link toParser}
|
|
83
|
+
* and {@link toPrinter}. The same as
|
|
84
|
+
* `schema.annotate({ [ParserSchema.annotationKey]: annotation })`, except
|
|
85
|
+
* that the parser's output must have the schema's type.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* import { Schema } from "effect"
|
|
90
|
+
* import { ParserSchema } from "@thomasfosterau/effect-parse"
|
|
91
|
+
*
|
|
92
|
+
* const Point = Schema.Struct({ x: Schema.Int, y: Schema.Int }).pipe(
|
|
93
|
+
* ParserSchema.annotate({ prefix: "(", separator: ",", suffix: ")" })
|
|
94
|
+
* )
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* @since 0.3.0
|
|
98
|
+
* @category annotations
|
|
99
|
+
*/
|
|
100
|
+
const annotate = /*#__PURE__*/ dual(2, (self, annotation) => self.annotate({ [annotationKey]: annotation }));
|
|
101
|
+
/**
|
|
102
|
+
* Derives a parser from a schema. The parser reads the text the schema's
|
|
103
|
+
* annotations ({@link Grammar}) describe and returns a value of the schema's
|
|
104
|
+
* type, which the schema's checks and transformations have already
|
|
105
|
+
* validated; their failures become parse errors whose `cause` is the
|
|
106
|
+
* `SchemaError`.
|
|
107
|
+
*
|
|
108
|
+
* Without an annotation, a schema is parsed as follows:
|
|
109
|
+
*
|
|
110
|
+
* - `String`: one or more characters up to the next separator, key
|
|
111
|
+
* separator, suffix or literal around it (to the end of the input at the
|
|
112
|
+
* top level).
|
|
113
|
+
* - `Number`: a decimal number (`Ascii.float`), or a decimal integer when
|
|
114
|
+
* the schema checks `isInt` (as `Schema.Int` does). `BigInt`: a decimal
|
|
115
|
+
* integer. `Boolean`: `true` or `false`. `Null`: `null`. `Undefined` and
|
|
116
|
+
* `Void`: nothing.
|
|
117
|
+
* - `Literal`, `Enum` and `TemplateLiteral`: their text.
|
|
118
|
+
* - `Struct` and `Tuple`: their fields or elements in order, between
|
|
119
|
+
* separators; an optional one may be left out along with its separator.
|
|
120
|
+
* `Array`: elements between separators.
|
|
121
|
+
* - `Record`, and `Struct` with a `keySeparator`: `key`, key separator,
|
|
122
|
+
* value entries in any order, between separators.
|
|
123
|
+
* - `Union`: the first member that parses, in order (literals longest
|
|
124
|
+
* first). `suspend`: recursion.
|
|
125
|
+
* - A transformation (a `Class`, `NumberFromString`, ...): its encoded side,
|
|
126
|
+
* decoded by the schema. A grammar on it with a separator applies to the
|
|
127
|
+
* encoded side.
|
|
128
|
+
*
|
|
129
|
+
* A parser that fails where the schema starts reports the schema's
|
|
130
|
+
* `identifier` annotation (or its `expected` annotation, unless that belongs
|
|
131
|
+
* to a check) as what it expected. Other declarations, `Unknown`, `Any`,
|
|
132
|
+
* symbols and `object` need a parser in their annotation; without one,
|
|
133
|
+
* `toParser` throws. Transformations must decode synchronously.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```ts
|
|
137
|
+
* import { Schema } from "effect"
|
|
138
|
+
* import { Parser, ParserSchema } from "@thomasfosterau/effect-parse"
|
|
139
|
+
*
|
|
140
|
+
* const Version = Schema.Struct({ major: Schema.Int, minor: Schema.Int }).pipe(
|
|
141
|
+
* ParserSchema.annotate({ separator: "." })
|
|
142
|
+
* )
|
|
143
|
+
*
|
|
144
|
+
* Parser.parse(ParserSchema.toParser(Version), "1.2") // Result.succeed({ major: 1, minor: 2 })
|
|
145
|
+
* ```
|
|
146
|
+
*
|
|
147
|
+
* @since 0.3.0
|
|
148
|
+
* @category derivation
|
|
149
|
+
*/
|
|
150
|
+
const toParser = (schema) => {
|
|
151
|
+
const ast = schema.ast;
|
|
152
|
+
const parser = derive(ast, initial());
|
|
153
|
+
return grammarOf(ast).whitespace === true ? P.delimited(Ascii.multispace0, parser, Ascii.multispace0) : parser;
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Derives a printer from a schema: it writes a value of the schema's type as
|
|
157
|
+
* the text {@link toParser} reads, following the same annotations, with no
|
|
158
|
+
* optional whitespace. Transformations encode the value first, and a
|
|
159
|
+
* {@link Grammar.print} replaces the derived printer.
|
|
160
|
+
*
|
|
161
|
+
* The printer fails with a `SchemaError` when the value is not of the
|
|
162
|
+
* schema's type, or cannot be written so that it reads back: a string that
|
|
163
|
+
* is empty or contains a character that would end it (a separator, a
|
|
164
|
+
* suffix), or a part annotated with a parser but no `print`. Where the
|
|
165
|
+
* grammar is ambiguous (an optional field followed by a field of the same
|
|
166
|
+
* type), the text can still read back as a different value;
|
|
167
|
+
* {@link fromSchema} checks that it doesn't.
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* ```ts
|
|
171
|
+
* import { Schema } from "effect"
|
|
172
|
+
* import { ParserSchema } from "@thomasfosterau/effect-parse"
|
|
173
|
+
*
|
|
174
|
+
* const Version = Schema.Struct({ major: Schema.Int, minor: Schema.Int }).pipe(
|
|
175
|
+
* ParserSchema.annotate({ separator: "." })
|
|
176
|
+
* )
|
|
177
|
+
*
|
|
178
|
+
* ParserSchema.toPrinter(Version)({ major: 1, minor: 2 }) // Result.succeed("1.2")
|
|
179
|
+
* ```
|
|
180
|
+
*
|
|
181
|
+
* @since 0.3.0
|
|
182
|
+
* @category derivation
|
|
183
|
+
*/
|
|
184
|
+
const toPrinter = (schema) => {
|
|
185
|
+
const validate = typeDecoder(schema.ast);
|
|
186
|
+
const print = printerOf(schema.ast, initial());
|
|
187
|
+
return (value) => {
|
|
188
|
+
const valid = validate(value);
|
|
189
|
+
if (Result.isFailure(valid)) return Result.fail(valid.failure);
|
|
190
|
+
const out = [];
|
|
191
|
+
const issue = print(valid.success, out);
|
|
192
|
+
return issue === void 0 ? Result.succeed(out.join("")) : Result.fail(schemaError(issue));
|
|
193
|
+
};
|
|
194
|
+
};
|
|
34
195
|
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
196
|
+
* Creates a schema that decodes a string with the parser {@link toParser}
|
|
197
|
+
* derives from `schema`, into `schema`'s type, and encodes with the printer
|
|
198
|
+
* {@link toPrinter} derives, or with `encode` when given. A derived printer's
|
|
199
|
+
* text is parsed back, and encoding fails unless it gives a value equivalent
|
|
200
|
+
* to the one encoded, so decoding what was encoded always gives it back.
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```ts
|
|
204
|
+
* import { Schema } from "effect"
|
|
205
|
+
* import { ParserSchema } from "@thomasfosterau/effect-parse"
|
|
206
|
+
*
|
|
207
|
+
* const Pair = ParserSchema.fromSchema(
|
|
208
|
+
* Schema.Tuple([Schema.String, Schema.Number]).pipe(ParserSchema.annotate({ separator: "=" }))
|
|
209
|
+
* )
|
|
210
|
+
*
|
|
211
|
+
* Schema.decodeUnknownSync(Pair)("width=10") // ["width", 10]
|
|
212
|
+
* Schema.encodeSync(Pair)(["height", 2]) // "height=2"
|
|
213
|
+
* ```
|
|
214
|
+
*
|
|
215
|
+
* @since 0.3.0
|
|
216
|
+
* @category constructors
|
|
217
|
+
*/
|
|
218
|
+
const fromSchema = (schema, options) => {
|
|
219
|
+
const parser = toParser(schema);
|
|
220
|
+
const encode = options?.encode;
|
|
221
|
+
return decodeWith(Schema.toType(schema), parser, encode === void 0 ? roundTrip(schema, parser) : encodeWith(encode));
|
|
222
|
+
};
|
|
223
|
+
/** Prints with the derived printer, then checks that the text reads back as the value. */
|
|
224
|
+
const roundTrip = (schema, parser) => {
|
|
225
|
+
const print = toPrinter(schema);
|
|
226
|
+
const equivalent = Schema.toEquivalence(Schema.toType(schema));
|
|
227
|
+
return (value) => Result.flatMap(Result.mapError(print(value), (error) => error.issue), (text) => {
|
|
228
|
+
const back = P.parse(parser, text);
|
|
229
|
+
return Result.isSuccess(back) && equivalent(back.success, value) ? Result.succeed(text) : Result.fail(new SchemaIssue.InvalidValue({ message: `The value is written ${describeLiteral(text)}, which does not read back as the value` }));
|
|
230
|
+
});
|
|
231
|
+
};
|
|
232
|
+
const initial = () => ({
|
|
233
|
+
stop: "",
|
|
234
|
+
whitespace: false,
|
|
235
|
+
inherit: void 0,
|
|
236
|
+
path: [],
|
|
237
|
+
recursions: []
|
|
238
|
+
});
|
|
239
|
+
/** The annotation `key` on `ast`, from its last check back to the node, as `Schema.resolveAnnotations` resolves it. */
|
|
240
|
+
const lookup = (ast, key) => {
|
|
241
|
+
const checks = ast.checks;
|
|
242
|
+
if (checks !== void 0) for (let i = checks.length - 1; i >= 0; i--) {
|
|
243
|
+
const value = checks[i].annotations?.[key];
|
|
244
|
+
if (value !== void 0) return value;
|
|
245
|
+
}
|
|
246
|
+
return ast.annotations?.[key];
|
|
247
|
+
};
|
|
248
|
+
const grammarOf = (ast) => {
|
|
249
|
+
const value = lookup(ast, annotationKey);
|
|
250
|
+
if (value === void 0) return {};
|
|
251
|
+
return P.isParser(value) ? { parser: value } : value;
|
|
252
|
+
};
|
|
253
|
+
const isAnnotated = (ast) => lookup(ast, annotationKey) !== void 0;
|
|
254
|
+
const withStop = (ctx, text) => {
|
|
255
|
+
if (text === void 0 || text === "" || ctx.stop.includes(text[0])) return ctx;
|
|
256
|
+
return {
|
|
257
|
+
...ctx,
|
|
258
|
+
stop: Arr.sort([...ctx.stop, text[0]], Order.String).join("")
|
|
259
|
+
};
|
|
260
|
+
};
|
|
261
|
+
const at = (ctx, key) => ({
|
|
262
|
+
...ctx,
|
|
263
|
+
inherit: void 0,
|
|
264
|
+
path: [...ctx.path, key]
|
|
265
|
+
});
|
|
266
|
+
const where = (ctx) => `[${ctx.path.map(String).join(", ")}]`;
|
|
267
|
+
const unsupported = (ast, ctx, what) => {
|
|
268
|
+
throw new TypeError(`ParserSchema: cannot derive a ${what} for ${ast._tag} at ${where(ctx)}; annotate it`);
|
|
269
|
+
};
|
|
270
|
+
/** The node's grammar, with what its transformation passed down, and the context inside it. */
|
|
271
|
+
const resolve = (ast, ctx) => {
|
|
272
|
+
const own = grammarOf(ast);
|
|
273
|
+
const inherit = ctx.inherit;
|
|
274
|
+
const grammar = inherit === void 0 ? own : {
|
|
275
|
+
...own,
|
|
276
|
+
separator: own.separator ?? inherit.separator,
|
|
277
|
+
keySeparator: own.keySeparator ?? inherit.keySeparator
|
|
278
|
+
};
|
|
279
|
+
const whitespace = grammar.whitespace ?? ctx.whitespace;
|
|
280
|
+
return {
|
|
281
|
+
grammar,
|
|
282
|
+
whitespace,
|
|
283
|
+
inner: {
|
|
284
|
+
...withStop(ctx, grammar.suffix),
|
|
285
|
+
whitespace,
|
|
286
|
+
inherit: void 0
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
};
|
|
290
|
+
/** The context of a transformation's encoded side, which the transformation's separators apply to. */
|
|
291
|
+
const encodedContext = (grammar, ctx) => ({
|
|
292
|
+
...ctx,
|
|
293
|
+
inherit: {
|
|
294
|
+
separator: grammar.separator,
|
|
295
|
+
keySeparator: grammar.keySeparator
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
/**
|
|
299
|
+
* The node with its chain of links ending at the type of its encoded side,
|
|
300
|
+
* which is what the encoded side is parsed into and printed from.
|
|
301
|
+
*/
|
|
302
|
+
const linkTarget = (ast, links) => {
|
|
303
|
+
const last = links[links.length - 1];
|
|
304
|
+
let target = SchemaAST.toType(ast);
|
|
305
|
+
for (const link of links) {
|
|
306
|
+
const source = link === last ? SchemaAST.toType(last.to) : link.to;
|
|
307
|
+
target = SchemaAST.decodeTo(source, target, link.transformation);
|
|
308
|
+
}
|
|
309
|
+
return target;
|
|
310
|
+
};
|
|
311
|
+
/**
|
|
312
|
+
* What is derived for a recursive schema: deriving it again in the same
|
|
313
|
+
* context gives the same result, so the derivation ends.
|
|
314
|
+
*/
|
|
315
|
+
const recursive = (ast, ctx, make) => {
|
|
316
|
+
const key = `${ctx.whitespace ? 1 : 0}${ctx.stop}`;
|
|
317
|
+
const known = ctx.recursions.find((r) => r.ast === ast && r.key === key);
|
|
318
|
+
if (known !== void 0) return known.derived;
|
|
319
|
+
const derived = make();
|
|
320
|
+
ctx.recursions.push({
|
|
321
|
+
ast,
|
|
322
|
+
key,
|
|
323
|
+
derived
|
|
324
|
+
});
|
|
325
|
+
return derived;
|
|
326
|
+
};
|
|
327
|
+
/** The context for the items of a sequence: unannotated strings also stop at the separator. */
|
|
328
|
+
const itemContext = (grammar, ctx) => withStop(ctx, grammar.separator);
|
|
329
|
+
/** The rest element of an array schema, if any; elements after it are not supported. */
|
|
330
|
+
const restOf = (ast, ctx, what) => {
|
|
331
|
+
const [head, ...after] = ast.rest;
|
|
332
|
+
return Arr.isReadonlyArrayNonEmpty(after) ? unsupported(ast, ctx, what) : head;
|
|
333
|
+
};
|
|
334
|
+
/** The contexts of an array schema's elements and its rest. */
|
|
335
|
+
const elementContexts = (ast, rest, grammar, ctx) => {
|
|
336
|
+
const n = ast.elements.length;
|
|
337
|
+
return ast.elements.map((_, i) => at(i < n - 1 || rest !== void 0 ? itemContext(grammar, ctx) : ctx, i));
|
|
338
|
+
};
|
|
339
|
+
/** The contexts of a struct's fields, written in order. */
|
|
340
|
+
const fieldContexts = (ast, grammar, ctx) => {
|
|
341
|
+
const fields = ast.propertySignatures;
|
|
342
|
+
return fields.map((field, i) => at(i < fields.length - 1 ? itemContext(grammar, ctx) : ctx, field.name));
|
|
343
|
+
};
|
|
344
|
+
const isKeyed = (ast, grammar) => grammar.keySeparator !== void 0 || Arr.isReadonlyArrayNonEmpty(ast.indexSignatures);
|
|
345
|
+
const keySeparatorOf = (grammar, ctx) => {
|
|
346
|
+
if (grammar.keySeparator === void 0) throw new TypeError(`ParserSchema: a record at ${where(ctx)} needs a keySeparator in its annotation`);
|
|
347
|
+
return grammar.keySeparator;
|
|
348
|
+
};
|
|
349
|
+
/** The field name of a struct in keyed form: a string. */
|
|
350
|
+
const keyName = (ast, field, ctx, what) => Predicate.isString(field.name) ? field.name : unsupported(ast, ctx, what);
|
|
351
|
+
const propertyName = (key) => Predicate.isNumber(key) ? String(key) : key;
|
|
352
|
+
/** Whether a check (as `Schema.Int` has) restricts numbers to integers. */
|
|
353
|
+
const isIntegral = (checks) => checks !== void 0 && checks.some((check) => check instanceof SchemaAST.FilterGroup ? isIntegral(check.checks) : check.annotations?.arbitraryConstraint?.["number"] === "integer");
|
|
354
|
+
/** Literals with the longest text first, so a literal is never cut short by one it starts with. */
|
|
355
|
+
const byLength = (values) => Arr.sort(values, Order.mapInput(Order.Number, (value) => -String(value).length));
|
|
356
|
+
const isPlainLiteral = (ast) => SchemaAST.isLiteral(ast) && ast.encoding === void 0 && !isAnnotated(ast);
|
|
357
|
+
/** The literals of a union of plain literals, longest first, or `undefined`. */
|
|
358
|
+
const plainLiterals = (ast) => {
|
|
359
|
+
const literals = Arr.filter(ast.types, isPlainLiteral);
|
|
360
|
+
return literals.length === ast.types.length ? byLength(literals.map((member) => member.literal)) : void 0;
|
|
361
|
+
};
|
|
362
|
+
const schemaError = (issue) => new Schema.SchemaError(issue);
|
|
363
|
+
const decoder = (schema) => Schema.decodeUnknownResult(schema);
|
|
364
|
+
/** Decodes into the type of `ast`, checks included. */
|
|
365
|
+
const typeDecoder = (ast) => decoder(Schema.make(SchemaAST.toType(ast)));
|
|
366
|
+
const messageOf = (cause) => Predicate.isString(cause) ? cause : Predicate.hasProperty(cause, "message") && Predicate.isString(cause.message) ? cause.message : String(cause);
|
|
367
|
+
/** A parser whose failures' causes are `SchemaError`s, as {@link toParser}'s are. */
|
|
368
|
+
const asSchemaErrors = (parser) => P.mapError(parser, (cause) => cause instanceof Schema.SchemaError ? cause : schemaError(new SchemaIssue.InvalidValue({ message: messageOf(cause) })));
|
|
369
|
+
/** Literal text, with optional whitespace around it. */
|
|
370
|
+
const text = (s, ws) => ws ? P.delimited(Ascii.multispace0, Token.literal(s), Ascii.multispace0) : Token.literal(s);
|
|
371
|
+
const derive = (ast, ctx) => {
|
|
372
|
+
const { grammar, inner, whitespace } = resolve(ast, ctx);
|
|
373
|
+
let parser;
|
|
374
|
+
if (grammar.parser !== void 0) parser = P.tryMap(asSchemaErrors(grammar.parser), typeDecoder(ast));
|
|
375
|
+
else if (ast.encoding !== void 0) parser = decoded(ast, ast.encoding, grammar, inner);
|
|
376
|
+
else parser = checked(ast, structural(ast, grammar, inner));
|
|
377
|
+
if (grammar.prefix !== void 0) parser = P.preceded(text(grammar.prefix, whitespace), parser);
|
|
378
|
+
if (grammar.suffix !== void 0) parser = P.terminated(parser, text(grammar.suffix, whitespace));
|
|
379
|
+
const label = lookup(ast, "identifier") ?? ast.annotations?.expected;
|
|
380
|
+
return Predicate.isString(label) ? P.expected(parser, label) : parser;
|
|
381
|
+
};
|
|
382
|
+
/** Runs the checks on `ast` (its filters) on the parser's output. */
|
|
383
|
+
const checked = (ast, parser) => ast.checks === void 0 ? parser : P.tryMap(parser, decoder(Schema.Unknown.check(...ast.checks)));
|
|
384
|
+
/**
|
|
385
|
+
* Parses the encoded side of a transformation and decodes it. The encoded
|
|
386
|
+
* side is parsed into its own type, so the last link decodes from that type
|
|
387
|
+
* rather than from the fully encoded form.
|
|
388
|
+
*/
|
|
389
|
+
const decoded = (ast, links, grammar, ctx) => {
|
|
390
|
+
const from = derive(links[links.length - 1].to, encodedContext(grammar, ctx));
|
|
391
|
+
return P.tryMap(from, decoder(Schema.make(linkTarget(ast, links))));
|
|
392
|
+
};
|
|
393
|
+
const structural = (ast, grammar, ctx) => Match.valueTags(ast, {
|
|
394
|
+
String: () => unannotatedString(ctx, 1),
|
|
395
|
+
Number: (number) => isIntegral(number.checks) ? integer : Ascii.float,
|
|
396
|
+
BigInt: () => Ascii.decBigInt,
|
|
397
|
+
Boolean: () => P.alt(P.as(Token.literal("true"), true), P.as(Token.literal("false"), false)),
|
|
398
|
+
Null: () => P.as(Token.literal("null"), null),
|
|
399
|
+
Undefined: () => P.succeed(void 0),
|
|
400
|
+
Void: () => P.succeed(void 0),
|
|
401
|
+
Never: () => P.reject,
|
|
402
|
+
Literal: (lit) => literal(lit.literal),
|
|
403
|
+
Enum: (e) => P.alt(...byLength(e.enums.map(([, value]) => value)).map(literal)),
|
|
404
|
+
TemplateLiteral: (template) => templateLiteral(template, ctx),
|
|
405
|
+
Arrays: (arrays) => sequenceOf(arrays, grammar, ctx),
|
|
406
|
+
Objects: (objects) => isKeyed(objects, grammar) ? keyed(objects, grammar, ctx) : positional(objects, grammar, ctx),
|
|
407
|
+
Union: (union) => alternatives(union, ctx),
|
|
408
|
+
Suspend: (suspend) => recursive(suspend, ctx, () => P.suspend(() => derive(suspend.thunk(), ctx))),
|
|
409
|
+
Declaration: (other) => unsupported(other, ctx, "parser"),
|
|
410
|
+
Unknown: (other) => unsupported(other, ctx, "parser"),
|
|
411
|
+
Any: (other) => unsupported(other, ctx, "parser"),
|
|
412
|
+
Symbol: (other) => unsupported(other, ctx, "parser"),
|
|
413
|
+
UniqueSymbol: (other) => unsupported(other, ctx, "parser"),
|
|
414
|
+
ObjectKeyword: (other) => unsupported(other, ctx, "parser")
|
|
415
|
+
});
|
|
416
|
+
const unannotatedString = (ctx, min) => {
|
|
417
|
+
const s = Token.takeTill(ctx.stop, { min });
|
|
418
|
+
return ctx.whitespace ? P.preceded(Ascii.multispace0, P.map(s, (value) => value.trimEnd())) : s;
|
|
419
|
+
};
|
|
420
|
+
const integer = /*#__PURE__*/ asSchemaErrors(Ascii.decInt);
|
|
421
|
+
const literal = (value) => P.as(Token.literal(String(value)), value);
|
|
422
|
+
const templateLiteral = (ast, ctx) => P.taken(P.seq(...ast.parts.map((part, i) => {
|
|
423
|
+
const next = ast.parts[i + 1];
|
|
424
|
+
const partCtx = next !== void 0 && SchemaAST.isLiteral(next) ? withStop(ctx, String(next.literal)) : ctx;
|
|
425
|
+
return SchemaAST.isString(part) && !isAnnotated(part) ? unannotatedString(partCtx, 0) : derive(part, at(partCtx, i));
|
|
426
|
+
})));
|
|
427
|
+
const alternatives = (ast, ctx) => {
|
|
428
|
+
const literals = plainLiterals(ast);
|
|
429
|
+
return literals !== void 0 ? P.alt(...literals.map(literal)) : P.alt(...ast.types.map((member) => derive(member, ctx)));
|
|
430
|
+
};
|
|
431
|
+
/**
|
|
432
|
+
* Parses `items` in order, then any number of `rest`, with `separator`
|
|
433
|
+
* between the ones present. The output has the value of item `i` at index
|
|
434
|
+
* `i` (a hole when an optional item is missing), then the rest.
|
|
435
|
+
*/
|
|
436
|
+
const sequence = (items, rest, separator) => {
|
|
437
|
+
const n = items.length;
|
|
438
|
+
const tail = (needSeparator) => {
|
|
439
|
+
if (rest === void 0) return P.sync(() => new Array(n));
|
|
440
|
+
const values = separator === void 0 ? P.repeat(rest) : needSeparator ? P.repeat(P.preceded(separator, rest)) : P.separated(rest, separator);
|
|
441
|
+
return P.map(values, (xs) => {
|
|
442
|
+
const out = new Array(n);
|
|
443
|
+
for (let j = 0; j < xs.length; j++) out[n + j] = xs[j];
|
|
444
|
+
return out;
|
|
445
|
+
});
|
|
446
|
+
};
|
|
447
|
+
const memo = [];
|
|
448
|
+
const from = (i, needSeparator) => {
|
|
449
|
+
const key = i * 2 + (needSeparator ? 1 : 0);
|
|
450
|
+
const known = memo[key];
|
|
451
|
+
if (known !== void 0) return known;
|
|
452
|
+
let parser;
|
|
453
|
+
if (i === n) parser = tail(needSeparator);
|
|
454
|
+
else {
|
|
455
|
+
const item = items[i];
|
|
456
|
+
const head = needSeparator && separator !== void 0 ? P.preceded(separator, item.parser) : item.parser;
|
|
457
|
+
const present = P.map(P.seq(head, from(i + 1, true)), ([value, out]) => {
|
|
458
|
+
out[i] = value;
|
|
459
|
+
return out;
|
|
460
|
+
});
|
|
461
|
+
parser = item.optional ? P.alt(present, from(i + 1, needSeparator)) : present;
|
|
462
|
+
}
|
|
463
|
+
memo[key] = parser;
|
|
464
|
+
return parser;
|
|
465
|
+
};
|
|
466
|
+
return from(0, false);
|
|
467
|
+
};
|
|
468
|
+
const separatorOf = (grammar, ctx) => grammar.separator === void 0 ? void 0 : text(grammar.separator, ctx.whitespace);
|
|
469
|
+
const sequenceOf = (ast, grammar, ctx) => {
|
|
470
|
+
const head = restOf(ast, ctx, "parser");
|
|
471
|
+
const contexts = elementContexts(ast, head, grammar, ctx);
|
|
472
|
+
const items = ast.elements.map((element, i) => ({
|
|
473
|
+
parser: derive(element, contexts[i]),
|
|
474
|
+
optional: SchemaAST.isOptional(element)
|
|
475
|
+
}));
|
|
476
|
+
const rest = head === void 0 ? void 0 : derive(head, at(itemContext(grammar, ctx), items.length));
|
|
477
|
+
return P.map(sequence(items, rest, separatorOf(grammar, ctx)), (out) => out.filter((_, i) => i in out));
|
|
478
|
+
};
|
|
479
|
+
const positional = (ast, grammar, ctx) => {
|
|
480
|
+
const fields = ast.propertySignatures;
|
|
481
|
+
const contexts = fieldContexts(ast, grammar, ctx);
|
|
482
|
+
const items = fields.map((field, i) => ({
|
|
483
|
+
parser: derive(field.type, contexts[i]),
|
|
484
|
+
optional: SchemaAST.isOptional(field.type)
|
|
485
|
+
}));
|
|
486
|
+
return P.map(sequence(items, void 0, separatorOf(grammar, ctx)), (out) => Record.fromEntries(fields.flatMap((field, i) => i in out ? [[propertyName(field.name), out[i]]] : [])));
|
|
487
|
+
};
|
|
488
|
+
const keyed = (ast, grammar, ctx) => {
|
|
489
|
+
const keySeparator = keySeparatorOf(grammar, ctx);
|
|
490
|
+
const between = text(keySeparator, ctx.whitespace);
|
|
491
|
+
const valueCtx = itemContext(grammar, ctx);
|
|
492
|
+
const known = ast.propertySignatures.map((field) => {
|
|
493
|
+
const name = keyName(ast, field, ctx, "parser");
|
|
494
|
+
const value = derive(field.type, at(valueCtx, name));
|
|
495
|
+
return P.map(P.preceded(P.seq(Token.literal(name), between), value), (v) => [
|
|
496
|
+
name,
|
|
497
|
+
v,
|
|
498
|
+
true
|
|
499
|
+
]);
|
|
500
|
+
});
|
|
501
|
+
const indexed = ast.indexSignatures.map((signature) => {
|
|
502
|
+
const key = derive(signature.parameter, at(withStop(ctx, keySeparator), "<key>"));
|
|
503
|
+
const value = derive(signature.type, at(valueCtx, "<value>"));
|
|
504
|
+
return P.map(P.separatedPair(key, between, value), ([k, v]) => [
|
|
505
|
+
propertyName(k),
|
|
506
|
+
v,
|
|
507
|
+
false
|
|
508
|
+
]);
|
|
509
|
+
});
|
|
510
|
+
const entry = P.alt(...known, ...indexed);
|
|
511
|
+
const separator = separatorOf(grammar, ctx);
|
|
512
|
+
const entries = separator === void 0 ? P.repeat(entry) : P.separated(entry, separator);
|
|
513
|
+
const missing = ast.propertySignatures.map((field) => P.expected(P.reject, describeLiteral(`${String(field.name)}${keySeparator}`)));
|
|
514
|
+
return P.flatMap(P.tryMap(entries, (es) => assemble(ast, es)), ([value, absent]) => absent === void 0 ? P.succeed(value) : missing[absent]);
|
|
515
|
+
};
|
|
516
|
+
/**
|
|
517
|
+
* Builds the object from its entries, declared fields first, rejecting
|
|
518
|
+
* duplicate keys. Also returns the index of the first required field
|
|
519
|
+
* missing, if any.
|
|
520
|
+
*/
|
|
521
|
+
const assemble = (ast, entries) => {
|
|
522
|
+
const values = MutableHashMap.empty();
|
|
523
|
+
for (const [key, value] of entries) {
|
|
524
|
+
if (MutableHashMap.has(values, key)) return Result.fail(schemaError(new SchemaIssue.Pointer([key], new SchemaIssue.InvalidValue({ message: "Duplicate key" }))));
|
|
525
|
+
MutableHashMap.set(values, key, value);
|
|
526
|
+
}
|
|
527
|
+
const out = [];
|
|
528
|
+
let absent;
|
|
529
|
+
ast.propertySignatures.forEach((field, i) => {
|
|
530
|
+
const name = propertyName(field.name);
|
|
531
|
+
const value = MutableHashMap.get(values, name);
|
|
532
|
+
if (Option.isSome(value)) out.push([name, value.value]);
|
|
533
|
+
else if (absent === void 0 && !SchemaAST.isOptional(field.type)) absent = i;
|
|
534
|
+
});
|
|
535
|
+
for (const [key, value, isKnown] of entries) if (!isKnown) out.push([key, value]);
|
|
536
|
+
return Result.succeed([Record.fromEntries(out), absent]);
|
|
537
|
+
};
|
|
538
|
+
/** The issue for a value that cannot be written at the node `ctx` is at. */
|
|
539
|
+
const unwritable = (ctx, message, value) => {
|
|
540
|
+
const issue = new SchemaIssue.InvalidValue({ message }, value, { reportInput: true });
|
|
541
|
+
return Arr.isReadonlyArrayNonEmpty(ctx.path) ? new SchemaIssue.Pointer(ctx.path, issue) : issue;
|
|
542
|
+
};
|
|
543
|
+
const writing = (show) => (value, out) => {
|
|
544
|
+
out.push(show(value));
|
|
545
|
+
};
|
|
546
|
+
const nothing = (_, __) => void 0;
|
|
547
|
+
const printerOf = (ast, ctx) => {
|
|
548
|
+
const { grammar, inner } = resolve(ast, ctx);
|
|
549
|
+
let print;
|
|
550
|
+
if (grammar.print !== void 0) print = writing(grammar.print);
|
|
551
|
+
else if (grammar.parser !== void 0) print = unprintable(ctx);
|
|
552
|
+
else if (ast.encoding !== void 0) print = encoded(ast, ast.encoding, grammar, inner);
|
|
553
|
+
else print = printerFor(ast, grammar, inner);
|
|
554
|
+
const prefix = grammar.prefix;
|
|
555
|
+
const suffix = grammar.suffix;
|
|
556
|
+
if (prefix === void 0 && suffix === void 0) return print;
|
|
557
|
+
const inside = print;
|
|
558
|
+
return (value, out) => {
|
|
559
|
+
if (prefix !== void 0) out.push(prefix);
|
|
560
|
+
const issue = inside(value, out);
|
|
561
|
+
if (suffix !== void 0) out.push(suffix);
|
|
562
|
+
return issue;
|
|
563
|
+
};
|
|
564
|
+
};
|
|
565
|
+
const unprintable = (ctx) => (value) => unwritable(ctx, "This value has a parser in its annotation but no print, so it cannot be written", value);
|
|
566
|
+
/** Encodes a transformation's value and prints its encoded side. */
|
|
567
|
+
const encoded = (ast, links, grammar, ctx) => {
|
|
568
|
+
const to = printerOf(links[links.length - 1].to, encodedContext(grammar, ctx));
|
|
569
|
+
const encode = Schema.encodeUnknownResult(Schema.make(linkTarget(ast, links)));
|
|
570
|
+
return (value, out) => {
|
|
571
|
+
const result = encode(value);
|
|
572
|
+
return Result.isSuccess(result) ? to(result.success, out) : result.failure.issue;
|
|
573
|
+
};
|
|
574
|
+
};
|
|
575
|
+
const printerFor = (ast, grammar, ctx) => Match.valueTags(ast, {
|
|
576
|
+
String: () => stringPrinter(ctx),
|
|
577
|
+
Number: () => writing(String),
|
|
578
|
+
BigInt: () => writing(String),
|
|
579
|
+
Boolean: () => writing(String),
|
|
580
|
+
Null: () => writing(String),
|
|
581
|
+
Undefined: () => nothing,
|
|
582
|
+
Void: () => nothing,
|
|
583
|
+
Never: () => nothing,
|
|
584
|
+
Literal: () => writing(String),
|
|
585
|
+
Enum: () => writing(String),
|
|
586
|
+
TemplateLiteral: () => writing(String),
|
|
587
|
+
Arrays: (arrays) => arraysPrinter(arrays, grammar, ctx),
|
|
588
|
+
Objects: (objects) => isKeyed(objects, grammar) ? keyedPrinter(objects, grammar, ctx) : positionalPrinter(objects, grammar, ctx),
|
|
589
|
+
Union: (union) => unionPrinter(union, ctx),
|
|
590
|
+
Suspend: (suspend) => recursive(suspend, ctx, () => {
|
|
591
|
+
let print;
|
|
592
|
+
return (value, out) => (print ??= printerOf(suspend.thunk(), ctx))(value, out);
|
|
593
|
+
}),
|
|
594
|
+
Declaration: (other) => unsupported(other, ctx, "printer"),
|
|
595
|
+
Unknown: (other) => unsupported(other, ctx, "printer"),
|
|
596
|
+
Any: (other) => unsupported(other, ctx, "printer"),
|
|
597
|
+
Symbol: (other) => unsupported(other, ctx, "printer"),
|
|
598
|
+
UniqueSymbol: (other) => unsupported(other, ctx, "printer"),
|
|
599
|
+
ObjectKeyword: (other) => unsupported(other, ctx, "printer")
|
|
600
|
+
});
|
|
601
|
+
/**
|
|
602
|
+
* Writes a string as an unannotated string is parsed: refusing one that
|
|
603
|
+
* would not read back, being empty or holding a character that ends it.
|
|
604
|
+
*/
|
|
605
|
+
const stringPrinter = (ctx) => (value, out) => {
|
|
606
|
+
if (value === "") return unwritable(ctx, "An empty string cannot be written here", value);
|
|
607
|
+
const stop = Arr.findFirst(Array.from(ctx.stop), (c) => value.includes(c));
|
|
608
|
+
if (Option.isSome(stop)) return unwritable(ctx, `A string containing ${describeLiteral(stop.value)} cannot be written here`, value);
|
|
609
|
+
if (ctx.whitespace && value.trim() !== value) return unwritable(ctx, "A string with whitespace around it cannot be written here", value);
|
|
610
|
+
out.push(value);
|
|
611
|
+
};
|
|
612
|
+
/** Writes values with a separator between them, stopping at the first that cannot be written. */
|
|
613
|
+
const writeAll = (writes, separator, out) => {
|
|
614
|
+
for (let i = 0; i < writes.length; i++) {
|
|
615
|
+
if (i > 0) out.push(separator);
|
|
616
|
+
const issue = writes[i](out);
|
|
617
|
+
if (issue !== void 0) return issue;
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
const arraysPrinter = (ast, grammar, ctx) => {
|
|
621
|
+
const head = restOf(ast, ctx, "printer");
|
|
622
|
+
const contexts = elementContexts(ast, head, grammar, ctx);
|
|
623
|
+
const elements = ast.elements.map((element, i) => printerOf(element, contexts[i]));
|
|
624
|
+
const rest = head === void 0 ? nothing : printerOf(head, at(itemContext(grammar, ctx), elements.length));
|
|
625
|
+
const separator = grammar.separator ?? "";
|
|
626
|
+
return (value, out) => writeAll(value.map((element, i) => (o) => (elements[i] ?? rest)(element, o)), separator, out);
|
|
627
|
+
};
|
|
628
|
+
const positionalPrinter = (ast, grammar, ctx) => {
|
|
629
|
+
const contexts = fieldContexts(ast, grammar, ctx);
|
|
630
|
+
const fields = ast.propertySignatures.map((field, i) => ({
|
|
631
|
+
name: propertyName(field.name),
|
|
632
|
+
print: printerOf(field.type, contexts[i])
|
|
633
|
+
}));
|
|
634
|
+
const separator = grammar.separator ?? "";
|
|
635
|
+
return (value, out) => writeAll(fields.filter((field) => Record.has(value, field.name)).map((field) => (o) => field.print(value[field.name], o)), separator, out);
|
|
636
|
+
};
|
|
637
|
+
const keyedPrinter = (ast, grammar, ctx) => {
|
|
638
|
+
const keySeparator = keySeparatorOf(grammar, ctx);
|
|
639
|
+
const valueCtx = itemContext(grammar, ctx);
|
|
640
|
+
const fields = ast.propertySignatures.map((field) => {
|
|
641
|
+
const name = keyName(ast, field, ctx, "printer");
|
|
642
|
+
return {
|
|
643
|
+
name,
|
|
644
|
+
print: printerOf(field.type, at(valueCtx, name))
|
|
645
|
+
};
|
|
646
|
+
});
|
|
647
|
+
const names = fields.map((field) => field.name);
|
|
648
|
+
const indexed = ast.indexSignatures.map((signature) => ({
|
|
649
|
+
is: Schema.is(Schema.make(SchemaAST.toType(signature.parameter))),
|
|
650
|
+
key: printerOf(signature.parameter, at(withStop(ctx, keySeparator), "<key>")),
|
|
651
|
+
value: printerOf(signature.type, at(valueCtx, "<value>"))
|
|
652
|
+
}));
|
|
653
|
+
const entry = (key, print, k, v) => (out) => key(k, out) ?? (out.push(keySeparator), print(v, out));
|
|
654
|
+
return (value, out) => {
|
|
655
|
+
const writes = fields.filter((field) => Record.has(value, field.name)).map((field) => entry(writing(String), field.print, field.name, value[field.name]));
|
|
656
|
+
for (const key of Record.keys(value)) {
|
|
657
|
+
const signature = names.includes(key) ? void 0 : indexed.find((s) => s.is(key));
|
|
658
|
+
if (signature !== void 0) writes.push(entry(signature.key, signature.value, key, value[key]));
|
|
659
|
+
}
|
|
660
|
+
return writeAll(writes, grammar.separator ?? "", out);
|
|
661
|
+
};
|
|
662
|
+
};
|
|
663
|
+
/** Writes a union's value with the first member it belongs to that can write it. */
|
|
664
|
+
const unionPrinter = (ast, ctx) => {
|
|
665
|
+
if (plainLiterals(ast) !== void 0) return writing(String);
|
|
666
|
+
const members = ast.types.map((member) => ({
|
|
667
|
+
is: Schema.is(Schema.make(SchemaAST.toType(member))),
|
|
668
|
+
print: printerOf(member, ctx)
|
|
669
|
+
}));
|
|
670
|
+
return (value, out) => {
|
|
671
|
+
let issue;
|
|
672
|
+
for (const member of members) {
|
|
673
|
+
if (!member.is(value)) continue;
|
|
674
|
+
const mark = out.length;
|
|
675
|
+
issue = member.print(value, out);
|
|
676
|
+
if (issue === void 0) return void 0;
|
|
677
|
+
out.length = mark;
|
|
678
|
+
}
|
|
679
|
+
return issue ?? unwritable(ctx, "No member of the union can write this value", value);
|
|
680
|
+
};
|
|
55
681
|
};
|
|
682
|
+
//#endregion
|
|
683
|
+
export { annotate, annotationKey, fromSchema, make, toParser, toPrinter };
|
|
684
|
+
|
|
56
685
|
//# sourceMappingURL=ParserSchema.js.map
|