@endevops/effect-codec-xml 0.1.0-beta.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 CHANGED
@@ -62,6 +62,24 @@ decision rather than the parser's:
62
62
  `Schema.Struct({ '@id': Schema.String, '#text': Schema.String })` matches
63
63
  `<a id="1">hello</a>`.
64
64
 
65
+ A field that wants a plain value does not have to describe the attributes
66
+ themselves. Where the schema wants a scalar, a `#text` node is read on its own
67
+ and the other attributes are discarded, so
68
+ `Schema.Struct({ title: Schema.String })` also matches
69
+ `<title lang="en">Dune</title>` and yields `'Dune'`. An element that also
70
+ carries a child element there is refused instead, as an effect error: a plain
71
+ value has nowhere to put a child, and dropping one silently would lose a field
72
+ the document actually carried.
73
+
74
+ The same holds wherever a scalar sits: under a union, or under a repeated field,
75
+ which derives as a union of an array and `undefined`. Each branch is folded in
76
+ turn, so `Schema.Struct({ id: Schema.Literals(['E', 'S']) })` matches
77
+ `<id schemeID="UNCL5305">E</id>` inside either. The reverse also works: an
78
+ element the schema reads as a struct with an `xmlValue` field, reduced by the
79
+ parser to bare character data because it carries no attributes, has that string
80
+ put back under the value's key, so a nested price struct still reads
81
+ `<price>1.5</price>` instead of failing on the bare string.
82
+
65
83
  ## Namespaces
66
84
 
67
85
  A schema describes a value in local names, so a namespace is an annotation on
@@ -1 +1 @@
1
- {"version":3,"file":"codec.d.ts","names":[],"sources":["../src/codec.ts"],"mappings":";;;;;;;;;;YA0CY,kBAAkB,mBAAmB;;;;;iBAMhC,WAAW,UAAU,OAAO,oBAAoB,OAAO,SAAS,OAAO,kBAAkB,IAAI,OAAO;WAC1G,SAAS,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAsDlB;GACV,UAAU,OAAO,YAAY,QAAQ,GAAG,UAAU,kBAAkB,WAAW;GAC/E,UAAU,mBAAmB,UAAU,OAAO,YAAY,QAAQ,MAAM,WAAW"}
1
+ {"version":3,"file":"codec.d.ts","names":[],"sources":["../src/codec.ts"],"mappings":";;;;;;;;;;YA2CY,kBAAkB,mBAAmB;;;;;iBAMhC,WAAW,UAAU,OAAO,oBAAoB,OAAO,SAAS,OAAO,kBAAkB,IAAI,OAAO;WAC1G,SAAS,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAsDlB;GACV,UAAU,OAAO,YAAY,QAAQ,GAAG,UAAU,kBAAkB,WAAW;GAC/E,UAAU,mBAAmB,UAAU,OAAO,YAAY,QAAQ,MAAM,WAAW"}
package/dist/codec.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import "./conventions.js";
2
2
  import { decodeNames, encodeNames, namespacePlan } from "./namespaces.js";
3
3
  import { parseXml } from "./parse.js";
4
+ import { normalizePlainValue } from "./plain-value.js";
4
5
  import { renderXml } from "./render.js";
5
6
  import { Effect, Function, Predicate, Schema, SchemaAST, SchemaIssue, SchemaTransformation } from "effect";
6
7
  //#region src/codec.ts
@@ -51,13 +52,14 @@ const toCodecXml = Function.dual((args) => Schema.isSchema(args[0]), (schema, op
51
52
  const plan = planned.plan;
52
53
  const active = isActivePlan(plan);
53
54
  const renderOptions = resolveRenderOptions(schema, plan, options);
54
- return Schema.String.pipe(Schema.decodeTo(Schema.toCodecStringTree(schema), SchemaTransformation.transformEffect({
55
+ const stringTree = Schema.toCodecStringTree(schema);
56
+ return Schema.String.pipe(Schema.decodeTo(stringTree, SchemaTransformation.transformEffect({
55
57
  decode: (text, parseOptions) => {
56
58
  if (!Predicate.isString(text)) return Effect.fail(new SchemaIssue.InvalidValue({ message: `Expected a string, but received ${typeof text}.` }, text, parseOptions));
57
59
  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({
58
60
  cause: error,
59
61
  message: "XML parse error"
60
- }))), Effect.mapError((error) => new SchemaIssue.InvalidValue({ message: error.message }, text, parseOptions)));
62
+ }))), Effect.mapError((error) => new SchemaIssue.InvalidValue({ message: error.message }, text, parseOptions)), Effect.flatMap((value) => Effect.fromResult(normalizePlainValue(value, stringTree.ast, "")).pipe(Effect.mapError((message) => new SchemaIssue.InvalidValue({ message }, value, parseOptions)))));
61
63
  },
62
64
  encode: (value, parseOptions) => {
63
65
  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));
package/dist/codec.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"codec.js","names":[],"sources":["../src/codec.ts"],"sourcesContent":["// The codec: a schema, and the XML text that carries it.\n//\n// `toCodecXml` is this package's counterpart to `Schema.toCodecJson`. It\n// returns a `Schema` whose `Type` is the source schema's `Type` and whose\n// `Encoded` is XML text, so a value is written with `Schema.encodeSync(codec)`\n// and read back with `Schema.decodeSync(codec)`. That is one call each, as with\n// the JSON codec. There is no value tree at the call site and no second call to\n// a renderer or a parser.\n//\n// The derivation underneath is Effect's own `Schema.toCodecStringTree`, so\n// every schema feature Effect supports composes here without this package\n// re-implementing the walk over a schema AST. On encode the codec runs the value\n// tree through `renderXml`; on decode it runs the document through `parseXml`.\n// Both are the same text layer this package exports on its own, and both\n// failures arrive as the `SchemaIssue.Issue` a schema is expected to report,\n// with the underlying message preserved. Those failures are an illegal name and\n// a document that is not well-formed.\n//\n// The conventions stay in the keys: a key starting with `@` is an attribute,\n// `#text` is character data, and every other key is a child element. The root\n// element is named from the schema's `identifier` or `title` annotation when it\n// has one, and from the `rootName` option otherwise; it defaults to `'root'`,\n// the same name Effect's own XML encoder uses.\n\nimport { Effect, Function, Predicate, Schema, SchemaAST, SchemaIssue, SchemaTransformation } from 'effect';\n\nimport type { NamespacePlan } from './namespaces.ts';\nimport type { XmlParseOptions } from './parse.ts';\nimport type { XmlRenderOptions } from './render.ts';\nimport type { XmlValue } from './xml-value.ts';\n\nimport { DEFAULT_ROOT_NAME } from './conventions.ts';\nimport { decodeNames, encodeNames, namespacePlan, ROOT_ELEMENT } from './namespaces.ts';\nimport { parseXml } from './parse.ts';\nimport { renderXml } from './render.ts';\n\n/**\n * @description Options for {@link toCodecXml}.\\\n * The render options name and shape the document; the parse options decide how strictly it is read back.\\\n * `rootName` is the one the codec resolves for itself when the caller leaves it out. The codec takes it from the schema's `identifier` or `title` annotation and\n * falls back to `'root'`.\n */\nexport type XmlCodecOptions = XmlRenderOptions & XmlParseOptions;\n\n/**\n * @description The XML codec for a schema, as a `Schema`. `Type` is the schema's own `Type` and `Encoded` is XML text, so it encodes a value to a document and\n * decodes a document to a value in one step each. The service requirements of the source schema are preserved.\n */\nexport interface toCodecXml<S extends Schema.Constraint> extends Schema.decodeTo<Schema.toCodecStringTree<S>, Schema.String> {\n readonly Rebuild: toCodecXml<S>;\n}\n\n// Whether the plan places any node at all: a rename, an attribute, a value, or a root namespace or name. A plan with none of these leaves the value tree\n// alone on both sides, so the codec skips the name walk.\nconst isActivePlan = (plan: NamespacePlan): boolean =>\n plan.byKey.size > 0 ||\n plan.nameByKey.size > 0 ||\n plan.attributeKeys.size > 0 ||\n plan.valueByElement.size > 0 ||\n plan.root !== undefined ||\n plan.rootName !== undefined;\n\n// The render options with the root name resolved and any root prefix applied. The caller's `rootName` wins, then the plan's, then the schema's `identifier`\n// or `title` annotation, then `'root'`. A root namespace with a prefix qualifies the name unless the caller already wrote one.\nconst resolveRenderOptions = <S extends Schema.Constraint>(schema: S, plan: NamespacePlan, options: XmlCodecOptions): XmlRenderOptions => {\n const rootName =\n options.rootName ?? plan.rootName ?? SchemaAST.resolveIdentifier(schema.ast) ?? SchemaAST.resolveTitle(schema.ast) ?? DEFAULT_ROOT_NAME;\n const wireRootName = plan.root !== undefined && plan.root.prefix !== '' && !rootName.includes(':') ? `${plan.root.prefix}:${rootName}` : rootName;\n return { ...options, rootName: wireRootName };\n};\n\n/**\n * @description Derives the XML codec for a schema: a `Schema` whose `Encoded` is an XML document, so `Schema.encodeSync(codec)` writes text and\n * `Schema.decodeSync(codec)` reads it back. The derivation is Effect's `Schema.toCodecStringTree`; the text layer is this package's {@link renderXml}\n * and {@link parseXml}. Call it data-first, `toCodecXml(schema, options)`, or data-last, `toCodecXml(options)(schema)`, so it drops into `pipe` beside\n * the rest of the Effect combinators. The two forms are one function: the first argument decides the style, a schema is read as data-first and an\n * options object as data-last. `pipe(schema, toCodecXml)` carries no options and works because a schema on its own is data-first.\n *\n * @example\n * ```typescript\n * import { Schema, pipe } from 'effect';\n * import { toCodecXml } from '@endevops/effect-codec-xml';\n *\n * const Book = Schema.Struct({ '@id': Schema.String, title: Schema.String, pages: Schema.Number });\n * const codec = toCodecXml(Book, { rootName: 'book' });\n * const piped = pipe(Book, toCodecXml({ rootName: 'book' }));\n *\n * const value = { '@id': '1', title: 'Dune', pages: 412 };\n *\n * Schema.encodeSync(codec)(value);\n * // => '<book id=\"1\"><title>Dune</title><pages>412</pages></book>'\n *\n * Schema.decodeSync(codec)('<book id=\"1\"><title>Dune</title><pages>412</pages></book>');\n * // => { '@id': '1', title: 'Dune', pages: 412 }\n * ```;\n *\n * @param schema - The schema describing the value, in the data-first form.\n * @param options - Render and parse options. `rootName` defaults to the schema's `identifier` or `title` annotation, then to `'root'`. In the\n * data-last form this is the only argument, and the schema arrives from `pipe`.\n *\n * @returns The codec, with the source schema's `Type` and the same service requirements. In the data-last form, a function from the schema to the\n * codec.\n */\nexport const toCodecXml: {\n <S extends Schema.Constraint>(schema: S, options?: XmlCodecOptions): toCodecXml<S>;\n (options?: XmlCodecOptions): <S extends Schema.Constraint>(schema: S) => toCodecXml<S>;\n} = Function.dual(\n args => Schema.isSchema(args[0]),\n <S extends Schema.Constraint>(schema: S, options: XmlCodecOptions = {}): toCodecXml<S> => {\n const planned = namespacePlan(schema);\n if (Predicate.hasProperty(planned, 'error')) {\n throw new Error(`Invalid XML namespace annotation:\\n\\t- ${planned.error}.`);\n }\n const plan = planned.plan;\n const active = isActivePlan(plan);\n const renderOptions = resolveRenderOptions(schema, plan, options);\n\n return Schema.String.pipe(\n Schema.decodeTo(\n Schema.toCodecStringTree(schema),\n SchemaTransformation.transformEffect({\n decode: (text, parseOptions) => {\n if (!Predicate.isString(text)) {\n return Effect.fail(new SchemaIssue.InvalidValue({ message: `Expected a string, but received ${typeof text}.` }, text, parseOptions));\n }\n\n return parseXml(text, options).pipe(\n Effect.map(value => (active ? decodeNames(value, plan, {}, ROOT_ELEMENT) : value)),\n Effect.tapError(error =>\n Effect.logError(`XML parse error: ${error.message}`).pipe(Effect.annotateLogs({ cause: error, message: 'XML parse error' }))\n ),\n Effect.mapError(error => new SchemaIssue.InvalidValue({ message: error.message }, text, parseOptions))\n );\n },\n\n encode: (value, parseOptions) => {\n if (active && Array.isArray(value) && (plan.root !== undefined || plan.rootName !== undefined)) {\n return Effect.fail(\n new SchemaIssue.InvalidValue(\n { message: 'An array at the root of a namespaced schema cannot carry the root namespace or name.' },\n value,\n parseOptions\n )\n );\n }\n\n const wire = active ? encodeNames(value as XmlValue, plan, plan.root, {}, ROOT_ELEMENT) : (value as XmlValue);\n return renderXml(wire, renderOptions).pipe(\n Effect.mapError(error => new SchemaIssue.InvalidValue({ message: error.message }, value, parseOptions))\n );\n },\n })\n )\n );\n }\n);\n"],"mappings":";;;;;;AAsDA,MAAM,gBAAgB,SACpB,KAAK,MAAM,OAAO,KAClB,KAAK,UAAU,OAAO,KACtB,KAAK,cAAc,OAAO,KAC1B,KAAK,eAAe,OAAO,KAC3B,KAAK,SAAS,KAAA,KACd,KAAK,aAAa,KAAA;AAIpB,MAAM,wBAAqD,QAAW,MAAqB,YAA+C;CACxI,MAAM,WACJ,QAAQ,YAAY,KAAK,YAAY,UAAU,kBAAkB,OAAO,GAAG,KAAK,UAAU,aAAa,OAAO,GAAG,KAAA;CACnH,MAAM,eAAe,KAAK,SAAS,KAAA,KAAa,KAAK,KAAK,WAAW,MAAM,CAAC,SAAS,SAAS,GAAG,IAAI,GAAG,KAAK,KAAK,OAAO,GAAG,aAAa;CACzI,OAAO;EAAE,GAAG;EAAS,UAAU;CAAa;AAC9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,MAAa,aAGT,SAAS,MACX,SAAQ,OAAO,SAAS,KAAK,EAAE,IACD,QAAW,UAA2B,CAAC,MAAqB;CACxF,MAAM,UAAU,cAAc,MAAM;CACpC,IAAI,UAAU,YAAY,SAAS,OAAO,GACxC,MAAM,IAAI,MAAM,0CAA0C,QAAQ,MAAM,EAAE;CAE5E,MAAM,OAAO,QAAQ;CACrB,MAAM,SAAS,aAAa,IAAI;CAChC,MAAM,gBAAgB,qBAAqB,QAAQ,MAAM,OAAO;CAEhE,OAAO,OAAO,OAAO,KACnB,OAAO,SACL,OAAO,kBAAkB,MAAM,GAC/B,qBAAqB,gBAAgB;EACnC,SAAS,MAAM,iBAAiB;GAC9B,IAAI,CAAC,UAAU,SAAS,IAAI,GAC1B,OAAO,OAAO,KAAK,IAAI,YAAY,aAAa,EAAE,SAAS,mCAAmC,OAAO,KAAK,GAAG,GAAG,MAAM,YAAY,CAAC;GAGrI,OAAO,SAAS,MAAM,OAAO,CAAC,CAAC,KAC7B,OAAO,KAAI,UAAU,SAAS,YAAY,OAAO,MAAM,CAAC,GAAA,EAAe,IAAI,KAAM,GACjF,OAAO,UAAS,UACd,OAAO,SAAS,oBAAoB,MAAM,SAAS,CAAC,CAAC,KAAK,OAAO,aAAa;IAAE,OAAO;IAAO,SAAS;GAAkB,CAAC,CAAC,CAC7H,GACA,OAAO,UAAS,UAAS,IAAI,YAAY,aAAa,EAAE,SAAS,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,CACvG;EACF;EAEA,SAAS,OAAO,iBAAiB;GAC/B,IAAI,UAAU,MAAM,QAAQ,KAAK,MAAM,KAAK,SAAS,KAAA,KAAa,KAAK,aAAa,KAAA,IAClF,OAAO,OAAO,KACZ,IAAI,YAAY,aACd,EAAE,SAAS,uFAAuF,GAClG,OACA,YACF,CACF;GAGF,MAAM,OAAO,SAAS,YAAY,OAAmB,MAAM,KAAK,MAAM,CAAC,GAAA,EAAe,IAAK;GAC3F,OAAO,UAAU,MAAM,aAAa,CAAC,CAAC,KACpC,OAAO,UAAS,UAAS,IAAI,YAAY,aAAa,EAAE,SAAS,MAAM,QAAQ,GAAG,OAAO,YAAY,CAAC,CACxG;EACF;CACF,CAAC,CACH,CACF;AACF,CACF"}
1
+ {"version":3,"file":"codec.js","names":[],"sources":["../src/codec.ts"],"sourcesContent":["// The codec: a schema, and the XML text that carries it.\n//\n// `toCodecXml` is this package's counterpart to `Schema.toCodecJson`. It\n// returns a `Schema` whose `Type` is the source schema's `Type` and whose\n// `Encoded` is XML text, so a value is written with `Schema.encodeSync(codec)`\n// and read back with `Schema.decodeSync(codec)`. That is one call each, as with\n// the JSON codec. There is no value tree at the call site and no second call to\n// a renderer or a parser.\n//\n// The derivation underneath is Effect's own `Schema.toCodecStringTree`, so\n// every schema feature Effect supports composes here without this package\n// re-implementing the walk over a schema AST. On encode the codec runs the value\n// tree through `renderXml`; on decode it runs the document through `parseXml`.\n// Both are the same text layer this package exports on its own, and both\n// failures arrive as the `SchemaIssue.Issue` a schema is expected to report,\n// with the underlying message preserved. Those failures are an illegal name and\n// a document that is not well-formed.\n//\n// The conventions stay in the keys: a key starting with `@` is an attribute,\n// `#text` is character data, and every other key is a child element. The root\n// element is named from the schema's `identifier` or `title` annotation when it\n// has one, and from the `rootName` option otherwise; it defaults to `'root'`,\n// the same name Effect's own XML encoder uses.\n\nimport { Effect, Function, Predicate, Schema, SchemaAST, SchemaIssue, SchemaTransformation } from 'effect';\n\nimport type { NamespacePlan } from './namespaces.ts';\nimport type { XmlParseOptions } from './parse.ts';\nimport type { XmlRenderOptions } from './render.ts';\nimport type { XmlValue } from './xml-value.ts';\n\nimport { DEFAULT_ROOT_NAME } from './conventions.ts';\nimport { decodeNames, encodeNames, namespacePlan, ROOT_ELEMENT } from './namespaces.ts';\nimport { parseXml } from './parse.ts';\nimport { normalizePlainValue } from './plain-value.ts';\nimport { renderXml } from './render.ts';\n\n/**\n * @description Options for {@link toCodecXml}.\\\n * The render options name and shape the document; the parse options decide how strictly it is read back.\\\n * `rootName` is the one the codec resolves for itself when the caller leaves it out. The codec takes it from the schema's `identifier` or `title` annotation and\n * falls back to `'root'`.\n */\nexport type XmlCodecOptions = XmlRenderOptions & XmlParseOptions;\n\n/**\n * @description The XML codec for a schema, as a `Schema`. `Type` is the schema's own `Type` and `Encoded` is XML text, so it encodes a value to a document and\n * decodes a document to a value in one step each. The service requirements of the source schema are preserved.\n */\nexport interface toCodecXml<S extends Schema.Constraint> extends Schema.decodeTo<Schema.toCodecStringTree<S>, Schema.String> {\n readonly Rebuild: toCodecXml<S>;\n}\n\n// Whether the plan places any node at all: a rename, an attribute, a value, or a root namespace or name. A plan with none of these leaves the value tree\n// alone on both sides, so the codec skips the name walk.\nconst isActivePlan = (plan: NamespacePlan): boolean =>\n plan.byKey.size > 0 ||\n plan.nameByKey.size > 0 ||\n plan.attributeKeys.size > 0 ||\n plan.valueByElement.size > 0 ||\n plan.root !== undefined ||\n plan.rootName !== undefined;\n\n// The render options with the root name resolved and any root prefix applied. The caller's `rootName` wins, then the plan's, then the schema's `identifier`\n// or `title` annotation, then `'root'`. A root namespace with a prefix qualifies the name unless the caller already wrote one.\nconst resolveRenderOptions = <S extends Schema.Constraint>(schema: S, plan: NamespacePlan, options: XmlCodecOptions): XmlRenderOptions => {\n const rootName =\n options.rootName ?? plan.rootName ?? SchemaAST.resolveIdentifier(schema.ast) ?? SchemaAST.resolveTitle(schema.ast) ?? DEFAULT_ROOT_NAME;\n const wireRootName = plan.root !== undefined && plan.root.prefix !== '' && !rootName.includes(':') ? `${plan.root.prefix}:${rootName}` : rootName;\n return { ...options, rootName: wireRootName };\n};\n\n/**\n * @description Derives the XML codec for a schema: a `Schema` whose `Encoded` is an XML document, so `Schema.encodeSync(codec)` writes text and\n * `Schema.decodeSync(codec)` reads it back. The derivation is Effect's `Schema.toCodecStringTree`; the text layer is this package's {@link renderXml}\n * and {@link parseXml}. Call it data-first, `toCodecXml(schema, options)`, or data-last, `toCodecXml(options)(schema)`, so it drops into `pipe` beside\n * the rest of the Effect combinators. The two forms are one function: the first argument decides the style, a schema is read as data-first and an\n * options object as data-last. `pipe(schema, toCodecXml)` carries no options and works because a schema on its own is data-first.\n *\n * @example\n * ```typescript\n * import { Schema, pipe } from 'effect';\n * import { toCodecXml } from '@endevops/effect-codec-xml';\n *\n * const Book = Schema.Struct({ '@id': Schema.String, title: Schema.String, pages: Schema.Number });\n * const codec = toCodecXml(Book, { rootName: 'book' });\n * const piped = pipe(Book, toCodecXml({ rootName: 'book' }));\n *\n * const value = { '@id': '1', title: 'Dune', pages: 412 };\n *\n * Schema.encodeSync(codec)(value);\n * // => '<book id=\"1\"><title>Dune</title><pages>412</pages></book>'\n *\n * Schema.decodeSync(codec)('<book id=\"1\"><title>Dune</title><pages>412</pages></book>');\n * // => { '@id': '1', title: 'Dune', pages: 412 }\n * ```;\n *\n * @param schema - The schema describing the value, in the data-first form.\n * @param options - Render and parse options. `rootName` defaults to the schema's `identifier` or `title` annotation, then to `'root'`. In the\n * data-last form this is the only argument, and the schema arrives from `pipe`.\n *\n * @returns The codec, with the source schema's `Type` and the same service requirements. In the data-last form, a function from the schema to the\n * codec.\n */\nexport const toCodecXml: {\n <S extends Schema.Constraint>(schema: S, options?: XmlCodecOptions): toCodecXml<S>;\n (options?: XmlCodecOptions): <S extends Schema.Constraint>(schema: S) => toCodecXml<S>;\n} = Function.dual(\n args => Schema.isSchema(args[0]),\n <S extends Schema.Constraint>(schema: S, options: XmlCodecOptions = {}): toCodecXml<S> => {\n const planned = namespacePlan(schema);\n if (Predicate.hasProperty(planned, 'error')) {\n throw new Error(`Invalid XML namespace annotation:\\n\\t- ${planned.error}.`);\n }\n const plan = planned.plan;\n const active = isActivePlan(plan);\n const renderOptions = resolveRenderOptions(schema, plan, options);\n const stringTree = Schema.toCodecStringTree(schema);\n\n return Schema.String.pipe(\n Schema.decodeTo(\n stringTree,\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 Effect.flatMap(value =>\n Effect.fromResult(normalizePlainValue(value, stringTree.ast, '')).pipe(\n Effect.mapError(message => new SchemaIssue.InvalidValue({ message }, value, parseOptions))\n )\n )\n );\n },\n\n encode: (value, parseOptions) => {\n if (active && Array.isArray(value) && (plan.root !== undefined || plan.rootName !== undefined)) {\n return Effect.fail(\n new SchemaIssue.InvalidValue(\n { message: 'An array at the root of a namespaced schema cannot carry the root namespace or name.' },\n value,\n parseOptions\n )\n );\n }\n\n const wire = active ? encodeNames(value as XmlValue, plan, plan.root, {}, ROOT_ELEMENT) : (value as XmlValue);\n return renderXml(wire, renderOptions).pipe(\n Effect.mapError(error => new SchemaIssue.InvalidValue({ message: error.message }, value, parseOptions))\n );\n },\n })\n )\n );\n }\n);\n"],"mappings":";;;;;;;AAuDA,MAAM,gBAAgB,SACpB,KAAK,MAAM,OAAO,KAClB,KAAK,UAAU,OAAO,KACtB,KAAK,cAAc,OAAO,KAC1B,KAAK,eAAe,OAAO,KAC3B,KAAK,SAAS,KAAA,KACd,KAAK,aAAa,KAAA;AAIpB,MAAM,wBAAqD,QAAW,MAAqB,YAA+C;CACxI,MAAM,WACJ,QAAQ,YAAY,KAAK,YAAY,UAAU,kBAAkB,OAAO,GAAG,KAAK,UAAU,aAAa,OAAO,GAAG,KAAA;CACnH,MAAM,eAAe,KAAK,SAAS,KAAA,KAAa,KAAK,KAAK,WAAW,MAAM,CAAC,SAAS,SAAS,GAAG,IAAI,GAAG,KAAK,KAAK,OAAO,GAAG,aAAa;CACzI,OAAO;EAAE,GAAG;EAAS,UAAU;CAAa;AAC9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,MAAa,aAGT,SAAS,MACX,SAAQ,OAAO,SAAS,KAAK,EAAE,IACD,QAAW,UAA2B,CAAC,MAAqB;CACxF,MAAM,UAAU,cAAc,MAAM;CACpC,IAAI,UAAU,YAAY,SAAS,OAAO,GACxC,MAAM,IAAI,MAAM,0CAA0C,QAAQ,MAAM,EAAE;CAE5E,MAAM,OAAO,QAAQ;CACrB,MAAM,SAAS,aAAa,IAAI;CAChC,MAAM,gBAAgB,qBAAqB,QAAQ,MAAM,OAAO;CAChE,MAAM,aAAa,OAAO,kBAAkB,MAAM;CAElD,OAAO,OAAO,OAAO,KACnB,OAAO,SACL,YACA,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,GACrG,OAAO,SAAQ,UACb,OAAO,WAAW,oBAAoB,OAAO,WAAW,KAAK,EAAE,CAAC,CAAC,CAAC,KAChE,OAAO,UAAS,YAAW,IAAI,YAAY,aAAa,EAAE,QAAQ,GAAG,OAAO,YAAY,CAAC,CAC3F,CACF,CACF;EACF;EAEA,SAAS,OAAO,iBAAiB;GAC/B,IAAI,UAAU,MAAM,QAAQ,KAAK,MAAM,KAAK,SAAS,KAAA,KAAa,KAAK,aAAa,KAAA,IAClF,OAAO,OAAO,KACZ,IAAI,YAAY,aACd,EAAE,SAAS,uFAAuF,GAClG,OACA,YACF,CACF;GAGF,MAAM,OAAO,SAAS,YAAY,OAAmB,MAAM,KAAK,MAAM,CAAC,GAAA,EAAe,IAAK;GAC3F,OAAO,UAAU,MAAM,aAAa,CAAC,CAAC,KACpC,OAAO,UAAS,UAAS,IAAI,YAAY,aAAa,EAAE,SAAS,MAAM,QAAQ,GAAG,OAAO,YAAY,CAAC,CACxG;EACF;CACF,CAAC,CACH,CACF;AACF,CACF"}
@@ -623,29 +623,38 @@ const childScopeOf = (child, scope) => {
623
623
  return Predicate.isObject(child) ? scopeOf(child, scope) : scope;
624
624
  };
