@endevops/effect-codec-xml 0.0.1 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -62
- package/dist/codec.d.ts +17 -9
- package/dist/codec.d.ts.map +1 -1
- package/dist/codec.js +29 -18
- package/dist/codec.js.map +1 -1
- package/dist/conventions.d.ts +4 -4
- package/dist/conventions.js +7 -7
- package/dist/conventions.js.map +1 -1
- package/dist/entities/entity-decoder.d.ts +33 -33
- package/dist/entities/entity-decoder.d.ts.map +1 -1
- package/dist/entities/entity-decoder.js +63 -64
- package/dist/entities/entity-decoder.js.map +1 -1
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +2 -2
- package/dist/errors.js.map +1 -1
- package/dist/namespaces.js +40 -13
- package/dist/namespaces.js.map +1 -1
- package/dist/naming.d.ts +6 -6
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +3 -3
- package/dist/naming.js.map +1 -1
- package/dist/parse.d.ts +5 -5
- package/dist/parse.js +14 -14
- package/dist/parse.js.map +1 -1
- package/dist/plain-value.js +242 -0
- package/dist/plain-value.js.map +1 -0
- package/dist/render.d.ts +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +28 -28
- package/dist/render.js.map +1 -1
- package/dist/xml-error.d.ts +9 -9
- package/dist/xml-error.js +18 -18
- package/dist/xml-error.js.map +1 -1
- package/dist/xml-value.d.ts +7 -7
- package/dist/xml-value.d.ts.map +1 -1
- package/dist/xml-value.js +6 -7
- package/dist/xml-value.js.map +1 -1
- package/package.json +1 -1
- package/src/codec.ts +98 -71
- package/src/conventions.ts +7 -7
- package/src/entities/entity-decoder.ts +98 -99
- package/src/errors.ts +3 -3
- package/src/index.ts +3 -3
- package/src/namespaces.ts +62 -36
- package/src/naming.ts +34 -35
- package/src/parse.ts +26 -26
- package/src/plain-value.ts +312 -0
- package/src/render.ts +44 -44
- package/src/xml-error.ts +18 -18
- package/src/xml-value.ts +10 -11
package/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
|
-
// JSON codec
|
|
8
|
-
//
|
|
6
|
+
// and read back with `Schema.decodeSync(codec)`. That is one call each, as with
|
|
7
|
+
// the JSON codec. There is no value tree at the call site and no second call to
|
|
8
|
+
// 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
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
12
|
+
// re-implementing the walk over a schema AST. On encode the codec runs the value
|
|
13
|
+
// tree through `renderXml`; on decode it runs the document through `parseXml`.
|
|
14
|
+
// Both are the same text layer this package exports on its own, and both
|
|
15
|
+
// failures arrive as the `SchemaIssue.Issue` a schema is expected to report,
|
|
16
|
+
// with the underlying message preserved. Those failures are an illegal name and
|
|
17
|
+
// a document that is not well-formed.
|
|
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,8 +22,9 @@
|
|
|
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, Predicate, Schema, SchemaAST, SchemaIssue, SchemaTransformation } from 'effect';
|
|
25
|
+
import { Effect, Function, Predicate, Schema, SchemaAST, SchemaIssue, SchemaTransformation } from 'effect';
|
|
26
26
|
|
|
27
|
+
import type { NamespacePlan } from './namespaces.ts';
|
|
27
28
|
import type { XmlParseOptions } from './parse.ts';
|
|
28
29
|
import type { XmlRenderOptions } from './render.ts';
|
|
29
30
|
import type { XmlValue } from './xml-value.ts';
|
|
@@ -31,13 +32,14 @@ import type { XmlValue } from './xml-value.ts';
|
|
|
31
32
|
import { DEFAULT_ROOT_NAME } from './conventions.ts';
|
|
32
33
|
import { decodeNames, encodeNames, namespacePlan, ROOT_ELEMENT } from './namespaces.ts';
|
|
33
34
|
import { parseXml } from './parse.ts';
|
|
35
|
+
import { normalizePlainValue } from './plain-value.ts';
|
|
34
36
|
import { renderXml } from './render.ts';
|
|
35
37
|
|
|
36
38
|
/**
|
|
37
39
|
* @description Options for {@link toCodecXml}.\
|
|
38
40
|
* The render options name and shape the document; the parse options decide how strictly it is read back.\
|
|
39
|
-
* `rootName` is the one the codec resolves for itself when the caller leaves it out
|
|
40
|
-
* back to `'root'`.
|
|
41
|
+
* `rootName` is the one the codec resolves for itself when the caller leaves it out. The codec takes it from the schema's `identifier` or `title` annotation and
|
|
42
|
+
* falls back to `'root'`.
|
|
41
43
|
*/
|
|
42
44
|
export type XmlCodecOptions = XmlRenderOptions & XmlParseOptions;
|
|
43
45
|
|
|
@@ -49,18 +51,40 @@ export interface toCodecXml<S extends Schema.Constraint> extends Schema.decodeTo
|
|
|
49
51
|
readonly Rebuild: toCodecXml<S>;
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
// 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
|
|
55
|
+
// alone on both sides, so the codec skips the name walk.
|
|
56
|
+
const isActivePlan = (plan: NamespacePlan): boolean =>
|
|
57
|
+
plan.byKey.size > 0 ||
|
|
58
|
+
plan.nameByKey.size > 0 ||
|
|
59
|
+
plan.attributeKeys.size > 0 ||
|
|
60
|
+
plan.valueByElement.size > 0 ||
|
|
61
|
+
plan.root !== undefined ||
|
|
62
|
+
plan.rootName !== undefined;
|
|
63
|
+
|
|
64
|
+
// 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`
|
|
65
|
+
// or `title` annotation, then `'root'`. A root namespace with a prefix qualifies the name unless the caller already wrote one.
|
|
66
|
+
const resolveRenderOptions = <S extends Schema.Constraint>(schema: S, plan: NamespacePlan, options: XmlCodecOptions): XmlRenderOptions => {
|
|
67
|
+
const rootName =
|
|
68
|
+
options.rootName ?? plan.rootName ?? SchemaAST.resolveIdentifier(schema.ast) ?? SchemaAST.resolveTitle(schema.ast) ?? DEFAULT_ROOT_NAME;
|
|
69
|
+
const wireRootName = plan.root !== undefined && plan.root.prefix !== '' && !rootName.includes(':') ? `${plan.root.prefix}:${rootName}` : rootName;
|
|
70
|
+
return { ...options, rootName: wireRootName };
|
|
71
|
+
};
|
|
72
|
+
|
|
52
73
|
/**
|
|
53
74
|
* @description Derives the XML codec for a schema: a `Schema` whose `Encoded` is an XML document, so `Schema.encodeSync(codec)` writes text and
|
|
54
75
|
* `Schema.decodeSync(codec)` reads it back. The derivation is Effect's `Schema.toCodecStringTree`; the text layer is this package's {@link renderXml}
|
|
55
|
-
* and {@link parseXml}.
|
|
76
|
+
* and {@link parseXml}. Call it data-first, `toCodecXml(schema, options)`, or data-last, `toCodecXml(options)(schema)`, so it drops into `pipe` beside
|
|
77
|
+
* 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
|
|
78
|
+
* options object as data-last. `pipe(schema, toCodecXml)` carries no options and works because a schema on its own is data-first.
|
|
56
79
|
*
|
|
57
80
|
* @example
|
|
58
81
|
* ```typescript
|
|
59
|
-
* import { Schema } from 'effect';
|
|
60
|
-
* import { toCodecXml } from '@endevops/effect-xml
|
|
82
|
+
* import { Schema, pipe } from 'effect';
|
|
83
|
+
* import { toCodecXml } from '@endevops/effect-codec-xml';
|
|
61
84
|
*
|
|
62
85
|
* const Book = Schema.Struct({ '@id': Schema.String, title: Schema.String, pages: Schema.Number });
|
|
63
86
|
* const codec = toCodecXml(Book, { rootName: 'book' });
|
|
87
|
+
* const piped = pipe(Book, toCodecXml({ rootName: 'book' }));
|
|
64
88
|
*
|
|
65
89
|
* const value = { '@id': '1', title: 'Dune', pages: 412 };
|
|
66
90
|
*
|
|
@@ -71,66 +95,69 @@ export interface toCodecXml<S extends Schema.Constraint> extends Schema.decodeTo
|
|
|
71
95
|
* // => { '@id': '1', title: 'Dune', pages: 412 }
|
|
72
96
|
* ```;
|
|
73
97
|
*
|
|
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'`.
|
|
98
|
+
* @param schema - The schema describing the value, in the data-first form.
|
|
99
|
+
* @param options - Render and parse options. `rootName` defaults to the schema's `identifier` or `title` annotation, then to `'root'`. In the
|
|
100
|
+
* data-last form this is the only argument, and the schema arrives from `pipe`.
|
|
76
101
|
*
|
|
77
|
-
* @returns The codec, with the source schema's `Type` and the same service requirements.
|
|
102
|
+
* @returns The codec, with the source schema's `Type` and the same service requirements. In the data-last form, a function from the schema to the
|
|
103
|
+
* codec.
|
|
78
104
|
*/
|
|
79
|
-
export const toCodecXml
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
plan
|
|
90
|
-
|
|
91
|
-
plan
|
|
105
|
+
export const toCodecXml: {
|
|
106
|
+
<S extends Schema.Constraint>(schema: S, options?: XmlCodecOptions): toCodecXml<S>;
|
|
107
|
+
(options?: XmlCodecOptions): <S extends Schema.Constraint>(schema: S) => toCodecXml<S>;
|
|
108
|
+
} = Function.dual(
|
|
109
|
+
args => Schema.isSchema(args[0]),
|
|
110
|
+
<S extends Schema.Constraint>(schema: S, options: XmlCodecOptions = {}): toCodecXml<S> => {
|
|
111
|
+
const planned = namespacePlan(schema);
|
|
112
|
+
if (Predicate.hasProperty(planned, 'error')) {
|
|
113
|
+
throw new Error(`Invalid XML namespace annotation:\n\t- ${planned.error}.`);
|
|
114
|
+
}
|
|
115
|
+
const plan = planned.plan;
|
|
116
|
+
const active = isActivePlan(plan);
|
|
117
|
+
const renderOptions = resolveRenderOptions(schema, plan, options);
|
|
118
|
+
const stringTree = Schema.toCodecStringTree(schema);
|
|
92
119
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
}
|
|
107
|
-
|
|
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
|
-
},
|
|
120
|
+
return Schema.String.pipe(
|
|
121
|
+
Schema.decodeTo(
|
|
122
|
+
stringTree,
|
|
123
|
+
SchemaTransformation.transformEffect({
|
|
124
|
+
decode: (text, parseOptions) => {
|
|
125
|
+
if (!Predicate.isString(text)) {
|
|
126
|
+
return Effect.fail(new SchemaIssue.InvalidValue({ message: `Expected a string, but received ${typeof text}.` }, text, parseOptions));
|
|
127
|
+
}
|
|
116
128
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
129
|
+
return parseXml(text, options).pipe(
|
|
130
|
+
Effect.map(value => (active ? decodeNames(value, plan, {}, ROOT_ELEMENT) : value)),
|
|
131
|
+
Effect.tapError(error =>
|
|
132
|
+
Effect.logError(`XML parse error: ${error.message}`).pipe(Effect.annotateLogs({ cause: error, message: 'XML parse error' }))
|
|
133
|
+
),
|
|
134
|
+
Effect.mapError(error => new SchemaIssue.InvalidValue({ message: error.message }, text, parseOptions)),
|
|
135
|
+
Effect.flatMap(value =>
|
|
136
|
+
Effect.fromResult(normalizePlainValue(value, stringTree.ast, '')).pipe(
|
|
137
|
+
Effect.mapError(message => new SchemaIssue.InvalidValue({ message }, value, parseOptions))
|
|
138
|
+
)
|
|
124
139
|
)
|
|
125
140
|
);
|
|
126
|
-
}
|
|
141
|
+
},
|
|
127
142
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
143
|
+
encode: (value, parseOptions) => {
|
|
144
|
+
if (active && Array.isArray(value) && (plan.root !== undefined || plan.rootName !== undefined)) {
|
|
145
|
+
return Effect.fail(
|
|
146
|
+
new SchemaIssue.InvalidValue(
|
|
147
|
+
{ message: 'An array at the root of a namespaced schema cannot carry the root namespace or name.' },
|
|
148
|
+
value,
|
|
149
|
+
parseOptions
|
|
150
|
+
)
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const wire = active ? encodeNames(value as XmlValue, plan, plan.root, {}, ROOT_ELEMENT) : (value as XmlValue);
|
|
155
|
+
return renderXml(wire, renderOptions).pipe(
|
|
156
|
+
Effect.mapError(error => new SchemaIssue.InvalidValue({ message: error.message }, value, parseOptions))
|
|
157
|
+
);
|
|
158
|
+
},
|
|
159
|
+
})
|
|
160
|
+
)
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
);
|
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
|
-
* an {@link XmlParseError}; the callers that need it in a typed channel use {@link resolveName}, which wraps this.
|
|
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
|
+
* attribute, and that is the codec's hot path, so the walk calls this directly and keeps the work in plain JavaScript. `'error'` mode reports an
|
|
92
|
+
* illegal name by throwing 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
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
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. An illegal
|
|
135
|
+
* name under `'error'` mode is a rejection rather than a value, so this returns an `Effect` with the `XmlParseError` in its error channel instead of
|
|
136
|
+
* throwing it. Callers `yield*` it and the failure composes with `catchTag` and the rest. The two internal call sites in `parse.ts` and `render.ts`
|
|
137
|
+
* 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.
|