@endevops/effect-codec-xml 0.0.1 → 0.1.0-beta.2
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 +81 -62
- package/dist/codec.d.ts +17 -9
- package/dist/codec.d.ts.map +1 -1
- package/dist/codec.js +29 -18
- 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 +63 -64
- 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 +40 -13
- 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/plain-value.js +242 -0
- package/dist/plain-value.js.map +1 -0
- 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 +6 -7
- package/dist/xml-value.js.map +1 -1
- package/package.json +1 -1
- package/src/codec.ts +98 -71
- package/src/conventions.ts +7 -7
- package/src/entities/entity-decoder.ts +98 -99
- package/src/errors.ts +3 -3
- package/src/index.ts +3 -3
- package/src/namespaces.ts +62 -36
- package/src/naming.ts +34 -35
- package/src/parse.ts +26 -26
- package/src/plain-value.ts +312 -0
- package/src/render.ts +44 -44
- package/src/xml-error.ts +18 -18
- package/src/xml-value.ts +10 -11
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
# @endevops/effect-xml
|
|
1
|
+
# @endevops/effect-codec-xml
|
|
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, the way `Schema.toCodecJson`
|
|
6
|
-
JSON. There is no second call to a renderer or a parser at the call site.
|
|
5
|
+
and `Schema.decodeSync` reads one back, in the same way `Schema.toCodecJson` does
|
|
6
|
+
for 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-xml
|
|
12
|
+
import { toCodecXml } from '@endevops/effect-codec-xml';
|
|
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. Two key conventions give it XML meaning:
|
|
42
42
|
|
|
43
43
|
| In the value | In the document |
|
|
44
44
|
| ----------------------- | ------------------------------------- |
|
|
@@ -62,6 +62,24 @@ decision rather than the parser's:
|
|
|
62
62
|
`Schema.Struct({ '@id': Schema.String, '#text': Schema.String })` matches
|
|
63
63
|
`<a id="1">hello</a>`.
|
|
64
64
|
|
|
65
|
+
A field that wants a plain value does not have to describe the attributes
|
|
66
|
+
themselves. Where the schema wants a scalar, a `#text` node is read on its own
|
|
67
|
+
and the other attributes are discarded, so
|
|
68
|
+
`Schema.Struct({ title: Schema.String })` also matches
|
|
69
|
+
`<title lang="en">Dune</title>` and yields `'Dune'`. An element that also
|
|
70
|
+
carries a child element there is refused instead, as an effect error: a plain
|
|
71
|
+
value has nowhere to put a child, and dropping one silently would lose a field
|
|
72
|
+
the document actually carried.
|
|
73
|
+
|
|
74
|
+
The same holds wherever a scalar sits: under a union, or under a repeated field,
|
|
75
|
+
which derives as a union of an array and `undefined`. Each branch is folded in
|
|
76
|
+
turn, so `Schema.Struct({ id: Schema.Literals(['E', 'S']) })` matches
|
|
77
|
+
`<id schemeID="UNCL5305">E</id>` inside either. The reverse also works: an
|
|
78
|
+
element the schema reads as a struct with an `xmlValue` field, reduced by the
|
|
79
|
+
parser to bare character data because it carries no attributes, has that string
|
|
80
|
+
put back under the value's key, so a nested price struct still reads
|
|
81
|
+
`<price>1.5</price>` instead of failing on the bare string.
|
|
82
|
+
|
|
65
83
|
## Namespaces
|
|
66
84
|
|
|
67
85
|
A schema describes a value in local names, so a namespace is an annotation on
|
|
@@ -101,7 +119,7 @@ The annotation is attached with `Schema.annotate`:
|
|
|
101
119
|
|
|
102
120
|
```typescript
|
|
103
121
|
import { Schema } from 'effect';
|
|
104
|
-
import { toCodecXml } from '@endevops/effect-xml
|
|
122
|
+
import { toCodecXml } from '@endevops/effect-codec-xml';
|
|
105
123
|
|
|
106
124
|
const SOAP = 'http://schemas.xmlsoap.org/soap/envelope/';
|
|
107
125
|
const AUTH = 'urn:auth';
|
|
@@ -163,9 +181,9 @@ Two limits:
|
|
|
163
181
|
| `XmlError` | The failure `EntityDecoder` and `validate` report. |
|
|
164
182
|
|
|
165
183
|
`toCodecXml` returns a `Schema`, so encoding and decoding are `Schema.encodeSync`
|
|
166
|
-
and `Schema.decodeSync` (or the `Effect` forms)
|
|
167
|
-
|
|
168
|
-
unchanged. A failure in either direction arrives as a
|
|
184
|
+
and `Schema.decodeSync` (or the `Effect` forms). Every other Schema operation,
|
|
185
|
+
including `Schema.toFormatter`, `Schema.toJsonSchemaDocument` and the guards,
|
|
186
|
+
applies to it unchanged. A failure in either direction arrives as a
|
|
169
187
|
document that will not parse or a value that will not write is reported with its
|
|
170
188
|
underlying XML message, alongside the schema mismatches Effect already reports.
|
|
171
189
|
The root element is named from the `rootName` option, then the schema's
|
|
@@ -176,21 +194,21 @@ The root element is named from the `rootName` option, then the schema's
|
|
|
176
194
|
This package absorbed `@endevops/common-xml`, so the two primitives the text
|
|
177
195
|
layer reads documents with ship here rather than as a sibling dependency.
|
|
178
196
|
|
|
179
|
-
`EntityDecoder` expands the reference syntax XML inherits from HTML
|
|
180
|
-
`&#NNN;`, `&#xHH
|
|
197
|
+
`EntityDecoder` expands the reference syntax XML inherits from HTML: `&name;`,
|
|
198
|
+
`&#NNN;`, `&#xHH;`. It applies expansion limits, registration hooks, and a
|
|
181
199
|
numeric-reference policy. `parseXml` uses it for character data and falls back
|
|
182
200
|
to the raw text for a bare `&`. It reports failures as `XmlError`, a tagged
|
|
183
|
-
error whose `reason` union is
|
|
201
|
+
error whose `reason` union is the one `Effect.catchReason` narrows on.
|
|
184
202
|
|
|
185
|
-
The name validators are plain synchronous predicates
|
|
186
|
-
`isQName`, `isNmToken`, `isNmTokens`
|
|
203
|
+
The name validators are plain synchronous predicates (`isName`, `isNcName`,
|
|
204
|
+
`isQName`, `isNmToken`, `isNmTokens`), because a regex test cannot fail and the
|
|
187
205
|
renderer and parser call them per name in their hot loops. `sanitize` rewrites
|
|
188
206
|
an illegal name into the nearest legal one, and `validate` reports why a name
|
|
189
207
|
failed through an `Effect`.
|
|
190
208
|
|
|
191
209
|
```typescript
|
|
192
210
|
import { Effect } from 'effect';
|
|
193
|
-
import { EntityDecoder, isQName, sanitize, validate } from '@endevops/effect-xml
|
|
211
|
+
import { EntityDecoder, isQName, sanitize, validate } from '@endevops/effect-codec-xml';
|
|
194
212
|
|
|
195
213
|
isQName('svg:circle'); // true
|
|
196
214
|
sanitize('not a name', 'ncName'); // 'not_a_name'
|
|
@@ -208,10 +226,10 @@ did not come across: nothing in this package reaches them.
|
|
|
208
226
|
**The derivation is Effect's.** `toCodecXml` derives
|
|
209
227
|
`Schema.toCodecStringTree`, the same derivation `Schema.toEncoderXml` uses, and
|
|
210
228
|
runs it through this package's `renderXml` and `parseXml` on the two text
|
|
211
|
-
directions.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
229
|
+
directions. Every schema feature Effect supports then works here without this
|
|
230
|
+
package re-implementing the walk over a schema AST: structs,
|
|
231
|
+
arrays, unions, records, recursion, refinements, branded types, transformations.
|
|
232
|
+
The round-trip specs exercise that.
|
|
215
233
|
|
|
216
234
|
**The shape is `toCodecJson`'s.** The value `toCodecXml` returns is a `Schema`:
|
|
217
235
|
`Type` is the source schema's `Type`, `Encoded` is XML text, the service
|
|
@@ -232,8 +250,8 @@ normalization would otherwise eat it: a literal newline in an attribute comes
|
|
|
232
250
|
back as a space unless it is written ` `.
|
|
233
251
|
|
|
234
252
|
**Comments and processing instructions are markup, not data.** The parser skips
|
|
235
|
-
them, which is
|
|
236
|
-
|
|
253
|
+
them, which is the default `XMLBuilder` behaviour. CDATA becomes character data,
|
|
254
|
+
because that is what it is.
|
|
237
255
|
|
|
238
256
|
## Performance
|
|
239
257
|
|
|
@@ -256,40 +274,41 @@ Four findings shaped the code, and all are measured rather than assumed:
|
|
|
256
274
|
|
|
257
275
|
- **Escaping was the whole cost of a large document.** The entity encoder that
|
|
258
276
|
used to live here escaped by applying five sequential global replacements, one
|
|
259
|
-
per character
|
|
260
|
-
|
|
261
|
-
Escaping is now a single pattern scan to find the first character
|
|
262
|
-
replacing, then one pass to build the result
|
|
263
|
-
text and 28x faster for text with a character in it.
|
|
264
|
-
the fast path with explicit expectations.
|
|
277
|
+
per character. A document with a single `&` in twenty thousand characters was
|
|
278
|
+
therefore scanned five times over to change one byte, at 58µs for that one
|
|
279
|
+
document. Escaping is now a single pattern scan to find the first character
|
|
280
|
+
that needs replacing, then one pass to build the result. That is 7.6x faster
|
|
281
|
+
for clean text and 28x faster for text with a character in it.
|
|
282
|
+
`src/render.spec.ts` pins the fast path with explicit expectations.
|
|
265
283
|
- **Resolving a name is a regex test, not an `Effect`.** The validators used to
|
|
266
284
|
return `Effect`s for pure questions, which left the renderer and the parser
|
|
267
285
|
running an effect per distinct element and attribute name in every document.
|
|
268
286
|
Running a runtime to read a boolean cost roughly 1µs per name, and a small
|
|
269
|
-
document has about a dozen names
|
|
287
|
+
document has about a dozen names. Name resolution was therefore most of what a
|
|
270
288
|
serialize and a parse did. The merged package now exposes plain synchronous
|
|
271
289
|
predicates (`isQName` and the rest, `sanitize`) with no `Effect` wrapper, and
|
|
272
290
|
the renderer's namer and the parser's name cache call them directly.
|
|
273
291
|
`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
|
-
|
|
276
|
-
or a render. That
|
|
277
|
-
document improves by a few percent because it asks the same handful of
|
|
278
|
-
and the per-row work
|
|
292
|
+
rejects an illegal name, and a rejection is a failure rather than a value. It
|
|
293
|
+
reports through the error channel like every other fallible step of a parse
|
|
294
|
+
or a render. That accounts for most of the gain on a small document. The
|
|
295
|
+
500-row document improves by a few percent because it asks the same handful of
|
|
296
|
+
names, and the per-row work dominates there.
|
|
279
297
|
- **The codec adds no layer of its own.** `toCodecXml` derives
|
|
280
298
|
`Schema.toCodecStringTree` and runs the tree through `renderXml`, so a value is
|
|
281
299
|
encoded by Effect's parser and then rendered, with nothing wrapped around
|
|
282
|
-
either.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
300
|
+
either. The codec also no longer carries an extra
|
|
301
|
+
`Schema.toCodecArrayFromSingle` layer. Dropping it is part of why the table
|
|
302
|
+
above is higher than the numbers this README quoted before: the
|
|
303
|
+
single-element-array leniency is the caller's to compose now, and the plain
|
|
304
|
+
codec does not pay for it on every array.
|
|
286
305
|
- **A small document is dominated by something this package does not own.** Of
|
|
287
306
|
the ~9µs it takes to serialize one, roughly 2.5µs is Effect's
|
|
288
307
|
`toCodecStringTree` derivation, which walks the schema on every call, and the
|
|
289
308
|
rest is this package's renderer. The renderer and parser underneath run at
|
|
290
|
-
roughly 2.5µs and 2.5µs on a flat document. A caller
|
|
291
|
-
shape on every request should build the codec once and reuse it, which is
|
|
292
|
-
the API
|
|
309
|
+
roughly 2.5µs and 2.5µs on a flat document. A caller that serializes the same
|
|
310
|
+
shape on every request should build the codec once and reuse it, which is the
|
|
311
|
+
shape the API encourages.
|
|
293
312
|
|
|
294
313
|
The other things the benchmarks changed: one pass over a record's keys instead of
|
|
295
314
|
one per role a key can play, name resolution memoized per document rather than
|
|
@@ -311,14 +330,14 @@ fallback all see it.
|
|
|
311
330
|
### Against the libraries on npm
|
|
312
331
|
|
|
313
332
|
`packages/benchmarks/bench/comparison.bench.ts` measures the same object through
|
|
314
|
-
this codec and
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
333
|
+
this codec and through three npm libraries: `fast-xml-builder`, `fast-xml-parser`,
|
|
334
|
+
and `@nodable/flexible-xml-parser`. The benchmark establishes the equivalence
|
|
335
|
+
rather than assuming it. With `attributeNamePrefix: '@'`,
|
|
336
|
+
**`fast-xml-builder` produces byte-identical output to this codec**, and the
|
|
337
|
+
benchmark asserts it, so a change that breaks it fails the suite instead of
|
|
338
|
+
quietly comparing different work.
|
|
320
339
|
|
|
321
|
-
**Encoding
|
|
340
|
+
**Encoding**: one object to the same bytes:
|
|
322
341
|
|
|
323
342
|
| Document | This codec | `fast-xml-builder` |
|
|
324
343
|
| ------------------- | ---------- | ------------------ |
|
|
@@ -326,7 +345,7 @@ that breaks it fails the suite instead of quietly comparing different work.
|
|
|
326
345
|
| 500 rows | 1,698/s | 1,269/s (0.75x) |
|
|
327
346
|
| one large text node | 584,358/s | 254,355/s (0.44x) |
|
|
328
347
|
|
|
329
|
-
**Decoding
|
|
348
|
+
**Decoding**: one document to the same value:
|
|
330
349
|
|
|
331
350
|
| Document | This codec | `@nodable/flexible-xml-parser` | `fast-xml-parser` |
|
|
332
351
|
| ------------------- | ---------- | ------------------------------ | ----------------- |
|
|
@@ -334,9 +353,9 @@ that breaks it fails the suite instead of quietly comparing different work.
|
|
|
334
353
|
| 500 rows | 1,975/s | 70/s (0.04x) | 486/s (0.25x) |
|
|
335
354
|
| one large text node | 20,715/s | 5,694/s (0.27x) | 8,209/s (0.40x) |
|
|
336
355
|
|
|
337
|
-
**A full round trip**,
|
|
338
|
-
|
|
339
|
-
|
|
356
|
+
**A full round trip**, the number an application actually pays. Neither a
|
|
357
|
+
builder nor a parser can do both halves, so the last two rows are each ecosystem
|
|
358
|
+
doing the same work with two libraries and hand-joining them:
|
|
340
359
|
|
|
341
360
|
| Path | Throughput |
|
|
342
361
|
| ----------------------------------------------------------- | ---------------- |
|
|
@@ -346,14 +365,14 @@ ecosystem doing the same work with two libraries and hand-joining them:
|
|
|
346
365
|
|
|
347
366
|
**This codec reaches the parser and renderer directly.** It encodes and decodes
|
|
348
367
|
through `Schema.encodeSync` and `Schema.decodeSync`, with no runtime per call,
|
|
349
|
-
|
|
350
|
-
they do not validate the result against a schema
|
|
368
|
+
so it is the fastest row on both sides. The npm parsers do less work, because
|
|
369
|
+
they do not validate the result against a schema, so the comparison is
|
|
351
370
|
directional rather than like-for-like.
|
|
352
371
|
|
|
353
|
-
Four
|
|
372
|
+
Four caveats apply when reading those tables:
|
|
354
373
|
|
|
355
374
|
- **These are one machine's numbers, from one run.** The relative error is under
|
|
356
|
-
2% on every row, but the machine's load moved between runs
|
|
375
|
+
2% on every row, but the machine's load moved between runs. Treat the
|
|
357
376
|
ratios as the durable part and the absolute figures as a range.
|
|
358
377
|
- **This codec's decode does strictly more work.** It parses _and_ validates the
|
|
359
378
|
result against the schema, coercing `"30"` to `30` and failing on a mismatch.
|
|
@@ -361,11 +380,11 @@ Four things to be straight about when reading those tables:
|
|
|
361
380
|
2,984/s and the schema pass brings it to 1,975/s, so roughly a third of the
|
|
362
381
|
decode time is validation the comparison rows do not pay.
|
|
363
382
|
- **The parsers do work this codec does not.** They coerce tag values through a
|
|
364
|
-
value-parser pipeline
|
|
365
|
-
number parsing
|
|
366
|
-
the other lacks, and
|
|
383
|
+
value-parser pipeline: entity decoding, whitespace normalizing, boolean and
|
|
384
|
+
number parsing, where a schema already decided the type. Both sides have work
|
|
385
|
+
the other lacks, and both are busy.
|
|
367
386
|
- **Neither ecosystem is doing the whole job on its own.** A builder has no
|
|
368
|
-
reader and a parser has no writer, so the round-trip table is the
|
|
387
|
+
reader and a parser has no writer, so the round-trip table is the fair
|
|
369
388
|
comparison and the single-direction tables are the diagnostic ones.
|
|
370
389
|
|
|
371
390
|
## Limitations
|
|
@@ -396,7 +415,7 @@ quietly.
|
|
|
396
415
|
children and read back as a single run, so `<p>a<b/>c</p>` reads as
|
|
397
416
|
`{ b: '', '#text': 'ac' }`. Reading and re-rendering is stable from there on.
|
|
398
417
|
- **Whitespace at the edges of text is trimmed** unless `preserveWhitespace` is
|
|
399
|
-
set. That
|
|
418
|
+
set. That lets a pretty-printed document read as the same value as an
|
|
400
419
|
unindented one. Whitespace _inside_ a run is never touched.
|
|
401
420
|
- **`Schema.BigInt` is write-only.** Effect's `StringTree` derivation lowers one
|
|
402
421
|
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
|
-
* back to `'root'`.
|
|
8
|
+
* `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
|
|
9
|
+
* falls back to `'root'`.
|
|
10
10
|
*/
|
|
11
11
|
export type XmlCodecOptions = XmlRenderOptions & XmlParseOptions;
|
|
12
12
|
/**
|
|
@@ -19,15 +19,18 @@ 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}.
|
|
22
|
+
* and {@link parseXml}. Call it data-first, `toCodecXml(schema, options)`, or data-last, `toCodecXml(options)(schema)`, so it drops into `pipe` beside
|
|
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.
|
|
23
25
|
*
|
|
24
26
|
* @example
|
|
25
27
|
* ```typescript
|
|
26
|
-
* import { Schema } from 'effect';
|
|
27
|
-
* import { toCodecXml } from '@endevops/effect-xml
|
|
28
|
+
* import { Schema, pipe } from 'effect';
|
|
29
|
+
* import { toCodecXml } from '@endevops/effect-codec-xml';
|
|
28
30
|
*
|
|
29
31
|
* const Book = Schema.Struct({ '@id': Schema.String, title: Schema.String, pages: Schema.Number });
|
|
30
32
|
* const codec = toCodecXml(Book, { rootName: 'book' });
|
|
33
|
+
* const piped = pipe(Book, toCodecXml({ rootName: 'book' }));
|
|
31
34
|
*
|
|
32
35
|
* const value = { '@id': '1', title: 'Dune', pages: 412 };
|
|
33
36
|
*
|
|
@@ -38,11 +41,16 @@ export interface toCodecXml<S extends Schema.Constraint> extends Schema.decodeTo
|
|
|
38
41
|
* // => { '@id': '1', title: 'Dune', pages: 412 }
|
|
39
42
|
* ```;
|
|
40
43
|
*
|
|
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'`.
|
|
44
|
+
* @param schema - The schema describing the value, in the data-first form.
|
|
45
|
+
* @param options - Render and parse options. `rootName` defaults to the schema's `identifier` or `title` annotation, then to `'root'`. In the
|
|
46
|
+
* data-last form this is the only argument, and the schema arrives from `pipe`.
|
|
43
47
|
*
|
|
44
|
-
* @returns The codec, with the source schema's `Type` and the same service requirements.
|
|
48
|
+
* @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
|
|
49
|
+
* codec.
|
|
45
50
|
*/
|
|
46
|
-
export declare const toCodecXml:
|
|
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
|
+
};
|
|
47
55
|
//#endregion
|
|
48
56
|
//# 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":";;;;;;;;;;YA2CY,kBAAkB,mBAAmB;;;;;iBAMhC,WAAW,UAAU,OAAO,oBAAoB,OAAO,SAAS,OAAO,kBAAkB,IAAI,OAAO;WAC1G,SAAS,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAsDlB;GACV,UAAU,OAAO,YAAY,QAAQ,GAAG,UAAU,kBAAkB,WAAW;GAC/E,UAAU,mBAAmB,UAAU,OAAO,YAAY,QAAQ,MAAM,WAAW"}
|
package/dist/codec.js
CHANGED
|
@@ -1,21 +1,34 @@
|
|
|
1
1
|
import "./conventions.js";
|
|
2
2
|
import { decodeNames, encodeNames, namespacePlan } from "./namespaces.js";
|
|
3
3
|
import { parseXml } from "./parse.js";
|
|
4
|
+
import { normalizePlainValue } from "./plain-value.js";
|
|
4
5
|
import { renderXml } from "./render.js";
|
|
5
|
-
import { Effect, Predicate, Schema, SchemaAST, SchemaIssue, SchemaTransformation } from "effect";
|
|
6
|
+
import { Effect, Function, Predicate, Schema, SchemaAST, SchemaIssue, SchemaTransformation } from "effect";
|
|
6
7
|
//#region src/codec.ts
|
|
8
|
+
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;
|
|
9
|
+
const resolveRenderOptions = (schema, plan, options) => {
|
|
10
|
+
const rootName = options.rootName ?? plan.rootName ?? SchemaAST.resolveIdentifier(schema.ast) ?? SchemaAST.resolveTitle(schema.ast) ?? "root";
|
|
11
|
+
const wireRootName = plan.root !== void 0 && plan.root.prefix !== "" && !rootName.includes(":") ? `${plan.root.prefix}:${rootName}` : rootName;
|
|
12
|
+
return {
|
|
13
|
+
...options,
|
|
14
|
+
rootName: wireRootName
|
|
15
|
+
};
|
|
16
|
+
};
|
|
7
17
|
/**
|
|
8
18
|
* @description Derives the XML codec for a schema: a `Schema` whose `Encoded` is an XML document, so `Schema.encodeSync(codec)` writes text and
|
|
9
19
|
* `Schema.decodeSync(codec)` reads it back. The derivation is Effect's `Schema.toCodecStringTree`; the text layer is this package's {@link renderXml}
|
|
10
|
-
* and {@link parseXml}.
|
|
20
|
+
* and {@link parseXml}. Call it data-first, `toCodecXml(schema, options)`, or data-last, `toCodecXml(options)(schema)`, so it drops into `pipe` beside
|
|
21
|
+
* 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
|
|
22
|
+
* options object as data-last. `pipe(schema, toCodecXml)` carries no options and works because a schema on its own is data-first.
|
|
11
23
|
*
|
|
12
24
|
* @example
|
|
13
25
|
* ```typescript
|
|
14
|
-
* import { Schema } from 'effect';
|
|
15
|
-
* import { toCodecXml } from '@endevops/effect-xml
|
|
26
|
+
* import { Schema, pipe } from 'effect';
|
|
27
|
+
* import { toCodecXml } from '@endevops/effect-codec-xml';
|
|
16
28
|
*
|
|
17
29
|
* const Book = Schema.Struct({ '@id': Schema.String, title: Schema.String, pages: Schema.Number });
|
|
18
30
|
* const codec = toCodecXml(Book, { rootName: 'book' });
|
|
31
|
+
* const piped = pipe(Book, toCodecXml({ rootName: 'book' }));
|
|
19
32
|
*
|
|
20
33
|
* const value = { '@id': '1', title: 'Dune', pages: 412 };
|
|
21
34
|
*
|
|
@@ -26,29 +39,27 @@ import { Effect, Predicate, Schema, SchemaAST, SchemaIssue, SchemaTransformation
|
|
|
26
39
|
* // => { '@id': '1', title: 'Dune', pages: 412 }
|
|
27
40
|
* ```;
|
|
28
41
|
*
|
|
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'`.
|
|
42
|
+
* @param schema - The schema describing the value, in the data-first form.
|
|
43
|
+
* @param options - Render and parse options. `rootName` defaults to the schema's `identifier` or `title` annotation, then to `'root'`. In the
|
|
44
|
+
* data-last form this is the only argument, and the schema arrives from `pipe`.
|
|
31
45
|
*
|
|
32
|
-
* @returns The codec, with the source schema's `Type` and the same service requirements.
|
|
46
|
+
* @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
|
|
47
|
+
* codec.
|
|
33
48
|
*/
|
|
34
|
-
const toCodecXml = (schema, options = {}) => {
|
|
49
|
+
const toCodecXml = Function.dual((args) => Schema.isSchema(args[0]), (schema, options = {}) => {
|
|
35
50
|
const planned = namespacePlan(schema);
|
|
36
51
|
if (Predicate.hasProperty(planned, "error")) throw new Error(`Invalid XML namespace annotation:\n\t- ${planned.error}.`);
|
|
37
52
|
const plan = planned.plan;
|
|
38
|
-
const active = plan
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
...options,
|
|
43
|
-
rootName: wireRootName
|
|
44
|
-
};
|
|
45
|
-
return Schema.String.pipe(Schema.decodeTo(Schema.toCodecStringTree(schema), SchemaTransformation.transformEffect({
|
|
53
|
+
const active = isActivePlan(plan);
|
|
54
|
+
const renderOptions = resolveRenderOptions(schema, plan, options);
|
|
55
|
+
const stringTree = Schema.toCodecStringTree(schema);
|
|
56
|
+
return Schema.String.pipe(Schema.decodeTo(stringTree, SchemaTransformation.transformEffect({
|
|
46
57
|
decode: (text, parseOptions) => {
|
|
47
58
|
if (!Predicate.isString(text)) return Effect.fail(new SchemaIssue.InvalidValue({ message: `Expected a string, but received ${typeof text}.` }, text, parseOptions));
|
|
48
59
|
return parseXml(text, options).pipe(Effect.map((value) => active ? decodeNames(value, plan, {}, "") : value), Effect.tapError((error) => Effect.logError(`XML parse error: ${error.message}`).pipe(Effect.annotateLogs({
|
|
49
60
|
cause: error,
|
|
50
61
|
message: "XML parse error"
|
|
51
|
-
}))), Effect.mapError((error) => new SchemaIssue.InvalidValue({ message: error.message }, text, parseOptions)));
|
|
62
|
+
}))), Effect.mapError((error) => new SchemaIssue.InvalidValue({ message: error.message }, text, parseOptions)), Effect.flatMap((value) => Effect.fromResult(normalizePlainValue(value, stringTree.ast, "")).pipe(Effect.mapError((message) => new SchemaIssue.InvalidValue({ message }, value, parseOptions)))));
|
|
52
63
|
},
|
|
53
64
|
encode: (value, parseOptions) => {
|
|
54
65
|
if (active && Array.isArray(value) && (plan.root !== void 0 || plan.rootName !== void 0)) return Effect.fail(new SchemaIssue.InvalidValue({ message: "An array at the root of a namespaced schema cannot carry the root namespace or name." }, value, parseOptions));
|
|
@@ -56,7 +67,7 @@ const toCodecXml = (schema, options = {}) => {
|
|
|
56
67
|
return renderXml(wire, renderOptions).pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue({ message: error.message }, value, parseOptions)));
|
|
57
68
|
}
|
|
58
69
|
})));
|
|
59
|
-
};
|
|
70
|
+
});
|
|
60
71
|
//#endregion
|
|
61
72
|
export { toCodecXml };
|
|
62
73
|
|
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)` — 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"}
|
|
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 { normalizePlainValue } from './plain-value.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 const stringTree = Schema.toCodecStringTree(schema);\n\n return Schema.String.pipe(\n Schema.decodeTo(\n stringTree,\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 Effect.flatMap(value =>\n Effect.fromResult(normalizePlainValue(value, stringTree.ast, '')).pipe(\n Effect.mapError(message => new SchemaIssue.InvalidValue({ message }, value, parseOptions))\n )\n )\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":";;;;;;;AAuDA,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;CAChE,MAAM,aAAa,OAAO,kBAAkB,MAAM;CAElD,OAAO,OAAO,OAAO,KACnB,OAAO,SACL,YACA,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,GACrG,OAAO,SAAQ,UACb,OAAO,WAAW,oBAAoB,OAAO,WAAW,KAAK,EAAE,CAAC,CAAC,CAAC,KAChE,OAAO,UAAS,YAAW,IAAI,YAAY,aAAa,EAAE,QAAQ,GAAG,OAAO,YAAY,CAAC,CAC3F,CACF,CACF;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"}
|
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
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
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. An illegal
|
|
77
|
+
* name under `'error'` mode is a rejection rather than a value, so this returns an `Effect` with the `XmlParseError` in its error channel instead of
|
|
78
|
+
* throwing it. Callers `yield*` it and the failure composes with `catchTag` and the rest. The two internal call sites in `parse.ts` and `render.ts`
|
|
79
|
+
* 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
|
-
* an {@link XmlParseError}; the callers that need it in a typed channel use {@link resolveName}, which wraps this.
|
|
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
|
+
* attribute, and that is the codec's hot path, so the walk calls this directly and keeps the work in plain JavaScript. `'error'` mode reports an
|
|
56
|
+
* illegal name by throwing 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
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
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. An illegal
|
|
100
|
+
* name under `'error'` mode is a rejection rather than a value, so this returns an `Effect` with the `XmlParseError` in its error channel instead of
|
|
101
|
+
* throwing it. Callers `yield*` it and the failure composes with `catchTag` and the rest. The two internal call sites in `parse.ts` and `render.ts`
|
|
102
|
+
* 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
|
|
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 * attribute, and that is the codec's hot path, so the walk calls this directly and keeps the work in plain JavaScript. `'error'` mode reports an\n * illegal name by throwing 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. An illegal\n * name under `'error'` mode is a rejection rather than a value, so this returns an `Effect` with the `XmlParseError` in its error channel instead of\n * throwing it. Callers `yield*` it and the failure composes with `catchTag` and the rest. The two internal call sites in `parse.ts` and `render.ts`\n * 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"}
|