625
625
  /**
626
- * @description Rewrites a wire value tree back to the schema's local names, resolving every name against the declarations the document carries and dropping those
627
- * declarations. Character data maps to the value field of the element it belongs to. A record left holding only character data collapses back to that
628
- * string, which is how a namespaced leaf stays a `Schema.String`.
626
+ * @description Character data read back as a value tree. An element the schema reads as a struct with a value field derives that field from the element's
627
+ * character data, and the parser reduces an element with no attributes and no children to a bare string, so the string is put back under the value's
628
+ * key for that struct to read.
629
629
  *
630
- * @param value - The parsed wire tree.
630
+ * @param value - The character data the parser produced.
631
+ * @param plan - The namespace plan.
632
+ * @param elementPath - The path of the element the character data belongs to.
633
+ *
634
+ * @returns The character data, under the element's value key when it has one.
635
+ */
636
+ const decodeText = (value, plan, elementPath) => {
637
+ const valueKey = plan.valueByElement.get(elementPath);
638
+ return valueKey !== void 0 ? { [valueKey]: value } : value;
639
+ };
640
+ /**
641
+ * @description One record read back: its declarations dropped, its keys resolved to the schema's names, and its character data placed under the value field.
642
+ *
643
+ * @param record - The record to read.
631
644
  * @param plan - The namespace plan.
632
645
  * @param scope - The prefix bindings in scope above this element.
633
646
  * @param elementPath - The path of this element, or the root sentinel.
634
647
  *
635
- * @returns The value tree, keyed by the schema's names.
648
+ * @returns The record, keyed by the schema's names, or the string it collapses to.
636
649
  */
