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

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 { normalizeCodecOptions, registerDefault } from "./xml/codec.js";
5
- import { xmlModel } from "./xml/model.js";
4
+ import { XMLCodecError, normalizeCodecOptions, registerDefault } from "./xml/codec.js";
5
+ import { XMLBase, XMLBaseWithSource, xmlModel } from "./xml/model.js";
6
6
  import "./xml/index.js";
7
- export { DATA, XML, ZXMLCommentNode, ZXMLElementNode, ZXMLNode, ZXMLRoot, ZXMLTextNode, isModel, model, normalizeCodecOptions, registerDefault, xml, xmlModel };
7
+ export { DATA, XML, XMLBase, XMLBaseWithSource, XMLCodecError, ZXMLCommentNode, ZXMLElementNode, ZXMLNode, ZXMLRoot, ZXMLTextNode, isModel, model, normalizeCodecOptions, registerDefault, xml, xmlModel };
@@ -49,6 +49,7 @@ var require_sax = /* @__PURE__ */ __commonJSMin(((exports) => {
49
49
  clearBuffers(parser);
50
50
  parser.q = parser.c = "";
51
51
  parser.bufferCheckPosition = sax.MAX_BUFFER_LENGTH;
52
+ parser.encoding = null;
52
53
  parser.opt = opt || {};
53
54
  parser.opt.lowercase = parser.opt.lowercase || parser.opt.lowercasetags;
54
55
  parser.looseCase = parser.opt.lowercase ? "toLowerCase" : "toUpperCase";
@@ -146,6 +147,19 @@ var require_sax = /* @__PURE__ */ __commonJSMin(((exports) => {
146
147
  function createStream(strict, opt) {
147
148
  return new SAXStream(strict, opt);
148
149
  }
150
+ function determineBufferEncoding(data, isEnd) {
151
+ if (data.length >= 2) {
152
+ if (data[0] === 255 && data[1] === 254) return "utf-16le";
153
+ if (data[0] === 254 && data[1] === 255) return "utf-16be";
154
+ }
155
+ if (data.length >= 3 && data[0] === 239 && data[1] === 187 && data[2] === 191) return "utf8";
156
+ if (data.length >= 4) {
157
+ if (data[0] === 60 && data[1] === 0 && data[2] === 63 && data[3] === 0) return "utf-16le";
158
+ if (data[0] === 0 && data[1] === 60 && data[2] === 0 && data[3] === 63) return "utf-16be";
159
+ return "utf8";
160
+ }
161
+ return isEnd ? "utf8" : null;
162
+ }
149
163
  function SAXStream(strict, opt) {
150
164
  if (!(this instanceof SAXStream)) return new SAXStream(strict, opt);
151
165
  Stream.apply(this);
@@ -161,6 +175,7 @@ var require_sax = /* @__PURE__ */ __commonJSMin(((exports) => {
161
175
  me._parser.error = null;
162
176
  };
163
177
  this._decoder = null;
178
+ this._decoderBuffer = null;
164
179
  streamWraps.forEach(function(ev) {
165
180
  Object.defineProperty(me, "on" + ev, {
166
181
  get: function() {
@@ -180,10 +195,30 @@ var require_sax = /* @__PURE__ */ __commonJSMin(((exports) => {
180
195
  });
181
196
  }
182
197
  SAXStream.prototype = Object.create(Stream.prototype, { constructor: { value: SAXStream } });
198
+ SAXStream.prototype._decodeBuffer = function(data, isEnd) {
199
+ if (this._decoderBuffer) {
200
+ data = Buffer.concat([this._decoderBuffer, data]);
201
+ this._decoderBuffer = null;
202
+ }
203
+ if (!this._decoder) {
204
+ var encoding = determineBufferEncoding(data, isEnd);
205
+ if (!encoding) {
206
+ this._decoderBuffer = data;
207
+ return "";
208
+ }
209
+ this._parser.encoding = encoding;
210
+ this._decoder = new TextDecoder(encoding);
211
+ }
212
+ return this._decoder.decode(data, { stream: !isEnd });
213
+ };
183
214
  SAXStream.prototype.write = function(data) {
184
- if (typeof Buffer === "function" && typeof Buffer.isBuffer === "function" && Buffer.isBuffer(data)) {
185
- if (!this._decoder) this._decoder = new TextDecoder("utf8");
186
- data = this._decoder.decode(data, { stream: true });
215
+ if (typeof Buffer === "function" && typeof Buffer.isBuffer === "function" && Buffer.isBuffer(data)) data = this._decodeBuffer(data, false);
216
+ else if (this._decoderBuffer) {
217
+ var remaining = this._decodeBuffer(Buffer.alloc(0), true);
218
+ if (remaining) {
219
+ this._parser.write(remaining);
220
+ this.emit("data", remaining);
221
+ }
187
222
  }
188
223
  this._parser.write(data.toString());
189
224
  this.emit("data", data);
@@ -191,7 +226,13 @@ var require_sax = /* @__PURE__ */ __commonJSMin(((exports) => {
191
226
  };
192
227
  SAXStream.prototype.end = function(chunk) {
193
228
  if (chunk && chunk.length) this.write(chunk);
194
- if (this._decoder) {
229
+ if (this._decoderBuffer) {
230
+ var finalChunk = this._decodeBuffer(Buffer.alloc(0), true);
231
+ if (finalChunk) {
232
+ this._parser.write(finalChunk);
233
+ this.emit("data", finalChunk);
234
+ }
235
+ } else if (this._decoder) {
195
236
  var remaining = this._decoder.decode();
196
237
  if (remaining) {
197
238
  this._parser.write(remaining);
@@ -548,6 +589,26 @@ var require_sax = /* @__PURE__ */ __commonJSMin(((exports) => {
548
589
  function emit(parser, event, data) {
549
590
  parser[event] && parser[event](data);
550
591
  }
592
+ function getDeclaredEncoding(body) {
593
+ var match = body && body.match(/(?:^|\s)encoding\s*=\s*(['"])([^'"]+)\1/i);
594
+ return match ? match[2] : null;
595
+ }
596
+ function normalizeEncodingName(encoding) {
597
+ if (!encoding) return null;
598
+ return encoding.toLowerCase().replace(/[^a-z0-9]/g, "");
599
+ }
600
+ function encodingsMatch(detectedEncoding, declaredEncoding) {
601
+ const detected = normalizeEncodingName(detectedEncoding);
602
+ const declared = normalizeEncodingName(declaredEncoding);
603
+ if (!detected || !declared) return true;
604
+ if (declared === "utf16") return detected === "utf16le" || detected === "utf16be";
605
+ return detected === declared;
606
+ }
607
+ function validateXmlDeclarationEncoding(parser, data) {
608
+ if (!parser.strict || !parser.encoding || !data || data.name !== "xml") return;
609
+ var declaredEncoding = getDeclaredEncoding(data.body);
610
+ if (declaredEncoding && !encodingsMatch(parser.encoding, declaredEncoding)) strictFail(parser, "XML declaration encoding " + declaredEncoding + " does not match detected stream encoding " + parser.encoding.toUpperCase());
611
+ }
551
612
  function emitNode(parser, nodeType, data) {
552
613
  if (parser.textNode) closeText(parser);
553
614
  emit(parser, nodeType, data);
@@ -1014,10 +1075,12 @@ var require_sax = /* @__PURE__ */ __commonJSMin(((exports) => {
1014
1075
  continue;
1015
1076
  case S.PROC_INST_ENDING:
1016
1077
  if (c === ">") {
1017
- emitNode(parser, "onprocessinginstruction", {
1078
+ const procInstEndData = {
1018
1079
  name: parser.procInstName,
1019
1080
  body: parser.procInstBody
1020
- });
1081
+ };
1082
+ validateXmlDeclarationEncoding(parser, procInstEndData);
1083
+ emitNode(parser, "onprocessinginstruction", procInstEndData);
1021
1084
  parser.procInstName = parser.procInstBody = "";
1022
1085
  parser.state = S.TEXT;
1023
1086
  } else {
package/dist/util/zod.js CHANGED
@@ -9,6 +9,8 @@ function getParentSchema(schema) {
9
9
  if (isZodType(schema.def.in)) parent = schema.def.in;
10
10
  } else if (schema instanceof z.ZodOptional) {
11
11
  if (isZodType(schema.def.innerType)) parent = schema.def.innerType;
12
+ } else if (schema instanceof z.ZodDefault) {
13
+ if (isZodType(schema.def.innerType)) parent = schema.def.innerType;
12
14
  } else if (schema instanceof z.ZodLazy) {
13
15
  const value = schema.def.getter();
14
16
  if (isZodType(value)) parent = value;
@@ -1,5 +1,10 @@
1
1
  import { z } from 'zod';
2
2
  import { XMLElement } from './xml-js';
3
+ export declare class XMLCodecError extends Error {
4
+ readonly path: readonly (string | number)[];
5
+ readonly rawMessage: string;
6
+ constructor(rawMessage: string, path?: readonly (string | number)[], options?: ErrorOptions);
7
+ }
3
8
  export declare function assertSingleElement(xml: XMLElement[]): asserts xml is [XMLElement];
4
9
  export declare function assertSingleRoot(xml: XMLElement[]): asserts xml is [XMLElement & {
5
10
  elements: XMLElement[];
@@ -32,8 +37,8 @@ export interface CodecOptions<S extends z.ZodType> {
32
37
  */
33
38
  export type UserCodecOptions<S extends z.ZodType = z.ZodType> = {
34
39
  tagname?: string | CodecOptions<S>["tagname"];
35
- decode?: CodecOptions<S>["decode"];
36
- encode?: CodecOptions<S>["encode"];
40
+ decode?: (ctx: RootDecodingContext<S>, next: () => z.input<S>) => z.input<S>;
41
+ encode?: (ctx: RootEncodingContext<S>, next: () => XMLElement) => XMLElement;
37
42
  propertyTagname?: string | CodecOptions<S>["propertyTagname"];
38
43
  inlineProperty?: boolean;
39
44
  propertyMatch?: RegExp | CodecOptions<S>["propertyMatch"];
@@ -71,13 +76,40 @@ export declare function normalizeCodecOptions<S extends z.ZodType>(schema: S, op
71
76
  type OrderEntry = string | XMLElement;
72
77
  export interface XMLState {
73
78
  /** Preserves element ordering and unknown elements across a decode → encode round-trip. */
74
- fieldOrder: OrderEntry[];
79
+ sequence: OrderEntry[];
80
+ /** Present when xmlStateSchema({ source: true }) is used: the original XMLElement. */
81
+ source?: XMLElement;
75
82
  }
76
83
  /**
77
- * Non-enumerable Symbol attached to decoded data objects (and forwarded to model instances).
78
- * Groups all XML codec round-trip state under a single key.
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
+ /**
90
+ * Schema for the XML round-trip state field.
91
+ *
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.
94
+ *
95
+ * Pass `{ source: true }` to also record the original `XMLElement` on each instance.
96
+ *
97
+ * @example
98
+ * class XMLBase extends xmlModel(z.object({
99
+ * [XML_STATE_KEY]: xmlStateSchema(),
100
+ * }), { tagname: "base" }) {}
101
+ *
102
+ * // With source recording:
103
+ * class XMLBase extends xmlModel(z.object({
104
+ * [XML_STATE_KEY]: xmlStateSchema({ source: true }),
105
+ * }), { tagname: "base" }) {}
79
106
  */
80
- export declare const XML_STATE: unique symbol;
107
+ export declare function xmlStateSchema(): z.ZodOptional<z.ZodCustom<XMLState>>;
108
+ export declare function xmlStateSchema(options: {
109
+ source: true;
110
+ }): z.ZodOptional<z.ZodCustom<XMLState & {
111
+ source: XMLElement;
112
+ }>>;
81
113
  export declare function decode<S extends z.ZodType>(schema: S, xml: XMLElement): z.input<S>;
82
114
  export declare function encode<S extends z.ZodType>(schema: S, data: z.output<S>): XMLElement;
83
115
  type DefaultResolver<S extends z.ZodType = z.ZodType> = (schema: S) => CodecOptions<S> | void;
package/dist/xml/codec.js CHANGED
@@ -1,9 +1,24 @@
1
1
  import XML from "./xml-js.js";
2
2
  import { isZodType } from "../util/zod.js";
3
- import { getUserOptions } from "./schema-meta.js";
3
+ import { getUserOptions, prop } from "./schema-meta.js";
4
4
  import { kebabCase } from "../util/kebab-case.js";
5
5
  import { z } from "zod";
6
6
  //#region src/xml/codec.ts
7
+ var XMLCodecError = class extends Error {
8
+ path;
9
+ rawMessage;
10
+ constructor(rawMessage, path = [], options) {
11
+ super(path.length ? `[${path.join(".")}] ${rawMessage}` : rawMessage, options);
12
+ this.name = "XMLCodecError";
13
+ this.path = path;
14
+ this.rawMessage = rawMessage;
15
+ }
16
+ };
17
+ function rethrow(e, segment) {
18
+ const cause = e instanceof XMLCodecError ? e.cause : e;
19
+ const path = e instanceof XMLCodecError ? [segment, ...e.path] : [segment];
20
+ throw new XMLCodecError(e instanceof XMLCodecError ? e.rawMessage : e instanceof Error ? e.message : String(e), path, { cause });
21
+ }
7
22
  function assertSingleElement(xml) {
8
23
  if (xml.length !== 1) throw new Error(`Expected single XML element, got ${xml.length}`);
9
24
  }
@@ -28,8 +43,8 @@ function normalizeCodecOptions(schema, options = {}) {
28
43
  const result = {
29
44
  schema,
30
45
  tagname,
31
- decode: options.decode ?? defaultOptions().decode,
32
- encode: options.encode ?? defaultOptions().encode,
46
+ decode: options.decode ? (ctx) => options.decode(ctx, () => defaultOptions().decode(ctx)) : (ctx) => defaultOptions().decode(ctx),
47
+ encode: options.encode ? (ctx) => options.encode(ctx, () => defaultOptions().encode(ctx)) : (ctx) => defaultOptions().encode(ctx),
33
48
  propertyTagname,
34
49
  inlineProperty,
35
50
  propertyMatch,
@@ -51,7 +66,10 @@ function normalizeCodecOptions(schema, options = {}) {
51
66
  data: property.value
52
67
  });
53
68
  if (XML.isEmpty(res)) return;
54
- if (property.options.inlineProperty) ctx.result.elements.push(...res.elements);
69
+ if (property.options.inlineProperty) ctx.result.elements.push(...res.elements.map((el) => el.type === "element" ? {
70
+ ...el,
71
+ name: property.tagname
72
+ } : el));
55
73
  else ctx.result.elements.push(res);
56
74
  }
57
75
  };
@@ -66,10 +84,19 @@ function resolveCodecOptions(schema) {
66
84
  return options;
67
85
  }
68
86
  /**
69
- * Non-enumerable Symbol attached to decoded data objects (and forwarded to model instances).
70
- * Groups all XML codec round-trip state under a single key.
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().
71
90
  */
72
- var XML_STATE = Symbol("xml-model.state");
91
+ var XML_STATE_KEY = "__xml_state";
92
+ function xmlStateSchema(options) {
93
+ return prop(z.custom().optional(), {
94
+ decode: options?.source ? (ctx, _next) => {
95
+ (ctx.result[XML_STATE_KEY] ??= {}).source = ctx.xml;
96
+ } : () => {},
97
+ encode: () => {}
98
+ });
99
+ }
73
100
  function resolvePropertiesCodecOptions(schema) {
74
101
  const shape = schema.def.shape;
75
102
  const options = {};
@@ -103,7 +130,10 @@ function resolveDefault(schema) {
103
130
  }
104
131
  registerDefault((schema) => {
105
132
  if (schema instanceof z.ZodArray) {
106
- const elOptions = resolveCodecOptions(schema.def.element);
133
+ const elSchema = schema.def.element;
134
+ if (!isZodType(elSchema)) throw new Error(`Expected a ZodType, got ${elSchema}`);
135
+ const elOptions = resolveCodecOptions(elSchema);
136
+ const elHasOwnTagname = Boolean(getUserOptions(elSchema).tagname);
107
137
  return normalizeCodecOptions(schema, {
108
138
  decode(ctx) {
109
139
  const { xml } = ctx;
@@ -116,12 +146,16 @@ registerDefault((schema) => {
116
146
  encode(ctx) {
117
147
  const values = ctx.data;
118
148
  if (!Array.isArray(values)) throw new Error("expected array");
149
+ const elOptsForEncode = elHasOwnTagname ? elOptions : {
150
+ ...elOptions,
151
+ tagname: ctx.options.tagname
152
+ };
119
153
  return {
120
154
  type: "element",
121
155
  name: ctx.options.tagname(ctx),
122
156
  attributes: {},
123
- elements: values.map((v) => elOptions.encode({
124
- options: elOptions,
157
+ elements: values.map((v) => elOptsForEncode.encode({
158
+ options: elOptsForEncode,
125
159
  data: v
126
160
  }))
127
161
  };
@@ -134,12 +168,18 @@ registerDefault((schema) => {
134
168
  const innerOptions = resolveCodecOptions(inner);
135
169
  return normalizeCodecOptions(schema, {
136
170
  decode(ctx) {
137
- if (!ctx.xml) return void 0;
171
+ if (ctx.xml === null) return void 0;
138
172
  else return innerOptions.decode(ctx);
139
173
  },
140
174
  encode(ctx) {
141
175
  if (typeof ctx.data === "undefined") return {};
142
176
  else return innerOptions.encode(ctx);
177
+ },
178
+ decodeAsProperty(ctx) {
179
+ if (ctx.property.xml !== null) innerOptions.decodeAsProperty(ctx);
180
+ },
181
+ encodeAsProperty(ctx) {
182
+ if (typeof ctx.property.value !== "undefined") innerOptions.encodeAsProperty(ctx);
143
183
  }
144
184
  });
145
185
  }
@@ -181,7 +221,10 @@ registerDefault((schema) => {
181
221
  });
182
222
  },
183
223
  encode(ctx) {
184
- const data = outSchema.encode(ctx.data);
224
+ const data = schema.def.reverseTransform ? schema.def.reverseTransform(ctx.data, {
225
+ value: ctx.data,
226
+ issues: []
227
+ }) : outSchema.encode(ctx.data);
185
228
  const innerOpts = ctx.options.tagname !== inputCodecOptions.tagname ? {
186
229
  ...inputCodecOptions,
187
230
  tagname: ctx.options.tagname
@@ -238,6 +281,7 @@ registerDefault((schema) => {
238
281
  return normalizeCodecOptions(schema, {
239
282
  decode(ctx) {
240
283
  const sequence = [];
284
+ const result = { [XML_STATE_KEY]: { sequence } };
241
285
  const propContexts = Object.fromEntries(Object.entries(options).map(([name, propOpts]) => {
242
286
  return [name, {
243
287
  name,
@@ -274,7 +318,6 @@ registerDefault((schema) => {
274
318
  } else throw new Error(`Same element was matched by multiple properties: ${matches.join(", ")}`);
275
319
  }
276
320
  for (const propName in options) if (!seenProperties.has(propName)) sequence.push(propName);
277
- const result = {};
278
321
  for (const prop in options) {
279
322
  const o = options[prop];
280
323
  const propCtx = propContexts[prop];
@@ -286,19 +329,17 @@ registerDefault((schema) => {
286
329
  propCtx.xml = matches[0];
287
330
  }
288
331
  }
289
- o.decodeAsProperty({
290
- options: ctx.options,
291
- xml: ctx.xml,
292
- property: propCtx,
293
- result
294
- });
332
+ try {
333
+ o.decodeAsProperty({
334
+ options: ctx.options,
335
+ xml: ctx.xml,
336
+ property: propCtx,
337
+ result
338
+ });
339
+ } catch (e) {
340
+ rethrow(e, prop);
341
+ }
295
342
  }
296
- Object.defineProperty(result, XML_STATE, {
297
- value: { fieldOrder: sequence },
298
- enumerable: false,
299
- writable: true,
300
- configurable: true
301
- });
302
343
  return result;
303
344
  },
304
345
  encode(ctx) {
@@ -309,24 +350,28 @@ registerDefault((schema) => {
309
350
  attributes: {},
310
351
  elements: []
311
352
  };
312
- const sequence = data[XML_STATE]?.fieldOrder ?? Object.keys(options);
353
+ const sequence = data["__xml_state"]?.sequence ?? Object.keys(options);
313
354
  for (const item of sequence) if (typeof item === "string") {
314
355
  const o = options[item];
315
356
  if (!o) throw new Error(`Failed to resolve property options for sequence item ${item}`);
316
- o.encodeAsProperty({
317
- options: ctx.options,
318
- data,
319
- property: {
320
- name: item,
321
- options: o,
322
- tagname: o.propertyTagname({
357
+ try {
358
+ o.encodeAsProperty({
359
+ options: ctx.options,
360
+ data,
361
+ property: {
323
362
  name: item,
324
- options: o
325
- }),
326
- value: data[item]
327
- },
328
- result
329
- });
363
+ options: o,
364
+ tagname: o.propertyTagname({
365
+ name: item,
366
+ options: o
367
+ }),
368
+ value: data[item]
369
+ },
370
+ result
371
+ });
372
+ } catch (e) {
373
+ rethrow(e, item);
374
+ }
330
375
  } else result.elements.push(item);
331
376
  return result;
332
377
  }
@@ -334,6 +379,6 @@ registerDefault((schema) => {
334
379
  }
335
380
  });
336
381
  //#endregion
337
- export { XML_STATE, decode, encode, normalizeCodecOptions, registerDefault };
382
+ export { XMLCodecError, XML_STATE_KEY, decode, encode, normalizeCodecOptions, registerDefault, xmlStateSchema };
338
383
 
339
384
  //# sourceMappingURL=codec.js.map
@@ -1,4 +1,17 @@
1
1
  import { z } from 'zod';
2
+ declare const Event_base: import('./model').XmlModelConstructor<z.ZodObject<{
3
+ title: z.ZodString;
4
+ publishedAt: z.ZodCodec<z.ZodString, z.ZodDate>;
5
+ }, z.core.$strip>, {
6
+ title: string;
7
+ publishedAt?: Date;
8
+ }>;
9
+ /**
10
+ * An event with a typed `Date` field stored as an ISO 8601 string in XML.
11
+ * Demonstrates using `z.codec` to transform a raw XML string into a native JS type.
12
+ */
13
+ export declare class Event extends Event_base {
14
+ }
2
15
  declare const Engine_base: import('./model').XmlModelConstructor<z.ZodObject<{
3
16
  type: z.ZodString;
4
17
  horsepower: z.ZodNumber;
@@ -1,6 +1,6 @@
1
1
  export * from './xml-js';
2
2
  export { xml } from './schema-meta';
3
- export type { UserCodecOptions } from './codec';
4
- export { registerDefault, normalizeCodecOptions } from './codec';
5
- export { xmlModel, type XmlModelConstructor } from './model';
3
+ export type { UserCodecOptions, XMLState } from './codec';
4
+ export { registerDefault, normalizeCodecOptions, XMLCodecError } from './codec';
5
+ export { xmlModel, XMLBase, XMLBaseWithSource, type XmlModelConstructor } from './model';
6
6
  //# sourceMappingURL=index.d.ts.map
@@ -14,5 +14,37 @@ 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
+ }>;
17
49
  export declare function xmlModel<S extends z.ZodObject<any>>(schema: S, options?: UserCodecOptions<S>): XmlModelConstructor<S>;
18
50
  //# sourceMappingURL=model.d.ts.map
package/dist/xml/model.js CHANGED
@@ -1,21 +1,36 @@
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, decode, encode } from "./codec.js";
5
- import "zod";
4
+ import { XML_STATE_KEY, decode, encode, xmlStateSchema } from "./codec.js";
5
+ import { z } from "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 }) }));
7
27
  function xmlModel(schema, options) {
8
28
  const _schema = options ? root(schema, options) : schema;
9
29
  return class extends model(_schema) {
10
30
  static fromXML(input) {
11
31
  if (typeof input === "string") input = XML.parse(input);
12
32
  if (XML.isRoot(input)) input = XML.elementFromRoot(input);
13
- const schema = this.dataSchema;
14
- const inputData = decode(this.dataSchema, input);
15
- const xmlState = inputData[XML_STATE];
16
- const parsed = schema.parse(inputData);
17
- parsed[XML_STATE] = xmlState;
18
- return this.fromData(parsed);
33
+ return this.fromData(decode(this.dataSchema, input));
19
34
  }
20
35
  static toXML(instance) {
21
36
  const data = this.toData(instance);
@@ -28,6 +43,6 @@ function xmlModel(schema, options) {
28
43
  };
29
44
  }
30
45
  //#endregion
31
- export { xmlModel };
46
+ export { XMLBase, XMLBaseWithSource, xmlModel };
32
47
 
33
48
  //# sourceMappingURL=model.js.map
@@ -15,8 +15,8 @@ type UserPropOptions = {
15
15
  tagname?: string | UserCodecOptions["propertyTagname"];
16
16
  inline?: boolean;
17
17
  match?: RegExp | ((el: XMLElement) => boolean);
18
- decode?: (ctx: PropertyDecodingContext) => Partial<Record<string, unknown>> | undefined;
19
- encode?: (ctx: PropertyEncodingContext) => XMLElement | undefined;
18
+ decode?: (ctx: PropertyDecodingContext, next: () => void) => void;
19
+ encode?: (ctx: PropertyEncodingContext, next: () => void) => void;
20
20
  };
21
21
  export declare function root<S extends z.ZodType>(schema: S, options: UserRootOptions<S>): S;
22
22
  export declare function root(options: UserRootOptions): z.GlobalMeta;
@@ -1,3 +1,4 @@
1
+ import XML from "./xml-js.js";
1
2
  import { getParentSchema, isZodType } from "../util/zod.js";
2
3
  import "zod";
3
4
  //#region src/xml/schema-meta.ts
@@ -19,21 +20,40 @@ function normalizePropOptions(options) {
19
20
  if (options.decode !== void 0) {
20
21
  const userDecode = options.decode;
21
22
  partial.decodeAsProperty = function(ctx) {
22
- const res = userDecode(ctx);
23
- if (typeof res !== "undefined") Object.assign(ctx.result, res);
23
+ const next = () => {
24
+ const val = ctx.property.options.decode({
25
+ options: ctx.property.options,
26
+ xml: ctx.property.xml
27
+ });
28
+ ctx.result[ctx.property.name] = val;
29
+ };
30
+ userDecode(ctx, next);
24
31
  };
25
32
  }
26
33
  if (options.encode !== void 0) {
27
34
  const userEncode = options.encode;
28
35
  partial.encodeAsProperty = function(ctx) {
29
36
  const { property } = ctx;
30
- const res = userEncode(ctx);
31
- if (typeof res === "undefined") return;
32
- if (property.options.inlineProperty) ctx.result.elements.push(...res.elements);
33
- else {
34
- res.name = property.tagname;
35
- ctx.result.elements.push(res);
36
- }
37
+ const next = () => {
38
+ const optsWithTagname = {
39
+ ...property.options,
40
+ tagname: () => property.tagname
41
+ };
42
+ const res = property.options.encode({
43
+ options: optsWithTagname,
44
+ data: property.value
45
+ });
46
+ if (XML.isEmpty(res)) return;
47
+ if (property.options.inlineProperty) ctx.result.elements.push(...res.elements.map((el) => el.type === "element" ? {
48
+ ...el,
49
+ name: property.tagname
50
+ } : el));
51
+ else ctx.result.elements.push({
52
+ ...res,
53
+ name: property.tagname
54
+ });
55
+ };
56
+ userEncode(ctx, next);
37
57
  };
38
58
  }
39
59
  return partial;
@@ -52,7 +72,7 @@ function attr(optionsOrSchema, options) {
52
72
  decodeAsProperty(ctx) {
53
73
  const { name, options: propOptions } = ctx.property;
54
74
  const attrName = opts.name ?? name;
55
- const attrValue = ctx.xml?.attributes[attrName];
75
+ const attrValue = ctx.xml?.attributes?.[attrName];
56
76
  ctx.result[name] = propOptions.schema.parse(attrValue);
57
77
  },
58
78
  encodeAsProperty(ctx) {
@@ -73,16 +93,27 @@ var xml = {
73
93
  function getOwnUserOptions(schema) {
74
94
  return schema.meta()?.[metaKey] ?? {};
75
95
  }
96
+ var INHERITABLE_KEYS = [
97
+ "tagname",
98
+ "propertyTagname",
99
+ "inlineProperty",
100
+ "propertyMatch",
101
+ "decodeAsProperty",
102
+ "encodeAsProperty"
103
+ ];
76
104
  function getUserOptions(schema) {
77
105
  const own = getOwnUserOptions(schema);
78
106
  const parentSchema = getParentSchema(schema);
79
107
  if (!parentSchema) return own;
108
+ const parentOptions = getUserOptions(parentSchema);
109
+ const inherited = {};
110
+ for (const key of INHERITABLE_KEYS) if (parentOptions[key] !== void 0 && own[key] === void 0) inherited[key] = parentOptions[key];
80
111
  return {
81
- ...getUserOptions(parentSchema),
112
+ ...inherited,
82
113
  ...own
83
114
  };
84
115
  }
85
116
  //#endregion
86
- export { getUserOptions, root, xml };
117
+ export { getUserOptions, prop, root, xml };
87
118
 
88
119
  //# sourceMappingURL=schema-meta.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xml-model",
3
- "version": "2.0.0-beta.4",
3
+ "version": "2.0.0-beta.6",
4
4
  "description": "allows transparent XML <-> Object conversion in typescript",
5
5
  "license": "MIT",
6
6
  "author": "MathisTLD",
@@ -34,10 +34,10 @@
34
34
  "fmt:check": "vp fmt --check",
35
35
  "prepare": "vp config"
36
36
  },
37
- "dependencies": {},
38
37
  "devDependencies": {
39
38
  "@types/node": "^24.10.11",
40
39
  "marmotte": "^0.4.4",
40
+ "oxc-minify": "^0.121.0",
41
41
  "typescript": "^5.9.3",
42
42
  "vite-plus": "latest",
43
43
  "vitepress": "^2.0.0-alpha.16",
@@ -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
+ }