@endevops/effect-codec-xml 0.1.0-beta.1 → 0.1.0-effect.1
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 +62 -63
- package/dist/codec.d.ts +9 -17
- package/dist/codec.d.ts.map +1 -1
- package/dist/codec.js +16 -25
- package/dist/codec.js.map +1 -1
- package/dist/conventions.d.ts +4 -4
- package/dist/conventions.js +7 -7
- package/dist/conventions.js.map +1 -1
- package/dist/entities/entity-decoder.d.ts +33 -33
- package/dist/entities/entity-decoder.d.ts.map +1 -1
- package/dist/entities/entity-decoder.js +64 -63
- package/dist/entities/entity-decoder.js.map +1 -1
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +2 -2
- package/dist/errors.js.map +1 -1
- package/dist/namespaces.js +2 -2
- package/dist/namespaces.js.map +1 -1
- package/dist/naming.d.ts +6 -6
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +3 -3
- package/dist/naming.js.map +1 -1
- package/dist/parse.d.ts +5 -5
- package/dist/parse.js +14 -14
- package/dist/parse.js.map +1 -1
- package/dist/render.d.ts +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +28 -28
- package/dist/render.js.map +1 -1
- package/dist/xml-error.d.ts +9 -9
- package/dist/xml-error.js +18 -18
- package/dist/xml-error.js.map +1 -1
- package/dist/xml-value.d.ts +7 -7
- package/dist/xml-value.d.ts.map +1 -1
- package/dist/xml-value.js +7 -6
- package/dist/xml-value.js.map +1 -1
- package/package.json +1 -1
- package/src/codec.ts +71 -91
- package/src/conventions.ts +7 -7
- package/src/entities/entity-decoder.ts +99 -98
- package/src/errors.ts +3 -3
- package/src/index.ts +3 -3
- package/src/namespaces.ts +16 -16
- package/src/naming.ts +35 -34
- package/src/parse.ts +26 -26
- package/src/render.ts +44 -44
- package/src/xml-error.ts +18 -18
- package/src/xml-value.ts +11 -10
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
# @endevops/effect-codec
|
|
1
|
+
# @endevops/effect-xml-codec
|
|
2
2
|
|
|
3
3
|
A round-trip Effect Schema codec for XML. `toCodecXml(schema)` returns a
|
|
4
4
|
`Schema` whose `Encoded` is XML text, so `Schema.encodeSync` writes a document
|
|
5
|
-
and `Schema.decodeSync` reads one back,
|
|
6
|
-
|
|
5
|
+
and `Schema.decodeSync` reads one back, the way `Schema.toCodecJson` works for
|
|
6
|
+
JSON. There is no second call to a renderer or a parser at the call site.
|
|
7
7
|
`renderXml` and `parseXml` are the text layer underneath, and remain available
|
|
8
8
|
on their own.
|
|
9
9
|
|
|
10
10
|
```typescript
|
|
11
11
|
import { Schema } from 'effect';
|
|
12
|
-
import { toCodecXml } from '@endevops/effect-codec
|
|
12
|
+
import { toCodecXml } from '@endevops/effect-xml-codec';
|
|
13
13
|
|
|
14
14
|
const Book = Schema.Struct({ '@id': Schema.String, title: Schema.String, pages: Schema.Number, tag: Schema.Array(Schema.String) });
|
|
15
15
|
|
|
@@ -27,7 +27,7 @@ Schema.decodeSync(codec)('<book id="1"><title>Dune</title><pages>412</pages><tag
|
|
|
27
27
|
|
|
28
28
|
Effect 4 ships `Schema.toEncoderXml`, and it is one-way: a value goes in, an XML
|
|
29
29
|
string comes out, and there is no way back. It also has no notion of an
|
|
30
|
-
attribute
|
|
30
|
+
attribute — a field named `@id` becomes an element called `<@id>` with its name
|
|
31
31
|
rewritten.
|
|
32
32
|
|
|
33
33
|
This package pairs Effect's own XML-value derivation, `Schema.toCodecStringTree`,
|
|
@@ -37,8 +37,8 @@ the text layer and the `@`/`#text` conventions.
|
|
|
37
37
|
|
|
38
38
|
## The mapping
|
|
39
39
|
|
|
40
|
-
An `XmlValue` is the plainest value that can hold a document
|
|
41
|
-
and records.
|
|
40
|
+
An `XmlValue` is the plainest value that can hold a document — strings, arrays
|
|
41
|
+
and records. What gives it XML meaning is two key conventions:
|
|
42
42
|
|
|
43
43
|
| In the value | In the document |
|
|
44
44
|
| ----------------------- | ------------------------------------- |
|
|
@@ -101,7 +101,7 @@ The annotation is attached with `Schema.annotate`:
|
|
|
101
101
|
|
|
102
102
|
```typescript
|
|
103
103
|
import { Schema } from 'effect';
|
|
104
|
-
import { toCodecXml } from '@endevops/effect-codec
|
|
104
|
+
import { toCodecXml } from '@endevops/effect-xml-codec';
|
|
105
105
|
|
|
106
106
|
const SOAP = 'http://schemas.xmlsoap.org/soap/envelope/';
|
|
107
107
|
const AUTH = 'urn:auth';
|
|
@@ -163,9 +163,9 @@ Two limits:
|
|
|
163
163
|
| `XmlError` | The failure `EntityDecoder` and `validate` report. |
|
|
164
164
|
|
|
165
165
|
`toCodecXml` returns a `Schema`, so encoding and decoding are `Schema.encodeSync`
|
|
166
|
-
and `Schema.decodeSync` (or the `Effect` forms)
|
|
167
|
-
|
|
168
|
-
|
|
166
|
+
and `Schema.decodeSync` (or the `Effect` forms), and every other Schema operation
|
|
167
|
+
— `Schema.toFormatter`, `Schema.toJsonSchemaDocument`, the guards — applies to it
|
|
168
|
+
unchanged. A failure in either direction arrives as a `SchemaIssue.Issue`: a
|
|
169
169
|
document that will not parse or a value that will not write is reported with its
|
|
170
170
|
underlying XML message, alongside the schema mismatches Effect already reports.
|
|
171
171
|
The root element is named from the `rootName` option, then the schema's
|
|
@@ -176,21 +176,21 @@ The root element is named from the `rootName` option, then the schema's
|
|
|
176
176
|
This package absorbed `@endevops/common-xml`, so the two primitives the text
|
|
177
177
|
layer reads documents with ship here rather than as a sibling dependency.
|
|
178
178
|
|
|
179
|
-
`EntityDecoder` expands the reference syntax XML inherits from HTML
|
|
180
|
-
`&#NNN;`, `&#xHH
|
|
179
|
+
`EntityDecoder` expands the reference syntax XML inherits from HTML — `&name;`,
|
|
180
|
+
`&#NNN;`, `&#xHH;` — with expansion limits, registration hooks, and a
|
|
181
181
|
numeric-reference policy. `parseXml` uses it for character data and falls back
|
|
182
182
|
to the raw text for a bare `&`. It reports failures as `XmlError`, a tagged
|
|
183
|
-
error whose `reason` union is
|
|
183
|
+
error whose `reason` union is what `Effect.catchReason` narrows on.
|
|
184
184
|
|
|
185
|
-
The name validators are plain synchronous predicates
|
|
186
|
-
`isQName`, `isNmToken`, `isNmTokens`
|
|
185
|
+
The name validators are plain synchronous predicates — `isName`, `isNcName`,
|
|
186
|
+
`isQName`, `isNmToken`, `isNmTokens` — because a regex test cannot fail and the
|
|
187
187
|
renderer and parser call them per name in their hot loops. `sanitize` rewrites
|
|
188
188
|
an illegal name into the nearest legal one, and `validate` reports why a name
|
|
189
189
|
failed through an `Effect`.
|
|
190
190
|
|
|
191
191
|
```typescript
|
|
192
192
|
import { Effect } from 'effect';
|
|
193
|
-
import { EntityDecoder, isQName, sanitize, validate } from '@endevops/effect-codec
|
|
193
|
+
import { EntityDecoder, isQName, sanitize, validate } from '@endevops/effect-xml-codec';
|
|
194
194
|
|
|
195
195
|
isQName('svg:circle'); // true
|
|
196
196
|
sanitize('not a name', 'ncName'); // 'not_a_name'
|
|
@@ -208,10 +208,10 @@ did not come across: nothing in this package reaches them.
|
|
|
208
208
|
**The derivation is Effect's.** `toCodecXml` derives
|
|
209
209
|
`Schema.toCodecStringTree`, the same derivation `Schema.toEncoderXml` uses, and
|
|
210
210
|
runs it through this package's `renderXml` and `parseXml` on the two text
|
|
211
|
-
directions.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
211
|
+
directions. That is what makes every schema feature Effect supports — structs,
|
|
212
|
+
arrays, unions, records, recursion, refinements, branded types, transformations —
|
|
213
|
+
work here without this package re-implementing the walk over a schema AST, and it
|
|
214
|
+
is what the round-trip specs are exercising.
|
|
215
215
|
|
|
216
216
|
**The shape is `toCodecJson`'s.** The value `toCodecXml` returns is a `Schema`:
|
|
217
217
|
`Type` is the source schema's `Type`, `Encoded` is XML text, the service
|
|
@@ -232,8 +232,8 @@ normalization would otherwise eat it: a literal newline in an attribute comes
|
|
|
232
232
|
back as a space unless it is written ` `.
|
|
233
233
|
|
|
234
234
|
**Comments and processing instructions are markup, not data.** The parser skips
|
|
235
|
-
them, which is
|
|
236
|
-
|
|
235
|
+
them, which is what `XMLBuilder` does by default. CDATA becomes character data,
|
|
236
|
+
since that is what it is.
|
|
237
237
|
|
|
238
238
|
## Performance
|
|
239
239
|
|
|
@@ -256,41 +256,40 @@ Four findings shaped the code, and all are measured rather than assumed:
|
|
|
256
256
|
|
|
257
257
|
- **Escaping was the whole cost of a large document.** The entity encoder that
|
|
258
258
|
used to live here escaped by applying five sequential global replacements, one
|
|
259
|
-
per character
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
259
|
+
per character, so a document with a single `&` in twenty thousand characters
|
|
260
|
+
was scanned five times over to change one byte — 58µs for that one document.
|
|
261
|
+
Escaping is now a single pattern scan to find the first character that needs
|
|
262
|
+
replacing, then one pass to build the result, which is 7.6x faster for clean
|
|
263
|
+
text and 28x faster for text with a character in it. `src/render.spec.ts` pins
|
|
264
|
+
the fast path with explicit expectations.
|
|
265
265
|
- **Resolving a name is a regex test, not an `Effect`.** The validators used to
|
|
266
266
|
return `Effect`s for pure questions, which left the renderer and the parser
|
|
267
267
|
running an effect per distinct element and attribute name in every document.
|
|
268
268
|
Running a runtime to read a boolean cost roughly 1µs per name, and a small
|
|
269
|
-
document has about a dozen names
|
|
269
|
+
document has about a dozen names, so name resolution was most of what a
|
|
270
270
|
serialize and a parse did. The merged package now exposes plain synchronous
|
|
271
271
|
predicates (`isQName` and the rest, `sanitize`) with no `Effect` wrapper, and
|
|
272
272
|
the renderer's namer and the parser's name cache call them directly.
|
|
273
273
|
`resolveName` is the one exception: it is effectful because `'error'` mode
|
|
274
|
-
rejects an illegal name, and a rejection is a failure rather than a value
|
|
275
|
-
reports through the error channel like every other fallible step of a parse
|
|
276
|
-
or a render. That
|
|
277
|
-
|
|
278
|
-
|
|
274
|
+
rejects an illegal name, and a rejection is a failure rather than a value, so
|
|
275
|
+
it reports through the error channel like every other fallible step of a parse
|
|
276
|
+
or a render. That is the bulk of the gain on a small document; the 500-row
|
|
277
|
+
document improves by a few percent because it asks the same handful of names,
|
|
278
|
+
and the per-row work is what dominates there.
|
|
279
279
|
- **The codec adds no layer of its own.** `toCodecXml` derives
|
|
280
280
|
`Schema.toCodecStringTree` and runs the tree through `renderXml`, so a value is
|
|
281
281
|
encoded by Effect's parser and then rendered, with nothing wrapped around
|
|
282
|
-
either.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
codec does not pay for it on every array.
|
|
282
|
+
either. Dropping the extra `Schema.toCodecArrayFromSingle` layer the codec used
|
|
283
|
+
to carry is part of why the table above is higher than the numbers this README
|
|
284
|
+
quoted before: the single-element-array leniency is the caller's to compose now,
|
|
285
|
+
and the plain codec does not pay for it on every array.
|
|
287
286
|
- **A small document is dominated by something this package does not own.** Of
|
|
288
287
|
the ~9µs it takes to serialize one, roughly 2.5µs is Effect's
|
|
289
288
|
`toCodecStringTree` derivation, which walks the schema on every call, and the
|
|
290
289
|
rest is this package's renderer. The renderer and parser underneath run at
|
|
291
|
-
roughly 2.5µs and 2.5µs on a flat document. A caller
|
|
292
|
-
shape on every request should build the codec once and reuse it, which is
|
|
293
|
-
|
|
290
|
+
roughly 2.5µs and 2.5µs on a flat document. A caller serializing the same
|
|
291
|
+
shape on every request should build the codec once and reuse it, which is what
|
|
292
|
+
the API is shaped for.
|
|
294
293
|
|
|
295
294
|
The other things the benchmarks changed: one pass over a record's keys instead of
|
|
296
295
|
one per role a key can play, name resolution memoized per document rather than
|
|
@@ -312,14 +311,14 @@ fallback all see it.
|
|
|
312
311
|
### Against the libraries on npm
|
|
313
312
|
|
|
314
313
|
`packages/benchmarks/bench/comparison.bench.ts` measures the same object through
|
|
315
|
-
this codec and
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
**`fast-xml-builder` produces
|
|
319
|
-
benchmark asserts it, so a change
|
|
320
|
-
quietly comparing different work.
|
|
314
|
+
this codec and
|
|
315
|
+
through three npm libraries — `fast-xml-builder`, `fast-xml-parser`, and
|
|
316
|
+
`@nodable/flexible-xml-parser`. The equivalence is established rather than
|
|
317
|
+
assumed: with `attributeNamePrefix: '@'`, **`fast-xml-builder` produces
|
|
318
|
+
byte-identical output to this codec**, and the benchmark asserts it, so a change
|
|
319
|
+
that breaks it fails the suite instead of quietly comparing different work.
|
|
321
320
|
|
|
322
|
-
**Encoding
|
|
321
|
+
**Encoding** — one object to the same bytes:
|
|
323
322
|
|
|
324
323
|
| Document | This codec | `fast-xml-builder` |
|
|
325
324
|
| ------------------- | ---------- | ------------------ |
|
|
@@ -327,7 +326,7 @@ quietly comparing different work.
|
|
|
327
326
|
| 500 rows | 1,698/s | 1,269/s (0.75x) |
|
|
328
327
|
| one large text node | 584,358/s | 254,355/s (0.44x) |
|
|
329
328
|
|
|
330
|
-
**Decoding
|
|
329
|
+
**Decoding** — one document to the same value:
|
|
331
330
|
|
|
332
331
|
| Document | This codec | `@nodable/flexible-xml-parser` | `fast-xml-parser` |
|
|
333
332
|
| ------------------- | ---------- | ------------------------------ | ----------------- |
|
|
@@ -335,9 +334,9 @@ quietly comparing different work.
|
|
|
335
334
|
| 500 rows | 1,975/s | 70/s (0.04x) | 486/s (0.25x) |
|
|
336
335
|
| one large text node | 20,715/s | 5,694/s (0.27x) | 8,209/s (0.40x) |
|
|
337
336
|
|
|
338
|
-
**A full round trip**, the number an application actually pays. Neither
|
|
339
|
-
builder nor a parser can do both halves, so the last two rows are each
|
|
340
|
-
doing the same work with two libraries and hand-joining them:
|
|
337
|
+
**A full round trip**, which is the number an application actually pays. Neither
|
|
338
|
+
a builder nor a parser can do both halves, so the last two rows are each
|
|
339
|
+
ecosystem doing the same work with two libraries and hand-joining them:
|
|
341
340
|
|
|
342
341
|
| Path | Throughput |
|
|
343
342
|
| ----------------------------------------------------------- | ---------------- |
|
|
@@ -347,14 +346,14 @@ doing the same work with two libraries and hand-joining them:
|
|
|
347
346
|
|
|
348
347
|
**This codec reaches the parser and renderer directly.** It encodes and decodes
|
|
349
348
|
through `Schema.encodeSync` and `Schema.decodeSync`, with no runtime per call,
|
|
350
|
-
|
|
351
|
-
they do not validate the result against a schema
|
|
349
|
+
which is why it is the fastest row on both sides. The npm parsers do less work —
|
|
350
|
+
they do not validate the result against a schema — so the comparison is
|
|
352
351
|
directional rather than like-for-like.
|
|
353
352
|
|
|
354
|
-
Four
|
|
353
|
+
Four things to be straight about when reading those tables:
|
|
355
354
|
|
|
356
355
|
- **These are one machine's numbers, from one run.** The relative error is under
|
|
357
|
-
2% on every row, but the machine's load moved between runs
|
|
356
|
+
2% on every row, but the machine's load moved between runs, so treat the
|
|
358
357
|
ratios as the durable part and the absolute figures as a range.
|
|
359
358
|
- **This codec's decode does strictly more work.** It parses _and_ validates the
|
|
360
359
|
result against the schema, coercing `"30"` to `30` and failing on a mismatch.
|
|
@@ -362,11 +361,11 @@ Four caveats apply when reading those tables:
|
|
|
362
361
|
2,984/s and the schema pass brings it to 1,975/s, so roughly a third of the
|
|
363
362
|
decode time is validation the comparison rows do not pay.
|
|
364
363
|
- **The parsers do work this codec does not.** They coerce tag values through a
|
|
365
|
-
value-parser pipeline
|
|
366
|
-
number parsing
|
|
367
|
-
the other lacks, and
|
|
364
|
+
value-parser pipeline — entity decoding, whitespace normalizing, boolean and
|
|
365
|
+
number parsing — where a schema already decided the type. Both sides have work
|
|
366
|
+
the other lacks, and neither is idle.
|
|
368
367
|
- **Neither ecosystem is doing the whole job on its own.** A builder has no
|
|
369
|
-
reader and a parser has no writer, so the round-trip table is the
|
|
368
|
+
reader and a parser has no writer, so the round-trip table is the honest
|
|
370
369
|
comparison and the single-direction tables are the diagnostic ones.
|
|
371
370
|
|
|
372
371
|
## Limitations
|
|
@@ -397,7 +396,7 @@ quietly.
|
|
|
397
396
|
children and read back as a single run, so `<p>a<b/>c</p>` reads as
|
|
398
397
|
`{ b: '', '#text': 'ac' }`. Reading and re-rendering is stable from there on.
|
|
399
398
|
- **Whitespace at the edges of text is trimmed** unless `preserveWhitespace` is
|
|
400
|
-
set. That
|
|
399
|
+
set. That is what makes a pretty-printed document read as the same value as an
|
|
401
400
|
unindented one. Whitespace _inside_ a run is never touched.
|
|
402
401
|
- **`Schema.BigInt` is write-only.** Effect's `StringTree` derivation lowers one
|
|
403
402
|
to its decimal text and has no way to raise it again.
|
package/dist/codec.d.ts
CHANGED
|
@@ -5,8 +5,8 @@ import { Schema } from "effect";
|
|
|
5
5
|
/**
|
|
6
6
|
* @description Options for {@link toCodecXml}.\
|
|
7
7
|
* The render options name and shape the document; the parse options decide how strictly it is read back.\
|
|
8
|
-
* `rootName` is the one the codec resolves for itself when the caller leaves it out
|
|
9
|
-
*
|
|
8
|
+
* `rootName` is the one the codec resolves for itself when the caller leaves it out, taking it from the schema's `identifier` or `title` annotation and falling
|
|
9
|
+
* back to `'root'`.
|
|
10
10
|
*/
|
|
11
11
|
export type XmlCodecOptions = XmlRenderOptions & XmlParseOptions;
|
|
12
12
|
/**
|
|
@@ -19,18 +19,15 @@ export interface toCodecXml<S extends Schema.Constraint> extends Schema.decodeTo
|
|
|
19
19
|
/**
|
|
20
20
|
* @description Derives the XML codec for a schema: a `Schema` whose `Encoded` is an XML document, so `Schema.encodeSync(codec)` writes text and
|
|
21
21
|
* `Schema.decodeSync(codec)` reads it back. The derivation is Effect's `Schema.toCodecStringTree`; the text layer is this package's {@link renderXml}
|
|
22
|
-
* and {@link parseXml}.
|
|
23
|
-
* the rest of the Effect combinators. The two forms are one function: the first argument decides the style, a schema is read as data-first and an
|
|
24
|
-
* options object as data-last. `pipe(schema, toCodecXml)` carries no options and works because a schema on its own is data-first.
|
|
22
|
+
* and {@link parseXml}.
|
|
25
23
|
*
|
|
26
24
|
* @example
|
|
27
25
|
* ```typescript
|
|
28
|
-
* import { Schema
|
|
29
|
-
* import { toCodecXml } from '@endevops/effect-codec
|
|
26
|
+
* import { Schema } from 'effect';
|
|
27
|
+
* import { toCodecXml } from '@endevops/effect-xml-codec';
|
|
30
28
|
*
|
|
31
29
|
* const Book = Schema.Struct({ '@id': Schema.String, title: Schema.String, pages: Schema.Number });
|
|
32
30
|
* const codec = toCodecXml(Book, { rootName: 'book' });
|
|
33
|
-
* const piped = pipe(Book, toCodecXml({ rootName: 'book' }));
|
|
34
31
|
*
|
|
35
32
|
* const value = { '@id': '1', title: 'Dune', pages: 412 };
|
|
36
33
|
*
|
|
@@ -41,16 +38,11 @@ export interface toCodecXml<S extends Schema.Constraint> extends Schema.decodeTo
|
|
|
41
38
|
* // => { '@id': '1', title: 'Dune', pages: 412 }
|
|
42
39
|
* ```;
|
|
43
40
|
*
|
|
44
|
-
* @param schema - The schema describing the value
|
|
45
|
-
* @param options - Render and parse options. `rootName` defaults to the schema's `identifier` or `title` annotation, then to `'root'`.
|
|
46
|
-
* data-last form this is the only argument, and the schema arrives from `pipe`.
|
|
41
|
+
* @param schema - The schema describing the value.
|
|
42
|
+
* @param options - Render and parse options. `rootName` defaults to the schema's `identifier` or `title` annotation, then to `'root'`.
|
|
47
43
|
*
|
|
48
|
-
* @returns The codec, with the source schema's `Type` and the same service requirements.
|
|
49
|
-
* codec.
|
|
44
|
+
* @returns The codec, with the source schema's `Type` and the same service requirements.
|
|
50
45
|
*/
|
|
51
|
-
export declare const toCodecXml:
|
|
52
|
-
<S extends Schema.Constraint>(schema: S, options?: XmlCodecOptions): toCodecXml<S>;
|
|
53
|
-
(options?: XmlCodecOptions): <S extends Schema.Constraint>(schema: S) => toCodecXml<S>;
|
|
54
|
-
};
|
|
46
|
+
export declare const toCodecXml: <S extends Schema.Constraint>(schema: S, options?: XmlCodecOptions) => toCodecXml<S>;
|
|
55
47
|
//#endregion
|
|
56
48
|
//# sourceMappingURL=codec.d.ts.map
|
package/dist/codec.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codec.d.ts","names":[],"sources":["../src/codec.ts"],"mappings":";;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"codec.d.ts","names":[],"sources":["../src/codec.ts"],"mappings":";;;;;;;;;;YAyCY,kBAAkB,mBAAmB;;;;;iBAMhC,WAAW,UAAU,OAAO,oBAAoB,OAAO,SAAS,OAAO,kBAAkB,IAAI,OAAO;WAC1G,SAAS,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA8BlB,aAAc,UAAU,OAAO,YAAU,QAAU,GAAC,UAAW,oBAAuB,WAAW"}
|
package/dist/codec.js
CHANGED
|
@@ -2,32 +2,20 @@ import "./conventions.js";
|
|
|
2
2
|
import { decodeNames, encodeNames, namespacePlan } from "./namespaces.js";
|
|
3
3
|
import { parseXml } from "./parse.js";
|
|
4
4
|
import { renderXml } from "./render.js";
|
|
5
|
-
import { Effect,
|
|
5
|
+
import { Effect, Predicate, Schema, SchemaAST, SchemaIssue, SchemaTransformation } from "effect";
|
|
6
6
|
//#region src/codec.ts
|
|
7
|
-
const isActivePlan = (plan) => plan.byKey.size > 0 || plan.nameByKey.size > 0 || plan.attributeKeys.size > 0 || plan.valueByElement.size > 0 || plan.root !== void 0 || plan.rootName !== void 0;
|
|
8
|
-
const resolveRenderOptions = (schema, plan, options) => {
|
|
9
|
-
const rootName = options.rootName ?? plan.rootName ?? SchemaAST.resolveIdentifier(schema.ast) ?? SchemaAST.resolveTitle(schema.ast) ?? "root";
|
|
10
|
-
const wireRootName = plan.root !== void 0 && plan.root.prefix !== "" && !rootName.includes(":") ? `${plan.root.prefix}:${rootName}` : rootName;
|
|
11
|
-
return {
|
|
12
|
-
...options,
|
|
13
|
-
rootName: wireRootName
|
|
14
|
-
};
|
|
15
|
-
};
|
|
16
7
|
/**
|
|
17
8
|
* @description Derives the XML codec for a schema: a `Schema` whose `Encoded` is an XML document, so `Schema.encodeSync(codec)` writes text and
|
|
18
9
|
* `Schema.decodeSync(codec)` reads it back. The derivation is Effect's `Schema.toCodecStringTree`; the text layer is this package's {@link renderXml}
|
|
19
|
-
* and {@link parseXml}.
|
|
20
|
-
* the rest of the Effect combinators. The two forms are one function: the first argument decides the style, a schema is read as data-first and an
|
|
21
|
-
* options object as data-last. `pipe(schema, toCodecXml)` carries no options and works because a schema on its own is data-first.
|
|
10
|
+
* and {@link parseXml}.
|
|
22
11
|
*
|
|
23
12
|
* @example
|
|
24
13
|
* ```typescript
|
|
25
|
-
* import { Schema
|
|
26
|
-
* import { toCodecXml } from '@endevops/effect-codec
|
|
14
|
+
* import { Schema } from 'effect';
|
|
15
|
+
* import { toCodecXml } from '@endevops/effect-xml-codec';
|
|
27
16
|
*
|
|
28
17
|
* const Book = Schema.Struct({ '@id': Schema.String, title: Schema.String, pages: Schema.Number });
|
|
29
18
|
* const codec = toCodecXml(Book, { rootName: 'book' });
|
|
30
|
-
* const piped = pipe(Book, toCodecXml({ rootName: 'book' }));
|
|
31
19
|
*
|
|
32
20
|
* const value = { '@id': '1', title: 'Dune', pages: 412 };
|
|
33
21
|
*
|
|
@@ -38,19 +26,22 @@ const resolveRenderOptions = (schema, plan, options) => {
|
|
|
38
26
|
* // => { '@id': '1', title: 'Dune', pages: 412 }
|
|
39
27
|
* ```;
|
|
40
28
|
*
|
|
41
|
-
* @param schema - The schema describing the value
|
|
42
|
-
* @param options - Render and parse options. `rootName` defaults to the schema's `identifier` or `title` annotation, then to `'root'`.
|
|
43
|
-
* data-last form this is the only argument, and the schema arrives from `pipe`.
|
|
29
|
+
* @param schema - The schema describing the value.
|
|
30
|
+
* @param options - Render and parse options. `rootName` defaults to the schema's `identifier` or `title` annotation, then to `'root'`.
|
|
44
31
|
*
|
|
45
|
-
* @returns The codec, with the source schema's `Type` and the same service requirements.
|
|
46
|
-
* codec.
|
|
32
|
+
* @returns The codec, with the source schema's `Type` and the same service requirements.
|
|
47
33
|
*/
|
|
48
|
-
const toCodecXml =
|
|
34
|
+
const toCodecXml = (schema, options = {}) => {
|
|
49
35
|
const planned = namespacePlan(schema);
|
|
50
36
|
if (Predicate.hasProperty(planned, "error")) throw new Error(`Invalid XML namespace annotation:\n\t- ${planned.error}.`);
|
|
51
37
|
const plan = planned.plan;
|
|
52
|
-
const active =
|
|
53
|
-
const
|
|
38
|
+
const active = plan.byKey.size > 0 || plan.nameByKey.size > 0 || plan.attributeKeys.size > 0 || plan.valueByElement.size > 0 || plan.root !== void 0 || plan.rootName !== void 0;
|
|
39
|
+
const rootName = options.rootName ?? plan.rootName ?? SchemaAST.resolveIdentifier(schema.ast) ?? SchemaAST.resolveTitle(schema.ast) ?? "root";
|
|
40
|
+
const wireRootName = plan.root !== void 0 && plan.root.prefix !== "" && !rootName.includes(":") ? `${plan.root.prefix}:${rootName}` : rootName;
|
|
41
|
+
const renderOptions = {
|
|
42
|
+
...options,
|
|
43
|
+
rootName: wireRootName
|
|
44
|
+
};
|
|
54
45
|
return Schema.String.pipe(Schema.decodeTo(Schema.toCodecStringTree(schema), SchemaTransformation.transformEffect({
|
|
55
46
|
decode: (text, parseOptions) => {
|
|
56
47
|
if (!Predicate.isString(text)) return Effect.fail(new SchemaIssue.InvalidValue({ message: `Expected a string, but received ${typeof text}.` }, text, parseOptions));
|
|
@@ -65,7 +56,7 @@ const toCodecXml = Function.dual((args) => Schema.isSchema(args[0]), (schema, op
|
|
|
65
56
|
return renderXml(wire, renderOptions).pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue({ message: error.message }, value, parseOptions)));
|
|
66
57
|
}
|
|
67
58
|
})));
|
|
68
|
-
}
|
|
59
|
+
};
|
|
69
60
|
//#endregion
|
|
70
61
|
export { toCodecXml };
|
|
71
62
|
|
package/dist/codec.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codec.js","names":[],"sources":["../src/codec.ts"],"sourcesContent":["// The codec: a schema, and the XML text that carries it.\n//\n// `toCodecXml` is this package's counterpart to `Schema.toCodecJson`. It\n// returns a `Schema` whose `Type` is the source schema's `Type` and whose\n// `Encoded` is XML text, so a value is written with `Schema.encodeSync(codec)`\n// and read back with `Schema.decodeSync(codec)`. That is one call each, as with\n// the JSON codec. There is no value tree at the call site and no second call to\n// a renderer or a parser.\n//\n// The derivation underneath is Effect's own `Schema.toCodecStringTree`, so\n// every schema feature Effect supports composes here without this package\n// re-implementing the walk over a schema AST. On encode the codec runs the value\n// tree through `renderXml`; on decode it runs the document through `parseXml`.\n// Both are the same text layer this package exports on its own, and both\n// failures arrive as the `SchemaIssue.Issue` a schema is expected to report,\n// with the underlying message preserved. Those failures are an illegal name and\n// a document that is not well-formed.\n//\n// The conventions stay in the keys: a key starting with `@` is an attribute,\n// `#text` is character data, and every other key is a child element. The root\n// element is named from the schema's `identifier` or `title` annotation when it\n// has one, and from the `rootName` option otherwise; it defaults to `'root'`,\n// the same name Effect's own XML encoder uses.\n\nimport { Effect, Function, Predicate, Schema, SchemaAST, SchemaIssue, SchemaTransformation } from 'effect';\n\nimport type { NamespacePlan } from './namespaces.ts';\nimport type { XmlParseOptions } from './parse.ts';\nimport type { XmlRenderOptions } from './render.ts';\nimport type { XmlValue } from './xml-value.ts';\n\nimport { DEFAULT_ROOT_NAME } from './conventions.ts';\nimport { decodeNames, encodeNames, namespacePlan, ROOT_ELEMENT } from './namespaces.ts';\nimport { parseXml } from './parse.ts';\nimport { renderXml } from './render.ts';\n\n/**\n * @description Options for {@link toCodecXml}.\\\n * The render options name and shape the document; the parse options decide how strictly it is read back.\\\n * `rootName` is the one the codec resolves for itself when the caller leaves it out. The codec takes it from the schema's `identifier` or `title` annotation and\n * falls back to `'root'`.\n */\nexport type XmlCodecOptions = XmlRenderOptions & XmlParseOptions;\n\n/**\n * @description The XML codec for a schema, as a `Schema`. `Type` is the schema's own `Type` and `Encoded` is XML text, so it encodes a value to a document and\n * decodes a document to a value in one step each. The service requirements of the source schema are preserved.\n */\nexport interface toCodecXml<S extends Schema.Constraint> extends Schema.decodeTo<Schema.toCodecStringTree<S>, Schema.String> {\n readonly Rebuild: toCodecXml<S>;\n}\n\n// Whether the plan places any node at all: a rename, an attribute, a value, or a root namespace or name. A plan with none of these leaves the value tree\n// alone on both sides, so the codec skips the name walk.\nconst isActivePlan = (plan: NamespacePlan): boolean =>\n plan.byKey.size > 0 ||\n plan.nameByKey.size > 0 ||\n plan.attributeKeys.size > 0 ||\n plan.valueByElement.size > 0 ||\n plan.root !== undefined ||\n plan.rootName !== undefined;\n\n// The render options with the root name resolved and any root prefix applied. The caller's `rootName` wins, then the plan's, then the schema's `identifier`\n// or `title` annotation, then `'root'`. A root namespace with a prefix qualifies the name unless the caller already wrote one.\nconst resolveRenderOptions = <S extends Schema.Constraint>(schema: S, plan: NamespacePlan, options: XmlCodecOptions): XmlRenderOptions => {\n const rootName =\n options.rootName ?? plan.rootName ?? SchemaAST.resolveIdentifier(schema.ast) ?? SchemaAST.resolveTitle(schema.ast) ?? DEFAULT_ROOT_NAME;\n const wireRootName = plan.root !== undefined && plan.root.prefix !== '' && !rootName.includes(':') ? `${plan.root.prefix}:${rootName}` : rootName;\n return { ...options, rootName: wireRootName };\n};\n\n/**\n * @description Derives the XML codec for a schema: a `Schema` whose `Encoded` is an XML document, so `Schema.encodeSync(codec)` writes text and\n * `Schema.decodeSync(codec)` reads it back. The derivation is Effect's `Schema.toCodecStringTree`; the text layer is this package's {@link renderXml}\n * and {@link parseXml}. Call it data-first, `toCodecXml(schema, options)`, or data-last, `toCodecXml(options)(schema)`, so it drops into `pipe` beside\n * the rest of the Effect combinators. The two forms are one function: the first argument decides the style, a schema is read as data-first and an\n * options object as data-last. `pipe(schema, toCodecXml)` carries no options and works because a schema on its own is data-first.\n *\n * @example\n * ```typescript\n * import { Schema, pipe } from 'effect';\n * import { toCodecXml } from '@endevops/effect-codec-xml';\n *\n * const Book = Schema.Struct({ '@id': Schema.String, title: Schema.String, pages: Schema.Number });\n * const codec = toCodecXml(Book, { rootName: 'book' });\n * const piped = pipe(Book, toCodecXml({ rootName: 'book' }));\n *\n * const value = { '@id': '1', title: 'Dune', pages: 412 };\n *\n * Schema.encodeSync(codec)(value);\n * // => '<book id=\"1\"><title>Dune</title><pages>412</pages></book>'\n *\n * Schema.decodeSync(codec)('<book id=\"1\"><title>Dune</title><pages>412</pages></book>');\n * // => { '@id': '1', title: 'Dune', pages: 412 }\n * ```;\n *\n * @param schema - The schema describing the value, in the data-first form.\n * @param options - Render and parse options. `rootName` defaults to the schema's `identifier` or `title` annotation, then to `'root'`. In the\n * data-last form this is the only argument, and the schema arrives from `pipe`.\n *\n * @returns The codec, with the source schema's `Type` and the same service requirements. In the data-last form, a function from the schema to the\n * codec.\n */\nexport const toCodecXml: {\n <S extends Schema.Constraint>(schema: S, options?: XmlCodecOptions): toCodecXml<S>;\n (options?: XmlCodecOptions): <S extends Schema.Constraint>(schema: S) => toCodecXml<S>;\n} = Function.dual(\n args => Schema.isSchema(args[0]),\n <S extends Schema.Constraint>(schema: S, options: XmlCodecOptions = {}): toCodecXml<S> => {\n const planned = namespacePlan(schema);\n if (Predicate.hasProperty(planned, 'error')) {\n throw new Error(`Invalid XML namespace annotation:\\n\\t- ${planned.error}.`);\n }\n const plan = planned.plan;\n const active = isActivePlan(plan);\n const renderOptions = resolveRenderOptions(schema, plan, options);\n\n return Schema.String.pipe(\n Schema.decodeTo(\n Schema.toCodecStringTree(schema),\n SchemaTransformation.transformEffect({\n decode: (text, parseOptions) => {\n if (!Predicate.isString(text)) {\n return Effect.fail(new SchemaIssue.InvalidValue({ message: `Expected a string, but received ${typeof text}.` }, text, parseOptions));\n }\n\n return parseXml(text, options).pipe(\n Effect.map(value => (active ? decodeNames(value, plan, {}, ROOT_ELEMENT) : value)),\n Effect.tapError(error =>\n Effect.logError(`XML parse error: ${error.message}`).pipe(Effect.annotateLogs({ cause: error, message: 'XML parse error' }))\n ),\n Effect.mapError(error => new SchemaIssue.InvalidValue({ message: error.message }, text, parseOptions))\n );\n },\n\n encode: (value, parseOptions) => {\n if (active && Array.isArray(value) && (plan.root !== undefined || plan.rootName !== undefined)) {\n return Effect.fail(\n new SchemaIssue.InvalidValue(\n { message: 'An array at the root of a namespaced schema cannot carry the root namespace or name.' },\n value,\n parseOptions\n )\n );\n }\n\n const wire = active ? encodeNames(value as XmlValue, plan, plan.root, {}, ROOT_ELEMENT) : (value as XmlValue);\n return renderXml(wire, renderOptions).pipe(\n Effect.mapError(error => new SchemaIssue.InvalidValue({ message: error.message }, value, parseOptions))\n );\n },\n })\n )\n );\n }\n);\n"],"mappings":";;;;;;AAsDA,MAAM,gBAAgB,SACpB,KAAK,MAAM,OAAO,KAClB,KAAK,UAAU,OAAO,KACtB,KAAK,cAAc,OAAO,KAC1B,KAAK,eAAe,OAAO,KAC3B,KAAK,SAAS,KAAA,KACd,KAAK,aAAa,KAAA;AAIpB,MAAM,wBAAqD,QAAW,MAAqB,YAA+C;CACxI,MAAM,WACJ,QAAQ,YAAY,KAAK,YAAY,UAAU,kBAAkB,OAAO,GAAG,KAAK,UAAU,aAAa,OAAO,GAAG,KAAA;CACnH,MAAM,eAAe,KAAK,SAAS,KAAA,KAAa,KAAK,KAAK,WAAW,MAAM,CAAC,SAAS,SAAS,GAAG,IAAI,GAAG,KAAK,KAAK,OAAO,GAAG,aAAa;CACzI,OAAO;EAAE,GAAG;EAAS,UAAU;CAAa;AAC9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,MAAa,aAGT,SAAS,MACX,SAAQ,OAAO,SAAS,KAAK,EAAE,IACD,QAAW,UAA2B,CAAC,MAAqB;CACxF,MAAM,UAAU,cAAc,MAAM;CACpC,IAAI,UAAU,YAAY,SAAS,OAAO,GACxC,MAAM,IAAI,MAAM,0CAA0C,QAAQ,MAAM,EAAE;CAE5E,MAAM,OAAO,QAAQ;CACrB,MAAM,SAAS,aAAa,IAAI;CAChC,MAAM,gBAAgB,qBAAqB,QAAQ,MAAM,OAAO;CAEhE,OAAO,OAAO,OAAO,KACnB,OAAO,SACL,OAAO,kBAAkB,MAAM,GAC/B,qBAAqB,gBAAgB;EACnC,SAAS,MAAM,iBAAiB;GAC9B,IAAI,CAAC,UAAU,SAAS,IAAI,GAC1B,OAAO,OAAO,KAAK,IAAI,YAAY,aAAa,EAAE,SAAS,mCAAmC,OAAO,KAAK,GAAG,GAAG,MAAM,YAAY,CAAC;GAGrI,OAAO,SAAS,MAAM,OAAO,CAAC,CAAC,KAC7B,OAAO,KAAI,UAAU,SAAS,YAAY,OAAO,MAAM,CAAC,GAAA,EAAe,IAAI,KAAM,GACjF,OAAO,UAAS,UACd,OAAO,SAAS,oBAAoB,MAAM,SAAS,CAAC,CAAC,KAAK,OAAO,aAAa;IAAE,OAAO;IAAO,SAAS;GAAkB,CAAC,CAAC,CAC7H,GACA,OAAO,UAAS,UAAS,IAAI,YAAY,aAAa,EAAE,SAAS,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,CACvG;EACF;EAEA,SAAS,OAAO,iBAAiB;GAC/B,IAAI,UAAU,MAAM,QAAQ,KAAK,MAAM,KAAK,SAAS,KAAA,KAAa,KAAK,aAAa,KAAA,IAClF,OAAO,OAAO,KACZ,IAAI,YAAY,aACd,EAAE,SAAS,uFAAuF,GAClG,OACA,YACF,CACF;GAGF,MAAM,OAAO,SAAS,YAAY,OAAmB,MAAM,KAAK,MAAM,CAAC,GAAA,EAAe,IAAK;GAC3F,OAAO,UAAU,MAAM,aAAa,CAAC,CAAC,KACpC,OAAO,UAAS,UAAS,IAAI,YAAY,aAAa,EAAE,SAAS,MAAM,QAAQ,GAAG,OAAO,YAAY,CAAC,CACxG;EACF;CACF,CAAC,CACH,CACF;AACF,CACF"}
|
|
1
|
+
{"version":3,"file":"codec.js","names":[],"sources":["../src/codec.ts"],"sourcesContent":["// The codec: a schema, and the XML text that carries it.\n//\n// `toCodecXml` is this package's counterpart to `Schema.toCodecJson`. It\n// returns a `Schema` whose `Type` is the source schema's `Type` and whose\n// `Encoded` is XML text, so a value is written with `Schema.encodeSync(codec)`\n// and read back with `Schema.decodeSync(codec)` — one call each, the way the\n// JSON codec works. There is no value tree at the call site and no second call\n// to a renderer or a parser.\n//\n// The derivation underneath is Effect's own `Schema.toCodecStringTree`, so\n// every schema feature Effect supports composes here without this package\n// re-implementing the walk over a schema AST. On the way out the codec runs the\n// value tree through `renderXml`; on the way back in it runs the document\n// through `parseXml`. Both are the same text layer this package exports on\n// their own, and both failures — an illegal name, a document that is not\n// well-formed — arrive as the `SchemaIssue.Issue` a schema is expected to\n// report, with the underlying message preserved.\n//\n// The conventions stay in the keys: a key starting with `@` is an attribute,\n// `#text` is character data, and every other key is a child element. The root\n// element is named from the schema's `identifier` or `title` annotation when it\n// has one, and from the `rootName` option otherwise; it defaults to `'root'`,\n// the same name Effect's own XML encoder uses.\n\nimport { Effect, Predicate, Schema, SchemaAST, SchemaIssue, SchemaTransformation } from 'effect';\n\nimport type { XmlParseOptions } from './parse.ts';\nimport type { XmlRenderOptions } from './render.ts';\nimport type { XmlValue } from './xml-value.ts';\n\nimport { DEFAULT_ROOT_NAME } from './conventions.ts';\nimport { decodeNames, encodeNames, namespacePlan, ROOT_ELEMENT } from './namespaces.ts';\nimport { parseXml } from './parse.ts';\nimport { renderXml } from './render.ts';\n\n/**\n * @description Options for {@link toCodecXml}.\\\n * The render options name and shape the document; the parse options decide how strictly it is read back.\\\n * `rootName` is the one the codec resolves for itself when the caller leaves it out, taking it from the schema's `identifier` or `title` annotation and falling\n * back to `'root'`.\n */\nexport type XmlCodecOptions = XmlRenderOptions & XmlParseOptions;\n\n/**\n * @description The XML codec for a schema, as a `Schema`. `Type` is the schema's own `Type` and `Encoded` is XML text, so it encodes a value to a document and\n * decodes a document to a value in one step each. The service requirements of the source schema are preserved.\n */\nexport interface toCodecXml<S extends Schema.Constraint> extends Schema.decodeTo<Schema.toCodecStringTree<S>, Schema.String> {\n readonly Rebuild: toCodecXml<S>;\n}\n\n/**\n * @description Derives the XML codec for a schema: a `Schema` whose `Encoded` is an XML document, so `Schema.encodeSync(codec)` writes text and\n * `Schema.decodeSync(codec)` reads it back. The derivation is Effect's `Schema.toCodecStringTree`; the text layer is this package's {@link renderXml}\n * and {@link parseXml}.\n *\n * @example\n * ```typescript\n * import { Schema } from 'effect';\n * import { toCodecXml } from '@endevops/effect-xml-codec';\n *\n * const Book = Schema.Struct({ '@id': Schema.String, title: Schema.String, pages: Schema.Number });\n * const codec = toCodecXml(Book, { rootName: 'book' });\n *\n * const value = { '@id': '1', title: 'Dune', pages: 412 };\n *\n * Schema.encodeSync(codec)(value);\n * // => '<book id=\"1\"><title>Dune</title><pages>412</pages></book>'\n *\n * Schema.decodeSync(codec)('<book id=\"1\"><title>Dune</title><pages>412</pages></book>');\n * // => { '@id': '1', title: 'Dune', pages: 412 }\n * ```;\n *\n * @param schema - The schema describing the value.\n * @param options - Render and parse options. `rootName` defaults to the schema's `identifier` or `title` annotation, then to `'root'`.\n *\n * @returns The codec, with the source schema's `Type` and the same service requirements.\n */\nexport const toCodecXml = <S extends Schema.Constraint>(schema: S, options: XmlCodecOptions = {}): toCodecXml<S> => {\n const planned = namespacePlan(schema);\n if (Predicate.hasProperty(planned, 'error')) {\n throw new Error(`Invalid XML namespace annotation:\\n\\t- ${planned.error}.`);\n }\n const plan = planned.plan;\n const active =\n plan.byKey.size > 0 ||\n plan.nameByKey.size > 0 ||\n plan.attributeKeys.size > 0 ||\n plan.valueByElement.size > 0 ||\n plan.root !== undefined ||\n plan.rootName !== undefined;\n\n const rootName =\n options.rootName ?? plan.rootName ?? SchemaAST.resolveIdentifier(schema.ast) ?? SchemaAST.resolveTitle(schema.ast) ?? DEFAULT_ROOT_NAME;\n const wireRootName = plan.root !== undefined && plan.root.prefix !== '' && !rootName.includes(':') ? `${plan.root.prefix}:${rootName}` : rootName;\n\n const renderOptions: XmlRenderOptions = { ...options, rootName: wireRootName };\n\n return Schema.String.pipe(\n Schema.decodeTo(\n Schema.toCodecStringTree(schema),\n SchemaTransformation.transformEffect({\n decode: (text, parseOptions) => {\n if (!Predicate.isString(text)) {\n return Effect.fail(new SchemaIssue.InvalidValue({ message: `Expected a string, but received ${typeof text}.` }, text, parseOptions));\n }\n\n return parseXml(text, options).pipe(\n Effect.map(value => (active ? decodeNames(value, plan, {}, ROOT_ELEMENT) : value)),\n Effect.tapError(error =>\n Effect.logError(`XML parse error: ${error.message}`).pipe(Effect.annotateLogs({ cause: error, message: 'XML parse error' }))\n ),\n Effect.mapError(error => new SchemaIssue.InvalidValue({ message: error.message }, text, parseOptions))\n );\n },\n\n encode: (value, parseOptions) => {\n if (active && Array.isArray(value) && (plan.root !== undefined || plan.rootName !== undefined)) {\n return Effect.fail(\n new SchemaIssue.InvalidValue(\n { message: 'An array at the root of a namespaced schema cannot carry the root namespace or name.' },\n value,\n parseOptions\n )\n );\n }\n\n const wire = active ? encodeNames(value as XmlValue, plan, plan.root, {}, ROOT_ELEMENT) : (value as XmlValue);\n return renderXml(wire, renderOptions).pipe(\n Effect.mapError(error => new SchemaIssue.InvalidValue({ message: error.message }, value, parseOptions))\n );\n },\n })\n )\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8EA,MAAa,cAA2C,QAAW,UAA2B,CAAC,MAAqB;CAClH,MAAM,UAAU,cAAc,MAAM;CACpC,IAAI,UAAU,YAAY,SAAS,OAAO,GACxC,MAAM,IAAI,MAAM,0CAA0C,QAAQ,MAAM,EAAE;CAE5E,MAAM,OAAO,QAAQ;CACrB,MAAM,SACJ,KAAK,MAAM,OAAO,KAClB,KAAK,UAAU,OAAO,KACtB,KAAK,cAAc,OAAO,KAC1B,KAAK,eAAe,OAAO,KAC3B,KAAK,SAAS,KAAA,KACd,KAAK,aAAa,KAAA;CAEpB,MAAM,WACJ,QAAQ,YAAY,KAAK,YAAY,UAAU,kBAAkB,OAAO,GAAG,KAAK,UAAU,aAAa,OAAO,GAAG,KAAA;CACnH,MAAM,eAAe,KAAK,SAAS,KAAA,KAAa,KAAK,KAAK,WAAW,MAAM,CAAC,SAAS,SAAS,GAAG,IAAI,GAAG,KAAK,KAAK,OAAO,GAAG,aAAa;CAEzI,MAAM,gBAAkC;EAAE,GAAG;EAAS,UAAU;CAAa;CAE7E,OAAO,OAAO,OAAO,KACnB,OAAO,SACL,OAAO,kBAAkB,MAAM,GAC/B,qBAAqB,gBAAgB;EACnC,SAAS,MAAM,iBAAiB;GAC9B,IAAI,CAAC,UAAU,SAAS,IAAI,GAC1B,OAAO,OAAO,KAAK,IAAI,YAAY,aAAa,EAAE,SAAS,mCAAmC,OAAO,KAAK,GAAG,GAAG,MAAM,YAAY,CAAC;GAGrI,OAAO,SAAS,MAAM,OAAO,CAAC,CAAC,KAC7B,OAAO,KAAI,UAAU,SAAS,YAAY,OAAO,MAAM,CAAC,GAAA,EAAe,IAAI,KAAM,GACjF,OAAO,UAAS,UACd,OAAO,SAAS,oBAAoB,MAAM,SAAS,CAAC,CAAC,KAAK,OAAO,aAAa;IAAE,OAAO;IAAO,SAAS;GAAkB,CAAC,CAAC,CAC7H,GACA,OAAO,UAAS,UAAS,IAAI,YAAY,aAAa,EAAE,SAAS,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,CACvG;EACF;EAEA,SAAS,OAAO,iBAAiB;GAC/B,IAAI,UAAU,MAAM,QAAQ,KAAK,MAAM,KAAK,SAAS,KAAA,KAAa,KAAK,aAAa,KAAA,IAClF,OAAO,OAAO,KACZ,IAAI,YAAY,aACd,EAAE,SAAS,uFAAuF,GAClG,OACA,YACF,CACF;GAGF,MAAM,OAAO,SAAS,YAAY,OAAmB,MAAM,KAAK,MAAM,CAAC,GAAA,EAAe,IAAK;GAC3F,OAAO,UAAU,MAAM,aAAa,CAAC,CAAC,KACpC,OAAO,UAAS,UAAS,IAAI,YAAY,aAAa,EAAE,SAAS,MAAM,QAAQ,GAAG,OAAO,YAAY,CAAC,CACxG;EACF;CACF,CAAC,CACH,CACF;AACF"}
|
package/dist/conventions.d.ts
CHANGED
|
@@ -73,10 +73,10 @@ export declare const isTextKey: (key: string) => boolean;
|
|
|
73
73
|
*/
|
|
74
74
|
export declare const isReservedKey: (key: string) => boolean;
|
|
75
75
|
/**
|
|
76
|
-
* @description Resolves a name to something legal in an XML document. The renderer and the parser both resolve a name per element and per attribute
|
|
77
|
-
* name under `'error'` mode is a rejection rather than a value, so this returns an `Effect` with the `XmlParseError` in its error channel
|
|
78
|
-
* throwing it. Callers `yield*` it and the failure composes with `catchTag` and the rest
|
|
79
|
-
* use {@link resolveNameSync} directly, because their walks are synchronous hot paths.
|
|
76
|
+
* @description Resolves a name to something legal in an XML document. The renderer and the parser both resolve a name per element and per attribute, and an
|
|
77
|
+
* illegal name under `'error'` mode is a rejection rather than a value, so this returns an `Effect` with the `XmlParseError` in its error channel
|
|
78
|
+
* rather than throwing it. Callers `yield*` it and the failure composes with `catchTag` and the rest; the two internal call sites in `parse.ts` and
|
|
79
|
+
* `render.ts` use {@link resolveNameSync} directly, because their walks are synchronous hot paths.
|
|
80
80
|
*
|
|
81
81
|
* @param name - The candidate element or attribute name.
|
|
82
82
|
* @param options - Repair mode and XML version.
|
package/dist/conventions.js
CHANGED
|
@@ -51,9 +51,9 @@ const isTextKey = (key) => key === TEXT_KEY;
|
|
|
51
51
|
*/
|
|
52
52
|
const isReservedKey = (key) => isTextKey(key);
|
|
53
53
|
/**
|
|
54
|
-
* @description Resolves a name to something legal in an XML document, synchronously. The renderer and the parser both resolve a name per element and per
|
|
55
|
-
*
|
|
56
|
-
*
|
|
54
|
+
* @description Resolves a name to something legal in an XML document, synchronously. The renderer and the parser both resolve a name per element and per attribute
|
|
55
|
+
* — the codec's hot path — so the walk calls this directly and keeps the work in plain JavaScript. `'error'` mode reports an illegal name by throwing
|
|
56
|
+
* an {@link XmlParseError}; the callers that need it in a typed channel use {@link resolveName}, which wraps this.
|
|
57
57
|
*
|
|
58
58
|
* @param name - The candidate element or attribute name.
|
|
59
59
|
* @param options - Repair mode and XML version.
|
|
@@ -96,10 +96,10 @@ const resolveNameResult = (name, options) => {
|
|
|
96
96
|
}
|
|
97
97
|
};
|
|
98
98
|
/**
|
|
99
|
-
* @description Resolves a name to something legal in an XML document. The renderer and the parser both resolve a name per element and per attribute
|
|
100
|
-
* name under `'error'` mode is a rejection rather than a value, so this returns an `Effect` with the `XmlParseError` in its error channel
|
|
101
|
-
* throwing it. Callers `yield*` it and the failure composes with `catchTag` and the rest
|
|
102
|
-
* use {@link resolveNameSync} directly, because their walks are synchronous hot paths.
|
|
99
|
+
* @description Resolves a name to something legal in an XML document. The renderer and the parser both resolve a name per element and per attribute, and an
|
|
100
|
+
* illegal name under `'error'` mode is a rejection rather than a value, so this returns an `Effect` with the `XmlParseError` in its error channel
|
|
101
|
+
* rather than throwing it. Callers `yield*` it and the failure composes with `catchTag` and the rest; the two internal call sites in `parse.ts` and
|
|
102
|
+
* `render.ts` use {@link resolveNameSync} directly, because their walks are synchronous hot paths.
|
|
103
103
|
*
|
|
104
104
|
* @param name - The candidate element or attribute name.
|
|
105
105
|
* @param options - Repair mode and XML version.
|
package/dist/conventions.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conventions.js","names":[],"sources":["../src/conventions.ts"],"sourcesContent":["import { Effect, Predicate, Result } from 'effect';\n\nimport type { XmlVersion } from './naming.ts';\n\nimport { XmlParseError } from './errors.ts';\nimport { isQName, sanitize, validate } from './naming.ts';\n\n/**\n * @description The key prefix that marks a field as an XML attribute. `@xmlns` is written as `xmlns=\"…\"`.\n */\nexport const ATTRIBUTE_PREFIX = '@';\n\n/**\n * @description The reserved key holding an element's character data, alongside its attributes and child elements.\n */\nexport const TEXT_KEY = '#text';\n\n/**\n * @description The element name used when nothing else names the root. Matches the default Effect uses in `Schema.toEncoderXml`.\n */\nexport const DEFAULT_ROOT_NAME = 'root';\n\n/**\n * @description The element name used for array members that have no natural name of their own. Matches the default in `Schema.toEncoderXml`.\n */\nexport const DEFAULT_ITEM_NAME = 'item';\n\n/**\n * @description What to do with a name that is not a legal XML name.\n *\n * - `repair` rewrites it into the nearest legal name. The default, because a serializer that silently produces a different tag name is worse than one\n * that produces a legal one.\n * - `error` fails the render. Use it when a rewritten name would silently change the meaning of the document.\n * - `ignore` writes the name as given, producing a document that is not well-formed. Only useful when a downstream step rewrites names anyway.\n */\nexport type NameMode = 'error' | 'ignore' | 'repair';\n\n/**\n * @description Options for {@link resolveName}.\n */\nexport interface ResolveNameOptions {\n /**\n * @description What to do with an illegal name. Defaults to `'repair'`.\n */\n readonly mode?: NameMode | undefined;\n\n /**\n * @description XML version to validate against. Defaults to `'1.0'`.\n */\n readonly xmlVersion?: XmlVersion | undefined;\n}\n\n/**\n * @description Whether a record key names an attribute rather than a child element.\n *\n * @param key - The key to classify.\n *\n * @returns Whether the key carries the {@link ATTRIBUTE_PREFIX}.\n */\nexport const isAttributeKey = (key: string): boolean => key.charCodeAt(0) === 64 && key.length > 1;\n\n/**\n * @description The attribute name a record key stands for: `@xmlns` becomes `xmlns`.\n *\n * @param key - A key that {@link isAttributeKey} accepted.\n *\n * @returns The name with the prefix removed.\n */\nexport const attributeName = (key: string): string => key.slice(1);\n\n/**\n * @description Whether a record key holds character data rather than a child element or an attribute.\n *\n * @param key - The key to classify.\n *\n * @returns Whether the key is the reserved {@link TEXT_KEY}.\n */\nexport const isTextKey = (key: string): boolean => key === TEXT_KEY;\n\n/**\n * @description Whether a key is one this package reserves. Only {@link TEXT_KEY} is reserved today; every other key is read as a child element name.\n *\n * @param key - The key to classify.\n *\n * @returns Whether the key is reserved.\n */\nexport const isReservedKey = (key: string): boolean => isTextKey(key);\n\n/**\n * @description Resolves a name to something legal in an XML document, synchronously. The renderer and the parser both resolve a name per element and per\n *
|
|
1
|
+
{"version":3,"file":"conventions.js","names":[],"sources":["../src/conventions.ts"],"sourcesContent":["import { Effect, Predicate, Result } from 'effect';\n\nimport type { XmlVersion } from './naming.ts';\n\nimport { XmlParseError } from './errors.ts';\nimport { isQName, sanitize, validate } from './naming.ts';\n\n/**\n * @description The key prefix that marks a field as an XML attribute. `@xmlns` is written as `xmlns=\"…\"`.\n */\nexport const ATTRIBUTE_PREFIX = '@';\n\n/**\n * @description The reserved key holding an element's character data, alongside its attributes and child elements.\n */\nexport const TEXT_KEY = '#text';\n\n/**\n * @description The element name used when nothing else names the root. Matches the default Effect uses in `Schema.toEncoderXml`.\n */\nexport const DEFAULT_ROOT_NAME = 'root';\n\n/**\n * @description The element name used for array members that have no natural name of their own. Matches the default in `Schema.toEncoderXml`.\n */\nexport const DEFAULT_ITEM_NAME = 'item';\n\n/**\n * @description What to do with a name that is not a legal XML name.\n *\n * - `repair` rewrites it into the nearest legal name. The default, because a serializer that silently produces a different tag name is worse than one\n * that produces a legal one.\n * - `error` fails the render. Use it when a rewritten name would silently change the meaning of the document.\n * - `ignore` writes the name as given, producing a document that is not well-formed. Only useful when a downstream step rewrites names anyway.\n */\nexport type NameMode = 'error' | 'ignore' | 'repair';\n\n/**\n * @description Options for {@link resolveName}.\n */\nexport interface ResolveNameOptions {\n /**\n * @description What to do with an illegal name. Defaults to `'repair'`.\n */\n readonly mode?: NameMode | undefined;\n\n /**\n * @description XML version to validate against. Defaults to `'1.0'`.\n */\n readonly xmlVersion?: XmlVersion | undefined;\n}\n\n/**\n * @description Whether a record key names an attribute rather than a child element.\n *\n * @param key - The key to classify.\n *\n * @returns Whether the key carries the {@link ATTRIBUTE_PREFIX}.\n */\nexport const isAttributeKey = (key: string): boolean => key.charCodeAt(0) === 64 && key.length > 1;\n\n/**\n * @description The attribute name a record key stands for: `@xmlns` becomes `xmlns`.\n *\n * @param key - A key that {@link isAttributeKey} accepted.\n *\n * @returns The name with the prefix removed.\n */\nexport const attributeName = (key: string): string => key.slice(1);\n\n/**\n * @description Whether a record key holds character data rather than a child element or an attribute.\n *\n * @param key - The key to classify.\n *\n * @returns Whether the key is the reserved {@link TEXT_KEY}.\n */\nexport const isTextKey = (key: string): boolean => key === TEXT_KEY;\n\n/**\n * @description Whether a key is one this package reserves. Only {@link TEXT_KEY} is reserved today; every other key is read as a child element name.\n *\n * @param key - The key to classify.\n *\n * @returns Whether the key is reserved.\n */\nexport const isReservedKey = (key: string): boolean => isTextKey(key);\n\n/**\n * @description Resolves a name to something legal in an XML document, synchronously. The renderer and the parser both resolve a name per element and per attribute\n * — the codec's hot path — so the walk calls this directly and keeps the work in plain JavaScript. `'error'` mode reports an illegal name by throwing\n * an {@link XmlParseError}; the callers that need it in a typed channel use {@link resolveName}, which wraps this.\n *\n * @param name - The candidate element or attribute name.\n * @param options - Repair mode and XML version.\n *\n * @returns The name to write, unchanged when it was already legal.\n *\n * @throws {XmlParseError} When the name is illegal and the mode is `'error'`.\n */\nexport const resolveNameSync = (name: string, { mode = 'repair', xmlVersion = '1.0' }: ResolveNameOptions = {}): string => {\n if (isQName(name, { xmlVersion })) return name;\n if (mode === 'ignore') return name;\n if (mode === 'repair') return sanitize(name, 'name', { replacement: '_' });\n\n // `validate` only fails for an unknown production, which `'qName'` is not, so\n // this runs a plain result and the failure is the reason the name is refused.\n const result = Effect.runSync(validate(name, 'qName', { xmlVersion }));\n const reason = !result.valid ? result.reason : 'is not a legal XML name';\n throw new XmlParseError({ message: `Invalid XML name ${JSON.stringify(name)}: ${reason}`, position: -1, input: name });\n};\n\n/**\n * @description {@link resolveNameSync} with the thrown failure folded into a {@link Result}, so an effectful caller can carry it in a typed channel without a\n * try/catch of its own.\n *\n * @param name - The candidate element or attribute name.\n * @param options - Repair mode and XML version.\n *\n * @returns The resolved name, or the failure to report.\n */\nconst resolveNameResult = (name: string, options: ResolveNameOptions): Result.Result<string, XmlParseError> => {\n try {\n return Result.succeed(resolveNameSync(name, options));\n } catch (cause) {\n if (cause instanceof XmlParseError) {\n return Result.fail(cause);\n }\n return Result.fail(new XmlParseError({ message: Predicate.isError(cause) ? cause.message : String(cause), position: -1, input: name }));\n }\n};\n\n/**\n * @description Resolves a name to something legal in an XML document. The renderer and the parser both resolve a name per element and per attribute, and an\n * illegal name under `'error'` mode is a rejection rather than a value, so this returns an `Effect` with the `XmlParseError` in its error channel\n * rather than throwing it. Callers `yield*` it and the failure composes with `catchTag` and the rest; the two internal call sites in `parse.ts` and\n * `render.ts` use {@link resolveNameSync} directly, because their walks are synchronous hot paths.\n *\n * @param name - The candidate element or attribute name.\n * @param options - Repair mode and XML version.\n *\n * @returns An effect producing the name to write, unchanged when it was already legal.\n */\nexport const resolveName = (name: string, options: ResolveNameOptions = {}): Effect.Effect<string, XmlParseError> =>\n Effect.suspend(() => Effect.fromResult(resolveNameResult(name, options)));\n"],"mappings":";;;;;;;AAUA,MAAa,mBAAmB;;;;AAKhC,MAAa,WAAW;;;;AAKxB,MAAa,oBAAoB;;;;AAKjC,MAAa,oBAAoB;;;;;;;;AAkCjC,MAAa,kBAAkB,QAAyB,IAAI,WAAW,CAAC,MAAM,MAAM,IAAI,SAAS;;;;;;;;AASjG,MAAa,iBAAiB,QAAwB,IAAI,MAAM,CAAC;;;;;;;;AASjE,MAAa,aAAa,QAAyB,QAAQ;;;;;;;;AAS3D,MAAa,iBAAiB,QAAyB,UAAU,GAAG;;;;;;;;;;;;;AAcpE,MAAa,mBAAmB,MAAc,EAAE,OAAO,UAAU,aAAa,UAA8B,CAAC,MAAc;CACzH,IAAI,QAAQ,MAAM,EAAE,WAAW,CAAC,GAAG,OAAO;CAC1C,IAAI,SAAS,UAAU,OAAO;CAC9B,IAAI,SAAS,UAAU,OAAO,SAAS,MAAM,QAAQ,EAAE,aAAa,IAAI,CAAC;CAIzE,MAAM,SAAS,OAAO,QAAQ,SAAS,MAAM,SAAS,EAAE,WAAW,CAAC,CAAC;CACrE,MAAM,SAAS,CAAC,OAAO,QAAQ,OAAO,SAAS;CAC/C,MAAM,IAAI,cAAc;EAAE,SAAS,oBAAoB,KAAK,UAAU,IAAI,EAAE,IAAI;EAAU,UAAU;EAAI,OAAO;CAAK,CAAC;AACvH;;;;;;;;;;AAWA,MAAM,qBAAqB,MAAc,YAAsE;CAC7G,IAAI;EACF,OAAO,OAAO,QAAQ,gBAAgB,MAAM,OAAO,CAAC;CACtD,SAAS,OAAO;EACd,IAAI,iBAAiB,eACnB,OAAO,OAAO,KAAK,KAAK;EAE1B,OAAO,OAAO,KAAK,IAAI,cAAc;GAAE,SAAS,UAAU,QAAQ,KAAK,IAAI,MAAM,UAAU,OAAO,KAAK;GAAG,UAAU;GAAI,OAAO;EAAK,CAAC,CAAC;CACxI;AACF;;;;;;;;;;;;AAaA,MAAa,eAAe,MAAc,UAA8B,CAAC,MACvE,OAAO,cAAc,OAAO,WAAW,kBAAkB,MAAM,OAAO,CAAC,CAAC"}
|