637
- const decodeNames = (value, plan, scope, elementPath) => {
638
- if (Array.isArray(value)) return value.map((member) => decodeNames(member, plan, scope, elementPath));
639
- if (Predicate.isString(value) || Predicate.isUndefined(value)) return value;
640
- if (!Predicate.isObject(value)) return value;
641
- const record = value;
650
+ const decodeRecord = (record, plan, scope, elementPath) => {
642
651
  const inner = scopeOf(record, scope);
643
652
  const valueKey = plan.valueByElement.get(elementPath);
644
653
  const out = {};
645
654
  for (const [key, child] of Object.entries(record)) {
646
655
  if (isDeclarationKey(key)) continue;
647
656
  if (key === "#text") {
648
- out[valueKey ?? "#text"] = decodeNames(child, plan, inner, elementPath);
657
+ out[valueKey ?? "#text"] = child;
649
658
  continue;
650
659
  }
651
660
  const childKey = schemaKey(plan, elementPath, key, childScopeOf(child, inner));
@@ -657,6 +666,24 @@ const decodeNames = (value, plan, scope, elementPath) => {
657
666
  if (keys.length === 1 && keys[0] === "#text") return out[TEXT_KEY];
658
667
  return out;
659
668
  };
669
+ /**
670
+ * @description Rewrites a wire value tree back to the schema's local names, resolving every name against the declarations the document carries and dropping those
671
+ * declarations. Character data maps to the value field of the element it belongs to. A record left holding only character data collapses back to that
672
+ * string, which is how a namespaced leaf stays a `Schema.String`.
673
+ *
674
+ * @param value - The parsed wire tree.
675
+ * @param plan - The namespace plan.
676
+ * @param scope - The prefix bindings in scope above this element.
677
+ * @param elementPath - The path of this element, or the root sentinel.
678
+ *
679
+ * @returns The value tree, keyed by the schema's names.
680
+ */
681
+ const decodeNames = (value, plan, scope, elementPath) => {
682
+ if (Array.isArray(value)) return value.map((member) => decodeNames(member, plan, scope, elementPath));
683
+ if (Predicate.isString(value)) return decodeText(value, plan, elementPath);
684
+ if (!Predicate.isObject(value)) return value;
685
+ return decodeRecord(value, plan, scope, elementPath);
686
+ };
660
687
  //#endregion
661
688
  export { ATTRIBUTE_KEY, NAMESPACE_KEY, NAME_KEY, PREFIX_KEY, VALUE_KEY, decodeNames, encodeNames, namespacePlan };
662
689
 
@@ -1 +1 @@
1
- {"version":3,"file":"namespaces.js","names":[],"sources":["../src/namespaces.ts"],"sourcesContent":["// Namespaces: attributing an element to a namespace URI, and resolving that at\n// the text boundary.\n//\n// A schema describes a value in local names. This module lets a schema node say\n// which namespace its element belongs to, through Effect's own annotations:\n//\n// - `xmlNamespace` is the namespace URI of the element. A field that carries\n// one has its element placed in that namespace; the field's name stays the\n// local name, so the schema does not hard-code a wire prefix.\n// - `xmlPrefix` is the wire prefix to write for that URI. When it is omitted\n// the namespace is written as the default namespace (`xmlns=\"…\"`), and an\n// unprefixed element name is used.\n// - `xmlName` is the wire local name to write when it differs from the schema\n// field's own name. It applies to an element or an attribute, and a colon\n// is not allowed because the prefix comes from `xmlPrefix`.\n// - `xmlAttribute` marks a field as an attribute without the schema key\n// carrying the `@` prefix. A namespaced attribute still needs `xmlPrefix`,\n// because a default namespace does not apply to attributes.\n// - `xmlValue` marks one field as the element's character data, the `#text`\n// value, for an element that also carries attributes or children.\n//\n// An element's namespace is inherited by its descendants, as an XML default\n// namespace is. An attribute never inherits: it is in a namespace only when it\n// is annotated with one explicitly, because a default namespace does not apply\n// to attributes.\n//\n// On encode, each element writes its own declaration when the prefix or default\n// is not already in scope. On decode, the parser's own declarations are read\n// into scope and every name is resolved to its URI, so a document that binds the\n// same URI to a different prefix still decodes to the same value. The\n// declaration attributes are dropped from the decoded value; the codec manages\n// them, not the schema.\n//\n// The plan is built per local name, and a schema field is one local name. One\n// local name cannot belong to two namespaces in one codec; the plan reports that\n// when the codec is built rather than guessing.\n\nimport type { Schema } from 'effect';\n\nimport { Predicate, SchemaAST } from 'effect';\n\nimport type { XmlRecord, XmlValue } from './xml-value.ts';\n\nimport { ATTRIBUTE_PREFIX, isAttributeKey, TEXT_KEY } from './conventions.ts';\n\ndeclare module 'effect/Schema' {\n namespace Annotations {\n interface XmlAnnotations {\n /**\n * @description The namespace URI this schema's element belongs to. Read as the local name on the wire, with `xmlPrefix` choosing the prefix.\n */\n readonly xmlNamespace?: string | undefined;\n\n /**\n * @description The wire prefix to write for {@link xmlNamespace}. Omit it to write the namespace as the default (`xmlns=\"…\"`) with unprefixed element names.\n */\n readonly xmlPrefix?: string | undefined;\n\n /**\n * @description The local name to write for an element or attribute, when it differs from the schema key. A colon is not allowed here; the prefix comes from\n * {@link xmlPrefix}.\n */\n readonly xmlName?: string | undefined;\n\n /**\n * @description Whether this field is an XML attribute rather than a child element. Use it to keep the schema key a plain name instead of carrying the `@`\n * prefix. A namespaced attribute still needs `xmlPrefix`, because a default namespace does not apply to attributes.\n */\n readonly xmlAttribute?: boolean | undefined;\n\n /**\n * @description Whether this field holds the element's character data, the `#text` value, rather than a child element. It has no name, so it cannot be\n * combined with `xmlAttribute`, `xmlName`, or a namespace.\n */\n readonly xmlValue?: boolean | undefined;\n }\n\n interface Annotations extends XmlAnnotations {}\n }\n}\n\n/**\n * @description The annotation key holding an element's namespace URI.\n */\nexport const NAMESPACE_KEY = 'xmlNamespace';\n\n/**\n * @description The annotation key holding the wire prefix for an element's namespace.\n */\nexport const PREFIX_KEY = 'xmlPrefix';\n\n/**\n * @description The annotation key holding the wire local name for an element or attribute.\n */\nexport const NAME_KEY = 'xmlName';\n\n/**\n * @description The annotation key marking a field as an XML attribute.\n */\nexport const ATTRIBUTE_KEY = 'xmlAttribute';\n\n/**\n * @description The annotation key marking a field as the element's character data.\n */\nexport const VALUE_KEY = 'xmlValue';\n\n/**\n * @description An element's namespace: the URI, and the prefix to write it with. An empty prefix is the default namespace.\n */\nexport interface XmlNamespace {\n readonly uri: string;\n readonly prefix: string;\n}\n\n/**\n * @description What one codec needs to place and resolve names: the namespace of every local name in the schema, the root element's namespace, and the reverse\n * lookup from a resolved `(uri, local)` back to the schema's key.\n */\nexport interface NamespacePlan {\n /**\n * @description The namespace of each field, keyed by the field's element path, so the same name nested differently stays apart.\n */\n readonly byKey: ReadonlyMap<string, XmlNamespace>;\n\n /**\n * @description The wire local name of each field that overrides the schema's own name with `xmlName`, keyed by the field's element path.\n */\n readonly nameByKey: ReadonlyMap<string, string>;\n\n /**\n * @description The paths of the fields that `xmlAttribute` marks as attributes but whose names do not carry the `@` prefix.\n */\n readonly attributeKeys: ReadonlySet<string>;\n\n /**\n * @description The value field of each element, keyed by the element's path. The root's path is {@link ROOT_ELEMENT}.\n */\n readonly valueByElement: ReadonlyMap<string, string>;\n\n /**\n * @description The paths of array fields that name their element with `xmlName`, so a single occurrence decodes as a one-member array.\n */\n readonly arrayKeys: ReadonlySet<string>;\n\n /**\n * @description The root element's namespace, or `undefined` when the root is unannotated.\n */\n readonly root: XmlNamespace | undefined;\n\n /**\n * @description The root element's local name from its `xmlName` annotation, or `undefined`.\n */\n readonly rootName: string | undefined;\n\n /**\n * @description The schema key for a resolved name, keyed by kind, `uri`, and local name. Lets a document with any prefix, and an element beside an attribute of\n * the same name, resolve back to the schema.\n */\n readonly byResolved: ReadonlyMap<string, string>;\n}\n\n/**\n * @description The namespace an AST's own annotations declare, or `undefined`. `Schema.optional` and `Schema.suspend` wrap a node without moving its annotation,\n * so both are unwrapped to find the namespace the field actually carries.\n *\n * @param ast - The AST to read.\n *\n * @returns The namespace, or `undefined`.\n */\nconst annotationAt = (ast: SchemaAST.AST, key: string): unknown => {\n const seen = new Set<SchemaAST.AST>();\n const find = (node: SchemaAST.AST): unknown => {\n if (seen.has(node)) return undefined;\n seen.add(node);\n const value = SchemaAST.resolve(node)?.[key];\n if (value !== undefined) return value;\n if (node._tag === 'Union') {\n for (const member of node.types) {\n const found = find(member);\n if (found !== undefined) return found;\n }\n return undefined;\n }\n return node._tag === 'Suspend' ? find(node.thunk()) : undefined;\n };\n return find(ast);\n};\n\n/**\n * @description The namespace an AST's own annotations declare, or `undefined`. `Schema.optional` and `Schema.suspend` wrap a node without moving its annotation,\n * so both are unwrapped to find the namespace the field carries.\n *\n * @param ast - The AST to read.\n *\n * @returns The namespace, or `undefined`.\n */\nconst namespaceOf = (ast: SchemaAST.AST): XmlNamespace | undefined => {\n const uri = annotationAt(ast, NAMESPACE_KEY);\n if (!Predicate.isString(uri)) return undefined;\n const prefix = annotationAt(ast, PREFIX_KEY);\n return { uri, prefix: Predicate.isString(prefix) ? prefix : '' };\n};\n\n/**\n * @description The wire local name an AST's own annotations declare, or `undefined`.\n *\n * @param ast - The AST to read.\n *\n * @returns The local name, or `undefined`.\n */\nconst nameOf = (ast: SchemaAST.AST): string | undefined => {\n const name = annotationAt(ast, NAME_KEY);\n return Predicate.isString(name) ? name : undefined;\n};\n\n/**\n * @description The namespace a property carries when its annotation was attached with `Schema.annotateKey` rather than to the field's schema.\n *\n * @param ast - The property's value AST, whose context holds the key annotations.\n *\n * @returns The namespace, or `undefined`.\n */\nconst keyNamespaceOf = (ast: SchemaAST.AST): XmlNamespace | undefined => {\n const annotations = ast.context?.annotations;\n const uri = annotations?.[NAMESPACE_KEY];\n if (!Predicate.isString(uri)) return undefined;\n const prefix = annotations?.[PREFIX_KEY];\n return { uri, prefix: Predicate.isString(prefix) ? prefix : '' };\n};\n\n/**\n * @description The wire local name a property's key annotations declare, or `undefined`.\n *\n * @param ast - The property's value AST, whose context holds the key annotations.\n *\n * @returns The local name, or `undefined`.\n */\nconst keyNameOf = (ast: SchemaAST.AST): string | undefined => {\n const name = ast.context?.annotations?.[NAME_KEY];\n return Predicate.isString(name) ? name : undefined;\n};\n\n/**\n * @description Whether an AST's own annotations mark the field as an XML attribute.\n *\n * @param ast - The AST to read.\n *\n * @returns Whether the annotation is set.\n */\nconst attributeOf = (ast: SchemaAST.AST): boolean => annotationAt(ast, ATTRIBUTE_KEY) === true;\n\n/**\n * @description Whether a property's key annotations mark the field as an XML attribute.\n *\n * @param ast - The property's value AST, whose context holds the key annotations.\n *\n * @returns Whether the annotation is set.\n */\nconst keyAttributeOf = (ast: SchemaAST.AST): boolean => ast.context?.annotations?.[ATTRIBUTE_KEY] === true;\n\n/**\n * @description Whether an AST's own annotations mark the field as the element's character data.\n *\n * @param ast - The AST to read.\n *\n * @returns Whether the annotation is set.\n */\nconst valueOf = (ast: SchemaAST.AST): boolean => annotationAt(ast, VALUE_KEY) === true;\n\n/**\n * @description Whether a property's key annotations mark the field as the element's character data.\n *\n * @param ast - The property's value AST, whose context holds the key annotations.\n *\n * @returns Whether the annotation is set.\n */\nconst keyValueOf = (ast: SchemaAST.AST): boolean => ast.context?.annotations?.[VALUE_KEY] === true;\n\n/**\n * @description Whether a struct property holds the element's character data, marked with `xmlValue` on its schema or its key.\n *\n * @param ast - The property's value AST.\n *\n * @returns Whether the property is the value.\n */\nconst isValueProperty = (ast: SchemaAST.AST): boolean => keyValueOf(ast) || valueOf(ast);\n\n/**\n * @description The local name a schema key names, with the attribute prefix removed. This is what a declaration resolves to.\n *\n * @param key - The schema key.\n *\n * @returns The local name.\n */\nconst localName = (key: string): string => (isAttributeKey(key) ? key.slice(ATTRIBUTE_PREFIX.length) : key);\n\n/**\n * @description The path of the root element. A plan is keyed by the path of the element a field belongs to, so the same name nested differently stays apart. The\n * path is the schema keys from the root joined by {@link PATH_SEPARATOR}.\n */\nexport const ROOT_ELEMENT = '';\n\n/**\n * @description The separator between the schema keys in an element path. A NUL is not a legal XML name character, so it cannot collide with a key.\n */\nconst PATH_SEPARATOR = '\\u0000';\n\n/**\n * @description The path of a child element, appended to its parent's path.\n *\n * @param parent - The parent element's path.\n * @param key - The child's schema key.\n *\n * @returns The child's path.\n */\nconst childPath = (parent: string, key: string): string => (parent === ROOT_ELEMENT ? key : `${parent}${PATH_SEPARATOR}${key}`);\n\n/**\n * @description The schema key at the end of an element path.\n *\n * @param path - The element path.\n *\n * @returns The schema key of the element itself.\n */\nconst pathKey = (path: string): string => {\n const at = path.lastIndexOf(PATH_SEPARATOR);\n return at === -1 ? path : path.slice(at + 1);\n};\n\n/**\n * @description Whether a field is an attribute: the key at the end of its path carries the `@` prefix, or `xmlAttribute` marks the field.\n *\n * @param plan - The namespace plan.\n * @param path - The field's element path.\n *\n * @returns Whether the field is an attribute.\n */\nconst isAttributeOf = (plan: NamespacePlan, path: string): boolean => isAttributeKey(pathKey(path)) || plan.attributeKeys.has(path);\n\n/**\n * @description The wire local name of a field: its `xmlName` override, or the key at the end of its path with the attribute prefix removed.\n *\n * @param plan - The namespace plan.\n * @param path - The field's element path.\n *\n * @returns The local name.\n */\nconst localOf = (plan: NamespacePlan, path: string): string => plan.nameByKey.get(path) ?? localName(pathKey(path));\n\n/**\n * @description The reverse-lookup key for a resolved wire name. The attribute flag is part of it, so an element and an attribute of the same local name in the\n * same namespace stay distinct.\n *\n * @param isAttribute - Whether the name is an attribute.\n * @param uri - The resolved namespace URI, or `undefined`.\n * @param local - The resolved local name.\n *\n * @returns The lookup key.\n */\nconst resolvedKey = (isAttribute: boolean, uri: string | undefined, local: string): string =>\n `${isAttribute ? ATTRIBUTE_PREFIX : ''}${uri ?? ''}|${local}`;\n\n/**\n * @description The path of the parent element, one segment shorter than the field's own path.\n *\n * @param path - The field's element path.\n *\n * @returns The parent element's path, or the root sentinel.\n */\nconst parentPath = (path: string): string => {\n const at = path.lastIndexOf(PATH_SEPARATOR);\n return at === -1 ? ROOT_ELEMENT : path.slice(0, at);\n};\n\n/**\n * @description The reverse-lookup key for a wire name under one parent element. The parent's path is part of it, so the same wire name under two parents resolves\n * to each parent's own field.\n *\n * @param parent - The parent element's path.\n * @param resolved - A name from {@link resolvedKey}.\n *\n * @returns The lookup key.\n */\nconst lookupKey = (parent: string, resolved: string): string => `${parent}\\u0001${resolved}`;\n\n/**\n * @description The mutable state one namespace scan carries: the plan under construction, the local names that resolve to more than one namespace, and the AST\n * nodes already visited so a recursive schema terminates.\n */\ninterface Scan {\n readonly byKey: Map<string, XmlNamespace>;\n readonly nameByKey: Map<string, string>;\n readonly attributeKeys: Set<string>;\n readonly valueByElement: Map<string, string>;\n readonly arrayKeys: Set<string>;\n readonly problems: Array<string>;\n /**\n * @description The AST nodes on the current scan path. A recursive schema terminates because the `Suspend` node is still on the path when its thunk is reached,\n * and a schema reused under two sibling paths is scanned once per path because each node is removed again on the way back out.\n */\n readonly seen: Set<SchemaAST.AST>;\n}\n\n/**\n * @description Records a field's namespace, reporting a local name that would belong to two namespaces at once.\n *\n * @param scan - The scan state.\n * @param path - The field's element path.\n * @param namespace - The namespace, or `undefined` when the field has none.\n */\nconst record = (scan: Scan, path: string, namespace: XmlNamespace | undefined): void => {\n if (Predicate.isUndefined(namespace)) {\n return;\n }\n\n const previous = scan.byKey.get(path);\n if (Predicate.isNotUndefined(previous) && (previous.uri !== namespace.uri || previous.prefix !== namespace.prefix)) {\n scan.problems.push(\n `the local name \"${pathKey(path)}\" belongs to more than one namespace (${previous.uri}:${previous.prefix} vs ${namespace.uri}:${namespace.prefix})`\n );\n } else {\n scan.byKey.set(path, namespace);\n }\n};\n\n/**\n * @description Records a field's `xmlName` override, refusing one that carries a prefix because the prefix comes from `xmlPrefix`.\n *\n * @param scan - The scan state.\n * @param path - The field's element path.\n * @param name - The annotated local name, or `undefined`.\n */\nconst recordName = (scan: Scan, path: string, name: string | undefined): void => {\n if (Predicate.isUndefined(name)) return;\n\n if (name.includes(':')) {\n scan.problems.push(`xmlName \"${name}\" on \"${pathKey(path)}\" must be a local name; use xmlPrefix for the prefix`);\n } else {\n scan.nameByKey.set(path, name);\n }\n};\n\n/**\n * @description Records a field's namespace, refusing the two annotations that cannot be honored: a namespaced field whose name already carries a prefix, and a\n * namespaced attribute without a prefix, because a default namespace does not apply to attributes.\n *\n * @param scan - The scan state.\n * @param path - The field's element path.\n * @param isAttribute - Whether the field is an attribute.\n * @param field - The field's own namespace, or `undefined`.\n * @param inherited - The namespace the enclosing element passes down.\n */\nconst recordFieldNamespace = (\n scan: Scan,\n path: string,\n isAttribute: boolean,\n field: XmlNamespace | undefined,\n inherited: XmlNamespace | undefined\n): void => {\n const key = pathKey(path);\n if (Predicate.isNotUndefined(field) && key.includes(':')) {\n scan.problems.push(`\"${key}\" already carries a prefix, so it cannot also carry a namespace annotation`);\n } else if (isAttribute && Predicate.isNotUndefined(field) && field.prefix === '') {\n scan.problems.push(`the attribute \"${key}\" needs xmlPrefix, because a default namespace does not apply to attributes`);\n } else {\n record(scan, path, isAttribute ? field : (field ?? inherited));\n }\n};\n\n/**\n * @description Whether a struct property is an attribute: its key carries the `@` prefix, or its own or key annotation marks it.\n *\n * @param key - The schema key.\n * @param ast - The property's value AST.\n *\n * @returns Whether the property is an attribute.\n */\nconst isAttributeProperty = (key: string, ast: SchemaAST.AST): boolean => isAttributeKey(key) || keyAttributeOf(ast) || attributeOf(ast);\n\n/**\n * @description Notes a field that `xmlAttribute` marks as an attribute but whose name does not carry the `@` prefix.\n *\n * @param scan - The scan state.\n * @param path - The field's element path.\n * @param isAttribute - Whether the field is an attribute.\n */\nconst noteAttribute = (scan: Scan, path: string, isAttribute: boolean): void => {\n if (isAttribute && !isAttributeKey(pathKey(path))) {\n scan.attributeKeys.add(path);\n }\n};\n\n/**\n * @description Records a field marked `xmlValue` against the element that owns it, refusing the annotations a value cannot combine with: character data has no\n * name and no namespace, and an element has room for only one value.\n *\n * @param scan - The scan state.\n * @param elementPath - The path of the element the field belongs to, or the root sentinel.\n * @param valueKey - The schema key of the value field.\n * @param isAttribute - Whether the field is an attribute.\n * @param name - The field's `xmlName` override, or `undefined`.\n * @param field - The field's own namespace, or `undefined`.\n */\nconst recordValue = (\n scan: Scan,\n elementPath: string,\n valueKey: string,\n isAttribute: boolean,\n name: string | undefined,\n field: XmlNamespace | undefined\n): void => {\n if (isAttribute) scan.problems.push(`the field \"${valueKey}\" is marked as both an attribute and the element's value`);\n else if (Predicate.isNotUndefined(name))\n scan.problems.push(`the value field \"${valueKey}\" cannot have an xmlName, because character data has no name`);\n else if (Predicate.isNotUndefined(field))\n scan.problems.push(`the value field \"${valueKey}\" cannot have a namespace, because character data has none`);\n else {\n const previous = scan.valueByElement.get(elementPath);\n if (Predicate.isNotUndefined(previous) && previous !== valueKey) {\n scan.problems.push(\n `the element \"${elementPath === ROOT_ELEMENT ? 'root' : elementPath}\" has more than one value field, \"${previous}\" and \"${valueKey}\"`\n );\n } else {\n scan.valueByElement.set(elementPath, valueKey);\n }\n }\n};\n\n/**\n * @description Records one struct property: its `xmlName`, its namespace, and the namespace its descendants inherit. An attribute carries a namespace only when\n * annotated; an element field falls back to the namespace it inherits. A value field is character data, so it is recorded against its element\n * instead.\n *\n * @param scan - The scan state.\n * @param property - The property signature.\n * @param inherited - The namespace the enclosing element passes down.\n * @param elementPath - The path of the element the property belongs to, or the root sentinel.\n */\nconst scanProperty = (scan: Scan, property: SchemaAST.PropertySignature, inherited: XmlNamespace | undefined, elementPath: string): void => {\n const key = Predicate.isString(property.name) ? property.name : String(property.name);\n const path = childPath(elementPath, key);\n const isAttribute = isAttributeProperty(key, property.type);\n const isValue = isValueProperty(property.type);\n const name = keyNameOf(property.type) ?? nameOf(property.type);\n const field = keyNamespaceOf(property.type) ?? namespaceOf(property.type);\n if (isValue) recordValue(scan, elementPath, key, isAttribute, name, field);\n else {\n noteAttribute(scan, path, isAttribute);\n recordName(scan, path, name);\n recordFieldNamespace(scan, path, isAttribute, field, inherited);\n }\n scanNode(scan, property.type, isAttribute ? inherited : (field ?? inherited), path);\n};\n\n/**\n * @description Walks a list of child ASTs under one inherited namespace.\n *\n * @param scan - The scan state.\n * @param nodes - The child ASTs.\n * @param inherited - The namespace they inherit.\n * @param elementPath - The path of the element they belong to, or the root sentinel.\n */\nconst scanAll = (scan: Scan, nodes: ReadonlyArray<SchemaAST.AST>, inherited: XmlNamespace | undefined, elementPath: string): void => {\n for (const node of nodes) scanNode(scan, node, inherited, elementPath);\n};\n\n/**\n * @description Records the names in one object node: its properties and its index signatures, each under the namespace the object passes down.\n *\n * @param scan - The scan state.\n * @param ast - The object AST.\n * @param namespace - The namespace the object passes to its members.\n * @param elementPath - The path of the element the object describes, or the root sentinel.\n */\nconst scanObject = (scan: Scan, ast: SchemaAST.Objects, namespace: XmlNamespace | undefined, elementPath: string): void => {\n for (const property of ast.propertySignatures) {\n scanProperty(scan, property, namespace, elementPath);\n }\n for (const index of ast.indexSignatures) {\n scanNode(scan, index.type, namespace, elementPath);\n }\n};\n\n/**\n * @description Notes an array field that names its element with `xmlName`, so the decoder reads a single occurrence as a one-member array. An unnamed array keeps\n * the ambiguity and does not wrap.\n *\n * @param scan - The scan state.\n * @param ast - The AST being scanned.\n * @param elementPath - The path of the field the AST describes.\n */\nconst noteArray = (scan: Scan, ast: SchemaAST.AST, elementPath: string): void => {\n if (ast._tag === 'Arrays' && scan.nameByKey.has(elementPath)) {\n scan.arrayKeys.add(elementPath);\n }\n};\n\n/**\n * @description Records every name in one schema AST, carrying the namespace an element passes to its descendants and the element the names belong to.\n *\n * @param scan - The scan state.\n * @param ast - The AST to walk.\n * @param inherited - The namespace the enclosing element passes down.\n * @param elementPath - The path of the element this AST describes, or the root sentinel.\n */\nconst scanNode = (scan: Scan, ast: SchemaAST.AST, inherited: XmlNamespace | undefined, elementPath: string): void => {\n if (scan.seen.has(ast)) return;\n scan.seen.add(ast);\n try {\n noteArray(scan, ast, elementPath);\n const namespace = namespaceOf(ast) ?? inherited;\n switch (ast._tag) {\n case 'Objects':\n scanObject(scan, ast, namespace, elementPath);\n return;\n case 'Arrays':\n scanAll(scan, [...ast.elements, ...ast.rest], namespace, elementPath);\n return;\n case 'Union':\n scanAll(scan, ast.types, namespace, elementPath);\n return;\n case 'Suspend':\n scanNode(scan, ast.thunk(), namespace, elementPath);\n return;\n case 'Declaration':\n scanAll(scan, ast.typeParameters, namespace, elementPath);\n return;\n default:\n return;\n }\n } finally {\n scan.seen.delete(ast);\n }\n};\n\n/**\n * @description Collects the namespace and name of every field in a schema. Descendant elements inherit a namespace, as they inherit a default namespace, and an\n * element field records its own namespace, so encode and decode can find it by the local name alone.\n *\n * @param schema - The schema to walk.\n *\n * @returns The plan, or the annotations that cannot be honored.\n */\nexport const namespacePlan = (schema: Schema.Constraint): { readonly plan: NamespacePlan } | { readonly error: string } => {\n const scan: Scan = {\n byKey: new Map(),\n nameByKey: new Map(),\n attributeKeys: new Set(),\n valueByElement: new Map(),\n arrayKeys: new Set(),\n problems: [],\n seen: new Set(),\n };\n scanNode(scan, schema.ast, undefined, ROOT_ELEMENT);\n\n const byResolved = new Map<string, string>();\n for (const path of new Set([...scan.byKey.keys(), ...scan.nameByKey.keys(), ...scan.attributeKeys])) {\n const key = pathKey(path);\n const namespace = scan.byKey.get(path);\n const isAttribute = isAttributeKey(key) || scan.attributeKeys.has(path);\n const local = scan.nameByKey.get(path) ?? localName(key);\n const resolved = lookupKey(parentPath(path), resolvedKey(isAttribute, namespace?.uri, local));\n const previous = byResolved.get(resolved);\n\n if (previous !== undefined && previous !== key) {\n scan.problems.push(`\"${previous}\" and \"${key}\" both resolve to \"${local}\" under the same element`);\n } else {\n byResolved.set(resolved, key);\n }\n }\n\n if (scan.problems.length > 0) {\n return { error: [...new Set(scan.problems)].join(';\\n\\t- ') };\n }\n\n return {\n plan: {\n byKey: scan.byKey,\n nameByKey: scan.nameByKey,\n attributeKeys: scan.attributeKeys,\n valueByElement: scan.valueByElement,\n arrayKeys: scan.arrayKeys,\n root: namespaceOf(schema.ast),\n rootName: nameOf(schema.ast),\n byResolved,\n },\n };\n};\n\n/**\n * @description Whether a key is a namespace declaration the codec manages: `@xmlns` or `@xmlns:prefix`.\n *\n * @param key - The record key.\n *\n * @returns Whether the key is a declaration.\n */\nconst isDeclarationKey = (key: string): boolean => key === `${ATTRIBUTE_PREFIX}xmlns` || key.startsWith(`${ATTRIBUTE_PREFIX}xmlns:`);\n\n/**\n * @description The prefix a declaration key carries, or `''` for the default namespace.\n *\n * @param key - A key {@link isDeclarationKey} accepted.\n *\n * @returns The prefix.\n */\nconst declarationPrefix = (key: string): string => (key === `${ATTRIBUTE_PREFIX}xmlns` ? '' : key.slice(`${ATTRIBUTE_PREFIX}xmlns:`.length));\n\n/**\n * @description Writes the declaration for a namespace into an element's record, when the scope does not already bind it. A prefixed namespace is declared as\n * `xmlns:prefix`; the default namespace as `xmlns`; and an element that leaves a default namespace in scope for no namespace of its own clears it\n * with `xmlns=\"\"`.\n *\n * @param out - The element's record, written in place.\n * @param scope - The in-scope prefix bindings, updated in place.\n * @param namespace - The element's namespace, or `undefined`.\n */\nconst declare = (out: Record<string, XmlValue>, scope: Record<string, string | undefined>, namespace: XmlNamespace | undefined): void => {\n if (Predicate.isNotUndefined(namespace) && namespace.prefix !== '') {\n if (scope[namespace.prefix] === namespace.uri) return;\n out[`${ATTRIBUTE_PREFIX}xmlns:${namespace.prefix}`] = namespace.uri;\n scope[namespace.prefix] = namespace.uri;\n return;\n }\n\n if (!Predicate.isUndefined(namespace)) {\n if (scope[''] === namespace.uri) return;\n out[`${ATTRIBUTE_PREFIX}xmlns`] = namespace.uri;\n scope[''] = namespace.uri;\n return;\n }\n\n if (Predicate.isUndefined(scope[''])) return;\n\n out[`${ATTRIBUTE_PREFIX}xmlns`] = '';\n scope[''] = undefined;\n};\n\n/**\n * @description The wire key for one field: the local name with the prefix its namespace declares, or the local name unchanged when there is no prefix.\n *\n * @param plan - The namespace plan.\n * @param path - The field's element path.\n * @param namespace - The field's namespace, or `undefined`.\n *\n * @returns The key to write.\n */\nconst wireKey = (plan: NamespacePlan, path: string, namespace: XmlNamespace | undefined): string => {\n const isAttribute = isAttributeOf(plan, path);\n const local = localOf(plan, path);\n\n if (Predicate.isUndefined(namespace) || namespace.prefix === '') {\n return isAttribute ? `${ATTRIBUTE_PREFIX}${local}` : local;\n }\n\n return `${isAttribute ? ATTRIBUTE_PREFIX : ''}${namespace.prefix}:${local}`;\n};\n\n/**\n * @description Writes one record's fields under the element's scope: an attribute is a leaf and declares its prefix on this element, the value field becomes the\n * element's character data, and a child element recurses with its own namespace.\n *\n * @param value - The element's record, keyed by the schema's local names.\n * @param plan - The namespace plan.\n * @param out - The wire record, written in place.\n * @param scope - The prefix bindings in scope for this element.\n * @param elementPath - The path of this element, or the root sentinel.\n */\nconst encodeFields = (\n value: XmlRecord,\n plan: NamespacePlan,\n out: Record<string, XmlValue>,\n scope: Record<string, string | undefined>,\n elementPath: string\n): void => {\n const valueKey = plan.valueByElement.get(elementPath);\n for (const [key, child] of Object.entries(value)) {\n if (isDeclarationKey(key)) {\n continue;\n }\n\n if (key === valueKey) {\n out[TEXT_KEY] = child;\n continue;\n }\n\n const path = childPath(elementPath, key);\n const childNamespace = plan.byKey.get(path);\n if (isAttributeOf(plan, path)) {\n if (Predicate.isNotUndefined(childNamespace)) {\n declare(out, scope, childNamespace);\n }\n out[wireKey(plan, path, childNamespace)] = child;\n continue;\n }\n\n out[wireKey(plan, path, childNamespace)] = encodeNames(child, plan, childNamespace, scope, path);\n }\n};\n\n/**\n * @description Rewrites a value tree into its namespaced wire form: every field with a plan entry gets its prefix, and every element declares the namespace its\n * subtree uses. A leaf that has to carry a declaration is wrapped as a `#text` record so the attribute has somewhere to live.\n *\n * @param value - The value tree, keyed by the schema's local names.\n * @param plan - The namespace plan.\n * @param namespace - This element's namespace.\n * @param scope - The prefix bindings in scope above this element.\n * @param elementPath - The path of this element, or the root sentinel.\n *\n * @returns The wire tree.\n */\nexport const encodeNames = (\n value: XmlValue,\n plan: NamespacePlan,\n namespace: XmlNamespace | undefined,\n scope: Record<string, string | undefined>,\n elementPath: string\n): XmlValue => {\n if (Array.isArray(value)) {\n return value.map(member => encodeNames(member, plan, namespace, scope, elementPath));\n }\n\n const out: Record<string, XmlValue> = {};\n const inner = { ...scope };\n declare(out, inner, namespace);\n\n if (Predicate.isUndefined(value)) {\n return undefined;\n }\n if (Predicate.isString(value)) {\n return Object.keys(out).length > 0 ? { ...out, [TEXT_KEY]: value } : value;\n }\n // `Predicate.isObject` narrows to a generic index signature, so the value\n // tree's own record type is named here.\n if (!Predicate.isObject(value)) {\n return value;\n }\n\n encodeFields(value as XmlRecord, plan, out, inner, elementPath);\n return out;\n};\n\n/**\n * @description Resolves a wire name to its URI and local name against the in-scope bindings. An unprefixed element takes the default namespace; an unprefixed\n * attribute has no namespace, because a default namespace does not apply to attributes.\n *\n * @param name - The name as written, without the attribute prefix.\n * @param isAttribute - Whether the name belongs to an attribute.\n * @param scope - The in-scope prefix bindings.\n *\n * @returns The resolved URI (possibly `undefined`) and local name.\n */\nconst resolveName = (\n name: string,\n isAttribute: boolean,\n scope: Record<string, string | undefined>\n): { readonly uri: string | undefined; readonly local: string } => {\n const colon = name.indexOf(':');\n if (colon === -1) return { uri: isAttribute ? undefined : scope[''], local: name };\n return { uri: scope[name.slice(0, colon)], local: name.slice(colon + 1) };\n};\n\n/**\n * @description Reads an element's declarations into a fresh scope that falls back to the enclosing one.\n *\n * @param value - The element's record.\n * @param scope - The bindings in scope above the element.\n *\n * @returns The element's own scope.\n */\nconst scopeOf = (value: XmlRecord, scope: Record<string, string | undefined>): Record<string, string | undefined> => {\n const inner = { ...scope };\n\n for (const [key, declaration] of Object.entries(value)) {\n if (isDeclarationKey(key) && Predicate.isString(declaration)) {\n inner[declarationPrefix(key)] = declaration;\n }\n }\n\n return inner;\n};\n\n/**\n * @description The schema key a wire key resolves to under one parent element: the plan's key for its URI and local name, or the local name when the schema left\n * it unannotated.\n *\n * @param plan - The namespace plan.\n * @param parent - The parent element's path.\n * @param key - The wire key.\n * @param scope - The in-scope prefix bindings.\n *\n * @returns The schema key.\n */\nconst schemaKey = (plan: NamespacePlan, parent: string, key: string, scope: Record<string, string | undefined>): string => {\n const isAttribute = isAttributeKey(key);\n const { uri, local } = resolveName(localName(key), isAttribute, scope);\n return plan.byResolved.get(lookupKey(parent, resolvedKey(isAttribute, uri, local))) ?? (isAttribute ? `${ATTRIBUTE_PREFIX}${local}` : local);\n};\n\n/**\n * @description The scope a child element resolves its own name against: the declarations it carries on itself, layered over the parent scope. An element may\n * declare the prefix it uses on the element itself, so its own name is read with those bindings in scope. A repeated element arrives as an array, so\n * the first member stands in for the run; every member describes the same element and carries the same declaration.\n *\n * @param child - The child value.\n * @param scope - The bindings in scope above the child.\n *\n * @returns The scope to resolve the child's own name with.\n */\nconst childScopeOf = (child: XmlValue, scope: Record<string, string | undefined>): Record<string, string | undefined> => {\n if (Array.isArray(child)) {\n return child.length > 0 ? childScopeOf(child[0], scope) : scope;\n }\n return Predicate.isObject(child) ? scopeOf(child as XmlRecord, scope) : scope;\n};\n\n/**\n * @description Rewrites a wire value tree back to the schema's local names, resolving every name against the declarations the document carries and dropping those\n * declarations. Character data maps to the value field of the element it belongs to. A record left holding only character data collapses back to that\n * string, which is how a namespaced leaf stays a `Schema.String`.\n *\n * @param value - The parsed wire tree.\n * @param plan - The namespace plan.\n * @param scope - The prefix bindings in scope above this element.\n * @param elementPath - The path of this element, or the root sentinel.\n *\n * @returns The value tree, keyed by the schema's names.\n */\nexport const decodeNames = (value: XmlValue, plan: NamespacePlan, scope: Record<string, string | undefined>, elementPath: string): XmlValue => {\n if (Array.isArray(value)) {\n return value.map(member => decodeNames(member, plan, scope, elementPath));\n }\n if (Predicate.isString(value) || Predicate.isUndefined(value)) {\n return value;\n }\n if (!Predicate.isObject(value)) {\n return value;\n }\n\n // `Predicate.isObject` narrows to a generic index signature, so the value\n // tree's own record type is named here.\n const record = value as XmlRecord;\n const inner = scopeOf(record, scope);\n const valueKey = plan.valueByElement.get(elementPath);\n const out: Record<string, XmlValue> = {};\n\n for (const [key, child] of Object.entries(record)) {\n if (isDeclarationKey(key)) {\n continue;\n }\n\n if (key === TEXT_KEY) {\n out[valueKey ?? TEXT_KEY] = decodeNames(child, plan, inner, elementPath);\n continue;\n }\n\n const childKey = schemaKey(plan, elementPath, key, childScopeOf(child, inner));\n const path = childPath(elementPath, childKey);\n const decoded = decodeNames(child, plan, inner, path);\n out[childKey] = plan.arrayKeys.has(path) && !Array.isArray(decoded) ? [decoded] : decoded;\n }\n\n const keys = Object.keys(out);\n if (keys.length === 1 && keys[0] === TEXT_KEY) {\n return out[TEXT_KEY];\n }\n return out;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"namespaces.js","names":[],"sources":["../src/namespaces.ts"],"sourcesContent":["// Namespaces: attributing an element to a namespace URI, and resolving that at\n// the text boundary.\n//\n// A schema describes a value in local names. This module lets a schema node say\n// which namespace its element belongs to, through Effect's own annotations:\n//\n// - `xmlNamespace` is the namespace URI of the element. A field that carries\n// one has its element placed in that namespace; the field's name stays the\n// local name, so the schema does not hard-code a wire prefix.\n// - `xmlPrefix` is the wire prefix to write for that URI. When it is omitted\n// the namespace is written as the default namespace (`xmlns=\"…\"`), and an\n// unprefixed element name is used.\n// - `xmlName` is the wire local name to write when it differs from the schema\n// field's own name. It applies to an element or an attribute, and a colon\n// is not allowed because the prefix comes from `xmlPrefix`.\n// - `xmlAttribute` marks a field as an attribute without the schema key\n// carrying the `@` prefix. A namespaced attribute still needs `xmlPrefix`,\n// because a default namespace does not apply to attributes.\n// - `xmlValue` marks one field as the element's character data, the `#text`\n// value, for an element that also carries attributes or children.\n//\n// An element's namespace is inherited by its descendants, as an XML default\n// namespace is. An attribute never inherits: it is in a namespace only when it\n// is annotated with one explicitly, because a default namespace does not apply\n// to attributes.\n//\n// On encode, each element writes its own declaration when the prefix or default\n// is not already in scope. On decode, the parser's own declarations are read\n// into scope and every name is resolved to its URI, so a document that binds the\n// same URI to a different prefix still decodes to the same value. The\n// declaration attributes are dropped from the decoded value; the codec manages\n// them, not the schema.\n//\n// The plan is built per local name, and a schema field is one local name. One\n// local name cannot belong to two namespaces in one codec; the plan reports that\n// when the codec is built rather than guessing.\n\nimport type { Schema } from 'effect';\n\nimport { Predicate, SchemaAST } from 'effect';\n\nimport type { XmlRecord, XmlValue } from './xml-value.ts';\n\nimport { ATTRIBUTE_PREFIX, isAttributeKey, TEXT_KEY } from './conventions.ts';\n\ndeclare module 'effect/Schema' {\n namespace Annotations {\n interface XmlAnnotations {\n /**\n * @description The namespace URI this schema's element belongs to. Read as the local name on the wire, with `xmlPrefix` choosing the prefix.\n */\n readonly xmlNamespace?: string | undefined;\n\n /**\n * @description The wire prefix to write for {@link xmlNamespace}. Omit it to write the namespace as the default (`xmlns=\"…\"`) with unprefixed element names.\n */\n readonly xmlPrefix?: string | undefined;\n\n /**\n * @description The local name to write for an element or attribute, when it differs from the schema key. A colon is not allowed here; the prefix comes from\n * {@link xmlPrefix}.\n */\n readonly xmlName?: string | undefined;\n\n /**\n * @description Whether this field is an XML attribute rather than a child element. Use it to keep the schema key a plain name instead of carrying the `@`\n * prefix. A namespaced attribute still needs `xmlPrefix`, because a default namespace does not apply to attributes.\n */\n readonly xmlAttribute?: boolean | undefined;\n\n /**\n * @description Whether this field holds the element's character data, the `#text` value, rather than a child element. It has no name, so it cannot be\n * combined with `xmlAttribute`, `xmlName`, or a namespace.\n */\n readonly xmlValue?: boolean | undefined;\n }\n\n interface Annotations extends XmlAnnotations {}\n }\n}\n\n/**\n * @description The annotation key holding an element's namespace URI.\n */\nexport const NAMESPACE_KEY = 'xmlNamespace';\n\n/**\n * @description The annotation key holding the wire prefix for an element's namespace.\n */\nexport const PREFIX_KEY = 'xmlPrefix';\n\n/**\n * @description The annotation key holding the wire local name for an element or attribute.\n */\nexport const NAME_KEY = 'xmlName';\n\n/**\n * @description The annotation key marking a field as an XML attribute.\n */\nexport const ATTRIBUTE_KEY = 'xmlAttribute';\n\n/**\n * @description The annotation key marking a field as the element's character data.\n */\nexport const VALUE_KEY = 'xmlValue';\n\n/**\n * @description An element's namespace: the URI, and the prefix to write it with. An empty prefix is the default namespace.\n */\nexport interface XmlNamespace {\n readonly uri: string;\n readonly prefix: string;\n}\n\n/**\n * @description What one codec needs to place and resolve names: the namespace of every local name in the schema, the root element's namespace, and the reverse\n * lookup from a resolved `(uri, local)` back to the schema's key.\n */\nexport interface NamespacePlan {\n /**\n * @description The namespace of each field, keyed by the field's element path, so the same name nested differently stays apart.\n */\n readonly byKey: ReadonlyMap<string, XmlNamespace>;\n\n /**\n * @description The wire local name of each field that overrides the schema's own name with `xmlName`, keyed by the field's element path.\n */\n readonly nameByKey: ReadonlyMap<string, string>;\n\n /**\n * @description The paths of the fields that `xmlAttribute` marks as attributes but whose names do not carry the `@` prefix.\n */\n readonly attributeKeys: ReadonlySet<string>;\n\n /**\n * @description The value field of each element, keyed by the element's path. The root's path is {@link ROOT_ELEMENT}.\n */\n readonly valueByElement: ReadonlyMap<string, string>;\n\n /**\n * @description The paths of array fields that name their element with `xmlName`, so a single occurrence decodes as a one-member array.\n */\n readonly arrayKeys: ReadonlySet<string>;\n\n /**\n * @description The root element's namespace, or `undefined` when the root is unannotated.\n */\n readonly root: XmlNamespace | undefined;\n\n /**\n * @description The root element's local name from its `xmlName` annotation, or `undefined`.\n */\n readonly rootName: string | undefined;\n\n /**\n * @description The schema key for a resolved name, keyed by kind, `uri`, and local name. Lets a document with any prefix, and an element beside an attribute of\n * the same name, resolve back to the schema.\n */\n readonly byResolved: ReadonlyMap<string, string>;\n}\n\n/**\n * @description The namespace an AST's own annotations declare, or `undefined`. `Schema.optional` and `Schema.suspend` wrap a node without moving its annotation,\n * so both are unwrapped to find the namespace the field actually carries.\n *\n * @param ast - The AST to read.\n *\n * @returns The namespace, or `undefined`.\n */\nconst annotationAt = (ast: SchemaAST.AST, key: string): unknown => {\n const seen = new Set<SchemaAST.AST>();\n const find = (node: SchemaAST.AST): unknown => {\n if (seen.has(node)) return undefined;\n seen.add(node);\n const value = SchemaAST.resolve(node)?.[key];\n if (value !== undefined) return value;\n if (node._tag === 'Union') {\n for (const member of node.types) {\n const found = find(member);\n if (found !== undefined) return found;\n }\n return undefined;\n }\n return node._tag === 'Suspend' ? find(node.thunk()) : undefined;\n };\n return find(ast);\n};\n\n/**\n * @description The namespace an AST's own annotations declare, or `undefined`. `Schema.optional` and `Schema.suspend` wrap a node without moving its annotation,\n * so both are unwrapped to find the namespace the field carries.\n *\n * @param ast - The AST to read.\n *\n * @returns The namespace, or `undefined`.\n */\nconst namespaceOf = (ast: SchemaAST.AST): XmlNamespace | undefined => {\n const uri = annotationAt(ast, NAMESPACE_KEY);\n if (!Predicate.isString(uri)) return undefined;\n const prefix = annotationAt(ast, PREFIX_KEY);\n return { uri, prefix: Predicate.isString(prefix) ? prefix : '' };\n};\n\n/**\n * @description The wire local name an AST's own annotations declare, or `undefined`.\n *\n * @param ast - The AST to read.\n *\n * @returns The local name, or `undefined`.\n */\nconst nameOf = (ast: SchemaAST.AST): string | undefined => {\n const name = annotationAt(ast, NAME_KEY);\n return Predicate.isString(name) ? name : undefined;\n};\n\n/**\n * @description The namespace a property carries when its annotation was attached with `Schema.annotateKey` rather than to the field's schema.\n *\n * @param ast - The property's value AST, whose context holds the key annotations.\n *\n * @returns The namespace, or `undefined`.\n */\nconst keyNamespaceOf = (ast: SchemaAST.AST): XmlNamespace | undefined => {\n const annotations = ast.context?.annotations;\n const uri = annotations?.[NAMESPACE_KEY];\n if (!Predicate.isString(uri)) return undefined;\n const prefix = annotations?.[PREFIX_KEY];\n return { uri, prefix: Predicate.isString(prefix) ? prefix : '' };\n};\n\n/**\n * @description The wire local name a property's key annotations declare, or `undefined`.\n *\n * @param ast - The property's value AST, whose context holds the key annotations.\n *\n * @returns The local name, or `undefined`.\n */\nconst keyNameOf = (ast: SchemaAST.AST): string | undefined => {\n const name = ast.context?.annotations?.[NAME_KEY];\n return Predicate.isString(name) ? name : undefined;\n};\n\n/**\n * @description Whether an AST's own annotations mark the field as an XML attribute.\n *\n * @param ast - The AST to read.\n *\n * @returns Whether the annotation is set.\n */\nconst attributeOf = (ast: SchemaAST.AST): boolean => annotationAt(ast, ATTRIBUTE_KEY) === true;\n\n/**\n * @description Whether a property's key annotations mark the field as an XML attribute.\n *\n * @param ast - The property's value AST, whose context holds the key annotations.\n *\n * @returns Whether the annotation is set.\n */\nconst keyAttributeOf = (ast: SchemaAST.AST): boolean => ast.context?.annotations?.[ATTRIBUTE_KEY] === true;\n\n/**\n * @description Whether an AST's own annotations mark the field as the element's character data.\n *\n * @param ast - The AST to read.\n *\n * @returns Whether the annotation is set.\n */\nconst valueOf = (ast: SchemaAST.AST): boolean => annotationAt(ast, VALUE_KEY) === true;\n\n/**\n * @description Whether a property's key annotations mark the field as the element's character data.\n *\n * @param ast - The property's value AST, whose context holds the key annotations.\n *\n * @returns Whether the annotation is set.\n */\nconst keyValueOf = (ast: SchemaAST.AST): boolean => ast.context?.annotations?.[VALUE_KEY] === true;\n\n/**\n * @description Whether a struct property holds the element's character data, marked with `xmlValue` on its schema or its key.\n *\n * @param ast - The property's value AST.\n *\n * @returns Whether the property is the value.\n */\nconst isValueProperty = (ast: SchemaAST.AST): boolean => keyValueOf(ast) || valueOf(ast);\n\n/**\n * @description The local name a schema key names, with the attribute prefix removed. This is what a declaration resolves to.\n *\n * @param key - The schema key.\n *\n * @returns The local name.\n */\nconst localName = (key: string): string => (isAttributeKey(key) ? key.slice(ATTRIBUTE_PREFIX.length) : key);\n\n/**\n * @description The path of the root element. A plan is keyed by the path of the element a field belongs to, so the same name nested differently stays apart. The\n * path is the schema keys from the root joined by {@link PATH_SEPARATOR}.\n */\nexport const ROOT_ELEMENT = '';\n\n/**\n * @description The separator between the schema keys in an element path. A NUL is not a legal XML name character, so it cannot collide with a key.\n */\nconst PATH_SEPARATOR = '\\u0000';\n\n/**\n * @description The path of a child element, appended to its parent's path.\n *\n * @param parent - The parent element's path.\n * @param key - The child's schema key.\n *\n * @returns The child's path.\n */\nconst childPath = (parent: string, key: string): string => (parent === ROOT_ELEMENT ? key : `${parent}${PATH_SEPARATOR}${key}`);\n\n/**\n * @description The schema key at the end of an element path.\n *\n * @param path - The element path.\n *\n * @returns The schema key of the element itself.\n */\nconst pathKey = (path: string): string => {\n const at = path.lastIndexOf(PATH_SEPARATOR);\n return at === -1 ? path : path.slice(at + 1);\n};\n\n/**\n * @description Whether a field is an attribute: the key at the end of its path carries the `@` prefix, or `xmlAttribute` marks the field.\n *\n * @param plan - The namespace plan.\n * @param path - The field's element path.\n *\n * @returns Whether the field is an attribute.\n */\nconst isAttributeOf = (plan: NamespacePlan, path: string): boolean => isAttributeKey(pathKey(path)) || plan.attributeKeys.has(path);\n\n/**\n * @description The wire local name of a field: its `xmlName` override, or the key at the end of its path with the attribute prefix removed.\n *\n * @param plan - The namespace plan.\n * @param path - The field's element path.\n *\n * @returns The local name.\n */\nconst localOf = (plan: NamespacePlan, path: string): string => plan.nameByKey.get(path) ?? localName(pathKey(path));\n\n/**\n * @description The reverse-lookup key for a resolved wire name. The attribute flag is part of it, so an element and an attribute of the same local name in the\n * same namespace stay distinct.\n *\n * @param isAttribute - Whether the name is an attribute.\n * @param uri - The resolved namespace URI, or `undefined`.\n * @param local - The resolved local name.\n *\n * @returns The lookup key.\n */\nconst resolvedKey = (isAttribute: boolean, uri: string | undefined, local: string): string =>\n `${isAttribute ? ATTRIBUTE_PREFIX : ''}${uri ?? ''}|${local}`;\n\n/**\n * @description The path of the parent element, one segment shorter than the field's own path.\n *\n * @param path - The field's element path.\n *\n * @returns The parent element's path, or the root sentinel.\n */\nconst parentPath = (path: string): string => {\n const at = path.lastIndexOf(PATH_SEPARATOR);\n return at === -1 ? ROOT_ELEMENT : path.slice(0, at);\n};\n\n/**\n * @description The reverse-lookup key for a wire name under one parent element. The parent's path is part of it, so the same wire name under two parents resolves\n * to each parent's own field.\n *\n * @param parent - The parent element's path.\n * @param resolved - A name from {@link resolvedKey}.\n *\n * @returns The lookup key.\n */\nconst lookupKey = (parent: string, resolved: string): string => `${parent}\\u0001${resolved}`;\n\n/**\n * @description The mutable state one namespace scan carries: the plan under construction, the local names that resolve to more than one namespace, and the AST\n * nodes already visited so a recursive schema terminates.\n */\ninterface Scan {\n readonly byKey: Map<string, XmlNamespace>;\n readonly nameByKey: Map<string, string>;\n readonly attributeKeys: Set<string>;\n readonly valueByElement: Map<string, string>;\n readonly arrayKeys: Set<string>;\n readonly problems: Array<string>;\n /**\n * @description The AST nodes on the current scan path. A recursive schema terminates because the `Suspend` node is still on the path when its thunk is reached,\n * and a schema reused under two sibling paths is scanned once per path because each node is removed again on the way back out.\n */\n readonly seen: Set<SchemaAST.AST>;\n}\n\n/**\n * @description Records a field's namespace, reporting a local name that would belong to two namespaces at once.\n *\n * @param scan - The scan state.\n * @param path - The field's element path.\n * @param namespace - The namespace, or `undefined` when the field has none.\n */\nconst record = (scan: Scan, path: string, namespace: XmlNamespace | undefined): void => {\n if (Predicate.isUndefined(namespace)) {\n return;\n }\n\n const previous = scan.byKey.get(path);\n if (Predicate.isNotUndefined(previous) && (previous.uri !== namespace.uri || previous.prefix !== namespace.prefix)) {\n scan.problems.push(\n `the local name \"${pathKey(path)}\" belongs to more than one namespace (${previous.uri}:${previous.prefix} vs ${namespace.uri}:${namespace.prefix})`\n );\n } else {\n scan.byKey.set(path, namespace);\n }\n};\n\n/**\n * @description Records a field's `xmlName` override, refusing one that carries a prefix because the prefix comes from `xmlPrefix`.\n *\n * @param scan - The scan state.\n * @param path - The field's element path.\n * @param name - The annotated local name, or `undefined`.\n */\nconst recordName = (scan: Scan, path: string, name: string | undefined): void => {\n if (Predicate.isUndefined(name)) return;\n\n if (name.includes(':')) {\n scan.problems.push(`xmlName \"${name}\" on \"${pathKey(path)}\" must be a local name; use xmlPrefix for the prefix`);\n } else {\n scan.nameByKey.set(path, name);\n }\n};\n\n/**\n * @description Records a field's namespace, refusing the two annotations that cannot be honored: a namespaced field whose name already carries a prefix, and a\n * namespaced attribute without a prefix, because a default namespace does not apply to attributes.\n *\n * @param scan - The scan state.\n * @param path - The field's element path.\n * @param isAttribute - Whether the field is an attribute.\n * @param field - The field's own namespace, or `undefined`.\n * @param inherited - The namespace the enclosing element passes down.\n */\nconst recordFieldNamespace = (\n scan: Scan,\n path: string,\n isAttribute: boolean,\n field: XmlNamespace | undefined,\n inherited: XmlNamespace | undefined\n): void => {\n const key = pathKey(path);\n if (Predicate.isNotUndefined(field) && key.includes(':')) {\n scan.problems.push(`\"${key}\" already carries a prefix, so it cannot also carry a namespace annotation`);\n } else if (isAttribute && Predicate.isNotUndefined(field) && field.prefix === '') {\n scan.problems.push(`the attribute \"${key}\" needs xmlPrefix, because a default namespace does not apply to attributes`);\n } else {\n record(scan, path, isAttribute ? field : (field ?? inherited));\n }\n};\n\n/**\n * @description Whether a struct property is an attribute: its key carries the `@` prefix, or its own or key annotation marks it.\n *\n * @param key - The schema key.\n * @param ast - The property's value AST.\n *\n * @returns Whether the property is an attribute.\n */\nconst isAttributeProperty = (key: string, ast: SchemaAST.AST): boolean => isAttributeKey(key) || keyAttributeOf(ast) || attributeOf(ast);\n\n/**\n * @description Notes a field that `xmlAttribute` marks as an attribute but whose name does not carry the `@` prefix.\n *\n * @param scan - The scan state.\n * @param path - The field's element path.\n * @param isAttribute - Whether the field is an attribute.\n */\nconst noteAttribute = (scan: Scan, path: string, isAttribute: boolean): void => {\n if (isAttribute && !isAttributeKey(pathKey(path))) {\n scan.attributeKeys.add(path);\n }\n};\n\n/**\n * @description Records a field marked `xmlValue` against the element that owns it, refusing the annotations a value cannot combine with: character data has no\n * name and no namespace, and an element has room for only one value.\n *\n * @param scan - The scan state.\n * @param elementPath - The path of the element the field belongs to, or the root sentinel.\n * @param valueKey - The schema key of the value field.\n * @param isAttribute - Whether the field is an attribute.\n * @param name - The field's `xmlName` override, or `undefined`.\n * @param field - The field's own namespace, or `undefined`.\n */\nconst recordValue = (\n scan: Scan,\n elementPath: string,\n valueKey: string,\n isAttribute: boolean,\n name: string | undefined,\n field: XmlNamespace | undefined\n): void => {\n if (isAttribute) scan.problems.push(`the field \"${valueKey}\" is marked as both an attribute and the element's value`);\n else if (Predicate.isNotUndefined(name))\n scan.problems.push(`the value field \"${valueKey}\" cannot have an xmlName, because character data has no name`);\n else if (Predicate.isNotUndefined(field))\n scan.problems.push(`the value field \"${valueKey}\" cannot have a namespace, because character data has none`);\n else {\n const previous = scan.valueByElement.get(elementPath);\n if (Predicate.isNotUndefined(previous) && previous !== valueKey) {\n scan.problems.push(\n `the element \"${elementPath === ROOT_ELEMENT ? 'root' : elementPath}\" has more than one value field, \"${previous}\" and \"${valueKey}\"`\n );\n } else {\n scan.valueByElement.set(elementPath, valueKey);\n }\n }\n};\n\n/**\n * @description Records one struct property: its `xmlName`, its namespace, and the namespace its descendants inherit. An attribute carries a namespace only when\n * annotated; an element field falls back to the namespace it inherits. A value field is character data, so it is recorded against its element\n * instead.\n *\n * @param scan - The scan state.\n * @param property - The property signature.\n * @param inherited - The namespace the enclosing element passes down.\n * @param elementPath - The path of the element the property belongs to, or the root sentinel.\n */\nconst scanProperty = (scan: Scan, property: SchemaAST.PropertySignature, inherited: XmlNamespace | undefined, elementPath: string): void => {\n const key = Predicate.isString(property.name) ? property.name : String(property.name);\n const path = childPath(elementPath, key);\n const isAttribute = isAttributeProperty(key, property.type);\n const isValue = isValueProperty(property.type);\n const name = keyNameOf(property.type) ?? nameOf(property.type);\n const field = keyNamespaceOf(property.type) ?? namespaceOf(property.type);\n if (isValue) recordValue(scan, elementPath, key, isAttribute, name, field);\n else {\n noteAttribute(scan, path, isAttribute);\n recordName(scan, path, name);\n recordFieldNamespace(scan, path, isAttribute, field, inherited);\n }\n scanNode(scan, property.type, isAttribute ? inherited : (field ?? inherited), path);\n};\n\n/**\n * @description Walks a list of child ASTs under one inherited namespace.\n *\n * @param scan - The scan state.\n * @param nodes - The child ASTs.\n * @param inherited - The namespace they inherit.\n * @param elementPath - The path of the element they belong to, or the root sentinel.\n */\nconst scanAll = (scan: Scan, nodes: ReadonlyArray<SchemaAST.AST>, inherited: XmlNamespace | undefined, elementPath: string): void => {\n for (const node of nodes) scanNode(scan, node, inherited, elementPath);\n};\n\n/**\n * @description Records the names in one object node: its properties and its index signatures, each under the namespace the object passes down.\n *\n * @param scan - The scan state.\n * @param ast - The object AST.\n * @param namespace - The namespace the object passes to its members.\n * @param elementPath - The path of the element the object describes, or the root sentinel.\n */\nconst scanObject = (scan: Scan, ast: SchemaAST.Objects, namespace: XmlNamespace | undefined, elementPath: string): void => {\n for (const property of ast.propertySignatures) {\n scanProperty(scan, property, namespace, elementPath);\n }\n for (const index of ast.indexSignatures) {\n scanNode(scan, index.type, namespace, elementPath);\n }\n};\n\n/**\n * @description Notes an array field that names its element with `xmlName`, so the decoder reads a single occurrence as a one-member array. An unnamed array keeps\n * the ambiguity and does not wrap.\n *\n * @param scan - The scan state.\n * @param ast - The AST being scanned.\n * @param elementPath - The path of the field the AST describes.\n */\nconst noteArray = (scan: Scan, ast: SchemaAST.AST, elementPath: string): void => {\n if (ast._tag === 'Arrays' && scan.nameByKey.has(elementPath)) {\n scan.arrayKeys.add(elementPath);\n }\n};\n\n/**\n * @description Records every name in one schema AST, carrying the namespace an element passes to its descendants and the element the names belong to.\n *\n * @param scan - The scan state.\n * @param ast - The AST to walk.\n * @param inherited - The namespace the enclosing element passes down.\n * @param elementPath - The path of the element this AST describes, or the root sentinel.\n */\nconst scanNode = (scan: Scan, ast: SchemaAST.AST, inherited: XmlNamespace | undefined, elementPath: string): void => {\n if (scan.seen.has(ast)) return;\n scan.seen.add(ast);\n try {\n noteArray(scan, ast, elementPath);\n const namespace = namespaceOf(ast) ?? inherited;\n switch (ast._tag) {\n case 'Objects':\n scanObject(scan, ast, namespace, elementPath);\n return;\n case 'Arrays':\n scanAll(scan, [...ast.elements, ...ast.rest], namespace, elementPath);\n return;\n case 'Union':\n scanAll(scan, ast.types, namespace, elementPath);\n return;\n case 'Suspend':\n scanNode(scan, ast.thunk(), namespace, elementPath);\n return;\n case 'Declaration':\n scanAll(scan, ast.typeParameters, namespace, elementPath);\n return;\n default:\n return;\n }\n } finally {\n scan.seen.delete(ast);\n }\n};\n\n/**\n * @description Collects the namespace and name of every field in a schema. Descendant elements inherit a namespace, as they inherit a default namespace, and an\n * element field records its own namespace, so encode and decode can find it by the local name alone.\n *\n * @param schema - The schema to walk.\n *\n * @returns The plan, or the annotations that cannot be honored.\n */\nexport const namespacePlan = (schema: Schema.Constraint): { readonly plan: NamespacePlan } | { readonly error: string } => {\n const scan: Scan = {\n byKey: new Map(),\n nameByKey: new Map(),\n attributeKeys: new Set(),\n valueByElement: new Map(),\n arrayKeys: new Set(),\n problems: [],\n seen: new Set(),\n };\n scanNode(scan, schema.ast, undefined, ROOT_ELEMENT);\n\n const byResolved = new Map<string, string>();\n for (const path of new Set([...scan.byKey.keys(), ...scan.nameByKey.keys(), ...scan.attributeKeys])) {\n const key = pathKey(path);\n const namespace = scan.byKey.get(path);\n const isAttribute = isAttributeKey(key) || scan.attributeKeys.has(path);\n const local = scan.nameByKey.get(path) ?? localName(key);\n const resolved = lookupKey(parentPath(path), resolvedKey(isAttribute, namespace?.uri, local));\n const previous = byResolved.get(resolved);\n\n if (previous !== undefined && previous !== key) {\n scan.problems.push(`\"${previous}\" and \"${key}\" both resolve to \"${local}\" under the same element`);\n } else {\n byResolved.set(resolved, key);\n }\n }\n\n if (scan.problems.length > 0) {\n return { error: [...new Set(scan.problems)].join(';\\n\\t- ') };\n }\n\n return {\n plan: {\n byKey: scan.byKey,\n nameByKey: scan.nameByKey,\n attributeKeys: scan.attributeKeys,\n valueByElement: scan.valueByElement,\n arrayKeys: scan.arrayKeys,\n root: namespaceOf(schema.ast),\n rootName: nameOf(schema.ast),\n byResolved,\n },\n };\n};\n\n/**\n * @description Whether a key is a namespace declaration the codec manages: `@xmlns` or `@xmlns:prefix`.\n *\n * @param key - The record key.\n *\n * @returns Whether the key is a declaration.\n */\nconst isDeclarationKey = (key: string): boolean => key === `${ATTRIBUTE_PREFIX}xmlns` || key.startsWith(`${ATTRIBUTE_PREFIX}xmlns:`);\n\n/**\n * @description The prefix a declaration key carries, or `''` for the default namespace.\n *\n * @param key - A key {@link isDeclarationKey} accepted.\n *\n * @returns The prefix.\n */\nconst declarationPrefix = (key: string): string => (key === `${ATTRIBUTE_PREFIX}xmlns` ? '' : key.slice(`${ATTRIBUTE_PREFIX}xmlns:`.length));\n\n/**\n * @description Writes the declaration for a namespace into an element's record, when the scope does not already bind it. A prefixed namespace is declared as\n * `xmlns:prefix`; the default namespace as `xmlns`; and an element that leaves a default namespace in scope for no namespace of its own clears it\n * with `xmlns=\"\"`.\n *\n * @param out - The element's record, written in place.\n * @param scope - The in-scope prefix bindings, updated in place.\n * @param namespace - The element's namespace, or `undefined`.\n */\nconst declare = (out: Record<string, XmlValue>, scope: Record<string, string | undefined>, namespace: XmlNamespace | undefined): void => {\n if (Predicate.isNotUndefined(namespace) && namespace.prefix !== '') {\n if (scope[namespace.prefix] === namespace.uri) return;\n out[`${ATTRIBUTE_PREFIX}xmlns:${namespace.prefix}`] = namespace.uri;\n scope[namespace.prefix] = namespace.uri;\n return;\n }\n\n if (!Predicate.isUndefined(namespace)) {\n if (scope[''] === namespace.uri) return;\n out[`${ATTRIBUTE_PREFIX}xmlns`] = namespace.uri;\n scope[''] = namespace.uri;\n return;\n }\n\n if (Predicate.isUndefined(scope[''])) return;\n\n out[`${ATTRIBUTE_PREFIX}xmlns`] = '';\n scope[''] = undefined;\n};\n\n/**\n * @description The wire key for one field: the local name with the prefix its namespace declares, or the local name unchanged when there is no prefix.\n *\n * @param plan - The namespace plan.\n * @param path - The field's element path.\n * @param namespace - The field's namespace, or `undefined`.\n *\n * @returns The key to write.\n */\nconst wireKey = (plan: NamespacePlan, path: string, namespace: XmlNamespace | undefined): string => {\n const isAttribute = isAttributeOf(plan, path);\n const local = localOf(plan, path);\n\n if (Predicate.isUndefined(namespace) || namespace.prefix === '') {\n return isAttribute ? `${ATTRIBUTE_PREFIX}${local}` : local;\n }\n\n return `${isAttribute ? ATTRIBUTE_PREFIX : ''}${namespace.prefix}:${local}`;\n};\n\n/**\n * @description Writes one record's fields under the element's scope: an attribute is a leaf and declares its prefix on this element, the value field becomes the\n * element's character data, and a child element recurses with its own namespace.\n *\n * @param value - The element's record, keyed by the schema's local names.\n * @param plan - The namespace plan.\n * @param out - The wire record, written in place.\n * @param scope - The prefix bindings in scope for this element.\n * @param elementPath - The path of this element, or the root sentinel.\n */\nconst encodeFields = (\n value: XmlRecord,\n plan: NamespacePlan,\n out: Record<string, XmlValue>,\n scope: Record<string, string | undefined>,\n elementPath: string\n): void => {\n const valueKey = plan.valueByElement.get(elementPath);\n for (const [key, child] of Object.entries(value)) {\n if (isDeclarationKey(key)) {\n continue;\n }\n\n if (key === valueKey) {\n out[TEXT_KEY] = child;\n continue;\n }\n\n const path = childPath(elementPath, key);\n const childNamespace = plan.byKey.get(path);\n if (isAttributeOf(plan, path)) {\n if (Predicate.isNotUndefined(childNamespace)) {\n declare(out, scope, childNamespace);\n }\n out[wireKey(plan, path, childNamespace)] = child;\n continue;\n }\n\n out[wireKey(plan, path, childNamespace)] = encodeNames(child, plan, childNamespace, scope, path);\n }\n};\n\n/**\n * @description Rewrites a value tree into its namespaced wire form: every field with a plan entry gets its prefix, and every element declares the namespace its\n * subtree uses. A leaf that has to carry a declaration is wrapped as a `#text` record so the attribute has somewhere to live.\n *\n * @param value - The value tree, keyed by the schema's local names.\n * @param plan - The namespace plan.\n * @param namespace - This element's namespace.\n * @param scope - The prefix bindings in scope above this element.\n * @param elementPath - The path of this element, or the root sentinel.\n *\n * @returns The wire tree.\n */\nexport const encodeNames = (\n value: XmlValue,\n plan: NamespacePlan,\n namespace: XmlNamespace | undefined,\n scope: Record<string, string | undefined>,\n elementPath: string\n): XmlValue => {\n if (Array.isArray(value)) {\n return value.map(member => encodeNames(member, plan, namespace, scope, elementPath));\n }\n\n const out: Record<string, XmlValue> = {};\n const inner = { ...scope };\n declare(out, inner, namespace);\n\n if (Predicate.isUndefined(value)) {\n return undefined;\n }\n if (Predicate.isString(value)) {\n return Object.keys(out).length > 0 ? { ...out, [TEXT_KEY]: value } : value;\n }\n // `Predicate.isObject` narrows to a generic index signature, so the value\n // tree's own record type is named here.\n if (!Predicate.isObject(value)) {\n return value;\n }\n\n encodeFields(value as XmlRecord, plan, out, inner, elementPath);\n return out;\n};\n\n/**\n * @description Resolves a wire name to its URI and local name against the in-scope bindings. An unprefixed element takes the default namespace; an unprefixed\n * attribute has no namespace, because a default namespace does not apply to attributes.\n *\n * @param name - The name as written, without the attribute prefix.\n * @param isAttribute - Whether the name belongs to an attribute.\n * @param scope - The in-scope prefix bindings.\n *\n * @returns The resolved URI (possibly `undefined`) and local name.\n */\nconst resolveName = (\n name: string,\n isAttribute: boolean,\n scope: Record<string, string | undefined>\n): { readonly uri: string | undefined; readonly local: string } => {\n const colon = name.indexOf(':');\n if (colon === -1) return { uri: isAttribute ? undefined : scope[''], local: name };\n return { uri: scope[name.slice(0, colon)], local: name.slice(colon + 1) };\n};\n\n/**\n * @description Reads an element's declarations into a fresh scope that falls back to the enclosing one.\n *\n * @param value - The element's record.\n * @param scope - The bindings in scope above the element.\n *\n * @returns The element's own scope.\n */\nconst scopeOf = (value: XmlRecord, scope: Record<string, string | undefined>): Record<string, string | undefined> => {\n const inner = { ...scope };\n\n for (const [key, declaration] of Object.entries(value)) {\n if (isDeclarationKey(key) && Predicate.isString(declaration)) {\n inner[declarationPrefix(key)] = declaration;\n }\n }\n\n return inner;\n};\n\n/**\n * @description The schema key a wire key resolves to under one parent element: the plan's key for its URI and local name, or the local name when the schema left\n * it unannotated.\n *\n * @param plan - The namespace plan.\n * @param parent - The parent element's path.\n * @param key - The wire key.\n * @param scope - The in-scope prefix bindings.\n *\n * @returns The schema key.\n */\nconst schemaKey = (plan: NamespacePlan, parent: string, key: string, scope: Record<string, string | undefined>): string => {\n const isAttribute = isAttributeKey(key);\n const { uri, local } = resolveName(localName(key), isAttribute, scope);\n return plan.byResolved.get(lookupKey(parent, resolvedKey(isAttribute, uri, local))) ?? (isAttribute ? `${ATTRIBUTE_PREFIX}${local}` : local);\n};\n\n/**\n * @description The scope a child element resolves its own name against: the declarations it carries on itself, layered over the parent scope. An element may\n * declare the prefix it uses on the element itself, so its own name is read with those bindings in scope. A repeated element arrives as an array, so\n * the first member stands in for the run; every member describes the same element and carries the same declaration.\n *\n * @param child - The child value.\n * @param scope - The bindings in scope above the child.\n *\n * @returns The scope to resolve the child's own name with.\n */\nconst childScopeOf = (child: XmlValue, scope: Record<string, string | undefined>): Record<string, string | undefined> => {\n if (Array.isArray(child)) {\n return child.length > 0 ? childScopeOf(child[0], scope) : scope;\n }\n return Predicate.isObject(child) ? scopeOf(child as XmlRecord, scope) : scope;\n};\n\n/**\n * @description Character data read back as a value tree. An element the schema reads as a struct with a value field derives that field from the element's\n * character data, and the parser reduces an element with no attributes and no children to a bare string, so the string is put back under the value's\n * key for that struct to read.\n *\n * @param value - The character data the parser produced.\n * @param plan - The namespace plan.\n * @param elementPath - The path of the element the character data belongs to.\n *\n * @returns The character data, under the element's value key when it has one.\n */\nconst decodeText = (value: string, plan: NamespacePlan, elementPath: string): XmlValue => {\n const valueKey = plan.valueByElement.get(elementPath);\n return valueKey !== undefined ? { [valueKey]: value } : value;\n};\n\n/**\n * @description One record read back: its declarations dropped, its keys resolved to the schema's names, and its character data placed under the value field.\n *\n * @param record - The record to read.\n * @param plan - The namespace plan.\n * @param scope - The prefix bindings in scope above this element.\n * @param elementPath - The path of this element, or the root sentinel.\n *\n * @returns The record, keyed by the schema's names, or the string it collapses to.\n */\nconst decodeRecord = (record: XmlRecord, plan: NamespacePlan, scope: Record<string, string | undefined>, elementPath: string): XmlValue => {\n const inner = scopeOf(record, scope);\n const valueKey = plan.valueByElement.get(elementPath);\n const out: Record<string, XmlValue> = {};\n\n for (const [key, child] of Object.entries(record)) {\n if (isDeclarationKey(key)) {\n continue;\n }\n\n if (key === TEXT_KEY) {\n out[valueKey ?? TEXT_KEY] = child;\n continue;\n }\n\n const childKey = schemaKey(plan, elementPath, key, childScopeOf(child, inner));\n const path = childPath(elementPath, childKey);\n const decoded = decodeNames(child, plan, inner, path);\n out[childKey] = plan.arrayKeys.has(path) && !Array.isArray(decoded) ? [decoded] : decoded;\n }\n\n const keys = Object.keys(out);\n if (keys.length === 1 && keys[0] === TEXT_KEY) {\n return out[TEXT_KEY];\n }\n return out;\n};\n\n/**\n * @description Rewrites a wire value tree back to the schema's local names, resolving every name against the declarations the document carries and dropping those\n * declarations. Character data maps to the value field of the element it belongs to. A record left holding only character data collapses back to that\n * string, which is how a namespaced leaf stays a `Schema.String`.\n *\n * @param value - The parsed wire tree.\n * @param plan - The namespace plan.\n * @param scope - The prefix bindings in scope above this element.\n * @param elementPath - The path of this element, or the root sentinel.\n *\n * @returns The value tree, keyed by the schema's names.\n */\nexport const decodeNames = (value: XmlValue, plan: NamespacePlan, scope: Record<string, string | undefined>, elementPath: string): XmlValue => {\n if (Array.isArray(value)) {\n return value.map(member => decodeNames(member, plan, scope, elementPath));\n }\n if (Predicate.isString(value)) {\n return decodeText(value, plan, elementPath);\n }\n if (!Predicate.isObject(value)) {\n return value;\n }\n return decodeRecord(value as XmlRecord, plan, scope, elementPath);\n};\n"],"mappings":"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"}
@@ -0,0 +1,242 @@
1
+ import { TEXT_KEY, isAttributeKey } from "./conventions.js";
2
+ import { Predicate, Result, SchemaAST } from "effect";
3
+ //#region src/plain-value.ts
4
+ /**
5
+ * @description Whether an AST describes a structure - a struct, an array, or a union reachable to one - rather than a plain value. A node that describes a
6
+ * structure keeps a record as a record; a node that does not is where `#text` is read.
7
+ *
8
+ * @param ast - The derived StringTree AST to classify.
9
+ *
10
+ * @returns Whether the node is structural.
11
+ */
12
+ const isStructural = (ast) => {
13
+ if (SchemaAST.isSuspend(ast)) return isStructural(ast.thunk());
14
+ if (SchemaAST.isObjects(ast) || SchemaAST.isArrays(ast)) return true;
15
+ return SchemaAST.isUnion(ast) && ast.types.some(isStructural);
16
+ };
17
+ /**
18
+ * @description The AST a value is read against, with `Suspend` wrappers unwrapped so a recursive schema reaches its node.
19
+ *
20
+ * @param ast - The AST to unwrap.
21
+ *
22
+ * @returns The first node that is not a `Suspend`.
23
+ */
24
+ const resolveNode = (ast) => {
25
+ let node = ast;
26
+ while (SchemaAST.isSuspend(node)) node = node.thunk();
27
+ return node;
28
+ };
29
+ /**
30
+ * @description The AST a record value under one key is read against: the matching property signature, or the first index signature when the object is a record.
31
+ * `undefined` leaves the value alone, which is what an object with neither describes.
32
+ *
33
+ * @param node - The object node.
34
+ * @param key - The record key.
35
+ *
36
+ * @returns The AST for the value, or `undefined`.
37
+ */
38
+ const fieldAst = (node, key) => {
39
+ return node.propertySignatures.find((candidate) => candidate.name === key)?.type ?? node.indexSignatures[0]?.type;
40
+ };
41
+ /**
42
+ * @description The AST one member of an array is read against: the tuple element at that index, or the array's rest element.
43
+ *
44
+ * @param node - The array node.
45
+ * @param index - The member's index.
46
+ *
47
+ * @returns The AST for the member, or `undefined`.
48
+ */
49
+ const memberAst = (node, index) => node.elements[index] ?? node.rest[0];
50
+ /**
51
+ * @description The array node a value is read against. An optional repeated field derives as a union of an array and `undefined`, so an array node can sit behind
52
+ * a union rather than at the top; the first one reachable through the union's members is the one a repeated value belongs to.
53
+ *
54
+ * @param node - The resolved node to search.
55
+ *
56
+ * @returns The array node, or `undefined` when none is reachable.
57
+ */
58
+ const arrayNode = (node) => {
59
+ if (SchemaAST.isArrays(node)) return node;
60
+ if (SchemaAST.isUnion(node)) for (const member of node.types) {
61
+ const found = arrayNode(resolveNode(member));
62
+ if (found !== void 0) return found;
63
+ }
64
+ };
65
+ /**
66
+ * @description Whether an array's member is a structure - a struct, an array, or a union reachable to one - rather than a plain value. An empty element under such
67
+ * an array reads as one empty object, because a structural member cannot be empty character data; a plain-value member reads as an empty array
68
+ * instead.
69
+ *
70
+ * @param node - The array node.
71
+ *
72
+ * @returns Whether the member is structural.
73
+ */
74
+ const arrayMemberIsStructural = (node) => {
75
+ const member = memberAst(node, 0);
76
+ return member !== void 0 && isStructural(member);
77
+ };
78
+ /**
79
+ * @description The value an empty element under an array field reads as: one empty object when the member is structural, so a member the schema requires still
80
+ * reads, and an empty array otherwise.
81
+ *
82
+ * @param node - The array node.
83
+ *
84
+ * @returns The array to decode.
85
+ */
86
+ const emptyArrayElement = (node) => arrayMemberIsStructural(node) ? [{}] : [];
87
+ /**
88
+ * @description Whether a parsed element carries nothing at all: no character data, no attributes and no children. The parser reduces such an element to an empty
89
+ * string, and the codec reduces one whose only attributes were namespace declarations to an empty record. An array field reads either as one empty
90
+ * object or as an empty array, depending on whether its member is structural.
91
+ *
92
+ * @param value - The parsed value.
93
+ *
94
+ * @returns Whether the element is empty.
95
+ */
96
+ const isEmptyElement = (value) => value === "" || Predicate.isReadonlyObject(value) && Object.keys(value).length === 0;
97
+ /**
98
+ * @description The path of a field, for a failure message. The root has no name, so its fields are named on their own.
99
+ *
100
+ * @param parent - The parent's path.
101
+ * @param key - The field's key.
102
+ *
103
+ * @returns The field's path.
104
+ */
105
+ const childPath = (parent, key) => parent === "" ? key : `${parent}.${key}`;
106
+ /**
107
+ * @description Reads the character data of a record that wants a plain value, discarding the attributes around it. A record that also carries a child element is
108
+ * refused, because a plain value has nowhere to put one and dropping it would lose a field the document carried. A record with no `#text` at all is
109
+ * left for the decoder to refuse, which reports the attributes it found instead of a value.
110
+ *
111
+ * @param record - The record to read.
112
+ * @param path - The path of the value, for the failure message.
113
+ *
114
+ * @returns The `#text` value, or the message describing the child element that makes a plain value impossible.
115
+ */
116
+ const readCharacterData = (record, path) => {
117
+ if (!("#text" in record)) return Result.succeed(record);
118
+ const child = Object.keys(record).find((key) => key !== "#text" && !isAttributeKey(key));
119
+ if (child !== void 0) return Result.fail(`the field "${path === "" ? "root" : path}" wants a plain value, but the element also carries the child element "${child}"; only attributes are discarded alongside ${TEXT_KEY}`);
120
+ return Result.succeed(record[TEXT_KEY]);
121
+ };
122
+ /**
123
+ * @description Folds the fields of a record against the object node that describes them, so each field is normalized by its own AST.
124
+ *
125
+ * @param record - The record to fold.
126
+ * @param node - The object node.
127
+ * @param path - The path of the record, for a failure message.
128
+ *
129
+ * @returns The folded record, or the first field's failure.
130
+ */
131
+ const normalizeFields = (record, node, path) => {
132
+ const out = {};
133
+ for (const [key, child] of Object.entries(record)) {
134
+ const field = fieldAst(node, key);
135
+ if (field === void 0 || child === void 0) {
136
+ out[key] = child;
137
+ continue;
138
+ }
139
+ const normalized = normalizePlainValue(child, field, childPath(path, key));
140
+ if (Result.isFailure(normalized)) return normalized;
141
+ out[key] = normalized.success;
142
+ }
143
+ return Result.succeed(out);
144
+ };
145
+ /**
146
+ * @description Folds every member of an array against the array node that describes them. A member that is an empty element - the parser reduces it to an empty
147
+ * string, or to an empty record once the codec drops the namespace declarations it carried - cannot be a structural member, so it is kept as one
148
+ * empty object. That is how the renderer writes an empty array, and how a repeated empty tag reads back as one empty object per element.
149
+ *
150
+ * @param value - The array to fold.
151
+ * @param node - The array node.
152
+ * @param path - The path of the array, for a failure message.
153
+ *
154
+ * @returns The folded array, or the first member's failure.
155
+ */
156
+ const normalizeMembers = (value, node, path) => {
157
+ const out = [];
158
+ for (let index = 0; index < value.length; index++) {
159
+ const element = memberAst(node, index);
160
+ if (element === void 0) {
161
+ out.push(value[index]);
162
+ continue;
163
+ }
164
+ if (isEmptyElement(value[index]) && isStructural(element)) {
165
+ out.push({});
166
+ continue;
167
+ }
168
+ const normalized = normalizePlainValue(value[index], element, `${path}[${index}]`);
169
+ if (Result.isFailure(normalized)) return normalized;
170
+ out.push(normalized.success);
171
+ }
172
+ return Result.succeed(out);
173
+ };
174
+ /**
175
+ * @description Folds a record against every object member of a union, so a plain value nested under any branch is still read from its character data. Which branch
176
+ * the value belongs to is the decoder's job, so the first branch that folds the record cleanly wins; a branch that refuses the record - because a
177
+ * field it wants as a plain value also carries a child element - is skipped. When the union has no object member, or none of them folds the record,
178
+ * the record is left for the decoder to read as a plain value.
179
+ *
180
+ * @param record - The record to fold.
181
+ * @param node - The union node.
182
+ * @param path - The path of the record, for a failure message.
183
+ *
184
+ * @returns The folded value, the first object member's failure, or the record unchanged.
185
+ */
186
+ const normalizeUnion = (record, node, path) => {
187
+ let failure;
188
+ let sawObject = false;
189
+ for (const member of node.types) {
190
+ const resolved = resolveNode(member);
191
+ if (!SchemaAST.isObjects(resolved)) continue;
192
+ sawObject = true;
193
+ const normalized = normalizeFields(record, resolved, path);
194
+ if (Result.isSuccess(normalized)) return normalized;
195
+ failure ??= normalized;
196
+ }
197
+ if (failure !== void 0) return failure;
198
+ return sawObject ? Result.succeed(record) : readCharacterData(record, path);
199
+ };
200
+ /**
201
+ * @description Folds a record against the node that describes it. An object node is folded field by field; a union folds against its object members so a plain
202
+ * value nested under any branch is read; anything else wants a plain value and reads the character data.
203
+ *
204
+ * @param record - The record to fold.
205
+ * @param node - The resolved node.
206
+ * @param path - The path of the record, for a failure message.
207
+ *
208
+ * @returns The folded value, or the failure that makes it impossible.
209
+ */
210
+ const normalizeRecord = (record, node, path) => {
211
+ if (SchemaAST.isObjects(node)) return normalizeFields(record, node, path);
212
+ if (SchemaAST.isUnion(node)) return normalizeUnion(record, node, path);
213
+ if (isStructural(node)) return Result.succeed(record);
214
+ return readCharacterData(record, path);
215
+ };
216
+ /**
217
+ * @description Folds a parsed value against the derived StringTree AST, so a plain value can be read from an element that carries attributes. An empty element
218
+ * under an array field reads as one empty object when the member is structural, and as an empty array otherwise.
219
+ *
220
+ * @param value - The value the parser produced, after namespaces were resolved.
221
+ * @param ast - The derived StringTree AST the decoder will read the value with.
222
+ * @param path - The path of this value, for a failure message.
223
+ *
224
+ * @returns The value to decode, or the message describing why a plain value could not be read.
225
+ */
226
+ const normalizePlainValue = (value, ast, path) => {
227
+ if (Predicate.isUndefined(value)) return Result.succeed(value);
228
+ const node = resolveNode(ast);
229
+ const arrays = arrayNode(node);
230
+ if (Predicate.isString(value)) {
231
+ if (arrays !== void 0 && value === "") return Result.succeed(emptyArrayElement(arrays));
232
+ return Result.succeed(value);
233
+ }
234
+ if (Array.isArray(value)) return arrays !== void 0 ? normalizeMembers(value, arrays, path) : Result.succeed(value);
235
+ if (!Predicate.isReadonlyObject(value)) return Result.succeed(value);
236
+ if (arrays !== void 0 && isEmptyElement(value)) return Result.succeed(emptyArrayElement(arrays));
237
+ return normalizeRecord(value, node, path);
238
+ };
239
+ //#endregion
240
+ export { normalizePlainValue };
241
+
242
+ //# sourceMappingURL=plain-value.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plain-value.js","names":[],"sources":["../src/plain-value.ts"],"sourcesContent":["// A plain value read from an element that carries more than character data.\n//\n// The parser reduces an element with no attributes and no children to its\n// character data, so a field like `Schema.String` is usually handed a bare\n// string. An element that also carries attributes does not reduce: it arrives\n// as a record holding the attributes, the `#text` character data, and any child\n// elements, because the value model has nowhere else to put them:\n//\n// { \"@lang\": \"en\", \"#text\": \"Dune\" }\n//\n// Effect's `Schema.toCodecStringTree` derivation knows nothing about the `#text`\n// convention, so where a field wants a scalar it sees a record and fails with an\n// `InvalidType`. This module bridges the two. Walking the derived StringTree AST\n// beside the parsed value:\n//\n// - a node that wants character data takes the `#text` key and discards the\n// attributes, because an attribute is not part of the value the schema\n// describes;\n// - a node that wants character data but also finds a child element fails,\n// because a plain value cannot hold one and silently dropping it would lose\n// a field the document actually carried;\n// - a node that describes a struct, array or record keeps its record and\n// recurses, because `@`-prefixed fields and `#text` are ordinary field names\n// to such a schema;\n// - a union rewrites its record against each object member in turn, so a plain\n// value nested under any branch is still read; a repeated field derives as a\n// union of an array and `undefined`, so a record is also read through the\n// first array node reachable behind a union;\n// - an empty element under an array field reads as one empty object when the\n// member is structural, because an empty array renders as a single empty\n// element (see `renderRepeated`) and a member the schema requires must still\n// read; a plain-value member reads as an empty array.\n//\n// The AST is the one `Schema.toCodecStringTree` produces for the source schema,\n// so it is the same shape the decoder is about to read; this pass only rewrites\n// the value, it does not derive a schema.\n\nimport { Predicate, Result, SchemaAST } from 'effect';\n\nimport type { XmlRecord, XmlValue } from './xml-value.ts';\n\nimport { isAttributeKey, TEXT_KEY } from './conventions.ts';\n\n/**\n * @description Whether an AST describes a structure - a struct, an array, or a union reachable to one - rather than a plain value. A node that describes a\n * structure keeps a record as a record; a node that does not is where `#text` is read.\n *\n * @param ast - The derived StringTree AST to classify.\n *\n * @returns Whether the node is structural.\n */\nconst isStructural = (ast: SchemaAST.AST): boolean => {\n if (SchemaAST.isSuspend(ast)) return isStructural(ast.thunk());\n if (SchemaAST.isObjects(ast) || SchemaAST.isArrays(ast)) return true;\n return SchemaAST.isUnion(ast) && ast.types.some(isStructural);\n};\n\n/**\n * @description The AST a value is read against, with `Suspend` wrappers unwrapped so a recursive schema reaches its node.\n *\n * @param ast - The AST to unwrap.\n *\n * @returns The first node that is not a `Suspend`.\n */\nconst resolveNode = (ast: SchemaAST.AST): SchemaAST.AST => {\n let node = ast;\n while (SchemaAST.isSuspend(node)) node = node.thunk();\n return node;\n};\n\n/**\n * @description The AST a record value under one key is read against: the matching property signature, or the first index signature when the object is a record.\n * `undefined` leaves the value alone, which is what an object with neither describes.\n *\n * @param node - The object node.\n * @param key - The record key.\n *\n * @returns The AST for the value, or `undefined`.\n */\nconst fieldAst = (node: SchemaAST.Objects, key: string): SchemaAST.AST | undefined => {\n const property = node.propertySignatures.find(candidate => candidate.name === key);\n return property?.type ?? node.indexSignatures[0]?.type;\n};\n\n/**\n * @description The AST one member of an array is read against: the tuple element at that index, or the array's rest element.\n *\n * @param node - The array node.\n * @param index - The member's index.\n *\n * @returns The AST for the member, or `undefined`.\n */\nconst memberAst = (node: SchemaAST.Arrays, index: number): SchemaAST.AST | undefined => node.elements[index] ?? node.rest[0];\n\n/**\n * @description The array node a value is read against. An optional repeated field derives as a union of an array and `undefined`, so an array node can sit behind\n * a union rather than at the top; the first one reachable through the union's members is the one a repeated value belongs to.\n *\n * @param node - The resolved node to search.\n *\n * @returns The array node, or `undefined` when none is reachable.\n */\nconst arrayNode = (node: SchemaAST.AST): SchemaAST.Arrays | undefined => {\n if (SchemaAST.isArrays(node)) return node;\n if (SchemaAST.isUnion(node)) {\n for (const member of node.types) {\n const found = arrayNode(resolveNode(member));\n if (found !== undefined) return found;\n }\n }\n return undefined;\n};\n\n/**\n * @description Whether an array's member is a structure - a struct, an array, or a union reachable to one - rather than a plain value. An empty element under such\n * an array reads as one empty object, because a structural member cannot be empty character data; a plain-value member reads as an empty array\n * instead.\n *\n * @param node - The array node.\n *\n * @returns Whether the member is structural.\n */\nconst arrayMemberIsStructural = (node: SchemaAST.Arrays): boolean => {\n const member = memberAst(node, 0);\n return member !== undefined && isStructural(member);\n};\n\n/**\n * @description The value an empty element under an array field reads as: one empty object when the member is structural, so a member the schema requires still\n * reads, and an empty array otherwise.\n *\n * @param node - The array node.\n *\n * @returns The array to decode.\n */\nconst emptyArrayElement = (node: SchemaAST.Arrays): XmlValue => (arrayMemberIsStructural(node) ? [{}] : []);\n\n/**\n * @description Whether a parsed element carries nothing at all: no character data, no attributes and no children. The parser reduces such an element to an empty\n * string, and the codec reduces one whose only attributes were namespace declarations to an empty record. An array field reads either as one empty\n * object or as an empty array, depending on whether its member is structural.\n *\n * @param value - The parsed value.\n *\n * @returns Whether the element is empty.\n */\nconst isEmptyElement = (value: XmlValue): boolean => value === '' || (Predicate.isReadonlyObject(value) && Object.keys(value).length === 0);\n\n/**\n * @description The path of a field, for a failure message. The root has no name, so its fields are named on their own.\n *\n * @param parent - The parent's path.\n * @param key - The field's key.\n *\n * @returns The field's path.\n */\nconst childPath = (parent: string, key: string): string => (parent === '' ? key : `${parent}.${key}`);\n\n/**\n * @description Reads the character data of a record that wants a plain value, discarding the attributes around it. A record that also carries a child element is\n * refused, because a plain value has nowhere to put one and dropping it would lose a field the document carried. A record with no `#text` at all is\n * left for the decoder to refuse, which reports the attributes it found instead of a value.\n *\n * @param record - The record to read.\n * @param path - The path of the value, for the failure message.\n *\n * @returns The `#text` value, or the message describing the child element that makes a plain value impossible.\n */\nconst readCharacterData = (record: XmlRecord, path: string): Result.Result<XmlValue, string> => {\n if (!(TEXT_KEY in record)) return Result.succeed(record);\n\n const child = Object.keys(record).find(key => key !== TEXT_KEY && !isAttributeKey(key));\n if (child !== undefined) {\n return Result.fail(\n `the field \"${path === '' ? 'root' : path}\" wants a plain value, but the element also carries the child element \"${child}\"; only attributes are discarded alongside ${TEXT_KEY}`\n );\n }\n\n return Result.succeed(record[TEXT_KEY]);\n};\n\n/**\n * @description Folds the fields of a record against the object node that describes them, so each field is normalized by its own AST.\n *\n * @param record - The record to fold.\n * @param node - The object node.\n * @param path - The path of the record, for a failure message.\n *\n * @returns The folded record, or the first field's failure.\n */\nconst normalizeFields = (record: XmlRecord, node: SchemaAST.Objects, path: string): Result.Result<XmlValue, string> => {\n const out: Record<string, XmlValue> = {};\n for (const [key, child] of Object.entries(record)) {\n const field = fieldAst(node, key);\n if (field === undefined || child === undefined) {\n out[key] = child;\n continue;\n }\n const normalized = normalizePlainValue(child, field, childPath(path, key));\n if (Result.isFailure(normalized)) return normalized;\n out[key] = normalized.success;\n }\n return Result.succeed(out);\n};\n\n/**\n * @description Folds every member of an array against the array node that describes them. A member that is an empty element - the parser reduces it to an empty\n * string, or to an empty record once the codec drops the namespace declarations it carried - cannot be a structural member, so it is kept as one\n * empty object. That is how the renderer writes an empty array, and how a repeated empty tag reads back as one empty object per element.\n *\n * @param value - The array to fold.\n * @param node - The array node.\n * @param path - The path of the array, for a failure message.\n *\n * @returns The folded array, or the first member's failure.\n */\nconst normalizeMembers = (value: ReadonlyArray<XmlValue>, node: SchemaAST.Arrays, path: string): Result.Result<XmlValue, string> => {\n const out: Array<XmlValue> = [];\n for (let index = 0; index < value.length; index++) {\n const element = memberAst(node, index);\n if (element === undefined) {\n out.push(value[index]);\n continue;\n }\n if (isEmptyElement(value[index]) && isStructural(element)) {\n out.push({});\n continue;\n }\n const normalized = normalizePlainValue(value[index], element, `${path}[${index}]`);\n if (Result.isFailure(normalized)) return normalized;\n out.push(normalized.success);\n }\n return Result.succeed(out);\n};\n\n/**\n * @description Folds a record against every object member of a union, so a plain value nested under any branch is still read from its character data. Which branch\n * the value belongs to is the decoder's job, so the first branch that folds the record cleanly wins; a branch that refuses the record - because a\n * field it wants as a plain value also carries a child element - is skipped. When the union has no object member, or none of them folds the record,\n * the record is left for the decoder to read as a plain value.\n *\n * @param record - The record to fold.\n * @param node - The union node.\n * @param path - The path of the record, for a failure message.\n *\n * @returns The folded value, the first object member's failure, or the record unchanged.\n */\nconst normalizeUnion = (record: XmlRecord, node: SchemaAST.Union, path: string): Result.Result<XmlValue, string> => {\n let failure: Result.Result<XmlValue, string> | undefined;\n let sawObject = false;\n for (const member of node.types) {\n const resolved = resolveNode(member);\n if (!SchemaAST.isObjects(resolved)) continue;\n sawObject = true;\n const normalized = normalizeFields(record, resolved, path);\n if (Result.isSuccess(normalized)) return normalized;\n failure ??= normalized;\n }\n if (failure !== undefined) return failure;\n return sawObject ? Result.succeed(record) : readCharacterData(record, path);\n};\n\n/**\n * @description Folds a record against the node that describes it. An object node is folded field by field; a union folds against its object members so a plain\n * value nested under any branch is read; anything else wants a plain value and reads the character data.\n *\n * @param record - The record to fold.\n * @param node - The resolved node.\n * @param path - The path of the record, for a failure message.\n *\n * @returns The folded value, or the failure that makes it impossible.\n */\nconst normalizeRecord = (record: XmlRecord, node: SchemaAST.AST, path: string): Result.Result<XmlValue, string> => {\n if (SchemaAST.isObjects(node)) return normalizeFields(record, node, path);\n if (SchemaAST.isUnion(node)) return normalizeUnion(record, node, path);\n if (isStructural(node)) return Result.succeed(record);\n return readCharacterData(record, path);\n};\n\n/**\n * @description Folds a parsed value against the derived StringTree AST, so a plain value can be read from an element that carries attributes. An empty element\n * under an array field reads as one empty object when the member is structural, and as an empty array otherwise.\n *\n * @param value - The value the parser produced, after namespaces were resolved.\n * @param ast - The derived StringTree AST the decoder will read the value with.\n * @param path - The path of this value, for a failure message.\n *\n * @returns The value to decode, or the message describing why a plain value could not be read.\n */\nexport const normalizePlainValue = (value: XmlValue, ast: SchemaAST.AST, path: string): Result.Result<XmlValue, string> => {\n if (Predicate.isUndefined(value)) return Result.succeed(value);\n\n const node = resolveNode(ast);\n const arrays = arrayNode(node);\n\n if (Predicate.isString(value)) {\n // An empty element under an array field is how the renderer writes an empty array; a structural member still reads as one empty object.\n if (arrays !== undefined && value === '') return Result.succeed(emptyArrayElement(arrays));\n return Result.succeed(value);\n }\n\n if (Array.isArray(value)) {\n return arrays !== undefined ? normalizeMembers(value, arrays, path) : Result.succeed(value);\n }\n\n if (!Predicate.isReadonlyObject(value)) return Result.succeed(value);\n\n // The same empty element, after the codec dropped the namespace declarations it carried, arrives as an empty record.\n if (arrays !== undefined && isEmptyElement(value)) return Result.succeed(emptyArrayElement(arrays));\n\n return normalizeRecord(value as XmlRecord, node, path);\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@endevops/effect-codec-xml",
3
- "version": "0.1.0-beta.1",
3
+ "version": "0.1.0-beta.2",
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
@@ -32,6 +32,7 @@ import type { XmlValue } from './xml-value.ts';
32
32
  import { DEFAULT_ROOT_NAME } from './conventions.ts';
33
33
  import { decodeNames, encodeNames, namespacePlan, ROOT_ELEMENT } from './namespaces.ts';
34
34
  import { parseXml } from './parse.ts';
35
+ import { normalizePlainValue } from './plain-value.ts';
35
36
  import { renderXml } from './render.ts';
36
37
 
37
38
  /**
@@ -114,10 +115,11 @@ export const toCodecXml: {
114
115
  const plan = planned.plan;
115
116
  const active = isActivePlan(plan);
116
117
  const renderOptions = resolveRenderOptions(schema, plan, options);
118
+ const stringTree = Schema.toCodecStringTree(schema);
117
119
 
118
120
  return Schema.String.pipe(
119
121
  Schema.decodeTo(
120
- Schema.toCodecStringTree(schema),
122
+ stringTree,
121
123
  SchemaTransformation.transformEffect({
122
124
  decode: (text, parseOptions) => {
123
125
  if (!Predicate.isString(text)) {
@@ -129,7 +131,12 @@ export const toCodecXml: {
129
131
  Effect.tapError(error =>
130
132
  Effect.logError(`XML parse error: ${error.message}`).pipe(Effect.annotateLogs({ cause: error, message: 'XML parse error' }))
131
133
  ),
132
- Effect.mapError(error => new SchemaIssue.InvalidValue({ message: error.message }, text, parseOptions))
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
+ )
139
+ )
133
140
  );
134
141
  },
135
142
 
package/src/namespaces.ts CHANGED
@@ -915,31 +915,32 @@ const childScopeOf = (child: XmlValue, scope: Record<string, string | undefined>
915
915
  };
916
916
 
917
917
  /**
918
- * @description Rewrites a wire value tree back to the schema's local names, resolving every name against the declarations the document carries and dropping those
919
- * declarations. Character data maps to the value field of the element it belongs to. A record left holding only character data collapses back to that
920
- * string, which is how a namespaced leaf stays a `Schema.String`.
918
+ * @description Character data read back as a value tree. An element the schema reads as a struct with a value field derives that field from the element's
919
+ * character data, and the parser reduces an element with no attributes and no children to a bare string, so the string is put back under the value's
920
+ * key for that struct to read.
921
921
  *
922
- * @param value - The parsed wire tree.
922
+ * @param value - The character data the parser produced.
923
+ * @param plan - The namespace plan.
924
+ * @param elementPath - The path of the element the character data belongs to.
925
+ *
926
+ * @returns The character data, under the element's value key when it has one.
927
+ */
928
+ const decodeText = (value: string, plan: NamespacePlan, elementPath: string): XmlValue => {
929
+ const valueKey = plan.valueByElement.get(elementPath);
930
+ return valueKey !== undefined ? { [valueKey]: value } : value;
931
+ };
932
+
933
+ /**
934
+ * @description One record read back: its declarations dropped, its keys resolved to the schema's names, and its character data placed under the value field.
935
+ *
936
+ * @param record - The record to read.
923
937
  * @param plan - The namespace plan.
924
938
  * @param scope - The prefix bindings in scope above this element.
925
939
  * @param elementPath - The path of this element, or the root sentinel.
926
940
  *
927
- * @returns The value tree, keyed by the schema's names.
941
+ * @returns The record, keyed by the schema's names, or the string it collapses to.
928
942
  */
929
- export const decodeNames = (value: XmlValue, plan: NamespacePlan, scope: Record<string, string | undefined>, elementPath: string): XmlValue => {
930
- if (Array.isArray(value)) {
931
- return value.map(member => decodeNames(member, plan, scope, elementPath));
932
- }
933
- if (Predicate.isString(value) || Predicate.isUndefined(value)) {
934
- return value;
935
- }
936
- if (!Predicate.isObject(value)) {
937
- return value;
938
- }
939
-
940
- // `Predicate.isObject` narrows to a generic index signature, so the value
941
- // tree's own record type is named here.
942
- const record = value as XmlRecord;
943
+ const decodeRecord = (record: XmlRecord, plan: NamespacePlan, scope: Record<string, string | undefined>, elementPath: string): XmlValue => {
943
944
  const inner = scopeOf(record, scope);
944
945
  const valueKey = plan.valueByElement.get(elementPath);
945
946
  const out: Record<string, XmlValue> = {};
@@ -950,7 +951,7 @@ export const decodeNames = (value: XmlValue, plan: NamespacePlan, scope: Record<
950
951
  }
951
952
 
952
953
  if (key === TEXT_KEY) {
953
- out[valueKey ?? TEXT_KEY] = decodeNames(child, plan, inner, elementPath);
954
+ out[valueKey ?? TEXT_KEY] = child;
954
955
  continue;
955
956
  }
956
957
 
@@ -966,3 +967,28 @@ export const decodeNames = (value: XmlValue, plan: NamespacePlan, scope: Record<
966
967
  }
967
968
  return out;
968
969
  };
970
+
971
+ /**
972
+ * @description Rewrites a wire value tree back to the schema's local names, resolving every name against the declarations the document carries and dropping those
973
+ * declarations. Character data maps to the value field of the element it belongs to. A record left holding only character data collapses back to that
974
+ * string, which is how a namespaced leaf stays a `Schema.String`.
975
+ *
976
+ * @param value - The parsed wire tree.
977
+ * @param plan - The namespace plan.
978
+ * @param scope - The prefix bindings in scope above this element.
979
+ * @param elementPath - The path of this element, or the root sentinel.
980
+ *
981
+ * @returns The value tree, keyed by the schema's names.
982
+ */
983
+ export const decodeNames = (value: XmlValue, plan: NamespacePlan, scope: Record<string, string | undefined>, elementPath: string): XmlValue => {
984
+ if (Array.isArray(value)) {
985
+ return value.map(member => decodeNames(member, plan, scope, elementPath));
986
+ }
987
+ if (Predicate.isString(value)) {
988
+ return decodeText(value, plan, elementPath);
989
+ }
990
+ if (!Predicate.isObject(value)) {
991
+ return value;
992
+ }
993
+ return decodeRecord(value as XmlRecord, plan, scope, elementPath);
994
+ };
@@ -0,0 +1,312 @@
1
+ // A plain value read from an element that carries more than character data.
2
+ //
3
+ // The parser reduces an element with no attributes and no children to its
4
+ // character data, so a field like `Schema.String` is usually handed a bare
5
+ // string. An element that also carries attributes does not reduce: it arrives
6
+ // as a record holding the attributes, the `#text` character data, and any child
7
+ // elements, because the value model has nowhere else to put them:
8
+ //
9
+ // { "@lang": "en", "#text": "Dune" }
10
+ //
11
+ // Effect's `Schema.toCodecStringTree` derivation knows nothing about the `#text`
12
+ // convention, so where a field wants a scalar it sees a record and fails with an
13
+ // `InvalidType`. This module bridges the two. Walking the derived StringTree AST
14
+ // beside the parsed value:
15
+ //
16
+ // - a node that wants character data takes the `#text` key and discards the
17
+ // attributes, because an attribute is not part of the value the schema
18
+ // describes;
19
+ // - a node that wants character data but also finds a child element fails,
20
+ // because a plain value cannot hold one and silently dropping it would lose
21
+ // a field the document actually carried;
22
+ // - a node that describes a struct, array or record keeps its record and
23
+ // recurses, because `@`-prefixed fields and `#text` are ordinary field names
24
+ // to such a schema;
25
+ // - a union rewrites its record against each object member in turn, so a plain
26
+ // value nested under any branch is still read; a repeated field derives as a
27
+ // union of an array and `undefined`, so a record is also read through the
28
+ // first array node reachable behind a union;
29
+ // - an empty element under an array field reads as one empty object when the
30
+ // member is structural, because an empty array renders as a single empty
31
+ // element (see `renderRepeated`) and a member the schema requires must still
32
+ // read; a plain-value member reads as an empty array.
33
+ //
34
+ // The AST is the one `Schema.toCodecStringTree` produces for the source schema,
35
+ // so it is the same shape the decoder is about to read; this pass only rewrites
36
+ // the value, it does not derive a schema.
37
+
38
+ import { Predicate, Result, SchemaAST } from 'effect';
39
+
40
+ import type { XmlRecord, XmlValue } from './xml-value.ts';
41
+
42
+ import { isAttributeKey, TEXT_KEY } from './conventions.ts';
43
+
44
+ /**
45
+ * @description Whether an AST describes a structure - a struct, an array, or a union reachable to one - rather than a plain value. A node that describes a
46
+ * structure keeps a record as a record; a node that does not is where `#text` is read.
47
+ *
48
+ * @param ast - The derived StringTree AST to classify.
49
+ *
50
+ * @returns Whether the node is structural.
51
+ */
52
+ const isStructural = (ast: SchemaAST.AST): boolean => {
53
+ if (SchemaAST.isSuspend(ast)) return isStructural(ast.thunk());
54
+ if (SchemaAST.isObjects(ast) || SchemaAST.isArrays(ast)) return true;
55
+ return SchemaAST.isUnion(ast) && ast.types.some(isStructural);
56
+ };
57
+
58
+ /**
59
+ * @description The AST a value is read against, with `Suspend` wrappers unwrapped so a recursive schema reaches its node.
60
+ *
61
+ * @param ast - The AST to unwrap.
62
+ *
63
+ * @returns The first node that is not a `Suspend`.
64
+ */
65
+ const resolveNode = (ast: SchemaAST.AST): SchemaAST.AST => {
66
+ let node = ast;
67
+ while (SchemaAST.isSuspend(node)) node = node.thunk();
68
+ return node;
69
+ };
70
+
71
+ /**
72
+ * @description The AST a record value under one key is read against: the matching property signature, or the first index signature when the object is a record.
73
+ * `undefined` leaves the value alone, which is what an object with neither describes.
74
+ *
75
+ * @param node - The object node.
76
+ * @param key - The record key.
77
+ *
78
+ * @returns The AST for the value, or `undefined`.
79
+ */
80
+ const fieldAst = (node: SchemaAST.Objects, key: string): SchemaAST.AST | undefined => {
81
+ const property = node.propertySignatures.find(candidate => candidate.name === key);
82
+ return property?.type ?? node.indexSignatures[0]?.type;
83
+ };
84
+
85
+ /**
86
+ * @description The AST one member of an array is read against: the tuple element at that index, or the array's rest element.
87
+ *
88
+ * @param node - The array node.
89
+ * @param index - The member's index.
90
+ *
91
+ * @returns The AST for the member, or `undefined`.
92
+ */
93
+ const memberAst = (node: SchemaAST.Arrays, index: number): SchemaAST.AST | undefined => node.elements[index] ?? node.rest[0];
94
+
95
+ /**
96
+ * @description The array node a value is read against. An optional repeated field derives as a union of an array and `undefined`, so an array node can sit behind
97
+ * a union rather than at the top; the first one reachable through the union's members is the one a repeated value belongs to.
98
+ *
99
+ * @param node - The resolved node to search.
100
+ *
101
+ * @returns The array node, or `undefined` when none is reachable.
102
+ */
103
+ const arrayNode = (node: SchemaAST.AST): SchemaAST.Arrays | undefined => {
104
+ if (SchemaAST.isArrays(node)) return node;
105
+ if (SchemaAST.isUnion(node)) {
106
+ for (const member of node.types) {
107
+ const found = arrayNode(resolveNode(member));
108
+ if (found !== undefined) return found;
109
+ }
110
+ }
111
+ return undefined;
112
+ };
113
+
114
+ /**
115
+ * @description Whether an array's member is a structure - a struct, an array, or a union reachable to one - rather than a plain value. An empty element under such
116
+ * an array reads as one empty object, because a structural member cannot be empty character data; a plain-value member reads as an empty array
117
+ * instead.
118
+ *
119
+ * @param node - The array node.
120
+ *
121
+ * @returns Whether the member is structural.
122
+ */
123
+ const arrayMemberIsStructural = (node: SchemaAST.Arrays): boolean => {
124
+ const member = memberAst(node, 0);
125
+ return member !== undefined && isStructural(member);
126
+ };
127
+
128
+ /**
129
+ * @description The value an empty element under an array field reads as: one empty object when the member is structural, so a member the schema requires still
130
+ * reads, and an empty array otherwise.
131
+ *
132
+ * @param node - The array node.
133
+ *
134
+ * @returns The array to decode.
135
+ */
136
+ const emptyArrayElement = (node: SchemaAST.Arrays): XmlValue => (arrayMemberIsStructural(node) ? [{}] : []);
137
+
138
+ /**
139
+ * @description Whether a parsed element carries nothing at all: no character data, no attributes and no children. The parser reduces such an element to an empty
140
+ * string, and the codec reduces one whose only attributes were namespace declarations to an empty record. An array field reads either as one empty
141
+ * object or as an empty array, depending on whether its member is structural.
142
+ *
143
+ * @param value - The parsed value.
144
+ *
145
+ * @returns Whether the element is empty.
146
+ */
147
+ const isEmptyElement = (value: XmlValue): boolean => value === '' || (Predicate.isReadonlyObject(value) && Object.keys(value).length === 0);
148
+
149
+ /**
150
+ * @description The path of a field, for a failure message. The root has no name, so its fields are named on their own.
151
+ *
152
+ * @param parent - The parent's path.
153
+ * @param key - The field's key.
154
+ *
155
+ * @returns The field's path.
156
+ */
157
+ const childPath = (parent: string, key: string): string => (parent === '' ? key : `${parent}.${key}`);
158
+
159
+ /**
160
+ * @description Reads the character data of a record that wants a plain value, discarding the attributes around it. A record that also carries a child element is
161
+ * refused, because a plain value has nowhere to put one and dropping it would lose a field the document carried. A record with no `#text` at all is
162
+ * left for the decoder to refuse, which reports the attributes it found instead of a value.
163
+ *
164
+ * @param record - The record to read.
165
+ * @param path - The path of the value, for the failure message.
166
+ *
167
+ * @returns The `#text` value, or the message describing the child element that makes a plain value impossible.
168
+ */
169
+ const readCharacterData = (record: XmlRecord, path: string): Result.Result<XmlValue, string> => {
170
+ if (!(TEXT_KEY in record)) return Result.succeed(record);
171
+
172
+ const child = Object.keys(record).find(key => key !== TEXT_KEY && !isAttributeKey(key));
173
+ if (child !== undefined) {
174
+ return Result.fail(
175
+ `the field "${path === '' ? 'root' : path}" wants a plain value, but the element also carries the child element "${child}"; only attributes are discarded alongside ${TEXT_KEY}`
176
+ );
177
+ }
178
+
179
+ return Result.succeed(record[TEXT_KEY]);
180
+ };
181
+
182
+ /**
183
+ * @description Folds the fields of a record against the object node that describes them, so each field is normalized by its own AST.
184
+ *
185
+ * @param record - The record to fold.
186
+ * @param node - The object node.
187
+ * @param path - The path of the record, for a failure message.
188
+ *
189
+ * @returns The folded record, or the first field's failure.
190
+ */
191
+ const normalizeFields = (record: XmlRecord, node: SchemaAST.Objects, path: string): Result.Result<XmlValue, string> => {
192
+ const out: Record<string, XmlValue> = {};
193
+ for (const [key, child] of Object.entries(record)) {
194
+ const field = fieldAst(node, key);
195
+ if (field === undefined || child === undefined) {
196
+ out[key] = child;
197
+ continue;
198
+ }
199
+ const normalized = normalizePlainValue(child, field, childPath(path, key));
200
+ if (Result.isFailure(normalized)) return normalized;
201
+ out[key] = normalized.success;
202
+ }
203
+ return Result.succeed(out);
204
+ };
205
+
206
+ /**
207
+ * @description Folds every member of an array against the array node that describes them. A member that is an empty element - the parser reduces it to an empty
208
+ * string, or to an empty record once the codec drops the namespace declarations it carried - cannot be a structural member, so it is kept as one
209
+ * empty object. That is how the renderer writes an empty array, and how a repeated empty tag reads back as one empty object per element.
210
+ *
211
+ * @param value - The array to fold.
212
+ * @param node - The array node.
213
+ * @param path - The path of the array, for a failure message.
214
+ *
215
+ * @returns The folded array, or the first member's failure.
216
+ */
217
+ const normalizeMembers = (value: ReadonlyArray<XmlValue>, node: SchemaAST.Arrays, path: string): Result.Result<XmlValue, string> => {
218
+ const out: Array<XmlValue> = [];
219
+ for (let index = 0; index < value.length; index++) {
220
+ const element = memberAst(node, index);
221
+ if (element === undefined) {
222
+ out.push(value[index]);
223
+ continue;
224
+ }
225
+ if (isEmptyElement(value[index]) && isStructural(element)) {
226
+ out.push({});
227
+ continue;
228
+ }
229
+ const normalized = normalizePlainValue(value[index], element, `${path}[${index}]`);
230
+ if (Result.isFailure(normalized)) return normalized;
231
+ out.push(normalized.success);
232
+ }
233
+ return Result.succeed(out);
234
+ };
235
+
236
+ /**
237
+ * @description Folds a record against every object member of a union, so a plain value nested under any branch is still read from its character data. Which branch
238
+ * the value belongs to is the decoder's job, so the first branch that folds the record cleanly wins; a branch that refuses the record - because a
239
+ * field it wants as a plain value also carries a child element - is skipped. When the union has no object member, or none of them folds the record,
240
+ * the record is left for the decoder to read as a plain value.
241
+ *
242
+ * @param record - The record to fold.
243
+ * @param node - The union node.
244
+ * @param path - The path of the record, for a failure message.
245
+ *
246
+ * @returns The folded value, the first object member's failure, or the record unchanged.
247
+ */
248
+ const normalizeUnion = (record: XmlRecord, node: SchemaAST.Union, path: string): Result.Result<XmlValue, string> => {
249
+ let failure: Result.Result<XmlValue, string> | undefined;
250
+ let sawObject = false;
251
+ for (const member of node.types) {
252
+ const resolved = resolveNode(member);
253
+ if (!SchemaAST.isObjects(resolved)) continue;
254
+ sawObject = true;
255
+ const normalized = normalizeFields(record, resolved, path);
256
+ if (Result.isSuccess(normalized)) return normalized;
257
+ failure ??= normalized;
258
+ }
259
+ if (failure !== undefined) return failure;
260
+ return sawObject ? Result.succeed(record) : readCharacterData(record, path);
261
+ };
262
+
263
+ /**
264
+ * @description Folds a record against the node that describes it. An object node is folded field by field; a union folds against its object members so a plain
265
+ * value nested under any branch is read; anything else wants a plain value and reads the character data.
266
+ *
267
+ * @param record - The record to fold.
268
+ * @param node - The resolved node.
269
+ * @param path - The path of the record, for a failure message.
270
+ *
271
+ * @returns The folded value, or the failure that makes it impossible.
272
+ */
273
+ const normalizeRecord = (record: XmlRecord, node: SchemaAST.AST, path: string): Result.Result<XmlValue, string> => {
274
+ if (SchemaAST.isObjects(node)) return normalizeFields(record, node, path);
275
+ if (SchemaAST.isUnion(node)) return normalizeUnion(record, node, path);
276
+ if (isStructural(node)) return Result.succeed(record);
277
+ return readCharacterData(record, path);
278
+ };
279
+
280
+ /**
281
+ * @description Folds a parsed value against the derived StringTree AST, so a plain value can be read from an element that carries attributes. An empty element
282
+ * under an array field reads as one empty object when the member is structural, and as an empty array otherwise.
283
+ *
284
+ * @param value - The value the parser produced, after namespaces were resolved.
285
+ * @param ast - The derived StringTree AST the decoder will read the value with.
286
+ * @param path - The path of this value, for a failure message.
287
+ *
288
+ * @returns The value to decode, or the message describing why a plain value could not be read.
289
+ */
290
+ export const normalizePlainValue = (value: XmlValue, ast: SchemaAST.AST, path: string): Result.Result<XmlValue, string> => {
291
+ if (Predicate.isUndefined(value)) return Result.succeed(value);
292
+
293
+ const node = resolveNode(ast);
294
+ const arrays = arrayNode(node);
295
+
296
+ if (Predicate.isString(value)) {
297
+ // An empty element under an array field is how the renderer writes an empty array; a structural member still reads as one empty object.
298
+ if (arrays !== undefined && value === '') return Result.succeed(emptyArrayElement(arrays));
299
+ return Result.succeed(value);
300
+ }
301
+
302
+ if (Array.isArray(value)) {
303
+ return arrays !== undefined ? normalizeMembers(value, arrays, path) : Result.succeed(value);
304
+ }
305
+
306
+ if (!Predicate.isReadonlyObject(value)) return Result.succeed(value);
307
+
308
+ // The same empty element, after the codec dropped the namespace declarations it carried, arrives as an empty record.
309
+ if (arrays !== undefined && isEmptyElement(value)) return Result.succeed(emptyArrayElement(arrays));
310
+
311
+ return normalizeRecord(value as XmlRecord, node, path);
312
+ };