@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.
- package/LICENSE +21 -0
- package/LICENSE-is-entities +21 -0
- package/LICENSE-is-xml-naming +21 -0
- package/README.md +415 -0
- package/dist/codec.d.ts +48 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +63 -0
- package/dist/codec.js.map +1 -0
- package/dist/conventions.d.ts +88 -0
- package/dist/conventions.d.ts.map +1 -0
- package/dist/conventions.js +113 -0
- package/dist/conventions.js.map +1 -0
- package/dist/entities/entity-decoder.d.ts +333 -0
- package/dist/entities/entity-decoder.d.ts.map +1 -0
- package/dist/entities/entity-decoder.js +841 -0
- package/dist/entities/entity-decoder.js.map +1 -0
- package/dist/entities/entity-tables.js +16 -0
- package/dist/entities/entity-tables.js.map +1 -0
- package/dist/errors.d.ts +49 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +48 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/namespaces.d.ts +101 -0
- package/dist/namespaces.d.ts.map +1 -0
- package/dist/namespaces.js +663 -0
- package/dist/namespaces.js.map +1 -0
- package/dist/naming.d.ts +149 -0
- package/dist/naming.d.ts.map +1 -0
- package/dist/naming.js +296 -0
- package/dist/naming.js.map +1 -0
- package/dist/parse.d.ts +75 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +437 -0
- package/dist/parse.js.map +1 -0
- package/dist/render.d.ts +99 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +509 -0
- package/dist/render.js.map +1 -0
- package/dist/xml-error.d.ts +172 -0
- package/dist/xml-error.d.ts.map +1 -0
- package/dist/xml-error.js +157 -0
- package/dist/xml-error.js.map +1 -0
- package/dist/xml-value.d.ts +42 -0
- package/dist/xml-value.d.ts.map +1 -0
- package/dist/xml-value.js +79 -0
- package/dist/xml-value.js.map +1 -0
- package/package.json +69 -0
- package/src/codec.ts +136 -0
- package/src/conventions.ts +145 -0
- package/src/entities/entity-decoder.ts +1248 -0
- package/src/entities/entity-tables.ts +18 -0
- package/src/errors.ts +55 -0
- package/src/index.ts +79 -0
- package/src/namespaces.ts +968 -0
- package/src/naming.ts +519 -0
- package/src/parse.ts +597 -0
- package/src/render.ts +708 -0
- package/src/xml-error.ts +168 -0
- 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é é'));
|
|
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 `é` 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 ` `.
|
|
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).
|
package/dist/codec.d.ts
ADDED
|
@@ -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"}
|