@endevops/effect-codec-xml 0.0.1 → 0.1.0-beta.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.
Files changed (47) hide show
  1. package/README.md +63 -62
  2. package/dist/codec.d.ts +17 -9
  3. package/dist/codec.d.ts.map +1 -1
  4. package/dist/codec.js +25 -16
  5. package/dist/codec.js.map +1 -1
  6. package/dist/conventions.d.ts +4 -4
  7. package/dist/conventions.js +7 -7
  8. package/dist/conventions.js.map +1 -1
  9. package/dist/entities/entity-decoder.d.ts +33 -33
  10. package/dist/entities/entity-decoder.d.ts.map +1 -1
  11. package/dist/entities/entity-decoder.js +63 -64
  12. package/dist/entities/entity-decoder.js.map +1 -1
  13. package/dist/errors.d.ts +2 -2
  14. package/dist/errors.js +2 -2
  15. package/dist/errors.js.map +1 -1
  16. package/dist/namespaces.js +2 -2
  17. package/dist/namespaces.js.map +1 -1
  18. package/dist/naming.d.ts +6 -6
  19. package/dist/naming.d.ts.map +1 -1
  20. package/dist/naming.js +3 -3
  21. package/dist/naming.js.map +1 -1
  22. package/dist/parse.d.ts +5 -5
  23. package/dist/parse.js +14 -14
  24. package/dist/parse.js.map +1 -1
  25. package/dist/render.d.ts +1 -1
  26. package/dist/render.d.ts.map +1 -1
  27. package/dist/render.js +28 -28
  28. package/dist/render.js.map +1 -1
  29. package/dist/xml-error.d.ts +9 -9
  30. package/dist/xml-error.js +18 -18
  31. package/dist/xml-error.js.map +1 -1
  32. package/dist/xml-value.d.ts +7 -7
  33. package/dist/xml-value.d.ts.map +1 -1
  34. package/dist/xml-value.js +6 -7
  35. package/dist/xml-value.js.map +1 -1
  36. package/package.json +1 -1
  37. package/src/codec.ts +91 -71
  38. package/src/conventions.ts +7 -7
  39. package/src/entities/entity-decoder.ts +98 -99
  40. package/src/errors.ts +3 -3
  41. package/src/index.ts +3 -3
  42. package/src/namespaces.ts +16 -16
  43. package/src/naming.ts +34 -35
  44. package/src/parse.ts +26 -26
  45. package/src/render.ts +44 -44
  46. package/src/xml-error.ts +18 -18
  47. package/src/xml-value.ts +10 -11
package/README.md CHANGED
@@ -1,15 +1,15 @@
1
- # @endevops/effect-xml-codec
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` works for
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-codec';
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 — a field named `@id` becomes an element called `<@id>` with its name
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 — strings, arrays
41
- and records. What gives it XML meaning is two key conventions:
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
  | ----------------------- | ------------------------------------- |
@@ -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-xml-codec';
104
+ import { toCodecXml } from '@endevops/effect-codec-xml';
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), 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
166
+ and `Schema.decodeSync` (or the `Effect` forms). Every other Schema operation,
167
+ including `Schema.toFormatter`, `Schema.toJsonSchemaDocument` and the guards,
168
+ applies to it unchanged. A failure in either direction arrives as 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 — `&name;`,
180
- `&#NNN;`, `&#xHH;` — with expansion limits, registration hooks, and a
179
+ `EntityDecoder` expands the reference syntax XML inherits from HTML: `&name;`,
180
+ `&#NNN;`, `&#xHH;`. It applies 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 what `Effect.catchReason` narrows on.
183
+ error whose `reason` union is the one `Effect.catchReason` narrows on.
184
184
 
185
- The name validators are plain synchronous predicates — `isName`, `isNcName`,
186
- `isQName`, `isNmToken`, `isNmTokens` — because a regex test cannot fail and the
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-xml-codec';
193
+ import { EntityDecoder, isQName, sanitize, validate } from '@endevops/effect-codec-xml';
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. 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.
211
+ directions. Every schema feature Effect supports then works here without this
212
+ package re-implementing the walk over a schema AST: structs,
213
+ arrays, unions, records, recursion, refinements, branded types, transformations.
214
+ The round-trip specs exercise that.
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 `&#10;`.
233
233
 
234
234
  **Comments and processing instructions are markup, not data.** The parser skips
235
- them, which is what `XMLBuilder` does by default. CDATA becomes character data,
236
- since that is what it is.
235
+ them, which is the default `XMLBuilder` behaviour. CDATA becomes character data,
236
+ because that is what it is.
237
237
 
238
238
  ## Performance
239
239
 
