xml-model 2.0.0-beta.6 → 2.0.0-beta.7

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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { DATA, isModel, model } from "./model.js";
2
2
  import XML, { ZXMLCommentNode, ZXMLElementNode, ZXMLNode, ZXMLRoot, ZXMLTextNode } from "./xml/xml-js.js";
3
3
  import { xml } from "./xml/schema-meta.js";
4
- import { XMLCodecError, normalizeCodecOptions, registerDefault } from "./xml/codec.js";
5
- import { XMLBase, XMLBaseWithSource, xmlModel } from "./xml/model.js";
4
+ import { XMLCodecError, normalizeCodecOptions, registerDefault, xmlStateSchema } from "./xml/codec.js";
5
+ import { xmlModel } from "./xml/model.js";
6
6
  import "./xml/index.js";
7
- export { DATA, XML, XMLBase, XMLBaseWithSource, XMLCodecError, ZXMLCommentNode, ZXMLElementNode, ZXMLNode, ZXMLRoot, ZXMLTextNode, isModel, model, normalizeCodecOptions, registerDefault, xml, xmlModel };
7
+ export { DATA, XML, XMLCodecError, ZXMLCommentNode, ZXMLElementNode, ZXMLNode, ZXMLRoot, ZXMLTextNode, isModel, model, normalizeCodecOptions, registerDefault, xml, xmlModel, xmlStateSchema };
@@ -9,7 +9,7 @@ export declare function assertSingleElement(xml: XMLElement[]): asserts xml is [
9
9
  export declare function assertSingleRoot(xml: XMLElement[]): asserts xml is [XMLElement & {
10
10
  elements: XMLElement[];
11
11
  }];
12
- type PropKey<S extends z.ZodObject> = keyof z.input<S> & keyof z.output<S> & string;
12
+ type PropKey<S extends z.ZodObject> = keyof z.input<S> & string;
13
13
  export interface CodecOptions<S extends z.ZodType> {
14
14
  schema: S;
15
15
  tagname(ctx: RootEncodingContext<S>): string;
@@ -51,7 +51,7 @@ export interface RootDecodingContext<S extends z.ZodType> {
51
51
  }
52
52
  export interface RootEncodingContext<S extends z.ZodType> {
53
53
  options: CodecOptions<S>;
54
- data: z.output<S>;
54
+ data: z.input<S>;
55
55
  }
56
56
  export interface PropertyDecodingContext<S extends z.ZodObject = z.ZodObject, K extends PropKey<S> = PropKey<S>> extends RootDecodingContext<S> {
57
57
  property: {
@@ -68,7 +68,7 @@ export interface PropertyEncodingContext<S extends z.ZodObject = z.ZodObject, K
68
68
  name: K;
69
69
  options: CodecOptions<z.ZodType>;
70
70
  tagname: string;
71
- value: z.output<S>[K];
71
+ value: z.input<S>[K];
72
72
  };
73
73
  result: XMLElement;
74
74
  }
@@ -80,29 +80,27 @@ export interface XMLState {
80
80
  /** Present when xmlStateSchema({ source: true }) is used: the original XMLElement. */
81
81
  source?: XMLElement;
82
82
  }
83
- /**
84
- * String key used to store XML round-trip state on decoded data objects.
85
- * Using a string (rather than a Symbol) allows Zod's schema.parse() to
86
- * preserve it naturally when the key is included in the schema via xmlStateSchema().
87
- */
88
- export declare const XML_STATE_KEY: "__xml_state";
89
83
  /**
90
84
  * Schema for the XML round-trip state field.
91
85
  *
92
- * Include in your base model schema under `XML_STATE_KEY` to preserve element ordering
93
- * and unknown elements through Zod's `schema.parse()` for nested model instances.
86
+ * Add a field with this schema to any `xmlModel` ZodObject to opt in to:
87
+ * - **Element ordering** — elements are re-emitted in source order, not schema order.
88
+ * - **Unknown elements** — unrecognised elements are passed through verbatim on re-encode.
94
89
  *
95
- * Pass `{ source: true }` to also record the original `XMLElement` on each instance.
90
+ * The field can be named anything; the codec detects it automatically.
91
+ * Pass `{ source: true }` to additionally store the original `XMLElement` on the instance.
96
92
  *
97
93
  * @example
98
- * class XMLBase extends xmlModel(z.object({
99
- * [XML_STATE_KEY]: xmlStateSchema(),
100
- * }), { tagname: "base" }) {}
94
+ * class Device extends xmlModel(z.object({
95
+ * _xmlState: xmlStateSchema(),
96
+ * name: z.string(),
97
+ * }), { tagname: "device" }) {}
101
98
  *
102
99
  * // With source recording:
103
- * class XMLBase extends xmlModel(z.object({
104
- * [XML_STATE_KEY]: xmlStateSchema({ source: true }),
105
- * }), { tagname: "base" }) {}
100
+ * class Device extends xmlModel(z.object({
101
+ * _xmlState: xmlStateSchema({ source: true }),
102
+ * name: z.string(),
103
+ * }), { tagname: "device" }) {}
106
104
  */
107
105
  export declare function xmlStateSchema(): z.ZodOptional<z.ZodCustom<XMLState>>;
108
106
  export declare function xmlStateSchema(options: {
@@ -111,7 +109,7 @@ export declare function xmlStateSchema(options: {
111
109
  source: XMLElement;
112
110
  }>>;
113
111
  export declare function decode<S extends z.ZodType>(schema: S, xml: XMLElement): z.input<S>;
114
- export declare function encode<S extends z.ZodType>(schema: S, data: z.output<S>): XMLElement;
112
+ export declare function encode<S extends z.ZodType>(schema: S, data: z.input<S>): XMLElement;
115
113
  type DefaultResolver<S extends z.ZodType = z.ZodType> = (schema: S) => CodecOptions<S> | void;
116
114
  export declare function registerDefault(resolve: DefaultResolver): void;
117
115
  export declare function xmlCodec<S extends z.ZodType>(schema: S): z.ZodCodec<z.ZodString, S>;
package/dist/xml/codec.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import XML from "./xml-js.js";
2
2
  import { isZodType } from "../util/zod.js";
3
- import { getUserOptions, prop } from "./schema-meta.js";
4
3
  import { kebabCase } from "../util/kebab-case.js";
4
+ import { getUserOptions, prop } from "./schema-meta.js";
5
5
  import { z } from "zod";
6
6
  //#region src/xml/codec.ts
7
7
  var XMLCodecError = class extends Error {
@@ -83,19 +83,17 @@ function resolveCodecOptions(schema) {
83
83
  cache.set(schema, options);
84
84
  return options;
85
85
  }
86
- /**
87
- * String key used to store XML round-trip state on decoded data objects.
88
- * Using a string (rather than a Symbol) allows Zod's schema.parse() to
89
- * preserve it naturally when the key is included in the schema via xmlStateSchema().
90
- */
91
- var XML_STATE_KEY = "__xml_state";
86
+ /** Tracks all schemas created by `xmlStateSchema()` for fast detection at setup time. */
87
+ var xmlStateSchemas = /* @__PURE__ */ new WeakSet();
92
88
  function xmlStateSchema(options) {
93
- return prop(z.custom().optional(), {
89
+ const result = prop(z.custom().optional(), {
94
90
  decode: options?.source ? (ctx, _next) => {
95
- (ctx.result[XML_STATE_KEY] ??= {}).source = ctx.xml;
91
+ (ctx.result[ctx.property.name] ??= {}).source = ctx.xml;
96
92
  } : () => {},
97
93
  encode: () => {}
98
94
  });
95
+ xmlStateSchemas.add(result);
96
+ return result;
99
97
  }
100
98
  function resolvePropertiesCodecOptions(schema) {
101
99
  const shape = schema.def.shape;
@@ -103,6 +101,16 @@ function resolvePropertiesCodecOptions(schema) {
103
101
  for (const [prop, fieldSchema] of Object.entries(shape)) options[prop] = resolveCodecOptions(fieldSchema);
104
102
  return options;
105
103
  }
104
+ /**
105
+ * Scans a ZodObject shape for a field created with `xmlStateSchema()`.
106
+ * Returns the field key if found, `undefined` if absent.
107
+ * Throws if more than one such field is present (not supported).
108
+ */
109
+ function findXmlStateKey(shape) {
110
+ const keys = Object.keys(shape).filter((k) => xmlStateSchemas.has(shape[k]));
111
+ if (keys.length > 1) throw new Error(`Only one xmlStateSchema field is allowed per object schema, found: ${keys.join(", ")}`);
112
+ return keys[0];
113
+ }
106
114
  function decode(schema, xml) {
107
115
  const options = resolveCodecOptions(schema);
108
116
  return options.decode({
@@ -205,33 +213,23 @@ registerDefault((schema) => {
205
213
  }
206
214
  if (schema instanceof z.ZodCodec) {
207
215
  const inSchema = schema.def.in;
208
- const outSchema = schema.def.out;
209
216
  if (!isZodType(inSchema)) throw new Error(`Expected schema.def.in to be a ZodType, got ${inSchema}`);
210
- if (!isZodType(outSchema)) throw new Error(`Expected schema.def.out to be a ZodType, got ${outSchema}`);
211
217
  const inputCodecOptions = resolveCodecOptions(inSchema);
212
218
  return normalizeCodecOptions(schema, {
213
219
  decode({ xml }) {
214
- const input = inputCodecOptions.decode({
220
+ return inputCodecOptions.decode({
215
221
  options: inputCodecOptions,
216
222
  xml
217
223
  });
218
- return schema.def.transform(input, {
219
- value: input,
220
- issues: []
221
- });
222
224
  },
223
225
  encode(ctx) {
224
- const data = schema.def.reverseTransform ? schema.def.reverseTransform(ctx.data, {
225
- value: ctx.data,
226
- issues: []
227
- }) : outSchema.encode(ctx.data);
228
226
  const innerOpts = ctx.options.tagname !== inputCodecOptions.tagname ? {
229
227
  ...inputCodecOptions,
230
228
  tagname: ctx.options.tagname
231
229
  } : inputCodecOptions;
232
230
  return innerOpts.encode({
233
231
  options: innerOpts,
234
- data
232
+ data: ctx.data
235
233
  });
236
234
  }
237
235
  });
@@ -278,10 +276,12 @@ registerDefault((schema) => {
278
276
  registerDefault((schema) => {
279
277
  if (schema instanceof z.ZodObject) {
280
278
  const options = resolvePropertiesCodecOptions(schema);
279
+ const stateKey = findXmlStateKey(schema.def.shape);
281
280
  return normalizeCodecOptions(schema, {
282
281
  decode(ctx) {
283
- const sequence = [];
284
- const result = { [XML_STATE_KEY]: { sequence } };
282
+ const sequence = stateKey ? [] : void 0;
283
+ const result = {};
284
+ if (stateKey) result[stateKey] = { sequence };
285
285
  const propContexts = Object.fromEntries(Object.entries(options).map(([name, propOpts]) => {
286
286
  return [name, {
287
287
  name,
@@ -305,19 +305,19 @@ registerDefault((schema) => {
305
305
  }
306
306
  }
307
307
  if (!matches.length) {
308
- sequence.push(el);
308
+ if (sequence) sequence.push(el);
309
309
  continue;
310
310
  } else if (matches.length === 1) {
311
311
  const propName = matches[0];
312
312
  if (seenProperties.has(propName)) {
313
313
  if (!options[propName].inlineProperty) throw new Error("Matching multiple elements for a single property is only supported when `inlineProperty` is true");
314
314
  } else {
315
- sequence.push(propName);
315
+ if (sequence) sequence.push(propName);
316
316
  seenProperties.add(propName);
317
317
  }
318
318
  } else throw new Error(`Same element was matched by multiple properties: ${matches.join(", ")}`);
319
319
  }
320
- for (const propName in options) if (!seenProperties.has(propName)) sequence.push(propName);
320
+ for (const propName in options) if (!seenProperties.has(propName) && sequence) sequence.push(propName);
321
321
  for (const prop in options) {
322
322
  const o = options[prop];
323
323
  const propCtx = propContexts[prop];
@@ -350,8 +350,8 @@ registerDefault((schema) => {
350
350
  attributes: {},
351
351
  elements: []
352
352
  };
353
- const sequence = data["__xml_state"]?.sequence ?? Object.keys(options);
354
- for (const item of sequence) if (typeof item === "string") {
353
+ const iterOrder = (stateKey ? data[stateKey]?.sequence : void 0) ?? Object.keys(options);
354
+ for (const item of iterOrder) if (typeof item === "string") {
355
355
  const o = options[item];
356
356
  if (!o) throw new Error(`Failed to resolve property options for sequence item ${item}`);
357
357
  try {
@@ -378,7 +378,128 @@ registerDefault((schema) => {
378
378
  });
379
379
  }
380
380
  });
381
+ /**
382
+ * Recursively extracts the set of literal values from a schema,
383
+ * unwrapping ZodCodec, ZodOptional, etc. as needed.
384
+ */
385
+ function getLiteralValues(schema) {
386
+ if (schema instanceof z.ZodLiteral) return schema.def.values;
387
+ if (schema instanceof z.ZodCodec) return getLiteralValues(schema.def.in);
388
+ if (schema instanceof z.ZodOptional) return getLiteralValues(schema.def.innerType);
389
+ return [];
390
+ }
391
+ function formatReason(errors) {
392
+ return errors.map((e) => e instanceof Error ? e.message : String(e)).join("; ");
393
+ }
394
+ /**
395
+ * Reads the discriminator field value from an XML element without a full decode.
396
+ * Handles both XML-attribute discriminators (xml.attr) and child-element discriminators.
397
+ */
398
+ function peekDiscriminatorValue(discriminator, propertyOptions, ctx) {
399
+ const errors = [];
400
+ for (const options of propertyOptions) try {
401
+ const propCtx = {
402
+ name: discriminator,
403
+ options,
404
+ tagname: options.propertyTagname({
405
+ name: discriminator,
406
+ options
407
+ }),
408
+ xml: { elements: [] }
409
+ };
410
+ ctx.xml.elements.forEach((el) => {
411
+ if (el.type !== "element") return;
412
+ if (options.propertyMatch(el, propCtx)) propCtx.xml.elements.push(el);
413
+ });
414
+ if (propCtx.xml.elements.length === 0) propCtx.xml = null;
415
+ else if (propCtx.xml.elements.length !== 1) throw new Error("Matched multiple elements for a single property");
416
+ else propCtx.xml = propCtx.xml.elements[0];
417
+ const result = {};
418
+ options.decodeAsProperty({
419
+ options: ctx.options,
420
+ xml: ctx.xml,
421
+ property: propCtx,
422
+ result
423
+ });
424
+ return result[discriminator];
425
+ } catch (e) {
426
+ errors.push(e);
427
+ }
428
+ throw new XMLCodecError(`union: no option matched for decoding (${formatReason(errors)})`);
429
+ }
430
+ registerDefault((schema) => {
431
+ if (schema instanceof z.ZodDiscriminatedUnion) {
432
+ const discriminator = schema.def.discriminator;
433
+ const options = schema.def.options;
434
+ const optionCodecs = /* @__PURE__ */ new Map();
435
+ const discriminatorSchemas = [];
436
+ for (const option of options) {
437
+ const inSchema = option instanceof z.ZodCodec ? option.def.in : option;
438
+ if (!(inSchema instanceof z.ZodObject)) throw new TypeError(`Discriminated union members are supposed to be objects, got ${inSchema.type}`);
439
+ const discriminatorSchema = inSchema.shape[discriminator];
440
+ if (!discriminatorSchema) throw new TypeError(`Missing discriminator field "${discriminator}" in schema`);
441
+ discriminatorSchemas.push(discriminatorSchema);
442
+ const optCodec = resolveCodecOptions(inSchema);
443
+ for (const val of getLiteralValues(discriminatorSchema)) optionCodecs.set(val, optCodec);
444
+ }
445
+ const discriminatorOptions = discriminatorSchemas.map(resolveCodecOptions);
446
+ return normalizeCodecOptions(schema, {
447
+ decode(ctx) {
448
+ const { xml } = ctx;
449
+ if (!xml) throw new XMLCodecError(`discriminated union requires an XML element`);
450
+ const discValue = peekDiscriminatorValue(discriminator, discriminatorOptions, ctx);
451
+ const matched = optionCodecs.get(discValue);
452
+ if (!matched) throw new XMLCodecError(`no variant matched discriminator "${discriminator}" = "${String(discValue)}"`);
453
+ return matched.decode({
454
+ options: matched,
455
+ xml
456
+ });
457
+ },
458
+ encode(ctx) {
459
+ const discValue = ctx.data[discriminator];
460
+ const matched = optionCodecs.get(discValue);
461
+ if (!matched) throw new XMLCodecError(`no variant matched discriminator "${discriminator}" = "${String(discValue)}"`);
462
+ return matched.encode({
463
+ options: matched,
464
+ data: ctx.data
465
+ });
466
+ }
467
+ });
468
+ }
469
+ if (schema instanceof z.ZodUnion) {
470
+ const codecOptions = schema.def.options.map((option) => {
471
+ const inSchema = option instanceof z.ZodCodec ? option.def.in : option;
472
+ return resolveCodecOptions(inSchema instanceof z.ZodObject ? inSchema : option);
473
+ });
474
+ return normalizeCodecOptions(schema, {
475
+ decode(ctx) {
476
+ const errors = [];
477
+ for (const options of codecOptions) try {
478
+ return options.decode({
479
+ options,
480
+ xml: ctx.xml
481
+ });
482
+ } catch (e) {
483
+ errors.push(e);
484
+ }
485
+ throw new XMLCodecError(`union: no option matched for decoding (${formatReason(errors)})`);
486
+ },
487
+ encode(ctx) {
488
+ const errors = [];
489
+ for (const options of codecOptions) try {
490
+ return options.encode({
491
+ options,
492
+ data: ctx.data
493
+ });
494
+ } catch (e) {
495
+ errors.push(e);
496
+ }
497
+ throw new XMLCodecError(`union: no option matched for encoding (${formatReason(errors)})`);
498
+ }
499
+ });
500
+ }
501
+ });
381
502
  //#endregion
382
- export { XMLCodecError, XML_STATE_KEY, decode, encode, normalizeCodecOptions, registerDefault, xmlStateSchema };
503
+ export { XMLCodecError, decode, encode, normalizeCodecOptions, registerDefault, xmlStateSchema };
383
504
 
384
505
  //# sourceMappingURL=codec.js.map
@@ -1,54 +1,125 @@
1
1
  import { z } from 'zod';
2
- declare const Event_base: import('./model').XmlModelConstructor<z.ZodObject<{
2
+ /**
3
+ * Recommended base class for all xmlModel classes.
4
+ *
5
+ * Extending `XMLBase` instead of calling `xmlModel()` directly opts every subclass
6
+ * into round-trip preservation at no extra cost:
7
+ * - **Element ordering** — elements are re-emitted in source document order, not schema order.
8
+ * - **Unknown elements** — elements with no matching schema field are passed through verbatim.
9
+ *
10
+ * This matters whenever you read XML produced by a third party, modify a subset of fields,
11
+ * and write it back — plain `xmlModel()` would silently reorder elements and drop extensions.
12
+ *
13
+ * The `_xmlState` field holds the tracking state; it is intentionally excluded from XML output.
14
+ *
15
+ * @example
16
+ * class Device extends XMLBase.extend(
17
+ * { name: z.string() },
18
+ * xml.root({ tagname: "device" }),
19
+ * ) {}
20
+ */
21
+ export declare const XMLBase: import('./model').XmlModelConstructor<z.ZodObject<{
22
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
23
+ }, z.core.$strip>, {
24
+ _xmlState?: import('./codec').XMLState;
25
+ }>;
26
+ /**
27
+ * Like {@link XMLBase}, but also records the original `XMLElement` as `._xmlState.source`
28
+ * on each instance.
29
+ *
30
+ * @example
31
+ * const device = Device.fromXML(`<device>…</device>`);
32
+ * device._xmlState?.source; // XMLElement
33
+ */
34
+ export declare const XMLBaseWithSource: import('./model').XmlModelConstructor<z.ZodObject<{
35
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState & {
36
+ source: import('./xml-js').XMLElement;
37
+ }, unknown>>;
38
+ }, z.core.$strip>, {
39
+ _xmlState?: import('./codec').XMLState & {
40
+ source: import('./xml-js').XMLElement;
41
+ };
42
+ }>;
43
+ declare const Event_base: Omit<import('./model').XmlModelConstructor<z.ZodObject<{
44
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
45
+ }, z.core.$strip>, {
46
+ _xmlState?: import('./codec').XMLState;
47
+ }>, keyof import('..').ModelConstructor<S, Inst>> & import('..').ModelConstructor<z.ZodObject<{
48
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
3
49
  title: z.ZodString;
4
50
  publishedAt: z.ZodCodec<z.ZodString, z.ZodDate>;
5
51
  }, z.core.$strip>, {
52
+ _xmlState?: import('./codec').XMLState;
53
+ } & {
6
54
  title: string;
55
+ _xmlState?: import('./codec').XMLState;
7
56
  publishedAt?: Date;
8
57
  }>;
9
58
  /**
10
59
  * An event with a typed `Date` field stored as an ISO 8601 string in XML.
60
+ * Extends `XMLBase` so element order and unknown elements are preserved across round-trips.
11
61
  * Demonstrates using `z.codec` to transform a raw XML string into a native JS type.
12
62
  */
13
63
  export declare class Event extends Event_base {
14
64
  }
15
- declare const Engine_base: import('./model').XmlModelConstructor<z.ZodObject<{
65
+ declare const Engine_base: Omit<import('./model').XmlModelConstructor<z.ZodObject<{
66
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
67
+ }, z.core.$strip>, {
68
+ _xmlState?: import('./codec').XMLState;
69
+ }>, keyof import('..').ModelConstructor<S, Inst>> & import('..').ModelConstructor<z.ZodObject<{
70
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
16
71
  type: z.ZodString;
17
72
  horsepower: z.ZodNumber;
18
73
  }, z.core.$strip>, {
74
+ _xmlState?: import('./codec').XMLState;
75
+ } & {
19
76
  type: string;
20
77
  horsepower: number;
78
+ _xmlState?: import('./codec').XMLState;
21
79
  }>;
22
80
  /**
23
- * A car engine. Demonstrates a basic nested class with one XML attribute
24
- * (`type`) and one child element (`horsepower`).
81
+ * A car engine. Extends `XMLBase` so unknown vendor elements inside `<engine>`
82
+ * (e.g. manufacturer extensions) survive a read-modify-write cycle.
83
+ * Demonstrates a basic nested class with one XML attribute (`type`) and one
84
+ * child element (`horsepower`).
25
85
  */
26
86
  export declare class Engine extends Engine_base {
27
87
  }
28
- declare const Vehicle_base: import('./model').XmlModelConstructor<z.ZodObject<{
88
+ declare const Vehicle_base: Omit<import('./model').XmlModelConstructor<z.ZodObject<{
89
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
90
+ }, z.core.$strip>, {
91
+ _xmlState?: import('./codec').XMLState;
92
+ }>, keyof import('..').ModelConstructor<S, Inst>> & import('..').ModelConstructor<z.ZodObject<{
93
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
29
94
  vin: z.ZodString;
30
95
  make: z.ZodString;
31
96
  year: z.ZodNumber;
32
97
  }, z.core.$strip>, {
98
+ _xmlState?: import('./codec').XMLState;
99
+ } & {
33
100
  vin: string;
34
101
  make: string;
35
102
  year: number;
103
+ _xmlState?: import('./codec').XMLState;
36
104
  }>;
37
105
  /**
38
- * Base vehicle class. Demonstrates `xml.attr()` for identifier fields and
39
- * `xml.prop()` for child-element fields. Custom methods on the class are
40
- * available on every parsed instance.
106
+ * Base vehicle class. Extends `XMLBase` so all vehicle subclasses inherit
107
+ * round-trip preservation element order and unknown extensions are kept
108
+ * intact without any per-subclass ceremony.
109
+ * Demonstrates `xml.attr()` for identifier fields and custom instance methods.
41
110
  */
42
111
  export declare class Vehicle extends Vehicle_base {
43
112
  /** Returns a human-readable label for this vehicle. */
44
113
  label(): string;
45
114
  }
46
115
  declare const Car_base: Omit<typeof Vehicle, keyof import('..').ModelConstructor<S, Inst>> & import('..').ModelConstructor<z.ZodObject<{
116
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
47
117
  vin: z.ZodString;
48
118
  make: z.ZodString;
49
119
  year: z.ZodNumber;
50
120
  doors: z.ZodNumber;
51
121
  engine: z.ZodCodec<z.ZodObject<{
122
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
52
123
  type: z.ZodString;
53
124
  horsepower: z.ZodNumber;
54
125
  }, z.core.$strip>, z.ZodCustom<Engine, Engine>>;
@@ -57,6 +128,7 @@ declare const Car_base: Omit<typeof Vehicle, keyof import('..').ModelConstructor
57
128
  make: string;
58
129
  year: number;
59
130
  doors: number;
131
+ _xmlState?: import('./codec').XMLState;
60
132
  engine?: Engine;
61
133
  }>;
62
134
  /**
@@ -70,11 +142,13 @@ declare const Car_base: Omit<typeof Vehicle, keyof import('..').ModelConstructor
70
142
  export declare class Car extends Car_base {
71
143
  }
72
144
  declare const SportCar_base: Omit<typeof Car, keyof import('..').ModelConstructor<S, Inst>> & import('..').ModelConstructor<z.ZodObject<{
145
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
73
146
  vin: z.ZodString;
74
147
  make: z.ZodString;
75
148
  year: z.ZodNumber;
76
149
  doors: z.ZodNumber;
77
150
  engine: z.ZodCodec<z.ZodObject<{
151
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
78
152
  type: z.ZodString;
79
153
  horsepower: z.ZodNumber;
80
154
  }, z.core.$strip>, z.ZodCustom<Engine, Engine>>;
@@ -85,6 +159,7 @@ declare const SportCar_base: Omit<typeof Car, keyof import('..').ModelConstructo
85
159
  year: number;
86
160
  doors: number;
87
161
  topSpeed: number;
162
+ _xmlState?: import('./codec').XMLState;
88
163
  engine?: Engine;
89
164
  }>;
90
165
  /**
@@ -94,6 +169,7 @@ declare const SportCar_base: Omit<typeof Car, keyof import('..').ModelConstructo
94
169
  export declare class SportCar extends SportCar_base {
95
170
  }
96
171
  declare const Motorcycle_base: Omit<typeof Vehicle, keyof import('..').ModelConstructor<S, Inst>> & import('..').ModelConstructor<z.ZodObject<{
172
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
97
173
  vin: z.ZodString;
98
174
  make: z.ZodString;
99
175
  year: z.ZodNumber;
@@ -102,6 +178,7 @@ declare const Motorcycle_base: Omit<typeof Vehicle, keyof import('..').ModelCons
102
178
  vin: string;
103
179
  make: string;
104
180
  year: number;
181
+ _xmlState?: import('./codec').XMLState;
105
182
  sidecar?: boolean;
106
183
  }>;
107
184
  /**
@@ -111,28 +188,39 @@ declare const Motorcycle_base: Omit<typeof Vehicle, keyof import('..').ModelCons
111
188
  */
112
189
  export declare class Motorcycle extends Motorcycle_base {
113
190
  }
114
- declare const Fleet_base: import('./model').XmlModelConstructor<z.ZodObject<{
191
+ declare const Fleet_base: Omit<import('./model').XmlModelConstructor<z.ZodObject<{
192
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
193
+ }, z.core.$strip>, {
194
+ _xmlState?: import('./codec').XMLState;
195
+ }>, keyof import('..').ModelConstructor<S, Inst>> & import('..').ModelConstructor<z.ZodObject<{
196
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
115
197
  name: z.ZodString;
116
198
  cars: z.ZodArray<z.ZodCodec<z.ZodObject<{
199
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
117
200
  vin: z.ZodString;
118
201
  make: z.ZodString;
119
202
  year: z.ZodNumber;
120
203
  doors: z.ZodNumber;
121
204
  engine: z.ZodCodec<z.ZodObject<{
205
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
122
206
  type: z.ZodString;
123
207
  horsepower: z.ZodNumber;
124
208
  }, z.core.$strip>, z.ZodCustom<Engine, Engine>>;
125
209
  }, z.core.$strip>, z.ZodCustom<Car, Car>>>;
126
210
  motorcycles: z.ZodArray<z.ZodCodec<z.ZodObject<{
211
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
127
212
  vin: z.ZodString;
128
213
  make: z.ZodString;
129
214
  year: z.ZodNumber;
130
215
  sidecar: z.ZodOptional<z.ZodBoolean>;
131
216
  }, z.core.$strip>, z.ZodCustom<Motorcycle, Motorcycle>>>;
132
217
  }, z.core.$strip>, {
218
+ _xmlState?: import('./codec').XMLState;
219
+ } & {
133
220
  name: string;
134
221
  cars: Car[];
135
222
  motorcycles: Motorcycle[];
223
+ _xmlState?: import('./codec').XMLState;
136
224
  }>;
137
225
  /**
138
226
  * Fleet demonstrates **inline arrays** of multiple vehicle types.
@@ -143,12 +231,20 @@ export declare class Fleet extends Fleet_base {
143
231
  /** Total number of vehicles across all types in this fleet. */
144
232
  totalVehicles(): number;
145
233
  }
146
- declare const Showroom_base: import('./model').XmlModelConstructor<z.ZodObject<{
234
+ declare const Showroom_base: Omit<import('./model').XmlModelConstructor<z.ZodObject<{
235
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
236
+ }, z.core.$strip>, {
237
+ _xmlState?: import('./codec').XMLState;
238
+ }>, keyof import('..').ModelConstructor<S, Inst>> & import('..').ModelConstructor<z.ZodObject<{
239
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
147
240
  name: z.ZodString;
148
241
  models: z.ZodArray<z.ZodString>;
149
242
  }, z.core.$strip>, {
243
+ _xmlState?: import('./codec').XMLState;
244
+ } & {
150
245
  name: string;
151
246
  models: string[];
247
+ _xmlState?: import('./codec').XMLState;
152
248
  }>;
153
249
  /**
154
250
  * A showroom holds an inventory of car model names.
@@ -170,28 +266,113 @@ declare const Showroom_base: import('./model').XmlModelConstructor<z.ZodObject<{
170
266
  */
171
267
  export declare class Showroom extends Showroom_base {
172
268
  }
173
- declare const CarStandalone_base: import('./model').XmlModelConstructor<z.ZodObject<{
269
+ declare const PetrolEngine_base: Omit<import('./model').XmlModelConstructor<z.ZodObject<{
270
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
271
+ }, z.core.$strip>, {
272
+ _xmlState?: import('./codec').XMLState;
273
+ }>, keyof import('..').ModelConstructor<S, Inst>> & import('..').ModelConstructor<z.ZodObject<{
274
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
275
+ type: z.ZodLiteral<"petrol">;
276
+ horsepower: z.ZodNumber;
277
+ }, z.core.$strip>, {
278
+ _xmlState?: import('./codec').XMLState;
279
+ } & {
280
+ type: "petrol";
281
+ horsepower: number;
282
+ _xmlState?: import('./codec').XMLState;
283
+ }>;
284
+ /**
285
+ * A petrol engine, discriminated by `type="petrol"`.
286
+ */
287
+ export declare class PetrolEngine extends PetrolEngine_base {
288
+ }
289
+ declare const ElectricEngine_base: Omit<import('./model').XmlModelConstructor<z.ZodObject<{
290
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
291
+ }, z.core.$strip>, {
292
+ _xmlState?: import('./codec').XMLState;
293
+ }>, keyof import('..').ModelConstructor<S, Inst>> & import('..').ModelConstructor<z.ZodObject<{
294
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
295
+ type: z.ZodLiteral<"electric">;
296
+ range: z.ZodNumber;
297
+ }, z.core.$strip>, {
298
+ _xmlState?: import('./codec').XMLState;
299
+ } & {
300
+ type: "electric";
301
+ range: number;
302
+ _xmlState?: import('./codec').XMLState;
303
+ }>;
304
+ /**
305
+ * An electric engine, discriminated by `type="electric"`.
306
+ */
307
+ export declare class ElectricEngine extends ElectricEngine_base {
308
+ }
309
+ declare const UnknownEngine_base: Omit<import('./model').XmlModelConstructor<z.ZodObject<{
310
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
311
+ }, z.core.$strip>, {
312
+ _xmlState?: import('./codec').XMLState;
313
+ }>, keyof import('..').ModelConstructor<S, Inst>> & import('..').ModelConstructor<z.ZodObject<{
314
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
315
+ type: z.ZodString;
316
+ }, z.core.$strip>, {
317
+ _xmlState?: import('./codec').XMLState;
318
+ } & {
319
+ type: string;
320
+ _xmlState?: import('./codec').XMLState;
321
+ }>;
322
+ /**
323
+ * Fallback for unrecognised engine types. Unknown child elements are preserved
324
+ * through round-trips via XMLBase's state tracking.
325
+ */
326
+ export declare class UnknownEngine extends UnknownEngine_base {
327
+ }
328
+ /**
329
+ * A union that matches known engine types by discriminator and falls back to
330
+ * `UnknownEngine` for any unrecognised `type` value.
331
+ */
332
+ export declare const AnyEngine: z.ZodUnion<readonly [z.ZodDiscriminatedUnion<[z.ZodCodec<z.ZodObject<{
333
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
334
+ type: z.ZodLiteral<"petrol">;
335
+ horsepower: z.ZodNumber;
336
+ }, z.core.$strip>, z.ZodCustom<PetrolEngine, PetrolEngine>>, z.ZodCodec<z.ZodObject<{
337
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
338
+ type: z.ZodLiteral<"electric">;
339
+ range: z.ZodNumber;
340
+ }, z.core.$strip>, z.ZodCustom<ElectricEngine, ElectricEngine>>], "type">, z.ZodCodec<z.ZodObject<{
341
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
342
+ type: z.ZodString;
343
+ }, z.core.$strip>, z.ZodCustom<UnknownEngine, UnknownEngine>>]>;
344
+ declare const CarStandalone_base: Omit<import('./model').XmlModelConstructor<z.ZodObject<{
345
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
346
+ }, z.core.$strip>, {
347
+ _xmlState?: import('./codec').XMLState;
348
+ }>, keyof import('..').ModelConstructor<S, Inst>> & import('..').ModelConstructor<z.ZodObject<{
349
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
174
350
  vin: z.ZodString;
175
351
  make: z.ZodString;
176
352
  year: z.ZodNumber;
177
353
  doors: z.ZodNumber;
178
354
  engine: z.ZodCodec<z.ZodObject<{
355
+ _xmlState: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
179
356
  type: z.ZodString;
180
357
  horsepower: z.ZodNumber;
181
358
  }, z.core.$strip>, z.ZodCustom<Engine, Engine>>;
182
359
  }, z.core.$strip>, {
360
+ _xmlState?: import('./codec').XMLState;
361
+ } & {
183
362
  vin: string;
184
363
  make: string;
185
364
  year: number;
186
365
  doors: number;
366
+ _xmlState?: import('./codec').XMLState;
187
367
  engine?: Engine;
188
368
  }>;
189
369
  /**
190
- * Demonstrates the alternative to `.extend()`: passing a manually extended
191
- * schema to `xmlModel()`. This produces a **fresh class** with no prototype
192
- * link to `Vehicle` instances are **not** `instanceof Vehicle` and Vehicle's
193
- * methods are unavailable.
370
+ * Demonstrates the alternative to `Vehicle.extend()`: listing all fields
371
+ * manually inside `XMLBase.extend()`. This produces a class with the same
372
+ * XML shape as `Car` but **no prototype link to `Vehicle`** instances are
373
+ * **not** `instanceof Vehicle` and Vehicle's methods are unavailable.
194
374
  *
375
+ * Round-trip preservation still applies because the class extends `XMLBase`.
195
376
  * Use this pattern when you want a standalone class that reuses a schema shape
196
377
  * but does not need to be part of the parent class hierarchy.
197
378
  */
@@ -1,6 +1,6 @@
1
1
  export * from './xml-js';
2
2
  export { xml } from './schema-meta';
3
3
  export type { UserCodecOptions, XMLState } from './codec';
4
- export { registerDefault, normalizeCodecOptions, XMLCodecError } from './codec';
5
- export { xmlModel, XMLBase, XMLBaseWithSource, type XmlModelConstructor } from './model';
4
+ export { registerDefault, normalizeCodecOptions, XMLCodecError, xmlStateSchema } from './codec';
5
+ export { xmlModel, type XmlModelConstructor } from './model';
6
6
  //# sourceMappingURL=index.d.ts.map
@@ -14,37 +14,5 @@ export type XmlModelConstructor<S extends z.ZodObject<any> = z.ZodObject<any>, I
14
14
  /** Converts an instance to an XML string. */
15
15
  toXMLString(instance: z.infer<S>, options?: StringifyOptions): string;
16
16
  };
17
- /**
18
- * Base class for xmlModel classes. Preserves element ordering and unknown elements
19
- * through `schema.parse()` for nested model instances.
20
- *
21
- * @example
22
- * class Device extends XMLBase.extend(
23
- * { name: z.string() },
24
- * xml.root({ tagname: "device" }),
25
- * ) {}
26
- */
27
- export declare const XMLBase: XmlModelConstructor<z.ZodObject<{
28
- __xml_state: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
29
- }, z.core.$strip>, {
30
- __xml_state?: import('./codec').XMLState;
31
- }>;
32
- /**
33
- * Like {@link XMLBase}, but also records the original `XMLElement` as `.source`
34
- * on each instance's XML state.
35
- *
36
- * @example
37
- * const device = Device.fromXML(`<device>…</device>`);
38
- * device[XML_STATE_KEY]?.source; // XMLElement
39
- */
40
- export declare const XMLBaseWithSource: XmlModelConstructor<z.ZodObject<{
41
- __xml_state: z.ZodOptional<z.ZodCustom<import('./codec').XMLState & {
42
- source: XMLElement;
43
- }, unknown>>;
44
- }, z.core.$strip>, {
45
- __xml_state?: import('./codec').XMLState & {
46
- source: XMLElement;
47
- };
48
- }>;
49
17
  export declare function xmlModel<S extends z.ZodObject<any>>(schema: S, options?: UserCodecOptions<S>): XmlModelConstructor<S>;
50
18
  //# sourceMappingURL=model.d.ts.map
package/dist/xml/model.js CHANGED
@@ -1,40 +1,22 @@
1
1
  import { model } from "../model.js";
2
2
  import XML from "./xml-js.js";
3
3
  import { root } from "./schema-meta.js";
4
- import { XML_STATE_KEY, decode, encode, xmlStateSchema } from "./codec.js";
5
- import { z } from "zod";
4
+ import { decode, encode } from "./codec.js";
5
+ import "zod";
6
6
  //#region src/xml/model.ts
7
- /**
8
- * Base class for xmlModel classes. Preserves element ordering and unknown elements
9
- * through `schema.parse()` for nested model instances.
10
- *
11
- * @example
12
- * class Device extends XMLBase.extend(
13
- * { name: z.string() },
14
- * xml.root({ tagname: "device" }),
15
- * ) {}
16
- */
17
- var XMLBase = xmlModel(z.object({ [XML_STATE_KEY]: xmlStateSchema() }));
18
- /**
19
- * Like {@link XMLBase}, but also records the original `XMLElement` as `.source`
20
- * on each instance's XML state.
21
- *
22
- * @example
23
- * const device = Device.fromXML(`<device>…</device>`);
24
- * device[XML_STATE_KEY]?.source; // XMLElement
25
- */
26
- var XMLBaseWithSource = xmlModel(z.object({ [XML_STATE_KEY]: xmlStateSchema({ source: true }) }));
27
7
  function xmlModel(schema, options) {
28
8
  const _schema = options ? root(schema, options) : schema;
29
9
  return class extends model(_schema) {
30
10
  static fromXML(input) {
31
11
  if (typeof input === "string") input = XML.parse(input);
32
12
  if (XML.isRoot(input)) input = XML.elementFromRoot(input);
33
- return this.fromData(decode(this.dataSchema, input));
13
+ const rawData = decode(this.dataSchema, input);
14
+ return this.fromData(this.dataSchema.parse(rawData));
34
15
  }
35
16
  static toXML(instance) {
36
17
  const data = this.toData(instance);
37
- return { elements: [encode(this.dataSchema, data)] };
18
+ const rawData = this.dataSchema.encode(data);
19
+ return { elements: [encode(this.dataSchema, rawData)] };
38
20
  }
39
21
  static toXMLString(instance, options = {}) {
40
22
  const xml = this.toXML(instance);
@@ -43,6 +25,6 @@ function xmlModel(schema, options) {
43
25
  };
44
26
  }
45
27
  //#endregion
46
- export { XMLBase, XMLBaseWithSource, xmlModel };
28
+ export { xmlModel };
47
29
 
48
30
  //# sourceMappingURL=model.js.map
@@ -41,6 +41,21 @@ export declare function root(options: UserRootOptions): z.GlobalMeta;
41
41
  export declare function prop<PS extends z.ZodType>(schema: PS, options: UserPropOptions): PS;
42
42
  export declare function prop(options: UserPropOptions): z.GlobalMeta;
43
43
  type AttributePropOptions = {
44
+ /**
45
+ * XML attribute name. Defaults to the field key in kebab-case (the same
46
+ * conversion applied to child element tag names). Omit `name` when the
47
+ * attribute name is already the kebab-cased field key.
48
+ *
49
+ * @example
50
+ * // field key "vin" → attribute "vin" — no name needed
51
+ * vin: xml.attr(z.string())
52
+ *
53
+ * // field key "vehicleId" → attribute "vehicle-id" — no name needed (kebab-case default)
54
+ * vehicleId: xml.attr(z.string())
55
+ *
56
+ * // field key "vehicleId" → attribute "vehicle" — name required to override
57
+ * vehicleId: xml.attr(z.string(), { name: "vehicle" })
58
+ */
44
59
  name?: string;
45
60
  };
46
61
  export declare function attr<PS extends z.ZodType>(schema: PS, options?: AttributePropOptions): PS;
@@ -1,5 +1,6 @@
1
1
  import XML from "./xml-js.js";
2
2
  import { getParentSchema, isZodType } from "../util/zod.js";
3
+ import { kebabCase } from "../util/kebab-case.js";
3
4
  import "zod";
4
5
  //#region src/xml/schema-meta.ts
5
6
  var metaKey = "@@xml-model";
@@ -70,14 +71,14 @@ function attr(optionsOrSchema, options) {
70
71
  const opts = isZodType(optionsOrSchema) ? options ?? {} : optionsOrSchema ?? {};
71
72
  const partial = {
72
73
  decodeAsProperty(ctx) {
73
- const { name, options: propOptions } = ctx.property;
74
- const attrName = opts.name ?? name;
74
+ const { options: propOptions } = ctx.property;
75
+ const attrName = opts.name ?? kebabCase(ctx.property.name);
75
76
  const attrValue = ctx.xml?.attributes?.[attrName];
76
- ctx.result[name] = propOptions.schema.parse(attrValue);
77
+ ctx.result[ctx.property.name] = propOptions.schema.parse(attrValue);
77
78
  },
78
79
  encodeAsProperty(ctx) {
79
- const { value, name } = ctx.property;
80
- const attrName = opts.name ?? name;
80
+ const { value } = ctx.property;
81
+ const attrName = opts.name ?? kebabCase(ctx.property.name);
81
82
  ctx.result.attributes[attrName] = value.toString();
82
83
  }
83
84
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xml-model",
3
- "version": "2.0.0-beta.6",
3
+ "version": "2.0.0-beta.7",
4
4
  "description": "allows transparent XML <-> Object conversion in typescript",
5
5
  "license": "MIT",
6
6
  "author": "MathisTLD",
@@ -53,4 +53,4 @@
53
53
  "vitest": "npm:@voidzero-dev/vite-plus-test@latest"
54
54
  },
55
55
  "packageManager": "npm@11.12.0"
56
- }
56
+ }