@endevops/effect-codec-xml 0.1.0-beta.1 → 0.1.0-effect.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -63
- package/dist/codec.d.ts +9 -17
- package/dist/codec.d.ts.map +1 -1
- package/dist/codec.js +16 -25
- package/dist/codec.js.map +1 -1
- package/dist/conventions.d.ts +4 -4
- package/dist/conventions.js +7 -7
- package/dist/conventions.js.map +1 -1
- package/dist/entities/entity-decoder.d.ts +33 -33
- package/dist/entities/entity-decoder.d.ts.map +1 -1
- package/dist/entities/entity-decoder.js +64 -63
- package/dist/entities/entity-decoder.js.map +1 -1
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +2 -2
- package/dist/errors.js.map +1 -1
- package/dist/namespaces.js +2 -2
- package/dist/namespaces.js.map +1 -1
- package/dist/naming.d.ts +6 -6
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +3 -3
- package/dist/naming.js.map +1 -1
- package/dist/parse.d.ts +5 -5
- package/dist/parse.js +14 -14
- package/dist/parse.js.map +1 -1
- package/dist/render.d.ts +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +28 -28
- package/dist/render.js.map +1 -1
- package/dist/xml-error.d.ts +9 -9
- package/dist/xml-error.js +18 -18
- package/dist/xml-error.js.map +1 -1
- package/dist/xml-value.d.ts +7 -7
- package/dist/xml-value.d.ts.map +1 -1
- package/dist/xml-value.js +7 -6
- package/dist/xml-value.js.map +1 -1
- package/package.json +1 -1
- package/src/codec.ts +71 -91
- package/src/conventions.ts +7 -7
- package/src/entities/entity-decoder.ts +99 -98
- package/src/errors.ts +3 -3
- package/src/index.ts +3 -3
- package/src/namespaces.ts +16 -16
- package/src/naming.ts +35 -34
- package/src/parse.ts +26 -26
- package/src/render.ts +44 -44
- package/src/xml-error.ts +18 -18
- package/src/xml-value.ts +11 -10
package/dist/xml-error.js
CHANGED
|
@@ -8,35 +8,35 @@ import { Schema } from "effect";
|
|
|
8
8
|
*
|
|
9
9
|
* ## Why one error with a `reason`, and not one error per cause
|
|
10
10
|
*
|
|
11
|
-
* The causes would mean a class each, and a caller handling "any of these" would need `catchTags` with all of them.
|
|
12
|
-
*
|
|
13
|
-
* versus a bad _document_. So there is one error, and `reason` narrows to the specific cause. Recovery is a `catchReason` away:
|
|
11
|
+
* The causes would mean a class each, and a caller handling "any of these" would need `catchTags` with all of them. The causes are not independent
|
|
12
|
+
* decisions a caller usually wants to make separately — they are all "this XML input was not acceptable", and the useful split is coarse: a bad
|
|
13
|
+
* _configuration_ versus a bad _document_. So there is one error, and `reason` narrows to the specific cause. Recovery is a `catchReason` away:
|
|
14
14
|
*
|
|
15
15
|
* @example
|
|
16
16
|
* ```typescript
|
|
17
17
|
* import { Effect } from 'effect';
|
|
18
|
-
* import { EntityDecoder, XmlError } from '@endevops/effect-codec
|
|
18
|
+
* import { EntityDecoder, XmlError } from '@endevops/effect-xml-codec';
|
|
19
19
|
*
|
|
20
20
|
* const limited = new EntityDecoder({ limit: { maxTotalExpansions: 2 } }).decode('&&&').pipe(
|
|
21
21
|
* Effect.catchReason('XmlError', 'ExpansionLimitExceeded', reason => Effect.succeed(`gave up after ${reason.actual}`)),
|
|
22
22
|
* );
|
|
23
23
|
* ```;
|
|
24
24
|
*
|
|
25
|
-
* The message is kept alongside `reason` and is part of the schema, because
|
|
26
|
-
* `[EntityReplacer]` prefix in particular is documented as something callers match on, so
|
|
27
|
-
* rather than
|
|
25
|
+
* The message is kept alongside `reason` and is part of the schema, because the text is load-bearing: the
|
|
26
|
+
* `[EntityReplacer]` prefix in particular is documented as something callers match on, so it is reproduced exactly
|
|
27
|
+
* rather than reworded.
|
|
28
28
|
*/
|
|
29
29
|
/**
|
|
30
30
|
* @description The specific cause of an {@link XmlError}, as a tagged union. The `_tag` on each member is the discriminant `Effect.catchReason` matches on, and
|
|
31
|
-
* the payload is what a handler needs in order to decide or to report. Every member is a case the decoder or the name validators actually raise
|
|
32
|
-
*
|
|
31
|
+
* the payload is what a handler needs in order to decide or to report. Every member is a case the decoder or the name validators actually raise —
|
|
32
|
+
* there is no catch-all member, so an exhaustive `match` stays exhaustive as causes are added.
|
|
33
33
|
*/
|
|
34
34
|
const XmlErrorReason = Schema.TaggedUnion({
|
|
35
35
|
/**
|
|
36
36
|
* @description A required argument was `null` or another non-value where the package requires a real one. Raised by the factories that take caller-supplied
|
|
37
|
-
* input
|
|
38
|
-
* case from the rest because the argument is not _wrong_, it is _absent_, and a caller who wrote `make(null)` meant something the type
|
|
39
|
-
* not allow
|
|
37
|
+
* input — {@link EntityDecoder.make} among them — when they are handed `null` for an options object that has no meaningful default. It is a
|
|
38
|
+
* distinct case from the rest because the argument is not _wrong_, it is _absent_, and a caller who wrote `make(null)` meant something the type
|
|
39
|
+
* system does not allow: a decoder with every default is `make({})`, and saying so here is more useful than silently producing one.
|
|
40
40
|
*/
|
|
41
41
|
MissingOptions: {
|
|
42
42
|
/**
|
|
@@ -45,7 +45,7 @@ const XmlErrorReason = Schema.TaggedUnion({
|
|
|
45
45
|
parameter: Schema.String },
|
|
46
46
|
/**
|
|
47
47
|
* @description A name was checked against one of the five XML name productions and given a different one. Unreachable from TypeScript, where `Production` is a
|
|
48
|
-
* closed union
|
|
48
|
+
* closed union — it is the guard for untyped JavaScript callers and for values that crossed a boundary as `unknown`.
|
|
49
49
|
*/
|
|
50
50
|
InvalidProduction: {
|
|
51
51
|
production: Schema.String,
|
|
@@ -73,7 +73,7 @@ parameter: Schema.String },
|
|
|
73
73
|
*/
|
|
74
74
|
EntityRejected: {
|
|
75
75
|
/**
|
|
76
|
-
* @description Which registration was in progress.
|
|
76
|
+
* @description Which registration was in progress. Both are runtime-injected, which is why they share a tier for limit accounting.
|
|
77
77
|
*/
|
|
78
78
|
context: Schema.Literals(["external", "input"]),
|
|
79
79
|
/**
|
|
@@ -87,7 +87,7 @@ parameter: Schema.String },
|
|
|
87
87
|
*/
|
|
88
88
|
ExpansionLimitExceeded: {
|
|
89
89
|
/**
|
|
90
|
-
* @description The count that tripped the limit.
|
|
90
|
+
* @description The count that tripped the limit. Deliberately not reset on failure, so this is the real over-limit total rather than the ceiling.
|
|
91
91
|
*/
|
|
92
92
|
actual: Schema.Number,
|
|
93
93
|
/**
|
|
@@ -125,14 +125,14 @@ parameter: Schema.String },
|
|
|
125
125
|
}
|
|
126
126
|
});
|
|
127
127
|
/**
|
|
128
|
-
* @description Every failure this package can report, in the `E` channel of the effects that can fail. Carries both a `reason
|
|
128
|
+
* @description Every failure this package can report, in the `E` channel of the effects that can fail. Carries both a `reason` — the typed, matchable cause — and
|
|
129
129
|
* a `message`, which is the human-readable form the package has always produced. Both are part of the schema, so an error survives a round-trip
|
|
130
130
|
* through a serialised boundary without losing either.
|
|
131
131
|
*
|
|
132
132
|
* @example
|
|
133
133
|
* ```typescript
|
|
134
134
|
* import { Effect } from 'effect';
|
|
135
|
-
* import { EntityDecoder } from '@endevops/effect-codec
|
|
135
|
+
* import { EntityDecoder } from '@endevops/effect-xml-codec';
|
|
136
136
|
*
|
|
137
137
|
* const program = Effect.gen(function*() {
|
|
138
138
|
* const decoder = yield* EntityDecoder.make({});
|
|
@@ -147,7 +147,7 @@ var XmlError = class extends Schema.TaggedError()("XmlError", {
|
|
|
147
147
|
reason: XmlErrorReason,
|
|
148
148
|
/**
|
|
149
149
|
* @description Human-readable description. The `[EntityReplacer]` and `[EntityDecoder]` prefixes are reproduced verbatim from the original throw sites, because
|
|
150
|
-
*
|
|
150
|
+
* they are documented as load-bearing for anything matching on them.
|
|
151
151
|
*/
|
|
152
152
|
message: Schema.String
|
|
153
153
|
}) {};
|
package/dist/xml-error.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"xml-error.js","names":[],"sources":["../src/xml-error.ts"],"sourcesContent":["/**\n * @description Every way the entity decoder and the name validators can fail, as one typed error. The package used to throw plain `Error` and `TypeError` from\n * places spread across the entity and naming areas. A caller had to match on message text, and there was nothing to narrow on. All of it is now a\n * single {@link XmlError} in the `E` channel of the effects that can fail, with the specific cause in a `reason` field rather than parsed back out of\n * a string.\n *\n * ## Why one error with a `reason`, and not one error per cause\n *\n * The causes would mean a class each, and a caller handling \"any of these\" would need `catchTags` with all of them.
|
|
1
|
+
{"version":3,"file":"xml-error.js","names":[],"sources":["../src/xml-error.ts"],"sourcesContent":["/**\n * @description Every way the entity decoder and the name validators can fail, as one typed error. The package used to throw plain `Error` and `TypeError` from\n * places spread across the entity and naming areas. A caller had to match on message text, and there was nothing to narrow on. All of it is now a\n * single {@link XmlError} in the `E` channel of the effects that can fail, with the specific cause in a `reason` field rather than parsed back out of\n * a string.\n *\n * ## Why one error with a `reason`, and not one error per cause\n *\n * The causes would mean a class each, and a caller handling \"any of these\" would need `catchTags` with all of them. The causes are not independent\n * decisions a caller usually wants to make separately — they are all \"this XML input was not acceptable\", and the useful split is coarse: a bad\n * _configuration_ versus a bad _document_. So there is one error, and `reason` narrows to the specific cause. Recovery is a `catchReason` away:\n *\n * @example\n * ```typescript\n * import { Effect } from 'effect';\n * import { EntityDecoder, XmlError } from '@endevops/effect-xml-codec';\n *\n * const limited = new EntityDecoder({ limit: { maxTotalExpansions: 2 } }).decode('&&&').pipe(\n * Effect.catchReason('XmlError', 'ExpansionLimitExceeded', reason => Effect.succeed(`gave up after ${reason.actual}`)),\n * );\n * ```;\n *\n * The message is kept alongside `reason` and is part of the schema, because the text is load-bearing: the\n * `[EntityReplacer]` prefix in particular is documented as something callers match on, so it is reproduced exactly\n * rather than reworded.\n */\n\nimport { Schema } from 'effect';\n\n/**\n * @description The specific cause of an {@link XmlError}, as a tagged union. The `_tag` on each member is the discriminant `Effect.catchReason` matches on, and\n * the payload is what a handler needs in order to decide or to report. Every member is a case the decoder or the name validators actually raise —\n * there is no catch-all member, so an exhaustive `match` stays exhaustive as causes are added.\n */\nexport const XmlErrorReason = Schema.TaggedUnion({\n /**\n * @description A required argument was `null` or another non-value where the package requires a real one. Raised by the factories that take caller-supplied\n * input — {@link EntityDecoder.make} among them — when they are handed `null` for an options object that has no meaningful default. It is a\n * distinct case from the rest because the argument is not _wrong_, it is _absent_, and a caller who wrote `make(null)` meant something the type\n * system does not allow: a decoder with every default is `make({})`, and saying so here is more useful than silently producing one.\n */\n MissingOptions: {\n /**\n * @description The parameter that was given nothing, named as it appears in the signature.\n */\n parameter: Schema.String,\n },\n\n /**\n * @description A name was checked against one of the five XML name productions and given a different one. Unreachable from TypeScript, where `Production` is a\n * closed union — it is the guard for untyped JavaScript callers and for values that crossed a boundary as `unknown`.\n */\n InvalidProduction: {\n production: Schema.String,\n /**\n * @description The productions that would have been accepted, comma-separated, as they appear in the message.\n */\n expected: Schema.String,\n },\n\n /**\n * @description An entity name cannot be written as a reference, so it is refused at registration.\n */\n InvalidEntityName: {\n /**\n * @description The rejected name, as it was passed in.\n */\n name: Schema.String,\n /**\n * @description The offending character, or `#` for a name that starts with one. A leading `#` is refused by position rather than by the character sweep,\n * because a name starting with `#` is a numeric reference's token and would collide with the numeric pipeline.\n */\n character: Schema.String,\n },\n\n /**\n * @description A registration hook refused an entity and asked for the registration to abort.\n */\n EntityRejected: {\n /**\n * @description Which registration was in progress. Both are runtime-injected, which is why they share a tier for limit accounting.\n */\n context: Schema.Literals(['external', 'input']),\n /**\n * @description The entity name, without `&` or `;`.\n */\n name: Schema.String,\n },\n\n /**\n * @description A document expanded more tracked entity references than {@link EntityDecoderLimitOptions.maxTotalExpansions} allows. A document can define an\n * entity that references another ten times over. Ten deep is a denial of service; a hundred is a fork bomb written in XML.\n */\n ExpansionLimitExceeded: {\n /**\n * @description The count that tripped the limit. Deliberately not reset on failure, so this is the real over-limit total rather than the ceiling.\n */\n actual: Schema.Number,\n /**\n * @description The configured ceiling. The check is `actual > limit`, so a limit of `2` allows two expansions.\n */\n limit: Schema.Number,\n },\n\n /**\n * @description A document grew by more characters through entity expansion than {@link EntityDecoderLimitOptions.maxExpandedLength} allows. Only the surplus\n * counts: a reference whose replacement is no longer than the `&token;` it replaces contributes nothing, so this bounds growth rather than document\n * size.\n */\n ExpandedLengthLimitExceeded: {\n /**\n * @description The accumulated growth that tripped the limit.\n */\n actual: Schema.Number,\n /**\n * @description The configured ceiling.\n */\n limit: Schema.Number,\n },\n\n /**\n * @description A numeric character reference was prohibited by the configured policy.\n */\n ProhibitedCharacterReference: {\n /**\n * @description The raw token without `&` and `;`, e.g. `#38` or `#x26`.\n */\n token: Schema.String,\n /**\n * @description The codepoint the reference resolved to, so a log can name the character that was refused.\n */\n codepoint: Schema.Number,\n },\n});\n\n/**\n * @description The reason a well-formedness or policy failure occurred.\n */\nexport type XmlErrorReason = typeof XmlErrorReason.Type;\n\n/**\n * @description Every failure this package can report, in the `E` channel of the effects that can fail. Carries both a `reason` — the typed, matchable cause — and\n * a `message`, which is the human-readable form the package has always produced. Both are part of the schema, so an error survives a round-trip\n * through a serialised boundary without losing either.\n *\n * @example\n * ```typescript\n * import { Effect } from 'effect';\n * import { EntityDecoder } from '@endevops/effect-xml-codec';\n *\n * const program = Effect.gen(function*() {\n * const decoder = yield* EntityDecoder.make({});\n * return yield* decoder.decode('a & b');\n * });\n * ```;\n */\nexport class XmlError extends Schema.TaggedError<XmlError>()('XmlError', {\n /**\n * @description The specific cause. Narrow on `_tag`, or recover with `Effect.catchReason`.\n */\n reason: XmlErrorReason,\n\n /**\n * @description Human-readable description. The `[EntityReplacer]` and `[EntityDecoder]` prefixes are reproduced verbatim from the original throw sites, because\n * they are documented as load-bearing for anything matching on them.\n */\n message: Schema.String,\n}) {}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,MAAa,iBAAiB,OAAO,YAAY;;;;;;;CAO/C,gBAAgB;;;;AAId,WAAW,OAAO,OACpB;;;;;CAMA,mBAAmB;EACjB,YAAY,OAAO;;;;EAInB,UAAU,OAAO;CACnB;;;;CAKA,mBAAmB;;;;EAIjB,MAAM,OAAO;;;;;EAKb,WAAW,OAAO;CACpB;;;;CAKA,gBAAgB;;;;EAId,SAAS,OAAO,SAAS,CAAC,YAAY,OAAO,CAAC;;;;EAI9C,MAAM,OAAO;CACf;;;;;CAMA,wBAAwB;;;;EAItB,QAAQ,OAAO;;;;EAIf,OAAO,OAAO;CAChB;;;;;;CAOA,6BAA6B;;;;EAI3B,QAAQ,OAAO;;;;EAIf,OAAO,OAAO;CAChB;;;;CAKA,8BAA8B;;;;EAI5B,OAAO,OAAO;;;;EAId,WAAW,OAAO;CACpB;AACF,CAAC;;;;;;;;;;;;;;;;;AAuBD,IAAa,WAAb,cAA8B,OAAO,YAAsB,CAAC,CAAC,YAAY;;;;CAIvE,QAAQ;;;;;CAMR,SAAS,OAAO;AAClB,CAAC,CAAC,CAAC,CAAC"}
|
package/dist/xml-value.d.ts
CHANGED
|
@@ -10,10 +10,10 @@ export interface XmlRecord {
|
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
12
|
* @description One value in an XML document: nothing at all, character data, a repeated run of children, or a record of attributes, text and child elements.
|
|
13
|
-
* `undefined` is a value of its own rather than an omission, because that is how an absent optional field survives a round trip
|
|
14
|
-
* value stays distinguishable from a field whose value is the empty string, and the renderer writes neither of them. The shape is the
|
|
15
|
-
* `Schema.toCodecStringTree` derives,
|
|
16
|
-
*
|
|
13
|
+
* `undefined` is a value of its own rather than an omission, because that is how an absent optional field survives a round trip: a field with no
|
|
14
|
+
* value stays distinguishable from a field whose value is the empty string, and the renderer writes neither of them. The shape is deliberately the
|
|
15
|
+
* same one `Schema.toCodecStringTree` derives, which is what lets every schema feature Effect supports round-trip through this package without
|
|
16
|
+
* re-implementing the derivation.
|
|
17
17
|
*/
|
|
18
18
|
export type XmlValue = string | undefined | ReadonlyArray<XmlValue> | XmlRecord;
|
|
19
19
|
/**
|
|
@@ -25,13 +25,13 @@ export type XmlValue = string | undefined | ReadonlyArray<XmlValue> | XmlRecord;
|
|
|
25
25
|
*/
|
|
26
26
|
export declare const isXmlValue: (input: unknown) => input is XmlValue;
|
|
27
27
|
/**
|
|
28
|
-
* @description A schema for {@link XmlValue}, so a value can be validated on its own
|
|
29
|
-
*
|
|
28
|
+
* @description A schema for {@link XmlValue}, so a value can be validated on its own — when it arrives from a store or a queue rather than from {@link parseXml},
|
|
29
|
+
* and the schema it belongs to is not in hand.
|
|
30
30
|
*
|
|
31
31
|
* @example
|
|
32
32
|
* ```typescript
|
|
33
33
|
* import { Schema } from 'effect';
|
|
34
|
-
* import { XmlValue } from '@endevops/effect-codec
|
|
34
|
+
* import { XmlValue } from '@endevops/effect-xml-codec';
|
|
35
35
|
*
|
|
36
36
|
* Schema.decodeUnknownSync(XmlValue)({ book: { '@id': '1', title: 'Dune' } }); // => { book: { '@id': '1', title: 'Dune' } }
|
|
37
37
|
* Schema.decodeUnknownSync(XmlValue)({ book: { title: 42 } }); // => throws XmlValue
|
package/dist/xml-value.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"xml-value.d.ts","names":[],"sources":["../src/xml-value.ts"],"mappings":";;;;;;;iBAOiB;YACL,eAAe;;;;;;;;;YAUf,gCAAgC,cAAc,YAAY;;;;;;;;qBAezD,aAAU,mBAAqB,SAAS;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"xml-value.d.ts","names":[],"sources":["../src/xml-value.ts"],"mappings":";;;;;;;iBAOiB;YACL,eAAe;;;;;;;;;YAUf,gCAAgC,cAAc,YAAY;;;;;;;;qBAezD,aAAU,mBAAqB,SAAS;;;;;;;;;;;;;;qBAuExC,UAAU,OAAO,MAAM"}
|
package/dist/xml-value.js
CHANGED
|
@@ -14,9 +14,10 @@ const MAX_GUARD_DEPTH = 512;
|
|
|
14
14
|
*/
|
|
15
15
|
const isXmlValue = (input) => check(input, 0);
|
|
16
16
|
/**
|
|
17
|
-
* @description One level of {@link isXmlValue}, with the depth it was reached at. The depth is the whole defence against a value built to be hostile
|
|
18
|
-
* self-referential object would otherwise recurse until the stack gave out, and a value nested thousands deep would take it with it.
|
|
19
|
-
*
|
|
17
|
+
* @description One level of {@link isXmlValue}, with the depth it was reached at. The depth is the whole defence against a value built to be hostile: a
|
|
18
|
+
* self-referential object would otherwise recurse until the stack gave out, and a value nested thousands deep would take it with it. Both are
|
|
19
|
+
* rejected here instead, which is why this is a real recursion with a bound rather than a loop — the model is a tree, and a tree is walked by walking
|
|
20
|
+
* it.
|
|
20
21
|
*
|
|
21
22
|
* @param input - The candidate value.
|
|
22
23
|
* @param depth - How many levels down this value sits.
|
|
@@ -56,13 +57,13 @@ const everyFieldIs = (input, depth) => {
|
|
|
56
57
|
return true;
|
|
57
58
|
};
|
|
58
59
|
/**
|
|
59
|
-
* @description A schema for {@link XmlValue}, so a value can be validated on its own
|
|
60
|
-
*
|
|
60
|
+
* @description A schema for {@link XmlValue}, so a value can be validated on its own — when it arrives from a store or a queue rather than from {@link parseXml},
|
|
61
|
+
* and the schema it belongs to is not in hand.
|
|
61
62
|
*
|
|
62
63
|
* @example
|
|
63
64
|
* ```typescript
|
|
64
65
|
* import { Schema } from 'effect';
|
|
65
|
-
* import { XmlValue } from '@endevops/effect-codec
|
|
66
|
+
* import { XmlValue } from '@endevops/effect-xml-codec';
|
|
66
67
|
*
|
|
67
68
|
* Schema.decodeUnknownSync(XmlValue)({ book: { '@id': '1', title: 'Dune' } }); // => { book: { '@id': '1', title: 'Dune' } }
|
|
68
69
|
* Schema.decodeUnknownSync(XmlValue)({ book: { title: 42 } }); // => throws XmlValue
|
package/dist/xml-value.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"xml-value.js","names":[],"sources":["../src/xml-value.ts"],"sourcesContent":["import { Predicate, Schema } from 'effect';\n\n/**\n * @description A record of child elements, attributes and character data. A key starting with `@` is an attribute, the reserved `#text` key is character data, and\n * every other key is a child element name. An `undefined` value means the field is absent, which is how an absent optional field stays\n * distinguishable from an empty one.\n */\nexport interface XmlRecord {\n readonly [name: string]: XmlValue | undefined;\n}\n\n/**\n * @description One value in an XML document: nothing at all, character data, a repeated run of children, or a record of attributes, text and child elements.\n * `undefined` is a value of its own rather than an omission, because that is how an absent optional field survives a round trip
|
|
1
|
+
{"version":3,"file":"xml-value.js","names":[],"sources":["../src/xml-value.ts"],"sourcesContent":["import { Predicate, Schema } from 'effect';\n\n/**\n * @description A record of child elements, attributes and character data. A key starting with `@` is an attribute, the reserved `#text` key is character data, and\n * every other key is a child element name. An `undefined` value means the field is absent, which is how an absent optional field stays\n * distinguishable from an empty one.\n */\nexport interface XmlRecord {\n readonly [name: string]: XmlValue | undefined;\n}\n\n/**\n * @description One value in an XML document: nothing at all, character data, a repeated run of children, or a record of attributes, text and child elements.\n * `undefined` is a value of its own rather than an omission, because that is how an absent optional field survives a round trip: a field with no\n * value stays distinguishable from a field whose value is the empty string, and the renderer writes neither of them. The shape is deliberately the\n * same one `Schema.toCodecStringTree` derives, which is what lets every schema feature Effect supports round-trip through this package without\n * re-implementing the derivation.\n */\nexport type XmlValue = string | undefined | ReadonlyArray<XmlValue> | XmlRecord;\n\n/**\n * @description How deep {@link isXmlValue} will walk before giving up. A document nested deeper than this is treated as invalid rather than allowed to exhaust the\n * stack.\n */\nconst MAX_GUARD_DEPTH = 512;\n\n/**\n * @description Whether an arbitrary value is a well-formed {@link XmlValue}.\n *\n * @param input - The candidate value.\n *\n * @returns Whether the value is absent, character data, an array of `XmlValue`, or a plain record of them.\n */\nexport const isXmlValue = (input: unknown): input is XmlValue => check(input, 0);\n\n/**\n * @description One level of {@link isXmlValue}, with the depth it was reached at. The depth is the whole defence against a value built to be hostile: a\n * self-referential object would otherwise recurse until the stack gave out, and a value nested thousands deep would take it with it. Both are\n * rejected here instead, which is why this is a real recursion with a bound rather than a loop — the model is a tree, and a tree is walked by walking\n * it.\n *\n * @param input - The candidate value.\n * @param depth - How many levels down this value sits.\n *\n * @returns Whether this level is legal, and the rest of the value with it.\n */\nconst check = (input: unknown, depth: number): boolean => {\n // `undefined` is a value in its own right: it is how an absent optional field survives a round trip, so a\n // record is allowed to hold one and a document is allowed to be missing a field.\n if (Predicate.isUndefined(input) || Predicate.isString(input)) return true;\n if (Predicate.isNull(input) || !Predicate.isObjectOrArray(input)) return false;\n\n if (depth > MAX_GUARD_DEPTH) return false;\n\n // Arrays and records are the only two things left, and each is a walk of its\n // own rather than another branch here: an array of them or a record of them.\n return Array.isArray(input) ? everyMemberIs(input, depth) : everyFieldIs(input, depth);\n};\n\n/**\n * @description Whether an array holds nothing but legal values one level down.\n *\n * @param members - The array's members.\n * @param depth - The depth the array itself sits at.\n *\n * @returns Whether every member is a legal `XmlValue`.\n */\nconst everyMemberIs = (members: ReadonlyArray<unknown>, depth: number): boolean => {\n for (const member of members) {\n if (!check(member, depth + 1)) return false;\n }\n return true;\n};\n\n/**\n * @description Whether an object is a plain record of legal values one level down. Plainness is checked here rather than by the caller because a `Date` or a `Map`\n * has values a record cannot hold, and walking them would be walking something the model has no way to represent.\n *\n * @param input - The object to walk.\n * @param depth - The depth the object itself sits at.\n *\n * @returns Whether the object is a plain record whose every field is a legal `XmlValue`.\n */\nconst everyFieldIs = (input: object, depth: number): boolean => {\n if (!Predicate.isReadonlyObject(input)) return false;\n for (const key of Object.keys(input)) {\n if (!check(input[key], depth + 1)) return false;\n }\n return true;\n};\n\n/**\n * @description A schema for {@link XmlValue}, so a value can be validated on its own — when it arrives from a store or a queue rather than from {@link parseXml},\n * and the schema it belongs to is not in hand.\n *\n * @example\n * ```typescript\n * import { Schema } from 'effect';\n * import { XmlValue } from '@endevops/effect-xml-codec';\n *\n * Schema.decodeUnknownSync(XmlValue)({ book: { '@id': '1', title: 'Dune' } }); // => { book: { '@id': '1', title: 'Dune' } }\n * Schema.decodeUnknownSync(XmlValue)({ book: { title: 42 } }); // => throws XmlValue\n * ```;\n */\nexport const XmlValue: Schema.Codec<XmlValue> = Schema.declare<XmlValue>(isXmlValue, {\n identifier: 'XmlValue',\n expected: 'an XML value: character data, an array of them, or a record of them',\n});\n"],"mappings":";;;;;;AAwBA,MAAM,kBAAkB;;;;;;;;AASxB,MAAa,cAAc,UAAsC,MAAM,OAAO,CAAC;;;;;;;;;;;;AAa/E,MAAM,SAAS,OAAgB,UAA2B;CAGxD,IAAI,UAAU,YAAY,KAAK,KAAK,UAAU,SAAS,KAAK,GAAG,OAAO;CACtE,IAAI,UAAU,OAAO,KAAK,KAAK,CAAC,UAAU,gBAAgB,KAAK,GAAG,OAAO;CAEzE,IAAI,QAAQ,iBAAiB,OAAO;CAIpC,OAAO,MAAM,QAAQ,KAAK,IAAI,cAAc,OAAO,KAAK,IAAI,aAAa,OAAO,KAAK;AACvF;;;;;;;;;AAUA,MAAM,iBAAiB,SAAiC,UAA2B;CACjF,KAAK,MAAM,UAAU,SACnB,IAAI,CAAC,MAAM,QAAQ,QAAQ,CAAC,GAAG,OAAO;CAExC,OAAO;AACT;;;;;;;;;;AAWA,MAAM,gBAAgB,OAAe,UAA2B;CAC9D,IAAI,CAAC,UAAU,iBAAiB,KAAK,GAAG,OAAO;CAC/C,KAAK,MAAM,OAAO,OAAO,KAAK,KAAK,GACjC,IAAI,CAAC,MAAM,MAAM,MAAM,QAAQ,CAAC,GAAG,OAAO;CAE5C,OAAO;AACT;;;;;;;;;;;;;;AAeA,MAAa,WAAmC,OAAO,QAAkB,YAAY;CACnF,YAAY;CACZ,UAAU;AACZ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@endevops/effect-codec-xml",
|
|
3
|
-
"version": "0.1.0-
|
|
3
|
+
"version": "0.1.0-effect.1",
|
|
4
4
|
"description": "A round-trip Effect Schema codec for XML, with the entity decoder and XML name validators it reads documents with. Derives an XML representation from any Effect Schema and reads it back, with attributes as `@`-prefixed fields.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codec",
|
package/src/codec.ts
CHANGED
|
@@ -3,18 +3,18 @@
|
|
|
3
3
|
// `toCodecXml` is this package's counterpart to `Schema.toCodecJson`. It
|
|
4
4
|
// returns a `Schema` whose `Type` is the source schema's `Type` and whose
|
|
5
5
|
// `Encoded` is XML text, so a value is written with `Schema.encodeSync(codec)`
|
|
6
|
-
// and read back with `Schema.decodeSync(codec)
|
|
7
|
-
//
|
|
8
|
-
// a renderer or a parser.
|
|
6
|
+
// and read back with `Schema.decodeSync(codec)` — one call each, the way the
|
|
7
|
+
// JSON codec works. There is no value tree at the call site and no second call
|
|
8
|
+
// to a renderer or a parser.
|
|
9
9
|
//
|
|
10
10
|
// The derivation underneath is Effect's own `Schema.toCodecStringTree`, so
|
|
11
11
|
// every schema feature Effect supports composes here without this package
|
|
12
|
-
// re-implementing the walk over a schema AST. On
|
|
13
|
-
// tree through `renderXml`; on
|
|
14
|
-
// Both are the same text layer this package exports on
|
|
15
|
-
// failures
|
|
16
|
-
//
|
|
17
|
-
//
|
|
12
|
+
// re-implementing the walk over a schema AST. On the way out the codec runs the
|
|
13
|
+
// value tree through `renderXml`; on the way back in it runs the document
|
|
14
|
+
// through `parseXml`. Both are the same text layer this package exports on
|
|
15
|
+
// their own, and both failures — an illegal name, a document that is not
|
|
16
|
+
// well-formed — arrive as the `SchemaIssue.Issue` a schema is expected to
|
|
17
|
+
// report, with the underlying message preserved.
|
|
18
18
|
//
|
|
19
19
|
// The conventions stay in the keys: a key starting with `@` is an attribute,
|
|
20
20
|
// `#text` is character data, and every other key is a child element. The root
|
|
@@ -22,9 +22,8 @@
|
|
|
22
22
|
// has one, and from the `rootName` option otherwise; it defaults to `'root'`,
|
|
23
23
|
// the same name Effect's own XML encoder uses.
|
|
24
24
|
|
|
25
|
-
import { Effect,
|
|
25
|
+
import { Effect, Predicate, Schema, SchemaAST, SchemaIssue, SchemaTransformation } from 'effect';
|
|
26
26
|
|
|
27
|
-
import type { NamespacePlan } from './namespaces.ts';
|
|
28
27
|
import type { XmlParseOptions } from './parse.ts';
|
|
29
28
|
import type { XmlRenderOptions } from './render.ts';
|
|
30
29
|
import type { XmlValue } from './xml-value.ts';
|
|
@@ -37,8 +36,8 @@ import { renderXml } from './render.ts';
|
|
|
37
36
|
/**
|
|
38
37
|
* @description Options for {@link toCodecXml}.\
|
|
39
38
|
* The render options name and shape the document; the parse options decide how strictly it is read back.\
|
|
40
|
-
* `rootName` is the one the codec resolves for itself when the caller leaves it out
|
|
41
|
-
*
|
|
39
|
+
* `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
|
|
40
|
+
* back to `'root'`.
|
|
42
41
|
*/
|
|
43
42
|
export type XmlCodecOptions = XmlRenderOptions & XmlParseOptions;
|
|
44
43
|
|
|
@@ -50,40 +49,18 @@ export interface toCodecXml<S extends Schema.Constraint> extends Schema.decodeTo
|
|
|
50
49
|
readonly Rebuild: toCodecXml<S>;
|
|
51
50
|
}
|
|
52
51
|
|
|
53
|
-
// Whether the plan places any node at all: a rename, an attribute, a value, or a root namespace or name. A plan with none of these leaves the value tree
|
|
54
|
-
// alone on both sides, so the codec skips the name walk.
|
|
55
|
-
const isActivePlan = (plan: NamespacePlan): boolean =>
|
|
56
|
-
plan.byKey.size > 0 ||
|
|
57
|
-
plan.nameByKey.size > 0 ||
|
|
58
|
-
plan.attributeKeys.size > 0 ||
|
|
59
|
-
plan.valueByElement.size > 0 ||
|
|
60
|
-
plan.root !== undefined ||
|
|
61
|
-
plan.rootName !== undefined;
|
|
62
|
-
|
|
63
|
-
// The render options with the root name resolved and any root prefix applied. The caller's `rootName` wins, then the plan's, then the schema's `identifier`
|
|
64
|
-
// or `title` annotation, then `'root'`. A root namespace with a prefix qualifies the name unless the caller already wrote one.
|
|
65
|
-
const resolveRenderOptions = <S extends Schema.Constraint>(schema: S, plan: NamespacePlan, options: XmlCodecOptions): XmlRenderOptions => {
|
|
66
|
-
const rootName =
|
|
67
|
-
options.rootName ?? plan.rootName ?? SchemaAST.resolveIdentifier(schema.ast) ?? SchemaAST.resolveTitle(schema.ast) ?? DEFAULT_ROOT_NAME;
|
|
68
|
-
const wireRootName = plan.root !== undefined && plan.root.prefix !== '' && !rootName.includes(':') ? `${plan.root.prefix}:${rootName}` : rootName;
|
|
69
|
-
return { ...options, rootName: wireRootName };
|
|
70
|
-
};
|
|
71
|
-
|
|
72
52
|
/**
|
|
73
53
|
* @description Derives the XML codec for a schema: a `Schema` whose `Encoded` is an XML document, so `Schema.encodeSync(codec)` writes text and
|
|
74
54
|
* `Schema.decodeSync(codec)` reads it back. The derivation is Effect's `Schema.toCodecStringTree`; the text layer is this package's {@link renderXml}
|
|
75
|
-
* and {@link parseXml}.
|
|
76
|
-
* the rest of the Effect combinators. The two forms are one function: the first argument decides the style, a schema is read as data-first and an
|
|
77
|
-
* options object as data-last. `pipe(schema, toCodecXml)` carries no options and works because a schema on its own is data-first.
|
|
55
|
+
* and {@link parseXml}.
|
|
78
56
|
*
|
|
79
57
|
* @example
|
|
80
58
|
* ```typescript
|
|
81
|
-
* import { Schema
|
|
82
|
-
* import { toCodecXml } from '@endevops/effect-codec
|
|
59
|
+
* import { Schema } from 'effect';
|
|
60
|
+
* import { toCodecXml } from '@endevops/effect-xml-codec';
|
|
83
61
|
*
|
|
84
62
|
* const Book = Schema.Struct({ '@id': Schema.String, title: Schema.String, pages: Schema.Number });
|
|
85
63
|
* const codec = toCodecXml(Book, { rootName: 'book' });
|
|
86
|
-
* const piped = pipe(Book, toCodecXml({ rootName: 'book' }));
|
|
87
64
|
*
|
|
88
65
|
* const value = { '@id': '1', title: 'Dune', pages: 412 };
|
|
89
66
|
*
|
|
@@ -94,63 +71,66 @@ const resolveRenderOptions = <S extends Schema.Constraint>(schema: S, plan: Name
|
|
|
94
71
|
* // => { '@id': '1', title: 'Dune', pages: 412 }
|
|
95
72
|
* ```;
|
|
96
73
|
*
|
|
97
|
-
* @param schema - The schema describing the value
|
|
98
|
-
* @param options - Render and parse options. `rootName` defaults to the schema's `identifier` or `title` annotation, then to `'root'`.
|
|
99
|
-
* data-last form this is the only argument, and the schema arrives from `pipe`.
|
|
74
|
+
* @param schema - The schema describing the value.
|
|
75
|
+
* @param options - Render and parse options. `rootName` defaults to the schema's `identifier` or `title` annotation, then to `'root'`.
|
|
100
76
|
*
|
|
101
|
-
* @returns The codec, with the source schema's `Type` and the same service requirements.
|
|
102
|
-
* codec.
|
|
77
|
+
* @returns The codec, with the source schema's `Type` and the same service requirements.
|
|
103
78
|
*/
|
|
104
|
-
export const toCodecXml: {
|
|
105
|
-
|
|
106
|
-
(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
79
|
+
export const toCodecXml = <S extends Schema.Constraint>(schema: S, options: XmlCodecOptions = {}): toCodecXml<S> => {
|
|
80
|
+
const planned = namespacePlan(schema);
|
|
81
|
+
if (Predicate.hasProperty(planned, 'error')) {
|
|
82
|
+
throw new Error(`Invalid XML namespace annotation:\n\t- ${planned.error}.`);
|
|
83
|
+
}
|
|
84
|
+
const plan = planned.plan;
|
|
85
|
+
const active =
|
|
86
|
+
plan.byKey.size > 0 ||
|
|
87
|
+
plan.nameByKey.size > 0 ||
|
|
88
|
+
plan.attributeKeys.size > 0 ||
|
|
89
|
+
plan.valueByElement.size > 0 ||
|
|
90
|
+
plan.root !== undefined ||
|
|
91
|
+
plan.rootName !== undefined;
|
|
117
92
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
SchemaTransformation.transformEffect({
|
|
122
|
-
decode: (text, parseOptions) => {
|
|
123
|
-
if (!Predicate.isString(text)) {
|
|
124
|
-
return Effect.fail(new SchemaIssue.InvalidValue({ message: `Expected a string, but received ${typeof text}.` }, text, parseOptions));
|
|
125
|
-
}
|
|
93
|
+
const rootName =
|
|
94
|
+
options.rootName ?? plan.rootName ?? SchemaAST.resolveIdentifier(schema.ast) ?? SchemaAST.resolveTitle(schema.ast) ?? DEFAULT_ROOT_NAME;
|
|
95
|
+
const wireRootName = plan.root !== undefined && plan.root.prefix !== '' && !rootName.includes(':') ? `${plan.root.prefix}:${rootName}` : rootName;
|
|
126
96
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
97
|
+
const renderOptions: XmlRenderOptions = { ...options, rootName: wireRootName };
|
|
98
|
+
|
|
99
|
+
return Schema.String.pipe(
|
|
100
|
+
Schema.decodeTo(
|
|
101
|
+
Schema.toCodecStringTree(schema),
|
|
102
|
+
SchemaTransformation.transformEffect({
|
|
103
|
+
decode: (text, parseOptions) => {
|
|
104
|
+
if (!Predicate.isString(text)) {
|
|
105
|
+
return Effect.fail(new SchemaIssue.InvalidValue({ message: `Expected a string, but received ${typeof text}.` }, text, parseOptions));
|
|
106
|
+
}
|
|
135
107
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
);
|
|
145
|
-
}
|
|
108
|
+
return parseXml(text, options).pipe(
|
|
109
|
+
Effect.map(value => (active ? decodeNames(value, plan, {}, ROOT_ELEMENT) : value)),
|
|
110
|
+
Effect.tapError(error =>
|
|
111
|
+
Effect.logError(`XML parse error: ${error.message}`).pipe(Effect.annotateLogs({ cause: error, message: 'XML parse error' }))
|
|
112
|
+
),
|
|
113
|
+
Effect.mapError(error => new SchemaIssue.InvalidValue({ message: error.message }, text, parseOptions))
|
|
114
|
+
);
|
|
115
|
+
},
|
|
146
116
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
117
|
+
encode: (value, parseOptions) => {
|
|
118
|
+
if (active && Array.isArray(value) && (plan.root !== undefined || plan.rootName !== undefined)) {
|
|
119
|
+
return Effect.fail(
|
|
120
|
+
new SchemaIssue.InvalidValue(
|
|
121
|
+
{ message: 'An array at the root of a namespaced schema cannot carry the root namespace or name.' },
|
|
122
|
+
value,
|
|
123
|
+
parseOptions
|
|
124
|
+
)
|
|
150
125
|
);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const wire = active ? encodeNames(value as XmlValue, plan, plan.root, {}, ROOT_ELEMENT) : (value as XmlValue);
|
|
129
|
+
return renderXml(wire, renderOptions).pipe(
|
|
130
|
+
Effect.mapError(error => new SchemaIssue.InvalidValue({ message: error.message }, value, parseOptions))
|
|
131
|
+
);
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
)
|
|
135
|
+
);
|
|
136
|
+
};
|
package/src/conventions.ts
CHANGED
|
@@ -87,9 +87,9 @@ export const isTextKey = (key: string): boolean => key === TEXT_KEY;
|
|
|
87
87
|
export const isReservedKey = (key: string): boolean => isTextKey(key);
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
|
-
* @description Resolves a name to something legal in an XML document, synchronously. The renderer and the parser both resolve a name per element and per
|
|
91
|
-
*
|
|
92
|
-
*
|
|
90
|
+
* @description Resolves a name to something legal in an XML document, synchronously. The renderer and the parser both resolve a name per element and per attribute
|
|
91
|
+
* — the codec's hot path — so the walk calls this directly and keeps the work in plain JavaScript. `'error'` mode reports an illegal name by throwing
|
|
92
|
+
* an {@link XmlParseError}; the callers that need it in a typed channel use {@link resolveName}, which wraps this.
|
|
93
93
|
*
|
|
94
94
|
* @param name - The candidate element or attribute name.
|
|
95
95
|
* @param options - Repair mode and XML version.
|
|
@@ -131,10 +131,10 @@ const resolveNameResult = (name: string, options: ResolveNameOptions): Result.Re
|
|
|
131
131
|
};
|
|
132
132
|
|
|
133
133
|
/**
|
|
134
|
-
* @description Resolves a name to something legal in an XML document. The renderer and the parser both resolve a name per element and per attribute
|
|
135
|
-
* name under `'error'` mode is a rejection rather than a value, so this returns an `Effect` with the `XmlParseError` in its error channel
|
|
136
|
-
* throwing it. Callers `yield*` it and the failure composes with `catchTag` and the rest
|
|
137
|
-
* use {@link resolveNameSync} directly, because their walks are synchronous hot paths.
|
|
134
|
+
* @description Resolves a name to something legal in an XML document. The renderer and the parser both resolve a name per element and per attribute, and an
|
|
135
|
+
* illegal name under `'error'` mode is a rejection rather than a value, so this returns an `Effect` with the `XmlParseError` in its error channel
|
|
136
|
+
* rather than throwing it. Callers `yield*` it and the failure composes with `catchTag` and the rest; the two internal call sites in `parse.ts` and
|
|
137
|
+
* `render.ts` use {@link resolveNameSync} directly, because their walks are synchronous hot paths.
|
|
138
138
|
*
|
|
139
139
|
* @param name - The candidate element or attribute name.
|
|
140
140
|
* @param options - Repair mode and XML version.
|