xml-model 0.1.0
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/.eslintignore +3 -0
- package/.eslintrc.js +6 -0
- package/.gitignore +2 -0
- package/.vscode/launch.json +21 -0
- package/.vscode/settings.json +3 -0
- package/README.md +7 -0
- package/build/main/defaults/index.d.ts +14 -0
- package/build/main/defaults/index.js +165 -0
- package/build/main/defaults/models.d.ts +1 -0
- package/build/main/defaults/models.js +55 -0
- package/build/main/index.d.ts +5 -0
- package/build/main/index.js +31 -0
- package/build/main/middleware.d.ts +9 -0
- package/build/main/middleware.js +65 -0
- package/build/main/model/index.d.ts +26 -0
- package/build/main/model/index.js +304 -0
- package/build/main/model/property.d.ts +5 -0
- package/build/main/model/property.js +95 -0
- package/build/main/model/types.d.ts +69 -0
- package/build/main/model/types.js +214 -0
- package/build/main/model.spec.d.ts +1 -0
- package/build/main/model.spec.js +261 -0
- package/build/main/types.d.ts +8 -0
- package/build/main/types.js +18 -0
- package/build/main/xml.d.ts +31 -0
- package/build/main/xml.js +95 -0
- package/build/module/defaults/index.d.ts +13 -0
- package/build/module/defaults/index.js +71 -0
- package/build/module/defaults/models.d.ts +1 -0
- package/build/module/defaults/models.js +27 -0
- package/build/module/errors.d.ts +3 -0
- package/build/module/errors.js +37 -0
- package/build/module/index.d.ts +4 -0
- package/build/module/index.js +19 -0
- package/build/module/middleware.d.ts +9 -0
- package/build/module/middleware.js +60 -0
- package/build/module/model.d.ts +71 -0
- package/build/module/model.js +466 -0
- package/build/module/model.spec.d.ts +1 -0
- package/build/module/model.spec.js +76 -0
- package/build/module/types.d.ts +13 -0
- package/build/module/types.js +40 -0
- package/build/module/xml-from-object.d.ts +11 -0
- package/build/module/xml-from-object.js +58 -0
- package/build/module/xml-from-object.spec.d.ts +1 -0
- package/build/module/xml-from-object.spec.js +104 -0
- package/build/module/xml-to-object.d.ts +11 -0
- package/build/module/xml-to-object.js +55 -0
- package/build/module/xml.d.ts +15 -0
- package/build/module/xml.js +74 -0
- package/package.json +50 -0
- package/register-ts-node.js +9 -0
- package/src/defaults/index.ts +181 -0
- package/src/defaults/models.ts +45 -0
- package/src/index.ts +6 -0
- package/src/middleware.ts +34 -0
- package/src/model/index.ts +245 -0
- package/src/model/property.ts +104 -0
- package/src/model/types.ts +99 -0
- package/src/model.spec.ts +178 -0
- package/src/types.ts +8 -0
- package/src/xml.ts +80 -0
- package/tsconfig.json +106 -0
- package/tsconfig.module.json +9 -0
- package/yarn.lock +2217 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import type { Constructor } from "typescript-rtti";
|
|
3
|
+
import { reflect } from "typescript-rtti";
|
|
4
|
+
|
|
5
|
+
import { MiddlewareChain, resolve } from "../middleware";
|
|
6
|
+
import {
|
|
7
|
+
XMLModelProperty,
|
|
8
|
+
XMLModelOptions,
|
|
9
|
+
XMLModelPropertyOptions,
|
|
10
|
+
PropertiesRecord,
|
|
11
|
+
XMLPropertiesRecord,
|
|
12
|
+
} from "./types";
|
|
13
|
+
import { getPropertyConversionOptions } from "./property";
|
|
14
|
+
import { XMLRoot } from "../types";
|
|
15
|
+
import XML from "../xml";
|
|
16
|
+
import { defaults } from "../defaults";
|
|
17
|
+
|
|
18
|
+
function* ParentChain(constructor: Constructor<unknown>) {
|
|
19
|
+
let parent = Object.getPrototypeOf(constructor);
|
|
20
|
+
if (parent === constructor) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
while (parent) {
|
|
24
|
+
yield parent as Constructor<unknown>;
|
|
25
|
+
const _parent = Object.getPrototypeOf(constructor);
|
|
26
|
+
if (parent === _parent) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
parent = _parent;
|
|
30
|
+
}
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getParentModel(model: XMLModel<any>) {
|
|
35
|
+
for (const constructor of ParentChain(model.type)) {
|
|
36
|
+
const model = findModel(constructor);
|
|
37
|
+
if (model) {
|
|
38
|
+
return model as XMLModel<any>;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface XMLModelConversionOptions<T> {
|
|
45
|
+
fromXML?: XMLModelOptions<T>["fromXML"]["middlewares"][number];
|
|
46
|
+
tagname?: string;
|
|
47
|
+
toXML?: XMLModelOptions<T>["toXML"]["middlewares"][number];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class XMLModel<T = any> {
|
|
51
|
+
options: XMLModelOptions<T>;
|
|
52
|
+
constructor(
|
|
53
|
+
readonly type: Constructor<T>,
|
|
54
|
+
options: XMLModelConversionOptions<T>
|
|
55
|
+
) {
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
57
|
+
const model = this;
|
|
58
|
+
let parent: XMLModel<any> | null | undefined = undefined;
|
|
59
|
+
const getParent = () => {
|
|
60
|
+
if (typeof parent === "undefined") parent = getParentModel(this);
|
|
61
|
+
return parent;
|
|
62
|
+
};
|
|
63
|
+
let propertiesLoaded = false;
|
|
64
|
+
const properties: XMLModelOptions<T>["properties"] = {
|
|
65
|
+
options: new Map<XMLModelProperty<T>, XMLModelPropertyOptions<T>>(),
|
|
66
|
+
fromXML: {
|
|
67
|
+
get parent() {
|
|
68
|
+
return getParent()?.options.properties.fromXML || null;
|
|
69
|
+
},
|
|
70
|
+
middlewares: [
|
|
71
|
+
(context, next) => {
|
|
72
|
+
const record: PropertiesRecord<T> = getParent() ? next() : {};
|
|
73
|
+
properties.options.forEach((property) => {
|
|
74
|
+
const xml = context.xml;
|
|
75
|
+
const elements = property.resolveElements({
|
|
76
|
+
model,
|
|
77
|
+
xml,
|
|
78
|
+
property,
|
|
79
|
+
});
|
|
80
|
+
record[property.name] = property.fromXML({
|
|
81
|
+
model,
|
|
82
|
+
xml: context.xml,
|
|
83
|
+
property,
|
|
84
|
+
elements,
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
return record;
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
toXML: {
|
|
92
|
+
get parent() {
|
|
93
|
+
return getParent()?.options.properties.toXML || null;
|
|
94
|
+
},
|
|
95
|
+
middlewares: [
|
|
96
|
+
(context, next) => {
|
|
97
|
+
const record: XMLPropertiesRecord<T> = getParent() ? next() : {};
|
|
98
|
+
properties.options.forEach((options) => {
|
|
99
|
+
record[options.name] = options.toXML({
|
|
100
|
+
model,
|
|
101
|
+
object: context.object,
|
|
102
|
+
|
|
103
|
+
property: options,
|
|
104
|
+
value: context.object[options.name],
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
return record;
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
const loadProperties = () => {
|
|
113
|
+
const props = reflect(this.type).ownProperties.filter(
|
|
114
|
+
(prop) =>
|
|
115
|
+
typeof prop.host.constructor.prototype[prop.name] !== "function"
|
|
116
|
+
); // filter out methods like String.prototype.concat etc... that are seen as properties
|
|
117
|
+
|
|
118
|
+
props.forEach((property) => {
|
|
119
|
+
const options = getPropertyConversionOptions(
|
|
120
|
+
this.type,
|
|
121
|
+
property.name as XMLModelProperty<T>
|
|
122
|
+
);
|
|
123
|
+
if (!options.ignored) {
|
|
124
|
+
properties.options.set(property.name as XMLModelProperty<T>, options);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
propertiesLoaded = true;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
this.options = {
|
|
131
|
+
get properties() {
|
|
132
|
+
if (!propertiesLoaded) loadProperties();
|
|
133
|
+
return properties;
|
|
134
|
+
},
|
|
135
|
+
fromXML: {
|
|
136
|
+
middlewares: [],
|
|
137
|
+
get parent() {
|
|
138
|
+
return getParent()?.options.fromXML || null;
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
toXML: {
|
|
142
|
+
middlewares: [],
|
|
143
|
+
get parent() {
|
|
144
|
+
return getParent()?.options.toXML || null;
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
get tagname() {
|
|
148
|
+
return options.tagname || defaults.tagnameFromModel(model);
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
if (!getParent()) {
|
|
152
|
+
this.options.fromXML.middlewares.push((...args) =>
|
|
153
|
+
defaults.fromXML(...args)
|
|
154
|
+
);
|
|
155
|
+
this.options.toXML.middlewares.push((...args) => defaults.toXML(...args));
|
|
156
|
+
}
|
|
157
|
+
if (options.fromXML) this.options.fromXML.middlewares.push(options.fromXML);
|
|
158
|
+
if (options.toXML) this.options.toXML.middlewares.push(options.toXML);
|
|
159
|
+
}
|
|
160
|
+
fromXML(xml: XMLRoot | string) {
|
|
161
|
+
const _xml = typeof xml === "string" ? XML.parse(xml) : xml;
|
|
162
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
163
|
+
const model = this;
|
|
164
|
+
const context = {
|
|
165
|
+
xml: _xml,
|
|
166
|
+
get properties() {
|
|
167
|
+
const propContext = {
|
|
168
|
+
xml: _xml,
|
|
169
|
+
model,
|
|
170
|
+
};
|
|
171
|
+
return resolve(
|
|
172
|
+
MiddlewareChain(model.options.properties.fromXML),
|
|
173
|
+
propContext
|
|
174
|
+
);
|
|
175
|
+
},
|
|
176
|
+
model,
|
|
177
|
+
};
|
|
178
|
+
return resolve(MiddlewareChain(this.options.fromXML), context);
|
|
179
|
+
}
|
|
180
|
+
toXML(instance: object) {
|
|
181
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
182
|
+
const model = this;
|
|
183
|
+
if (instance instanceof this.type || instance.constructor === this.type) {
|
|
184
|
+
// intanceof won't work with type "String" for example
|
|
185
|
+
const context = {
|
|
186
|
+
object: instance as unknown as T,
|
|
187
|
+
get properties() {
|
|
188
|
+
const propContext = {
|
|
189
|
+
object: instance,
|
|
190
|
+
model,
|
|
191
|
+
};
|
|
192
|
+
return resolve(
|
|
193
|
+
MiddlewareChain(model.options.properties.toXML),
|
|
194
|
+
propContext as any
|
|
195
|
+
);
|
|
196
|
+
},
|
|
197
|
+
model: this,
|
|
198
|
+
};
|
|
199
|
+
return resolve(MiddlewareChain(this.options.toXML), context);
|
|
200
|
+
} else {
|
|
201
|
+
throw new TypeError(
|
|
202
|
+
`provided object is not an instance of ${this.type.name}`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
get reflectedClass() {
|
|
207
|
+
return reflect(this.type);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function createModel<T>(
|
|
212
|
+
type: Constructor<T>,
|
|
213
|
+
options: XMLModelConversionOptions<T>
|
|
214
|
+
): XMLModel<T> {
|
|
215
|
+
if (findModel(type)) {
|
|
216
|
+
throw new TypeError(`a model for type ${type.name} already exists`);
|
|
217
|
+
}
|
|
218
|
+
const model = new XMLModel(type, options);
|
|
219
|
+
Models.set(type, model as XMLModel<unknown>);
|
|
220
|
+
return model;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
type ModelID<T> = Constructor<T>;
|
|
224
|
+
export const Models = new Map<ModelID<unknown>, XMLModel<unknown>>();
|
|
225
|
+
|
|
226
|
+
export function findModel<T>(id: ModelID<T>) {
|
|
227
|
+
return Models.get(id) as XMLModel<T> | undefined;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function getModel<T>(id: ModelID<T>) {
|
|
231
|
+
const model = findModel(id);
|
|
232
|
+
if (model) return model;
|
|
233
|
+
else throw new TypeError(`couln't find model for type ${id.name}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Model decorator
|
|
237
|
+
function ModelDecoratorFactory<T>(options?: XMLModelConversionOptions<T>) {
|
|
238
|
+
return function (constructor: Constructor<T>): void {
|
|
239
|
+
findModel<T>(constructor) || createModel<T>(constructor, options || {});
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
export { ModelDecoratorFactory as Model };
|
|
243
|
+
export { Prop } from "./property";
|
|
244
|
+
|
|
245
|
+
import "../defaults/models";
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { Constructor } from "typescript-rtti";
|
|
2
|
+
import { reflect } from "typescript-rtti";
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
XMLModelProperty,
|
|
6
|
+
XMLModelPropertyOptions,
|
|
7
|
+
CreateXMLModelPropertyOptions,
|
|
8
|
+
} from "./types";
|
|
9
|
+
import { defaults } from "../defaults";
|
|
10
|
+
|
|
11
|
+
function resolvePropertyConversionOptions<T>(
|
|
12
|
+
options: CreateXMLModelPropertyOptions<T>,
|
|
13
|
+
constructor: Constructor<T>,
|
|
14
|
+
property: XMLModelProperty<T>
|
|
15
|
+
) {
|
|
16
|
+
const _options: XMLModelPropertyOptions<T> = {
|
|
17
|
+
name: property as keyof T,
|
|
18
|
+
get reflected() {
|
|
19
|
+
return reflect(constructor).getProperty(property);
|
|
20
|
+
},
|
|
21
|
+
get tagname() {
|
|
22
|
+
return options.tagname || defaults.tagnameFromProperty(this);
|
|
23
|
+
},
|
|
24
|
+
inline: !!options.inline,
|
|
25
|
+
ignored: !!options.ignore,
|
|
26
|
+
isSourceElement: (...args) =>
|
|
27
|
+
defaults.propertySourceElementsFilter(...args),
|
|
28
|
+
resolveElements: options.resolveElements
|
|
29
|
+
? options.resolveElements
|
|
30
|
+
: (...args) => defaults.propertyResolveSourceElements(...args),
|
|
31
|
+
fromXML: (context) =>
|
|
32
|
+
(options.fromXML || defaults.propertyFromXML)(context),
|
|
33
|
+
toXML: (context) => (options.toXML || defaults.propertyToXML)(context),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
if (options?.sourceElements) {
|
|
37
|
+
const _sourceElements = options.sourceElements;
|
|
38
|
+
if (typeof _sourceElements === "string") {
|
|
39
|
+
_options.isSourceElement = (element) => element.name === _sourceElements;
|
|
40
|
+
} else if (_sourceElements && _sourceElements instanceof RegExp) {
|
|
41
|
+
_options.isSourceElement = (element) =>
|
|
42
|
+
_sourceElements.test(element.name || "");
|
|
43
|
+
} else {
|
|
44
|
+
_options.isSourceElement = _sourceElements;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return _options;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const PropertyOptions = new Map<
|
|
52
|
+
Constructor<any>,
|
|
53
|
+
Map<XMLModelProperty<any>, XMLModelPropertyOptions<any>>
|
|
54
|
+
>();
|
|
55
|
+
|
|
56
|
+
function storePropertyConversionOptions<T>(
|
|
57
|
+
constructor: Constructor<T>,
|
|
58
|
+
property: XMLModelProperty<T>,
|
|
59
|
+
options: XMLModelPropertyOptions<T>
|
|
60
|
+
) {
|
|
61
|
+
let map = PropertyOptions.get(constructor);
|
|
62
|
+
if (!map) {
|
|
63
|
+
map = new Map<
|
|
64
|
+
XMLModelProperty<unknown>,
|
|
65
|
+
XMLModelPropertyOptions<unknown>
|
|
66
|
+
>();
|
|
67
|
+
PropertyOptions.set(constructor, map);
|
|
68
|
+
}
|
|
69
|
+
map.set(property, options);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function findPropertyConversionOptions<T>(
|
|
73
|
+
constructor: Constructor<T>,
|
|
74
|
+
property: XMLModelProperty<T>
|
|
75
|
+
) {
|
|
76
|
+
const options = PropertyOptions.get(constructor) as
|
|
77
|
+
| Map<XMLModelProperty<T>, XMLModelPropertyOptions<T>>
|
|
78
|
+
| undefined;
|
|
79
|
+
if (options) {
|
|
80
|
+
return options.get(property);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function getPropertyConversionOptions<T>(
|
|
85
|
+
constructor: Constructor<T>,
|
|
86
|
+
property: XMLModelProperty<T>
|
|
87
|
+
) {
|
|
88
|
+
const options = findPropertyConversionOptions(constructor, property);
|
|
89
|
+
return options || resolvePropertyConversionOptions({}, constructor, property);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function PropDecoratorFactory<T = any>(
|
|
93
|
+
options?: CreateXMLModelPropertyOptions<T>
|
|
94
|
+
) {
|
|
95
|
+
return function (prototype: any, property: XMLModelProperty<T>) {
|
|
96
|
+
const _options = resolvePropertyConversionOptions(
|
|
97
|
+
options || {},
|
|
98
|
+
prototype.constructor,
|
|
99
|
+
property
|
|
100
|
+
);
|
|
101
|
+
storePropertyConversionOptions(prototype.constructor, property, _options);
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export { PropDecoratorFactory as Prop };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { ReflectedProperty } from "typescript-rtti";
|
|
2
|
+
import type { XMLRoot, XMLElement } from "../types";
|
|
3
|
+
import type { Middleware } from "../middleware";
|
|
4
|
+
import type { XMLModel } from "./index";
|
|
5
|
+
|
|
6
|
+
/* PROPERTIES */
|
|
7
|
+
export type XMLModelProperty<T> = Extract<keyof T, string>;
|
|
8
|
+
|
|
9
|
+
export type PropertiesRecord<T> = {
|
|
10
|
+
[key in keyof T]?: T[key];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type XMLPropertiesRecord<T> = {
|
|
14
|
+
[key in keyof T]?: XMLRoot;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export interface PropertyToXMLContext<T>
|
|
18
|
+
extends Omit<toXMLContext<T>, "properties"> {
|
|
19
|
+
property: XMLModelPropertyOptions<T>;
|
|
20
|
+
value: T[keyof T]; // FIXME ???
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PropertyFromXMLContext<T>
|
|
24
|
+
extends Omit<fromXMLContext<T>, "properties"> {
|
|
25
|
+
property: XMLModelPropertyOptions<T>;
|
|
26
|
+
elements: XMLElement[];
|
|
27
|
+
}
|
|
28
|
+
export interface XMLModelPropertyOptions<T> {
|
|
29
|
+
name: keyof T;
|
|
30
|
+
reflected: ReflectedProperty;
|
|
31
|
+
tagname: string;
|
|
32
|
+
ignored: boolean;
|
|
33
|
+
inline: boolean;
|
|
34
|
+
// from XML
|
|
35
|
+
isSourceElement: (
|
|
36
|
+
element: XMLElement,
|
|
37
|
+
context: Omit<PropertyFromXMLContext<T>, "elements">
|
|
38
|
+
) => boolean;
|
|
39
|
+
resolveElements: (
|
|
40
|
+
context: Omit<PropertyFromXMLContext<T>, "elements">
|
|
41
|
+
) => XMLElement[];
|
|
42
|
+
fromXML: (context: PropertyFromXMLContext<T>) => T[keyof T];
|
|
43
|
+
// to XML
|
|
44
|
+
toXML: (context: PropertyToXMLContext<T>) => XMLRoot;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface CreateXMLModelPropertyOptions<T> {
|
|
48
|
+
tagname?: string;
|
|
49
|
+
sourceElements?:
|
|
50
|
+
| string
|
|
51
|
+
| RegExp
|
|
52
|
+
| XMLModelPropertyOptions<T>["isSourceElement"];
|
|
53
|
+
resolveElements?: XMLModelPropertyOptions<T>["resolveElements"];
|
|
54
|
+
toXML?: XMLModelPropertyOptions<T>["toXML"];
|
|
55
|
+
fromXML?: XMLModelPropertyOptions<T>["fromXML"];
|
|
56
|
+
inline?: boolean;
|
|
57
|
+
ignore?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* MODEL */
|
|
61
|
+
interface ConversionOptions<C, T> {
|
|
62
|
+
parent: ConversionOptions<C, T> | null;
|
|
63
|
+
middlewares: Middleware<C, T>[];
|
|
64
|
+
}
|
|
65
|
+
export interface fromXMLContext<T> {
|
|
66
|
+
xml: XMLRoot;
|
|
67
|
+
properties: PropertiesRecord<T>;
|
|
68
|
+
model: XMLModel<T>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface toXMLContext<T> {
|
|
72
|
+
object: T;
|
|
73
|
+
properties: XMLPropertiesRecord<T>;
|
|
74
|
+
model: XMLModel<T>;
|
|
75
|
+
}
|
|
76
|
+
export interface XMLModelOptions<T> {
|
|
77
|
+
properties: {
|
|
78
|
+
fromXML: ConversionOptions<
|
|
79
|
+
Omit<fromXMLContext<T>, "properties">,
|
|
80
|
+
PropertiesRecord<T>
|
|
81
|
+
>;
|
|
82
|
+
toXML: ConversionOptions<
|
|
83
|
+
Omit<toXMLContext<T>, "properties">,
|
|
84
|
+
XMLPropertiesRecord<T>
|
|
85
|
+
>;
|
|
86
|
+
options: Map<XMLModelProperty<T>, XMLModelPropertyOptions<T>>;
|
|
87
|
+
};
|
|
88
|
+
fromXML: ConversionOptions<fromXMLContext<T>, T>;
|
|
89
|
+
toXML: ConversionOptions<toXMLContext<T>, XMLRoot>;
|
|
90
|
+
tagname: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface CreateXMLModelOptions<T> {
|
|
94
|
+
fromXML?: XMLModelOptions<T>["fromXML"]["middlewares"][number];
|
|
95
|
+
tagname?: string;
|
|
96
|
+
toXML?: XMLModelOptions<T>["toXML"]["middlewares"][number];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { XMLModel };
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import "mocha";
|
|
2
|
+
import { expect, assert } from "chai";
|
|
3
|
+
|
|
4
|
+
import { Model, Prop, getModel } from "./model";
|
|
5
|
+
import XML from "./xml";
|
|
6
|
+
import { reflect, ReflectedClass } from "typescript-rtti";
|
|
7
|
+
import { UnknownRecord } from "./types";
|
|
8
|
+
|
|
9
|
+
@Model({
|
|
10
|
+
fromXML({ model, properties }) {
|
|
11
|
+
return new model.type(properties);
|
|
12
|
+
},
|
|
13
|
+
})
|
|
14
|
+
class Book {
|
|
15
|
+
name: string;
|
|
16
|
+
nbPages: number;
|
|
17
|
+
constructor(options: { name: string; nbPages: number }) {
|
|
18
|
+
this.name = options.name;
|
|
19
|
+
this.nbPages = options.nbPages;
|
|
20
|
+
}
|
|
21
|
+
equals(book: Book) {
|
|
22
|
+
if (this.name !== book.name) return false;
|
|
23
|
+
if (this.nbPages !== book.nbPages) return false;
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@Model<Library>({
|
|
29
|
+
fromXML({ model, properties }) {
|
|
30
|
+
return new model.type(properties.name, ...(properties.books as Book[]));
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
class Library {
|
|
34
|
+
name: string;
|
|
35
|
+
books: Book[] = [];
|
|
36
|
+
constructor(name: string, ...books: Book[]) {
|
|
37
|
+
this.name = name;
|
|
38
|
+
this.books.push(...books);
|
|
39
|
+
}
|
|
40
|
+
equals(library: Library) {
|
|
41
|
+
if (this.name !== library.name) return false;
|
|
42
|
+
if (this.books.length !== library.books.length) return false;
|
|
43
|
+
for (let index = 0; index < this.books.length; index++) {
|
|
44
|
+
if (!this.books[index].equals(library.books[index])) return false;
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("Library Example", () => {
|
|
51
|
+
const library = new Library("test");
|
|
52
|
+
for (let i = 1; i <= 4; i++) {
|
|
53
|
+
const book = new Book({ name: `Book #${i}`, nbPages: Math.pow(10, i) });
|
|
54
|
+
library.books.push(book);
|
|
55
|
+
}
|
|
56
|
+
const libraryXMLString = XML.stringify(
|
|
57
|
+
XML.parse(
|
|
58
|
+
`<library>
|
|
59
|
+
<name>${library.name}</name>
|
|
60
|
+
<books>
|
|
61
|
+
${library.books
|
|
62
|
+
.map(
|
|
63
|
+
(book) =>
|
|
64
|
+
` <book>
|
|
65
|
+
<name>${book.name}</name>
|
|
66
|
+
<nb-pages>${book.nbPages}</nb-pages>
|
|
67
|
+
</book>`
|
|
68
|
+
)
|
|
69
|
+
.join("")}
|
|
70
|
+
</books>
|
|
71
|
+
</library>`
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
it("Object -> XML", () => {
|
|
76
|
+
const xml = getModel(Library).toXML(library);
|
|
77
|
+
expect(XML.stringify(xml)).to.equal(libraryXMLString);
|
|
78
|
+
});
|
|
79
|
+
it("XML -> Object", () => {
|
|
80
|
+
const parsedLibrary = getModel(Library).fromXML(libraryXMLString);
|
|
81
|
+
expect(parsedLibrary instanceof Library).to.be.true;
|
|
82
|
+
expect(library.equals(parsedLibrary as Library)).to.be.true;
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
@Model({
|
|
87
|
+
fromXML({ model, properties }) {
|
|
88
|
+
return new model.type(properties);
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
class A {
|
|
92
|
+
propA: string = "";
|
|
93
|
+
propB: boolean = true;
|
|
94
|
+
@Prop({ tagname: "b", inline: true })
|
|
95
|
+
propC: B[] = [];
|
|
96
|
+
@Prop({ tagname: "propd" })
|
|
97
|
+
propD: 0 | 1 = 0;
|
|
98
|
+
equals(a: A) {
|
|
99
|
+
//
|
|
100
|
+
if (
|
|
101
|
+
this.propA !== a.propA ||
|
|
102
|
+
this.propB !== a.propB ||
|
|
103
|
+
this.propC.length !== a.propC.length ||
|
|
104
|
+
this.propD !== a.propD
|
|
105
|
+
)
|
|
106
|
+
return false;
|
|
107
|
+
for (let i = 0; i < this.propC.length; i++) {
|
|
108
|
+
if (!this.propC[i].equals(a.propC[i])) return false;
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
constructor(record?: UnknownRecord) {
|
|
113
|
+
if (record)
|
|
114
|
+
Object.entries(record).forEach(([key, val]) => {
|
|
115
|
+
(this[key as keyof A] as any) = val as any;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@Model({
|
|
121
|
+
fromXML({ model, properties }) {
|
|
122
|
+
return new model.type(properties);
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
class B {
|
|
126
|
+
propA: number = 0;
|
|
127
|
+
equals(b: B) {
|
|
128
|
+
return this.propA === b.propA;
|
|
129
|
+
}
|
|
130
|
+
constructor(record?: UnknownRecord) {
|
|
131
|
+
if (record)
|
|
132
|
+
Object.entries(record).forEach(([key, val]) => {
|
|
133
|
+
(this[key as keyof B] as any) = val as any;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
describe("Edgy Cases", () => {
|
|
139
|
+
const instance = new A();
|
|
140
|
+
for (let i = 0; i < 8; i++) {
|
|
141
|
+
const b = new B();
|
|
142
|
+
b.propA = i;
|
|
143
|
+
instance.propC.push(b);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const instanceXMLString = XML.stringify(
|
|
147
|
+
XML.parse(`<a>
|
|
148
|
+
<prop-a>${instance.propA}</prop-a>
|
|
149
|
+
<prop-b>${instance.propB}</prop-b>
|
|
150
|
+
${instance.propC.map((b) => `<b><prop-a>${b.propA}</prop-a></b>`).join("")}
|
|
151
|
+
<propd>${instance.propD}</propd>
|
|
152
|
+
</a>`)
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
it("should give right type infos", () => {
|
|
156
|
+
const reflectedA = reflect(A) as unknown as ReflectedClass;
|
|
157
|
+
assert(reflectedA === <ReflectedClass>(<unknown>reflect(A)));
|
|
158
|
+
expect(reflectedA.getProperty("propA").type.isClass(String)).to.be.true;
|
|
159
|
+
expect(reflectedA.getProperty("propB").type.isClass(Boolean)).to.be.true;
|
|
160
|
+
const ModelAPropCType = reflectedA.getProperty("propC").type;
|
|
161
|
+
expect(
|
|
162
|
+
ModelAPropCType.is("array") && ModelAPropCType.elementType.isClass(B)
|
|
163
|
+
).to.be.true;
|
|
164
|
+
const ModelAPropDType = reflectedA.getProperty("propD").type;
|
|
165
|
+
expect(ModelAPropDType.is("union")).to.be.true;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("XML -> Object", () => {
|
|
169
|
+
const parsed = getModel(A).fromXML(instanceXMLString);
|
|
170
|
+
expect(parsed instanceof A).to.be.true;
|
|
171
|
+
const equals = instance.equals(parsed as A);
|
|
172
|
+
expect(equals).to.be.true;
|
|
173
|
+
});
|
|
174
|
+
it("Object -> XML", () => {
|
|
175
|
+
const xml = getModel(A).toXML(instance);
|
|
176
|
+
expect(XML.stringify(xml)).to.equal(instanceXMLString);
|
|
177
|
+
});
|
|
178
|
+
});
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type { Constructor } from "typescript-rtti";
|
|
2
|
+
|
|
3
|
+
export type UnknownRecord = Record<string | number | symbol, unknown>;
|
|
4
|
+
export type UnknownObject = object; // Record<string | number | symbol, unknown>; // don't works with class' instances
|
|
5
|
+
|
|
6
|
+
import type { Element as _XMLElement } from "xml-js";
|
|
7
|
+
export type XMLElement = _XMLElement;
|
|
8
|
+
export type XMLRoot = { elements: XMLElement[] };
|