@endevops/effect-codec-xml 0.0.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 (61) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSE-is-entities +21 -0
  3. package/LICENSE-is-xml-naming +21 -0
  4. package/README.md +415 -0
  5. package/dist/codec.d.ts +48 -0
  6. package/dist/codec.d.ts.map +1 -0
  7. package/dist/codec.js +63 -0
  8. package/dist/codec.js.map +1 -0
  9. package/dist/conventions.d.ts +88 -0
  10. package/dist/conventions.d.ts.map +1 -0
  11. package/dist/conventions.js +113 -0
  12. package/dist/conventions.js.map +1 -0
  13. package/dist/entities/entity-decoder.d.ts +333 -0
  14. package/dist/entities/entity-decoder.d.ts.map +1 -0
  15. package/dist/entities/entity-decoder.js +841 -0
  16. package/dist/entities/entity-decoder.js.map +1 -0
  17. package/dist/entities/entity-tables.js +16 -0
  18. package/dist/entities/entity-tables.js.map +1 -0
  19. package/dist/errors.d.ts +49 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/errors.js +48 -0
  22. package/dist/errors.js.map +1 -0
  23. package/dist/index.d.ts +11 -0
  24. package/dist/index.js +11 -0
  25. package/dist/namespaces.d.ts +101 -0
  26. package/dist/namespaces.d.ts.map +1 -0
  27. package/dist/namespaces.js +663 -0
  28. package/dist/namespaces.js.map +1 -0
  29. package/dist/naming.d.ts +149 -0
  30. package/dist/naming.d.ts.map +1 -0
  31. package/dist/naming.js +296 -0
  32. package/dist/naming.js.map +1 -0
  33. package/dist/parse.d.ts +75 -0
  34. package/dist/parse.d.ts.map +1 -0
  35. package/dist/parse.js +437 -0
  36. package/dist/parse.js.map +1 -0
  37. package/dist/render.d.ts +99 -0
  38. package/dist/render.d.ts.map +1 -0
  39. package/dist/render.js +509 -0
  40. package/dist/render.js.map +1 -0
  41. package/dist/xml-error.d.ts +172 -0
  42. package/dist/xml-error.d.ts.map +1 -0
  43. package/dist/xml-error.js +157 -0
  44. package/dist/xml-error.js.map +1 -0
  45. package/dist/xml-value.d.ts +42 -0
  46. package/dist/xml-value.d.ts.map +1 -0
  47. package/dist/xml-value.js +79 -0
  48. package/dist/xml-value.js.map +1 -0
  49. package/package.json +69 -0
  50. package/src/codec.ts +136 -0
  51. package/src/conventions.ts +145 -0
  52. package/src/entities/entity-decoder.ts +1248 -0
  53. package/src/entities/entity-tables.ts +18 -0
  54. package/src/errors.ts +55 -0
  55. package/src/index.ts +79 -0
  56. package/src/namespaces.ts +968 -0
  57. package/src/naming.ts +519 -0
  58. package/src/parse.ts +597 -0
  59. package/src/render.ts +708 -0
  60. package/src/xml-error.ts +168 -0
  61. package/src/xml-value.ts +108 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Natural Intelligence
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Amit Gupta (https://solothought.com)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Natural Intelligence
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,415 @@
1
+ # @endevops/effect-xml-codec
2
+
3
+ A round-trip Effect Schema codec for XML. `toCodecXml(schema)` returns a
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.
7
+ `renderXml` and `parseXml` are the text layer underneath, and remain available
8
+ on their own.
9
+
10
+ ```typescript
11
+ import { Schema } from 'effect';
12
+ import { toCodecXml } from '@endevops/effect-xml-codec';
13
+
14
+ const Book = Schema.Struct({ '@id': Schema.String, title: Schema.String, pages: Schema.Number, tag: Schema.Array(Schema.String) });
15
+
16
+ const codec = toCodecXml(Book, { rootName: 'book' });
17
+ const value = { '@id': '1', title: 'Dune', pages: 412, tag: ['sci-fi', 'classic'] };
18
+
19
+ Schema.encodeSync(codec)(value);
20
+ // => '<book id="1"><title>Dune</title><pages>412</pages><tag>sci-fi</tag><tag>classic</tag></book>'
21
+
22
+ Schema.decodeSync(codec)('<book id="1"><title>Dune</title><pages>412</pages><tag>sci-fi</tag><tag>classic</tag></book>');
23
+ // => { '@id': '1', title: 'Dune', pages: 412, tag: ['sci-fi', 'classic'] }
24
+ ```
25
+
26
+ ## Why this exists
27
+
28
+ Effect 4 ships `Schema.toEncoderXml`, and it is one-way: a value goes in, an XML
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
31
+ rewritten.
32
+
33
+ This package pairs Effect's own XML-value derivation, `Schema.toCodecStringTree`,
34
+ with a renderer and a parser, then wraps both in a codec whose encoded side is
35
+ the document text. The derivation is Effect's and stays Effect's; the codec adds
36
+ the text layer and the `@`/`#text` conventions.
37
+
38
+ ## The mapping
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:
42
+
43
+ | In the value | In the document |
44
+ | ----------------------- | ------------------------------------- |
45
+ | a key starting with `@` | an attribute: `@xmlns` is `xmlns="…"` |
46
+ | the key `#text` | the element's character data |
47
+ | any other key | a child element, named by the key |
48
+ | an array | the element repeated once per member |
49
+ | a string | character data |
50
+ | `undefined` | nothing written at all |
51
+
52
+ `@` is a good discriminator because it is not a legal XML name character, so an
53
+ `@`-prefixed key can never collide with an element name, and a field that _is_ a
54
+ legal element name is never mistaken for an attribute.
55
+
56
+ Two shapes are read from a document, and which one a field wants is the schema's
57
+ decision rather than the parser's:
58
+
59
+ - a text-only element reads as a bare string, so
60
+ `Schema.Struct({ title: Schema.String })` matches `<title>Dune</title>`;
61
+ - an element that also carries attributes or children reads as a record, so
62
+ `Schema.Struct({ '@id': Schema.String, '#text': Schema.String })` matches
63
+ `<a id="1">hello</a>`.
64
+
65
+ ## Namespaces
66
+
67
+ A schema describes a value in local names, so a namespace is an annotation on
68
+ the schema node that owns the element rather than part of the field name. Five
69
+ annotations, all accepted by `Schema.annotate`:
70
+
71
+ | Annotation | Meaning |
72
+ | -------------- | --------------------------------------------------------------------------------------------- |
73
+ | `xmlNamespace` | The element's namespace URI. |
74
+ | `xmlPrefix` | The wire prefix to write for it. Omit it to write the namespace as the default (`xmlns="…"`). |
75
+ | `xmlName` | The wire local name, when it differs from the schema key. |
76
+ | `xmlAttribute` | The field is an attribute, without the schema key carrying the `@` prefix. |
77
+ | `xmlValue` | The field holds the element's character data, the `#text` value. |
78
+
79
+ `xmlName` renames one node, element or attribute, without touching the schema's
80
+ own name. The prefix still comes from `xmlPrefix`, so the name is a local name
81
+ and must not contain a colon. On the root schema it also names the root element,
82
+ unless the `rootName` option is given.
83
+
84
+ `xmlAttribute` is for a schema whose keys stay plain names: the field is written
85
+ as an attribute instead of a child element, and read back to the same key. It
86
+ composes with `xmlName` and with a namespace, but a namespaced attribute still
87
+ needs a prefix, because a default namespace does not apply to attributes.
88
+
89
+ `xmlValue` is for an element that carries both text and attributes or children:
90
+
91
+ ```typescript
92
+ Schema.Struct({ '@currency': Schema.String, amount: Schema.String.annotate({ xmlValue: true }) });
93
+ // <price currency="USD">19.99</price>
94
+ ```
95
+
96
+ Character data has no name and no namespace, so `xmlValue` cannot be combined
97
+ with `xmlAttribute`, `xmlName`, or `xmlNamespace`, and an element has room for
98
+ only one value field. Two sibling elements may each have their own.
99
+
100
+ The annotation is attached with `Schema.annotate`:
101
+
102
+ ```typescript
103
+ import { Schema } from 'effect';
104
+ import { toCodecXml } from '@endevops/effect-xml-codec';
105
+
106
+ const SOAP = 'http://schemas.xmlsoap.org/soap/envelope/';
107
+ const AUTH = 'urn:auth';
108
+
109
+ const Envelope = Schema.Struct({
110
+ Header: Schema.Struct({ Token: Schema.String.annotate({ xmlNamespace: AUTH, xmlPrefix: 'auth' }) }).annotate({
111
+ xmlNamespace: SOAP,
112
+ xmlPrefix: 'soap',
113
+ }),
114
+ Body: Schema.Struct({ GetPrice: Schema.Struct({ item: Schema.String }).annotate({ xmlNamespace: 'urn:shop' }) }).annotate({
115
+ xmlNamespace: SOAP,
116
+ xmlPrefix: 'soap',
117
+ }),
118
+ }).annotate({ xmlNamespace: SOAP, xmlPrefix: 'soap' });
119
+
120
+ const codec = toCodecXml(Envelope, { rootName: 'Envelope' });
121
+ const value = { Header: { Token: 'abc' }, Body: { GetPrice: { item: 'widget' } } };
122
+
123
+ Schema.encodeSync(codec)(value);
124
+ // => '<soap:Envelope xmlns:soap="…"><soap:Header><auth:Token xmlns:auth="urn:auth">abc</auth:Token></soap:Header><soap:Body><GetPrice xmlns="urn:shop"><item>widget</item></GetPrice></soap:Body></soap:Envelope>'
125
+
126
+ // A document that binds the same URI to another prefix decodes to the same value.
127
+ Schema.decodeSync(codec)(text.replaceAll('soap:', 's:').replace('xmlns:soap=', 'xmlns:s=')); // => value
128
+ ```
129
+
130
+ The namespace of an element is inherited by its descendant elements, and an
131
+ attribute is in a namespace only when it is annotated itself, because a default
132
+ namespace does not apply to attributes. On encode, an element writes its
133
+ declaration where the prefix or default is not already in scope. On decode,
134
+ every name is resolved to its URI against the declarations the document
135
+ carries, so the document's choice of prefixes does not matter, and the
136
+ declaration attributes are dropped from the value.
137
+
138
+ Two limits:
139
+
140
+ - One local name can belong to only one namespace in one codec. Two fields with
141
+ the same local name in different namespaces are rejected when the codec is
142
+ built. Give them distinct local names.
143
+ - A root array cannot carry the root element's declaration, because `renderXml`
144
+ wraps it in an element the codec does not build. Give the root a struct.
145
+
146
+ ## API
147
+
148
+ | Export | What it does |
149
+ | -------------------------------------------------------------- | ------------------------------------------------------------------ |
150
+ | `toCodecXml(schema, options?)` | The codec. A `Schema` whose `Encoded` is XML text. |
151
+ | `renderXml(value, options?)` | An XML value tree to XML text. Returns an `Effect`. |
152
+ | `parseXml(text, options?)` | XML text to an XML value tree. Returns an `Effect`. |
153
+ | `escapeText` / `escapeAttribute` | The escaping the renderer applies. |
154
+ | `resolveName` | Name repair for a render or parse. |
155
+ | `isXmlValue` | A runtime guard for the value model. |
156
+ | `XmlValueSchema` | A `Schema` for an `XmlValue`, for a value from outside. |
157
+ | `XmlParseError` | The failure a malformed document reports. |
158
+ | `XmlRenderError` | The failure a value that cannot be written reports. |
159
+ | `EntityDecoder` | The entity-reference decoder `parseXml` reads character data with. |
160
+ | `isName` / `isNcName` / `isQName` / `isNmToken` / `isNmTokens` | The XML name validators, as plain synchronous predicates. |
161
+ | `sanitize` | Rewrites an illegal name into the nearest legal one. |
162
+ | `validate` | Validates a name and reports why it failed, as an `Effect`. |
163
+ | `XmlError` | The failure `EntityDecoder` and `validate` report. |
164
+
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
169
+ document that will not parse or a value that will not write is reported with its
170
+ underlying XML message, alongside the schema mismatches Effect already reports.
171
+ The root element is named from the `rootName` option, then the schema's
172
+ `identifier` or `title` annotation, then `'root'`.
173
+
174
+ ## Entity decoder and name validators
175
+
176
+ This package absorbed `@endevops/common-xml`, so the two primitives the text
177
+ layer reads documents with ship here rather than as a sibling dependency.
178
+
179
+ `EntityDecoder` expands the reference syntax XML inherits from HTML — `&name;`,
180
+ `&#NNN;`, `&#xHH;` — with expansion limits, registration hooks, and a
181
+ numeric-reference policy. `parseXml` uses it for character data and falls back
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.
184
+
185
+ The name validators are plain synchronous predicates — `isName`, `isNcName`,
186
+ `isQName`, `isNmToken`, `isNmTokens` — because a regex test cannot fail and the
187
+ renderer and parser call them per name in their hot loops. `sanitize` rewrites
188
+ an illegal name into the nearest legal one, and `validate` reports why a name
189
+ failed through an `Effect`.
190
+
191
+ ```typescript
192
+ import { Effect } from 'effect';
193
+ import { EntityDecoder, isQName, sanitize, validate } from '@endevops/effect-xml-codec';
194
+
195
+ isQName('svg:circle'); // true
196
+ sanitize('not a name', 'ncName'); // 'not_a_name'
197
+ Effect.runSync(validate('1bad', 'qName')); // { valid: false, … }
198
+
199
+ const decoder = Effect.runSync(EntityDecoder.make({ limit: { maxTotalExpansions: 100 } }));
200
+ Effect.runSync(decoder.decode('caf&eacute; &#233;'));
201
+ ```
202
+
203
+ The path matcher and the entity encoder that were part of `@endevops/common-xml`
204
+ did not come across: nothing in this package reaches them.
205
+
206
+ ## Design notes
207
+
208
+ **The derivation is Effect's.** `toCodecXml` derives
209
+ `Schema.toCodecStringTree`, the same derivation `Schema.toEncoderXml` uses, and
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.
215
+
216
+ **The shape is `toCodecJson`'s.** The value `toCodecXml` returns is a `Schema`:
217
+ `Type` is the source schema's `Type`, `Encoded` is XML text, the service
218
+ requirements are preserved, and it composes with the rest of Schema. There is no
219
+ wrapper object and no separate render or parse call at the call site. The two
220
+ text steps are still there underneath, and are exported on their own so a caller
221
+ that wants the value tree can take it: `renderXml` and `parseXml`. Both answer
222
+ with an `Effect`, so a value that will not write and a document that will not
223
+ read are typed failures, which the codec folds into the `SchemaIssue.Issue` a
224
+ schema is expected to report.
225
+
226
+ **Escaping is XML's, not HTML's.** The renderer writes the five XML predefined
227
+ entities and nothing else. The entity decoder's named tables are HTML's, so an
228
+ HTML name such as `&eacute;` is well-formed XML that no parser will resolve, and
229
+ the only names this package writes are the five XML predefines. A character
230
+ reference in an attribute value is still spelled as one where XML's whitespace
231
+ normalization would otherwise eat it: a literal newline in an attribute comes
232
+ back as a space unless it is written `&#10;`.
233
+
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.
237
+
238
+ ## Performance
239
+
240
+ `packages/benchmarks/bench/codec.bench.ts` measures this package alone, split by
241
+ layer so the cost of Effect's derivation and the cost of this package's renderer
242
+ are told apart. `packages/benchmarks/bench/comparison.bench.ts` measures it
243
+ against `fast-xml-builder` and the two parsers, and
244
+ `packages/benchmarks/bench/naming.bench.ts` measures the five name validators.
245
+ Run them from the workspace root after a build: `pnpm build && vp run bench`.
246
+
247
+ | Benchmark | Throughput |
248
+ | --------------------------- | ---------- |
249
+ | a 300-byte document, encode | ~183k/sec |
250
+ | a 300-byte document, decode | ~194k/sec |
251
+ | a 500-row document, encode | ~1,800/sec |
252
+ | a 500-row document, decode | ~2,100/sec |
253
+ | 20,000 characters of text | ~627k/sec |
254
+
255
+ Four findings shaped the code, and all are measured rather than assumed:
256
+
257
+ - **Escaping was the whole cost of a large document.** The entity encoder that
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.
265
+ - **Resolving a name is a regex test, not an `Effect`.** The validators used to
266
+ return `Effect`s for pure questions, which left the renderer and the parser
267
+ running an effect per distinct element and attribute name in every document.
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
270
+ serialize and a parse did. The merged package now exposes plain synchronous
271
+ predicates (`isQName` and the rest, `sanitize`) with no `Effect` wrapper, and
272
+ the renderer's namer and the parser's name cache call them directly.
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.
279
+ - **The codec adds no layer of its own.** `toCodecXml` derives
280
+ `Schema.toCodecStringTree` and runs the tree through `renderXml`, so a value is
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.
286
+ - **A small document is dominated by something this package does not own.** Of
287
+ the ~9µs it takes to serialize one, roughly 2.5µs is Effect's
288
+ `toCodecStringTree` derivation, which walks the schema on every call, and the
289
+ 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.
293
+
294
+ The other things the benchmarks changed: one pass over a record's keys instead of
295
+ one per role a key can play, name resolution memoized per document rather than
296
+ per element, one object per parsed element instead of two, and the indent for
297
+ each depth built once per render rather than once per line.
298
+
299
+ ### Tracing
300
+
301
+ `parseXml` opens a span named `XmlCodec.parseXml`, carrying the document length
302
+ as an attribute, so a slow parse in a trace can be attributed to the input that
303
+ caused it. It is the package's traced entry point; `renderXml` is an `Effect`
304
+ too but opens no span, and `toCodecXml` is a `Schema`, so tracing a schema
305
+ encode or decode is Effect's concern. Provide a `Tracer` to a program to collect
306
+ the span, as `src/tracing.spec.ts` does. The codec calls `parseXml` on the way
307
+ in, so the span is opened for a schema decode too. A failed parse is a typed
308
+ `XmlParseError` in the error channel, not a defect, so `catchTag`, `retry` and a
309
+ fallback all see it.
310
+
311
+ ### Against the libraries on npm
312
+
313
+ `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.
320
+
321
+ **Encoding** — one object to the same bytes:
322
+
323
+ | Document | This codec | `fast-xml-builder` |
324
+ | ------------------- | ---------- | ------------------ |
325
+ | a small order | 166,919/s | 216,148/s (1.29x) |
326
+ | 500 rows | 1,698/s | 1,269/s (0.75x) |
327
+ | one large text node | 584,358/s | 254,355/s (0.44x) |
328
+
329
+ **Decoding** — one document to the same value:
330
+
331
+ | Document | This codec | `@nodable/flexible-xml-parser` | `fast-xml-parser` |
332
+ | ------------------- | ---------- | ------------------------------ | ----------------- |
333
+ | a small order | 177,143/s | 8,874/s (0.05x) | 64,492/s (0.36x) |
334
+ | 500 rows | 1,975/s | 70/s (0.04x) | 486/s (0.25x) |
335
+ | one large text node | 20,715/s | 5,694/s (0.27x) | 8,209/s (0.40x) |
336
+
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:
340
+
341
+ | Path | Throughput |
342
+ | ----------------------------------------------------------- | ---------------- |
343
+ | this codec | 81,335/s |
344
+ | npm `fast-xml-builder`, then npm `fast-xml-parser` | 47,562/s (0.58x) |
345
+ | npm `fast-xml-builder`, then `@nodable/flexible-xml-parser` | 5,514/s (0.07x) |
346
+
347
+ **This codec reaches the parser and renderer directly.** It encodes and decodes
348
+ 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
351
+ directional rather than like-for-like.
352
+
353
+ Four things to be straight about when reading those tables:
354
+
355
+ - **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
+ ratios as the durable part and the absolute figures as a range.
358
+ - **This codec's decode does strictly more work.** It parses _and_ validates the
359
+ result against the schema, coercing `"30"` to `30` and failing on a mismatch.
360
+ The parsers only parse. On the 500-row document, parsing alone runs at
361
+ 2,984/s and the schema pass brings it to 1,975/s, so roughly a third of the
362
+ decode time is validation the comparison rows do not pay.
363
+ - **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.
367
+ - **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
+ comparison and the single-direction tables are the diagnostic ones.
370
+
371
+ ## Limitations
372
+
373
+ Each of these is a property of the format or of the underlying derivation rather
374
+ than something the codec can decide. Each has a spec in
375
+ `src/round-trip.spec.ts` that pins the behaviour, so none of them can change
376
+ quietly.
377
+
378
+ - **An empty element is ambiguous.** `<a/>` is empty character data, and there is
379
+ no XML that says "an array with nothing in it" as against "a struct with no
380
+ fields". An empty array of structs, an empty record, or a struct whose fields
381
+ are all absent is written faithfully and cannot be read back. Reading it
382
+ reports a schema failure rather than inventing a value. Settling it needs the
383
+ schema at the field, which a plain `Schema` does not provide; a caller that
384
+ knows the shape can map `''` to `{}` before decoding.
385
+ - **A one-member array is ambiguous.** `<a>x</a>` is one element, and it is
386
+ equally a one-member array of that element; `<a id="1"><b>x</b></a>` is the
387
+ same. Two or more members are unambiguous and do round-trip. A caller that
388
+ wants the leniency composes Effect's `Schema.toCodecArrayFromSingle` on top of
389
+ the codec, which reads a bare value back as an array of one.
390
+ - **A repaired name is a different name.** XML cannot spell `not a name`, so the
391
+ renderer rewrites it and the document is well-formed. A schema that asks for
392
+ the illegal name then has nothing to match, and reading it fails. A schema that
393
+ uses the repaired name round-trips normally. Set `name: 'error'` to fail the
394
+ render instead of rewriting.
395
+ - **Mixed content loses its order.** An element's `#text` is written before its
396
+ children and read back as a single run, so `<p>a<b/>c</p>` reads as
397
+ `{ b: '', '#text': 'ac' }`. Reading and re-rendering is stable from there on.
398
+ - **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
+ unindented one. Whitespace _inside_ a run is never touched.
401
+ - **`Schema.BigInt` is write-only.** Effect's `StringTree` derivation lowers one
402
+ to its decimal text and has no way to raise it again.
403
+
404
+ ## Commands
405
+
406
+ ```bash
407
+ vp -C packages/effect-xml-codec check # format, lint, type-check
408
+ vp -C packages/effect-xml-codec test # the suite
409
+ vp -C packages/effect-xml-codec pack # build
410
+ pnpm build && vp run bench # the benchmarks, from the workspace root
411
+ ```
412
+
413
+ ## License
414
+
415
+ MIT. See [LICENSE](./LICENSE).
@@ -0,0 +1,48 @@
1
+ import { XmlParseOptions } from "./parse.js";
2
+ import { XmlRenderOptions } from "./render.js";
3
+ import { Schema } from "effect";
4
+ //#region src/codec.d.ts
5
+ /**
6
+ * @description Options for {@link toCodecXml}.\
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'`.
10
+ */
11
+ export type XmlCodecOptions = XmlRenderOptions & XmlParseOptions;
12
+ /**
13
+ * @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
14
+ * decodes a document to a value in one step each. The service requirements of the source schema are preserved.
15
+ */
16
+ export interface toCodecXml<S extends Schema.Constraint> extends Schema.decodeTo<Schema.toCodecStringTree<S>, Schema.String> {
17
+ readonly Rebuild: toCodecXml<S>;
18
+ }
19
+ /**
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
+ * `Schema.decodeSync(codec)` reads it back. The derivation is Effect's `Schema.toCodecStringTree`; the text layer is this package's {@link renderXml}
22
+ * and {@link parseXml}.
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * import { Schema } from 'effect';
27
+ * import { toCodecXml } from '@endevops/effect-xml-codec';
28
+ *
29
+ * const Book = Schema.Struct({ '@id': Schema.String, title: Schema.String, pages: Schema.Number });
30
+ * const codec = toCodecXml(Book, { rootName: 'book' });
31
+ *
32
+ * const value = { '@id': '1', title: 'Dune', pages: 412 };
33
+ *
34
+ * Schema.encodeSync(codec)(value);
35
+ * // => '<book id="1"><title>Dune</title><pages>412</pages></book>'
36
+ *
37
+ * Schema.decodeSync(codec)('<book id="1"><title>Dune</title><pages>412</pages></book>');
38
+ * // => { '@id': '1', title: 'Dune', pages: 412 }
39
+ * ```;
40
+ *
41
+ * @param schema - The schema describing the value.
42
+ * @param options - Render and parse options. `rootName` defaults to the schema's `identifier` or `title` annotation, then to `'root'`.
43
+ *
44
+ * @returns The codec, with the source schema's `Type` and the same service requirements.
45
+ */
46
+ export declare const toCodecXml: <S extends Schema.Constraint>(schema: S, options?: XmlCodecOptions) => toCodecXml<S>;
47
+ //#endregion
48
+ //# sourceMappingURL=codec.d.ts.map
@@ -0,0 +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"}
package/dist/codec.js ADDED
@@ -0,0 +1,63 @@
1
+ import "./conventions.js";
2
+ import { decodeNames, encodeNames, namespacePlan } from "./namespaces.js";
3
+ import { parseXml } from "./parse.js";
4
+ import { renderXml } from "./render.js";
5
+ import { Effect, Predicate, Schema, SchemaAST, SchemaIssue, SchemaTransformation } from "effect";
6
+ //#region src/codec.ts
7
+ /**
8
+ * @description Derives the XML codec for a schema: a `Schema` whose `Encoded` is an XML document, so `Schema.encodeSync(codec)` writes text and
9
+ * `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}.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { Schema } from 'effect';
15
+ * import { toCodecXml } from '@endevops/effect-xml-codec';
16
+ *
17
+ * const Book = Schema.Struct({ '@id': Schema.String, title: Schema.String, pages: Schema.Number });
18
+ * const codec = toCodecXml(Book, { rootName: 'book' });
19
+ *
20
+ * const value = { '@id': '1', title: 'Dune', pages: 412 };
21
+ *
22
+ * Schema.encodeSync(codec)(value);
23
+ * // => '<book id="1"><title>Dune</title><pages>412</pages></book>'
24
+ *
25
+ * Schema.decodeSync(codec)('<book id="1"><title>Dune</title><pages>412</pages></book>');
26
+ * // => { '@id': '1', title: 'Dune', pages: 412 }
27
+ * ```;
28
+ *
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'`.
31
+ *
32
+ * @returns The codec, with the source schema's `Type` and the same service requirements.
33
+ */
34
+ const toCodecXml = (schema, options = {}) => {
35
+ const planned = namespacePlan(schema);
36
+ if (Predicate.hasProperty(planned, "error")) throw new Error(`Invalid XML namespace annotation:\n\t- ${planned.error}.`);
37
+ 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
+ };
45
+ return Schema.String.pipe(Schema.decodeTo(Schema.toCodecStringTree(schema), SchemaTransformation.transformEffect({
46
+ decode: (text, parseOptions) => {
47
+ if (!Predicate.isString(text)) return Effect.fail(new SchemaIssue.InvalidValue({ message: `Expected a string, but received ${typeof text}.` }, text, parseOptions));
48
+ 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
+ cause: error,
50
+ message: "XML parse error"
51
+ }))), Effect.mapError((error) => new SchemaIssue.InvalidValue({ message: error.message }, text, parseOptions)));
52
+ },
53
+ encode: (value, parseOptions) => {
54
+ 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));
55
+ const wire = active ? encodeNames(value, plan, plan.root, {}, "") : value;
56
+ return renderXml(wire, renderOptions).pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue({ message: error.message }, value, parseOptions)));
57
+ }
58
+ })));
59
+ };
60
+ //#endregion
61
+ export { toCodecXml };
62
+
63
+ //# sourceMappingURL=codec.js.map
@@ -0,0 +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"}