@@ -256,40 +256,41 @@ 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, 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.
259
+ per character. A document with a single `&` in twenty thousand characters was
260
+ therefore scanned five times over to change one byte, at 58µs for that one
261
+ document. Escaping is now a single pattern scan to find the first character
262
+ that needs replacing, then one pass to build the result. That is 7.6x faster
263
+ for clean text and 28x faster for text with a character in it.
264
+ `src/render.spec.ts` pins 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, so name resolution was most of what a
269
+ document has about a dozen names. Name resolution was therefore 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, 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.
274
+ rejects an illegal name, and a rejection is a failure rather than a value. It
275
+ reports through the error channel like every other fallible step of a parse
276
+ or a render. That accounts for most of the gain on a small document. The
277
+ 500-row document improves by a few percent because it asks the same handful of
278
+ names, and the per-row work 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. 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.
282
+ either. The codec also no longer carries an extra
283
+ `Schema.toCodecArrayFromSingle` layer. Dropping it is part of why the table
284
+ above is higher than the numbers this README quoted before: the
285
+ single-element-array leniency is the caller's to compose now, and the plain
286
+ codec does not pay for it on every array.
286
287
  - **A small document is dominated by something this package does not own.** Of
287
288
  the ~9µs it takes to serialize one, roughly 2.5µs is Effect's
288
289
  `toCodecStringTree` derivation, which walks the schema on every call, and the
289
290
  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 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.
291
+ roughly 2.5µs and 2.5µs on a flat document. A caller that serializes the same
292
+ shape on every request should build the codec once and reuse it, which is the
293
+ shape the API encourages.
293
294
 
294
295
  The other things the benchmarks changed: one pass over a record's keys instead of
295
296
  one per role a key can play, name resolution memoized per document rather than
@@ -311,14 +312,14 @@ fallback all see it.
311
312
  ### Against the libraries on npm
312
313
 
313
314
  `packages/benchmarks/bench/comparison.bench.ts` measures the same object through
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.
315
+ this codec and through three npm libraries: `fast-xml-builder`, `fast-xml-parser`,
316
+ and `@nodable/flexible-xml-parser`. The benchmark establishes the equivalence
317
+ rather than assuming it. With `attributeNamePrefix: '@'`,
318
+ **`fast-xml-builder` produces byte-identical output to this codec**, and the
319
+ benchmark asserts it, so a change that breaks it fails the suite instead of
320
+ quietly comparing different work.
320
321
 
321
- **Encoding** — one object to the same bytes:
322
+ **Encoding**: one object to the same bytes:
322
323
 
323
324
  | Document | This codec | `fast-xml-builder` |
324
325
  | ------------------- | ---------- | ------------------ |
@@ -326,7 +327,7 @@ that breaks it fails the suite instead of quietly comparing different work.
326
327
  | 500 rows | 1,698/s | 1,269/s (0.75x) |
327
328
  | one large text node | 584,358/s | 254,355/s (0.44x) |
328
329
 
329
- **Decoding** — one document to the same value:
330
+ **Decoding**: one document to the same value:
330
331
 
331
332
  | Document | This codec | `@nodable/flexible-xml-parser` | `fast-xml-parser` |
332
333
  | ------------------- | ---------- | ------------------------------ | ----------------- |
@@ -334,9 +335,9 @@ that breaks it fails the suite instead of quietly comparing different work.
334
335
  | 500 rows | 1,975/s | 70/s (0.04x) | 486/s (0.25x) |
335
336
  | one large text node | 20,715/s | 5,694/s (0.27x) | 8,209/s (0.40x) |
336
337
 
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:
338
+ **A full round trip**, the number an application actually pays. Neither a
339
+ builder nor a parser can do both halves, so the last two rows are each ecosystem
340
+ doing the same work with two libraries and hand-joining them:
340
341
 
341
342
  | Path | Throughput |
342
343
  | ----------------------------------------------------------- | ---------------- |
@@ -346,14 +347,14 @@ ecosystem doing the same work with two libraries and hand-joining them:
346
347
 
347
348
  **This codec reaches the parser and renderer directly.** It encodes and decodes
348
349
  through `Schema.encodeSync` and `Schema.decodeSync`, with no runtime per call,
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
350
+ so it is the fastest row on both sides. The npm parsers do less work, because
351
+ they do not validate the result against a schema, so the comparison is
351
352
  directional rather than like-for-like.
352
353
 
353
- Four things to be straight about when reading those tables:
354
+ Four caveats apply when reading those tables:
354
355
 
355
356
  - **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, so treat the
357
+ 2% on every row, but the machine's load moved between runs. Treat the
357
358
  ratios as the durable part and the absolute figures as a range.
358
359
  - **This codec's decode does strictly more work.** It parses _and_ validates the
359
360
  result against the schema, coercing `"30"` to `30` and failing on a mismatch.
@@ -361,11 +362,11 @@ Four things to be straight about when reading those tables:
361
362
  2,984/s and the schema pass brings it to 1,975/s, so roughly a third of the
362
363
  decode time is validation the comparison rows do not pay.
