@hla4ts/fom-codegen 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/README.md +247 -0
- package/package.json +29 -0
- package/src/cli.ts +179 -0
- package/src/generated/index.ts +25 -0
- package/src/generated/xsd-types.ts +2073 -0
- package/src/index.ts +62 -0
- package/src/registry.ts +127 -0
- package/src/ts-codegen.ts +59 -0
- package/src/types.ts +172 -0
- package/src/xml-parser.ts +359 -0
- package/src/xml-writer.ts +502 -0
- package/src/xsd-parser.ts +404 -0
- package/src/xsd-type-generator.ts +452 -0
- package/src/xsd-types-adapter.ts +177 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AttributeSchema,
|
|
3
|
+
DataTypeSchema,
|
|
4
|
+
DimensionSchema,
|
|
5
|
+
FomModel,
|
|
6
|
+
InteractionClassSchema,
|
|
7
|
+
ModelIdentification,
|
|
8
|
+
ObjectClassSchema,
|
|
9
|
+
TransportationSchema,
|
|
10
|
+
} from "./types.ts";
|
|
11
|
+
|
|
12
|
+
type XmlElement = {
|
|
13
|
+
name: string;
|
|
14
|
+
attributes: Record<string, string>;
|
|
15
|
+
children: XmlElement[];
|
|
16
|
+
text: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function parseFomXml(xml: string): FomModel {
|
|
20
|
+
const root = parseXml(xml);
|
|
21
|
+
|
|
22
|
+
const modelIdentification = parseModelIdentification(findChild(root, "modelIdentification"));
|
|
23
|
+
const time = parseTime(findChild(root, "time"));
|
|
24
|
+
const objectClasses = parseObjectClasses(findChild(root, "objects"));
|
|
25
|
+
const interactionClasses = parseInteractionClasses(findChild(root, "interactions"));
|
|
26
|
+
const dataTypes = parseDataTypes(findChild(root, "dataTypes"));
|
|
27
|
+
const dimensions = parseDimensions(findChild(root, "dimensions"));
|
|
28
|
+
const transportations = parseTransportations(findChild(root, "transportations"));
|
|
29
|
+
const switches = parseSwitches(findChild(root, "switches"));
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
modelIdentification,
|
|
33
|
+
time,
|
|
34
|
+
objectClasses,
|
|
35
|
+
interactionClasses,
|
|
36
|
+
dataTypes,
|
|
37
|
+
dimensions,
|
|
38
|
+
transportations,
|
|
39
|
+
switches,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseModelIdentification(node?: XmlElement): ModelIdentification | undefined {
|
|
44
|
+
if (!node) return undefined;
|
|
45
|
+
return {
|
|
46
|
+
name: childText(node, "name"),
|
|
47
|
+
type: childText(node, "type") as ModelIdentification["type"],
|
|
48
|
+
version: childText(node, "version"),
|
|
49
|
+
modificationDate: childText(node, "modificationDate"),
|
|
50
|
+
securityClassification: childText(node, "securityClassification"),
|
|
51
|
+
description: childText(node, "description"),
|
|
52
|
+
purpose: optional(childText(node, "purpose")),
|
|
53
|
+
applicationDomain: optional(childText(node, "applicationDomain")),
|
|
54
|
+
useLimitation: optional(childText(node, "useLimitation")),
|
|
55
|
+
keyword: getChildren(node, "keyword").map((kw) => ({
|
|
56
|
+
taxonomy: optional(childText(kw, "taxonomy")),
|
|
57
|
+
keywordValue: childText(kw, "keywordValue"),
|
|
58
|
+
})),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseTime(node?: XmlElement): FomModel["time"] | undefined {
|
|
63
|
+
if (!node) return undefined;
|
|
64
|
+
const timeStamp = findChild(node, "timeStamp");
|
|
65
|
+
const lookahead = findChild(node, "lookahead");
|
|
66
|
+
if (!timeStamp || !lookahead) return undefined;
|
|
67
|
+
return {
|
|
68
|
+
timeStampType: childText(timeStamp, "dataType"),
|
|
69
|
+
lookaheadType: childText(lookahead, "dataType"),
|
|
70
|
+
semantics: {
|
|
71
|
+
timeStamp: optional(childText(timeStamp, "semantics")),
|
|
72
|
+
lookahead: optional(childText(lookahead, "semantics")),
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function parseObjectClasses(node?: XmlElement): ObjectClassSchema[] {
|
|
78
|
+
const root = node ? findChild(node, "objectClass") : undefined;
|
|
79
|
+
if (!root) return [];
|
|
80
|
+
return flattenObjectClasses(root, undefined);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function flattenObjectClasses(node: XmlElement, parent?: string): ObjectClassSchema[] {
|
|
84
|
+
const name = childText(node, "name");
|
|
85
|
+
const obj: ObjectClassSchema = {
|
|
86
|
+
name,
|
|
87
|
+
parent,
|
|
88
|
+
sharing: optional(childText(node, "sharing")) as ObjectClassSchema["sharing"],
|
|
89
|
+
semantics: optional(childText(node, "semantics")),
|
|
90
|
+
attributes: getChildren(node, "attribute").map(parseAttribute),
|
|
91
|
+
};
|
|
92
|
+
const children = getChildren(node, "objectClass").flatMap((child) =>
|
|
93
|
+
flattenObjectClasses(child, name)
|
|
94
|
+
);
|
|
95
|
+
return [obj, ...children];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseAttribute(node: XmlElement): AttributeSchema {
|
|
99
|
+
return {
|
|
100
|
+
name: childText(node, "name"),
|
|
101
|
+
dataType: childText(node, "dataType"),
|
|
102
|
+
updateType: (optional(childText(node, "updateType")) ?? "NA") as AttributeSchema["updateType"],
|
|
103
|
+
updateCondition: optional(childText(node, "updateCondition")) ?? "NA",
|
|
104
|
+
ownership: (optional(childText(node, "ownership")) ?? "NoTransfer") as AttributeSchema["ownership"],
|
|
105
|
+
sharing: (optional(childText(node, "sharing")) ?? "PublishSubscribe") as AttributeSchema["sharing"],
|
|
106
|
+
transportation: optional(childText(node, "transportation")) ?? "HLAreliable",
|
|
107
|
+
order: (optional(childText(node, "order")) ?? "Receive") as AttributeSchema["order"],
|
|
108
|
+
semantics: optional(childText(node, "semantics")),
|
|
109
|
+
valueRequired: parseBoolean(childText(node, "valueRequired")),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function parseInteractionClasses(node?: XmlElement): InteractionClassSchema[] {
|
|
114
|
+
const root = node ? findChild(node, "interactionClass") : undefined;
|
|
115
|
+
if (!root) return [];
|
|
116
|
+
return flattenInteractionClasses(root, undefined);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function flattenInteractionClasses(node: XmlElement, parent?: string): InteractionClassSchema[] {
|
|
120
|
+
const name = childText(node, "name");
|
|
121
|
+
const interaction: InteractionClassSchema = {
|
|
122
|
+
name,
|
|
123
|
+
parent,
|
|
124
|
+
sharing: optional(childText(node, "sharing")) as InteractionClassSchema["sharing"],
|
|
125
|
+
transportation: optional(childText(node, "transportation")),
|
|
126
|
+
order: optional(childText(node, "order")) as InteractionClassSchema["order"],
|
|
127
|
+
semantics: optional(childText(node, "semantics")),
|
|
128
|
+
parameters: getChildren(node, "parameter").map((param) => ({
|
|
129
|
+
name: childText(param, "name"),
|
|
130
|
+
dataType: childText(param, "dataType"),
|
|
131
|
+
semantics: optional(childText(param, "semantics")),
|
|
132
|
+
})),
|
|
133
|
+
};
|
|
134
|
+
const children = getChildren(node, "interactionClass").flatMap((child) =>
|
|
135
|
+
flattenInteractionClasses(child, name)
|
|
136
|
+
);
|
|
137
|
+
return [interaction, ...children];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function parseDataTypes(node?: XmlElement): DataTypeSchema[] {
|
|
141
|
+
if (!node) return [];
|
|
142
|
+
const dataTypes: DataTypeSchema[] = [];
|
|
143
|
+
|
|
144
|
+
const simpleDataTypes = findChild(node, "simpleDataTypes");
|
|
145
|
+
for (const simple of simpleDataTypes ? getChildren(simpleDataTypes, "simpleData") : []) {
|
|
146
|
+
dataTypes.push({
|
|
147
|
+
kind: "simple",
|
|
148
|
+
name: childText(simple, "name"),
|
|
149
|
+
representation: childText(simple, "representation"),
|
|
150
|
+
units: optional(childText(simple, "units")),
|
|
151
|
+
resolution: optional(childText(simple, "resolution")),
|
|
152
|
+
accuracy: optional(childText(simple, "accuracy")),
|
|
153
|
+
semantics: optional(childText(simple, "semantics")),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const enumeratedDataTypes = findChild(node, "enumeratedDataTypes");
|
|
158
|
+
for (const enumerated of enumeratedDataTypes ? getChildren(enumeratedDataTypes, "enumeratedData") : []) {
|
|
159
|
+
dataTypes.push({
|
|
160
|
+
kind: "enumerated",
|
|
161
|
+
name: childText(enumerated, "name"),
|
|
162
|
+
representation: childText(enumerated, "representation"),
|
|
163
|
+
semantics: optional(childText(enumerated, "semantics")),
|
|
164
|
+
enumerators: getChildren(enumerated, "enumerator").map((enumNode) => ({
|
|
165
|
+
name: childText(enumNode, "name"),
|
|
166
|
+
value: getChildren(enumNode, "value").map((valueNode) => textValue(valueNode)),
|
|
167
|
+
})),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const arrayDataTypes = findChild(node, "arrayDataTypes");
|
|
172
|
+
for (const array of arrayDataTypes ? getChildren(arrayDataTypes, "arrayData") : []) {
|
|
173
|
+
dataTypes.push({
|
|
174
|
+
kind: "array",
|
|
175
|
+
name: childText(array, "name"),
|
|
176
|
+
dataType: childText(array, "dataType"),
|
|
177
|
+
cardinality: childText(array, "cardinality"),
|
|
178
|
+
encoding: childText(array, "encoding"),
|
|
179
|
+
semantics: optional(childText(array, "semantics")),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const fixedRecordDataTypes = findChild(node, "fixedRecordDataTypes");
|
|
184
|
+
for (const record of fixedRecordDataTypes ? getChildren(fixedRecordDataTypes, "fixedRecordData") : []) {
|
|
185
|
+
dataTypes.push({
|
|
186
|
+
kind: "fixedRecord",
|
|
187
|
+
name: childText(record, "name"),
|
|
188
|
+
encoding: childText(record, "encoding"),
|
|
189
|
+
semantics: optional(childText(record, "semantics")),
|
|
190
|
+
fields: getChildren(record, "field").map((field) => ({
|
|
191
|
+
name: childText(field, "name"),
|
|
192
|
+
dataType: childText(field, "dataType"),
|
|
193
|
+
semantics: optional(childText(field, "semantics")),
|
|
194
|
+
})),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const variantRecordDataTypes = findChild(node, "variantRecordDataTypes");
|
|
199
|
+
for (const variant of variantRecordDataTypes ? getChildren(variantRecordDataTypes, "variantRecordData") : []) {
|
|
200
|
+
dataTypes.push({
|
|
201
|
+
kind: "variantRecord",
|
|
202
|
+
name: childText(variant, "name"),
|
|
203
|
+
discriminant: childText(variant, "discriminant"),
|
|
204
|
+
dataType: childText(variant, "dataType"),
|
|
205
|
+
encoding: childText(variant, "encoding"),
|
|
206
|
+
semantics: optional(childText(variant, "semantics")),
|
|
207
|
+
alternatives: getChildren(variant, "alternative").map((alt) => ({
|
|
208
|
+
enumerator: childText(alt, "enumerator"),
|
|
209
|
+
name: optional(childText(alt, "name")),
|
|
210
|
+
dataType: optional(childText(alt, "dataType")),
|
|
211
|
+
semantics: optional(childText(alt, "semantics")),
|
|
212
|
+
})),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return dataTypes;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function parseDimensions(node?: XmlElement): DimensionSchema[] {
|
|
220
|
+
if (!node) return [];
|
|
221
|
+
return getChildren(node, "dimension").map((dim) => ({
|
|
222
|
+
name: childText(dim, "name"),
|
|
223
|
+
dataType: optional(childText(dim, "dataType")),
|
|
224
|
+
}));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function parseTransportations(node?: XmlElement): TransportationSchema[] {
|
|
228
|
+
if (!node) return [];
|
|
229
|
+
return getChildren(node, "transportation").map((transport) => ({
|
|
230
|
+
name: childText(transport, "name"),
|
|
231
|
+
reliable: optional(childText(transport, "reliable")) as TransportationSchema["reliable"],
|
|
232
|
+
}));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function parseSwitches(node?: XmlElement): FomModel["switches"] | undefined {
|
|
236
|
+
if (!node) return undefined;
|
|
237
|
+
const readSwitch = (name: string): boolean => {
|
|
238
|
+
const element = findChild(node, name);
|
|
239
|
+
return parseBoolean(element?.attributes["isEnabled"]) ?? false;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const automaticResign = findChild(node, "automaticResignAction");
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
autoProvide: readSwitch("autoProvide"),
|
|
246
|
+
conveyRegionDesignatorSets: readSwitch("conveyRegionDesignatorSets"),
|
|
247
|
+
conveyProducingFederate: readSwitch("conveyProducingFederate"),
|
|
248
|
+
attributeScopeAdvisory: readSwitch("attributeScopeAdvisory"),
|
|
249
|
+
attributeRelevanceAdvisory: readSwitch("attributeRelevanceAdvisory"),
|
|
250
|
+
objectClassRelevanceAdvisory: readSwitch("objectClassRelevanceAdvisory"),
|
|
251
|
+
interactionRelevanceAdvisory: readSwitch("interactionRelevanceAdvisory"),
|
|
252
|
+
serviceReporting: readSwitch("serviceReporting"),
|
|
253
|
+
exceptionReporting: readSwitch("exceptionReporting"),
|
|
254
|
+
delaySubscriptionEvaluation: readSwitch("delaySubscriptionEvaluation"),
|
|
255
|
+
automaticResignAction: automaticResign?.attributes["resignAction"],
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function findChild(parent: XmlElement, name: string): XmlElement | undefined {
|
|
260
|
+
for (const child of parent.children) {
|
|
261
|
+
if (child.name === name) {
|
|
262
|
+
return child;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function getChildren(parent: XmlElement, name: string): XmlElement[] {
|
|
269
|
+
const result: XmlElement[] = [];
|
|
270
|
+
for (const child of parent.children) {
|
|
271
|
+
if (child.name === name) {
|
|
272
|
+
result.push(child);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function childText(parent: XmlElement, name: string): string {
|
|
279
|
+
const child = findChild(parent, name);
|
|
280
|
+
return child ? textValue(child) : "";
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function textValue(node: XmlElement): string {
|
|
284
|
+
return node.text.trim();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function optional(value: string): string | undefined {
|
|
288
|
+
return value ? value : undefined;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function parseBoolean(value: string | null | undefined): boolean | undefined {
|
|
292
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
293
|
+
return value.toLowerCase() === "true";
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function parseXml(xml: string): XmlElement {
|
|
297
|
+
const cleaned = xml
|
|
298
|
+
.replace(/<\?xml[^>]*\?>/g, "")
|
|
299
|
+
.replace(/<!--[\s\S]*?-->/g, "")
|
|
300
|
+
.trim();
|
|
301
|
+
const tokens = cleaned.match(/<[^>]+>|[^<]+/g) ?? [];
|
|
302
|
+
const stack: XmlElement[] = [];
|
|
303
|
+
let root: XmlElement | undefined;
|
|
304
|
+
|
|
305
|
+
for (const token of tokens) {
|
|
306
|
+
if (token.startsWith("<")) {
|
|
307
|
+
if (token.startsWith("</")) {
|
|
308
|
+
stack.pop();
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const selfClosing = token.endsWith("/>");
|
|
313
|
+
const tagBody = token.slice(1, token.length - (selfClosing ? 2 : 1)).trim();
|
|
314
|
+
const [rawName, ...attrParts] = tagBody.split(/\s+/);
|
|
315
|
+
const name = stripNamespace(rawName);
|
|
316
|
+
const attributes = parseAttributes(attrParts.join(" "));
|
|
317
|
+
const element: XmlElement = { name, attributes, children: [], text: "" };
|
|
318
|
+
|
|
319
|
+
if (!root) {
|
|
320
|
+
root = element;
|
|
321
|
+
}
|
|
322
|
+
if (stack.length > 0) {
|
|
323
|
+
stack[stack.length - 1].children.push(element);
|
|
324
|
+
}
|
|
325
|
+
if (!selfClosing) {
|
|
326
|
+
stack.push(element);
|
|
327
|
+
}
|
|
328
|
+
} else if (stack.length > 0) {
|
|
329
|
+
const text = token.trim();
|
|
330
|
+
if (text) {
|
|
331
|
+
stack[stack.length - 1].text += text;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (!root) {
|
|
337
|
+
throw new Error("Failed to parse XML: no root element found.");
|
|
338
|
+
}
|
|
339
|
+
return root;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function parseAttributes(text: string): Record<string, string> {
|
|
343
|
+
const attrs: Record<string, string> = {};
|
|
344
|
+
const regex = /([^\s=]+)\s*=\s*"(.*?)"|([^\s=]+)\s*=\s*'(.*?)'/g;
|
|
345
|
+
let match: RegExpExecArray | null;
|
|
346
|
+
while ((match = regex.exec(text)) !== null) {
|
|
347
|
+
const name = stripNamespace(match[1] ?? match[3] ?? "");
|
|
348
|
+
const value = match[2] ?? match[4] ?? "";
|
|
349
|
+
if (name) {
|
|
350
|
+
attrs[name] = value;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return attrs;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function stripNamespace(name: string): string {
|
|
357
|
+
const parts = name.split(":");
|
|
358
|
+
return parts[parts.length - 1];
|
|
359
|
+
}
|