363
364
  - **The parsers do work this codec does not.** They coerce tag values through a
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.
365
+ value-parser pipeline: entity decoding, whitespace normalizing, boolean and
366
+ number parsing, where a schema already decided the type. Both sides have work
367
+ the other lacks, and both are busy.
367
368
  - **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 honest
369
+ reader and a parser has no writer, so the round-trip table is the fair
369
370
  comparison and the single-direction tables are the diagnostic ones.
370
371
 
371
372
  ## Limitations
@@ -396,7 +397,7 @@ quietly.
396
397
  children and read back as a single run, so `<p>a<b/>c</p>` reads as
397
398
  `{ b: '', '#text': 'ac' }`. Reading and re-rendering is stable from there on.
398
399
  - **Whitespace at the edges of text is trimmed** unless `preserveWhitespace` is
399
- set. That is what makes a pretty-printed document read as the same value as an
400
+ set. That lets a pretty-printed document read as the same value as an
400
401
  unindented one. Whitespace _inside_ a run is never touched.
401
402
  - **`Schema.BigInt` is write-only.** Effect's `StringTree` derivation lowers one
402
403
  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, taking it from the schema's `identifier` or `title` annotation and falling
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-codec';
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: <S extends Schema.Constraint>(schema: S, options?: XmlCodecOptions) => toCodecXml<S>;
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
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"codec.d.ts","names":[],"sources":["../src/codec.ts"],"mappings":";;;;;;;;;;YA0CY,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
@@ -2,20 +2,32 @@ 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, Predicate, Schema, SchemaAST, SchemaIssue, SchemaTransformation } from "effect";
5
+ import { Effect, Function, 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
+ };
7
16
  /**
8
17
  * @description Derives the XML codec for a schema: a `Schema` whose `Encoded` is an XML document, so `Schema.encodeSync(codec)` writes text and
9
18
  * `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}.
19
+ * and {@link parseXml}. Call it data-first, `toCodecXml(schema, options)`, or data-last, `toCodecXml(options)(schema)`, so it drops into `pipe` beside
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.
11
22
  *
12
23
  * @example
13
24
  * ```typescript
14
- * import { Schema } from 'effect';
15
- * import { toCodecXml } from '@endevops/effect-xml-codec';
25
+ * import { Schema, pipe } from 'effect';
26
+ * import { toCodecXml } from '@endevops/effect-codec-xml';
16
27
  *
17
28
  * const Book = Schema.Struct({ '@id': Schema.String, title: Schema.String, pages: Schema.Number });
18
29
  * const codec = toCodecXml(Book, { rootName: 'book' });
30
+ * const piped = pipe(Book, toCodecXml({ rootName: 'book' }));
19
31
  *
20
32
  * const value = { '@id': '1', title: 'Dune', pages: 412 };
21
33
  *
@@ -26,22 +38,19 @@ import { Effect, Predicate, Schema, SchemaAST, SchemaIssue, SchemaTransformation
26
38
  * // => { '@id': '1', title: 'Dune', pages: 412 }
27
39
  * ```;
28
40
  *
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'`.
41
+ * @param schema - The schema describing the value, in the data-first form.
42
+ * @param options - Render and parse options. `rootName` defaults to the schema's `identifier` or `title` annotation, then to `'root'`. In the
43
+ * data-last form this is the only argument, and the schema arrives from `pipe`.
31
44
  *
32
- * @returns The codec, with the source schema's `Type` and the same service requirements.
45
+ * @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
46
+ * codec.
33
47
  */
34
- const toCodecXml = (schema, options = {}) => {
48
+ const toCodecXml = Function.dual((args) => Schema.isSchema(args[0]), (schema, options = {}) => {
35
49
  const planned = namespacePlan(schema);
36
50
  if (Predicate.hasProperty(planned, "error")) throw new Error(`Invalid XML namespace annotation:\n\t- ${planned.error}.`);
37
51
  const plan = planned.plan;
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
- };
52
+ const active = isActivePlan(plan);
53
+ const renderOptions = resolveRenderOptions(schema, plan, options);
45
54
  return Schema.String.pipe(Schema.decodeTo(Schema.toCodecStringTree(schema), SchemaTransformation.transformEffect({
46
55
  decode: (text, parseOptions) => {
47
56
  if (!Predicate.isString(text)) return Effect.fail(new SchemaIssue.InvalidValue({ message: `Expected a string, but received ${typeof text}.` }, text, parseOptions));
@@ -56,7 +65,7 @@ const toCodecXml = (schema, options = {}) => {
56
65
  return renderXml(wire, renderOptions).pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue({ message: error.message }, value, parseOptions)));
57
66
  }
58
67
  })));
59
- };
68
+ });
60
69
  //#endregion
61
70
  export { toCodecXml };
62
71
 
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 { 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"}
@@ -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, 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.
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.
@@ -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 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.
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, 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.
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.
@@ -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 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"}
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"}