@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.
@@ -0,0 +1,502 @@
1
+ import type {
2
+ AttributeSchema,
3
+ DataTypeSchema,
4
+ DimensionSchema,
5
+ FomModel,
6
+ FomOutputOptions,
7
+ InteractionClassSchema,
8
+ ObjectClassSchema,
9
+ TransportationSchema,
10
+ } from "./types.ts";
11
+
12
+ const DEFAULT_NAMESPACE = "http://standards.ieee.org/IEEE1516-2025";
13
+ const DEFAULT_SCHEMA_LOCATION_OMT =
14
+ "http://standards.ieee.org/IEEE1516-2025 IEEE1516-OMT-2025.xsd";
15
+ const DEFAULT_SCHEMA_LOCATION_FDD =
16
+ "http://standards.ieee.org/IEEE1516-2025 IEEE1516-FDD-2025.xsd";
17
+
18
+ const DEFAULT_TRANSPORTATIONS: TransportationSchema[] = [
19
+ { name: "HLAreliable", reliable: "Yes" },
20
+ { name: "HLAbestEffort", reliable: "No" },
21
+ ];
22
+
23
+ export function renderFomXml(model: FomModel, options: FomOutputOptions): string {
24
+ const indent = options.indent ?? " ";
25
+ const schemaLocation =
26
+ options.schemaLocation ??
27
+ (options.format === "omt" ? DEFAULT_SCHEMA_LOCATION_OMT : DEFAULT_SCHEMA_LOCATION_FDD);
28
+ const includeModelIdentification = options.includeModelIdentification ?? true;
29
+ const inferParentStubs = options.inferParentStubs ?? true;
30
+
31
+ const writer = new XmlWriter(indent);
32
+ writer.declaration();
33
+ writer.openTag("objectModel", {
34
+ xmlns: DEFAULT_NAMESPACE,
35
+ "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
36
+ "xsi:schemaLocation": schemaLocation,
37
+ });
38
+
39
+ if (includeModelIdentification && model.modelIdentification) {
40
+ renderModelIdentification(writer, model.modelIdentification);
41
+ }
42
+
43
+ renderObjects(writer, model.objectClasses, inferParentStubs);
44
+ renderInteractions(writer, model.interactionClasses, inferParentStubs);
45
+ renderDimensions(writer, model.dimensions ?? []);
46
+ if (model.time) {
47
+ renderTime(writer, model.time);
48
+ }
49
+ renderTransportations(writer, model.transportations ?? DEFAULT_TRANSPORTATIONS);
50
+ if (model.switches) {
51
+ renderSwitches(writer, model.switches);
52
+ }
53
+ renderDataTypes(writer, model.dataTypes);
54
+
55
+ writer.closeTag("objectModel");
56
+ return writer.toString();
57
+ }
58
+
59
+ function renderModelIdentification(writer: XmlWriter, model: FomModel["modelIdentification"]) {
60
+ if (!model) return;
61
+ writer.openTag("modelIdentification");
62
+ writer.element("name", model.name);
63
+ writer.element("type", model.type);
64
+ writer.element("version", model.version);
65
+ writer.element("modificationDate", model.modificationDate);
66
+ writer.element("securityClassification", model.securityClassification);
67
+ if (model.purpose) writer.element("purpose", model.purpose);
68
+ if (model.applicationDomain) writer.element("applicationDomain", model.applicationDomain);
69
+ writer.element("description", model.description);
70
+ if (model.useLimitation) writer.element("useLimitation", model.useLimitation);
71
+ if (model.keyword) {
72
+ for (const keyword of model.keyword) {
73
+ writer.openTag("keyword");
74
+ if (keyword.taxonomy) writer.element("taxonomy", keyword.taxonomy);
75
+ writer.element("keywordValue", keyword.keywordValue);
76
+ writer.closeTag("keyword");
77
+ }
78
+ }
79
+ writer.closeTag("modelIdentification");
80
+ }
81
+
82
+ function renderTime(writer: XmlWriter, time: FomModel["time"]) {
83
+ if (!time) return;
84
+ writer.openTag("time");
85
+ writer.openTag("timeStamp");
86
+ writer.element("dataType", time.timeStampType);
87
+ if (time.semantics?.timeStamp) {
88
+ writer.element("semantics", time.semantics.timeStamp);
89
+ }
90
+ writer.closeTag("timeStamp");
91
+ writer.openTag("lookahead");
92
+ writer.element("dataType", time.lookaheadType);
93
+ if (time.semantics?.lookahead) {
94
+ writer.element("semantics", time.semantics.lookahead);
95
+ }
96
+ writer.closeTag("lookahead");
97
+ writer.closeTag("time");
98
+ }
99
+
100
+ type ObjectNode = {
101
+ fullName: string;
102
+ name: string;
103
+ schema?: ObjectClassSchema;
104
+ __children?: ObjectNode[];
105
+ };
106
+ type InteractionNode = {
107
+ fullName: string;
108
+ name: string;
109
+ schema?: InteractionClassSchema;
110
+ __children?: InteractionNode[];
111
+ };
112
+
113
+ function renderObjects(writer: XmlWriter, classes: ObjectClassSchema[], inferParentStubs: boolean) {
114
+ writer.openTag("objects");
115
+ const roots = buildHierarchy<ObjectClassSchema, ObjectNode>(classes, "HLAobjectRoot", inferParentStubs);
116
+ const root =
117
+ roots.root ??
118
+ createNode<ObjectClassSchema, ObjectNode>("HLAobjectRoot", "HLAobjectRoot");
119
+ renderObjectClass(writer, root);
120
+ writer.closeTag("objects");
121
+ }
122
+
123
+ function renderObjectClass(writer: XmlWriter, node: ObjectNode) {
124
+ const obj = node.schema;
125
+ writer.openTag("objectClass");
126
+ writer.element("name", node.name);
127
+ if (obj?.sharing) writer.element("sharing", obj.sharing);
128
+ if (obj?.semantics) writer.element("semantics", obj.semantics);
129
+ if (obj?.attributes) {
130
+ for (const attr of obj.attributes) {
131
+ renderAttribute(writer, attr);
132
+ }
133
+ }
134
+ for (const child of node.__children ?? []) {
135
+ renderObjectClass(writer, child);
136
+ }
137
+ writer.closeTag("objectClass");
138
+ }
139
+
140
+ function renderAttribute(writer: XmlWriter, attr: AttributeSchema) {
141
+ writer.openTag("attribute");
142
+ writer.element("name", attr.name);
143
+ writer.element("dataType", attr.dataType);
144
+ writer.element("updateType", attr.updateType);
145
+ writer.element("updateCondition", attr.updateCondition);
146
+ writer.element("ownership", attr.ownership);
147
+ writer.element("sharing", attr.sharing);
148
+ writer.element("transportation", attr.transportation);
149
+ writer.element("order", attr.order);
150
+ if (attr.valueRequired !== undefined) {
151
+ writer.element("valueRequired", attr.valueRequired ? "true" : "false");
152
+ }
153
+ if (attr.semantics) writer.element("semantics", attr.semantics);
154
+ writer.closeTag("attribute");
155
+ }
156
+
157
+ function renderInteractions(
158
+ writer: XmlWriter,
159
+ classes: InteractionClassSchema[],
160
+ inferParentStubs: boolean
161
+ ) {
162
+ writer.openTag("interactions");
163
+ const roots = buildHierarchy<InteractionClassSchema, InteractionNode>(
164
+ classes,
165
+ "HLAinteractionRoot",
166
+ inferParentStubs
167
+ );
168
+ const root =
169
+ roots.root ??
170
+ createNode<InteractionClassSchema, InteractionNode>(
171
+ "HLAinteractionRoot",
172
+ "HLAinteractionRoot"
173
+ );
174
+ renderInteractionClass(writer, root);
175
+ writer.closeTag("interactions");
176
+ }
177
+
178
+ function renderInteractionClass(writer: XmlWriter, node: InteractionNode) {
179
+ const interaction = node.schema;
180
+ writer.openTag("interactionClass");
181
+ writer.element("name", node.name);
182
+ if (interaction?.sharing) writer.element("sharing", interaction.sharing);
183
+ if (interaction?.transportation) writer.element("transportation", interaction.transportation);
184
+ if (interaction?.order) writer.element("order", interaction.order);
185
+ if (interaction?.semantics) writer.element("semantics", interaction.semantics);
186
+ if (interaction?.parameters) {
187
+ for (const param of interaction.parameters) {
188
+ writer.openTag("parameter");
189
+ writer.element("name", param.name);
190
+ writer.element("dataType", param.dataType);
191
+ if (param.semantics) writer.element("semantics", param.semantics);
192
+ writer.closeTag("parameter");
193
+ }
194
+ }
195
+ for (const child of node.__children ?? []) {
196
+ renderInteractionClass(writer, child);
197
+ }
198
+ writer.closeTag("interactionClass");
199
+ }
200
+
201
+ function renderDimensions(writer: XmlWriter, dimensions: DimensionSchema[]) {
202
+ writer.openTag("dimensions");
203
+ for (const dim of dimensions) {
204
+ writer.openTag("dimension");
205
+ writer.element("name", dim.name);
206
+ if (dim.dataType) writer.element("dataType", dim.dataType);
207
+ writer.closeTag("dimension");
208
+ }
209
+ writer.closeTag("dimensions");
210
+ }
211
+
212
+ function renderTransportations(writer: XmlWriter, transportations: TransportationSchema[]) {
213
+ writer.openTag("transportations");
214
+ for (const transport of transportations) {
215
+ writer.openTag("transportation");
216
+ writer.element("name", transport.name);
217
+ if (transport.reliable) {
218
+ writer.element("reliable", transport.reliable);
219
+ }
220
+ writer.closeTag("transportation");
221
+ }
222
+ writer.closeTag("transportations");
223
+ }
224
+
225
+ function renderSwitches(writer: XmlWriter, switches: FomModel["switches"]) {
226
+ if (!switches) return;
227
+ writer.openTag("switches");
228
+ writer.emptyTag("autoProvide", { isEnabled: switches.autoProvide ? "true" : "false" });
229
+ writer.emptyTag("conveyRegionDesignatorSets", { isEnabled: switches.conveyRegionDesignatorSets ? "true" : "false" });
230
+ writer.emptyTag("conveyProducingFederate", { isEnabled: switches.conveyProducingFederate ? "true" : "false" });
231
+ writer.emptyTag("attributeScopeAdvisory", { isEnabled: switches.attributeScopeAdvisory ? "true" : "false" });
232
+ writer.emptyTag("attributeRelevanceAdvisory", { isEnabled: switches.attributeRelevanceAdvisory ? "true" : "false" });
233
+ writer.emptyTag("objectClassRelevanceAdvisory", { isEnabled: switches.objectClassRelevanceAdvisory ? "true" : "false" });
234
+ writer.emptyTag("interactionRelevanceAdvisory", { isEnabled: switches.interactionRelevanceAdvisory ? "true" : "false" });
235
+ writer.emptyTag("serviceReporting", { isEnabled: switches.serviceReporting ? "true" : "false" });
236
+ writer.emptyTag("exceptionReporting", { isEnabled: switches.exceptionReporting ? "true" : "false" });
237
+ writer.emptyTag("delaySubscriptionEvaluation", { isEnabled: switches.delaySubscriptionEvaluation ? "true" : "false" });
238
+ if (switches.automaticResignAction) {
239
+ writer.emptyTag("automaticResignAction", { resignAction: switches.automaticResignAction });
240
+ }
241
+ writer.closeTag("switches");
242
+ }
243
+
244
+ function renderDataTypes(writer: XmlWriter, dataTypes: DataTypeSchema[]) {
245
+ writer.openTag("dataTypes");
246
+ writer.openTag("simpleDataTypes");
247
+ for (const dt of dataTypes.filter((d) => d.kind === "simple")) {
248
+ writer.openTag("simpleData");
249
+ writer.element("name", dt.name);
250
+ writer.element("representation", dt.representation);
251
+ if (dt.units) writer.element("units", dt.units);
252
+ if (dt.resolution) writer.element("resolution", dt.resolution);
253
+ if (dt.accuracy) writer.element("accuracy", dt.accuracy);
254
+ if (dt.semantics) writer.element("semantics", dt.semantics);
255
+ writer.closeTag("simpleData");
256
+ }
257
+ writer.closeTag("simpleDataTypes");
258
+
259
+ writer.openTag("enumeratedDataTypes");
260
+ for (const dt of dataTypes.filter((d) => d.kind === "enumerated")) {
261
+ writer.openTag("enumeratedData");
262
+ writer.element("name", dt.name);
263
+ writer.element("representation", dt.representation);
264
+ if (dt.semantics) writer.element("semantics", dt.semantics);
265
+ for (const enumerator of dt.enumerators) {
266
+ writer.openTag("enumerator");
267
+ writer.element("name", enumerator.name);
268
+ const values = Array.isArray(enumerator.value) ? enumerator.value : [enumerator.value];
269
+ for (const value of values) {
270
+ writer.element("value", value);
271
+ }
272
+ writer.closeTag("enumerator");
273
+ }
274
+ writer.closeTag("enumeratedData");
275
+ }
276
+ writer.closeTag("enumeratedDataTypes");
277
+
278
+ writer.openTag("arrayDataTypes");
279
+ for (const dt of dataTypes.filter((d) => d.kind === "array")) {
280
+ writer.openTag("arrayData");
281
+ writer.element("name", dt.name);
282
+ writer.element("dataType", dt.dataType);
283
+ writer.element("cardinality", dt.cardinality);
284
+ writer.element("encoding", dt.encoding);
285
+ if (dt.semantics) writer.element("semantics", dt.semantics);
286
+ writer.closeTag("arrayData");
287
+ }
288
+ writer.closeTag("arrayDataTypes");
289
+
290
+ writer.openTag("fixedRecordDataTypes");
291
+ for (const dt of dataTypes.filter((d) => d.kind === "fixedRecord")) {
292
+ writer.openTag("fixedRecordData");
293
+ writer.element("name", dt.name);
294
+ writer.element("encoding", dt.encoding);
295
+ if (dt.semantics) writer.element("semantics", dt.semantics);
296
+ for (const field of dt.fields) {
297
+ writer.openTag("field");
298
+ writer.element("name", field.name);
299
+ writer.element("dataType", field.dataType);
300
+ if (field.semantics) writer.element("semantics", field.semantics);
301
+ writer.closeTag("field");
302
+ }
303
+ writer.closeTag("fixedRecordData");
304
+ }
305
+ writer.closeTag("fixedRecordDataTypes");
306
+
307
+ writer.openTag("variantRecordDataTypes");
308
+ for (const dt of dataTypes.filter((d) => d.kind === "variantRecord")) {
309
+ writer.openTag("variantRecordData");
310
+ writer.element("name", dt.name);
311
+ writer.element("discriminant", dt.discriminant);
312
+ writer.element("dataType", dt.dataType);
313
+ for (const alt of dt.alternatives) {
314
+ writer.openTag("alternative");
315
+ writer.element("enumerator", alt.enumerator);
316
+ if (alt.name) writer.element("name", alt.name);
317
+ if (alt.dataType) writer.element("dataType", alt.dataType);
318
+ if (alt.semantics) writer.element("semantics", alt.semantics);
319
+ writer.closeTag("alternative");
320
+ }
321
+ writer.element("encoding", dt.encoding);
322
+ if (dt.semantics) writer.element("semantics", dt.semantics);
323
+ writer.closeTag("variantRecordData");
324
+ }
325
+ writer.closeTag("variantRecordDataTypes");
326
+
327
+ writer.closeTag("dataTypes");
328
+ }
329
+
330
+ function buildHierarchy<
331
+ T extends { name: string; parent?: string },
332
+ N extends { fullName: string; name: string; schema?: T; __children?: N[] }
333
+ >(
334
+ classes: T[],
335
+ rootName: string,
336
+ inferParentStubs: boolean
337
+ ): { root: N | undefined; children: N[] } {
338
+ const map = new Map<string, N>();
339
+
340
+ for (const cls of classes) {
341
+ const fullName = resolveFullName(cls.name, cls.parent, rootName);
342
+ const node = map.get(fullName) ?? createNode<T, N>(fullName, localName(fullName));
343
+ node.schema = cls as T;
344
+ map.set(fullName, node);
345
+ }
346
+
347
+ if (inferParentStubs) {
348
+ for (const node of map.values()) {
349
+ ensureParentChain(map, node.fullName, rootName);
350
+ }
351
+ }
352
+
353
+ let root: N | undefined = map.get(rootName);
354
+ const orphans: N[] = [];
355
+
356
+ for (const node of map.values()) {
357
+ if (node.fullName === rootName) continue;
358
+ const parentFullName = resolveParentFullName(
359
+ node.fullName,
360
+ node.schema?.parent,
361
+ rootName
362
+ ) ?? rootName;
363
+ const parent = map.get(parentFullName);
364
+ if (parent) {
365
+ parent.__children?.push(node);
366
+ } else if (!inferParentStubs || parentFullName === rootName) {
367
+ orphans.push(node);
368
+ }
369
+ }
370
+
371
+ if (root) {
372
+ root.__children?.push(...orphans);
373
+ }
374
+
375
+ return {
376
+ root,
377
+ children: root ? root.__children ?? [] : orphans,
378
+ };
379
+ }
380
+
381
+ function createNode<
382
+ T extends { name: string; parent?: string },
383
+ N extends { fullName: string; name: string; schema?: T; __children?: N[] }
384
+ >(fullName: string, name: string, schema?: T): N {
385
+ return { fullName, name, schema, __children: [] } as unknown as N;
386
+ }
387
+
388
+ function ensureParentChain<
389
+ T extends { name: string; parent?: string },
390
+ N extends { fullName: string; name: string; schema?: T; __children?: N[] }
391
+ >(map: Map<string, N>, fullName: string, rootName: string): void {
392
+ let current = fullName;
393
+ while (true) {
394
+ const parent = inferParentFromFullName(current);
395
+ if (!parent) return;
396
+ if (!map.has(parent)) {
397
+ map.set(parent, createNode<T, N>(parent, localName(parent)));
398
+ }
399
+ if (parent === rootName) return;
400
+ current = parent;
401
+ }
402
+ }
403
+
404
+ function resolveFullName(name: string, parent: string | undefined, rootName: string): string {
405
+ if (name.includes(".")) return name;
406
+ if (name === rootName) return rootName;
407
+ const parentFull = resolveParentNameForFull(parent, rootName);
408
+ if (parentFull) return `${parentFull}.${name}`;
409
+ return `${rootName}.${name}`;
410
+ }
411
+
412
+ function resolveParentFullName(
413
+ fullName: string,
414
+ parent: string | undefined,
415
+ rootName: string
416
+ ): string | undefined {
417
+ if (parent) {
418
+ if (parent.includes(".")) return parent;
419
+ if (parent === rootName) return rootName;
420
+ return `${rootName}.${parent}`;
421
+ }
422
+ return inferParentFromFullName(fullName);
423
+ }
424
+
425
+ function inferParentFromFullName(fullName: string): string | undefined {
426
+ if (!fullName.includes(".")) return undefined;
427
+ const parent = fullName.slice(0, fullName.lastIndexOf("."));
428
+ if (!parent || parent === fullName) return undefined;
429
+ return parent;
430
+ }
431
+
432
+ function localName(fullName: string): string {
433
+ if (!fullName.includes(".")) return fullName;
434
+ return fullName.slice(fullName.lastIndexOf(".") + 1);
435
+ }
436
+
437
+ function resolveParentNameForFull(
438
+ parent: string | undefined,
439
+ rootName: string
440
+ ): string | undefined {
441
+ if (!parent) return undefined;
442
+ if (parent.includes(".")) return parent;
443
+ if (parent === rootName) return rootName;
444
+ return `${rootName}.${parent}`;
445
+ }
446
+
447
+ class XmlWriter {
448
+ private readonly _lines: string[] = [];
449
+ private readonly _indent: string;
450
+ private _level = 0;
451
+
452
+ constructor(indent: string) {
453
+ this._indent = indent;
454
+ }
455
+
456
+ declaration(): void {
457
+ this._lines.push('<?xml version="1.0" encoding="UTF-8"?>');
458
+ }
459
+
460
+ openTag(name: string, attrs?: Record<string, string>): void {
461
+ const attrText = attrs ? renderAttributes(attrs) : "";
462
+ this._lines.push(`${this._indent.repeat(this._level)}<${name}${attrText}>`);
463
+ this._level += 1;
464
+ }
465
+
466
+ closeTag(name: string): void {
467
+ this._level -= 1;
468
+ this._lines.push(`${this._indent.repeat(this._level)}</${name}>`);
469
+ }
470
+
471
+ element(name: string, content: string): void {
472
+ this._lines.push(
473
+ `${this._indent.repeat(this._level)}<${name}>${escapeXml(content)}</${name}>`
474
+ );
475
+ }
476
+
477
+ emptyTag(name: string, attrs?: Record<string, string>): void {
478
+ const attrText = attrs ? renderAttributes(attrs) : "";
479
+ this._lines.push(`${this._indent.repeat(this._level)}<${name}${attrText}/>`);
480
+ }
481
+
482
+ toString(): string {
483
+ return `${this._lines.join("\n")}\n`;
484
+ }
485
+ }
486
+
487
+ function renderAttributes(attrs: Record<string, string>): string {
488
+ const parts: string[] = [];
489
+ for (const [key, value] of Object.entries(attrs)) {
490
+ parts.push(`${key}="${escapeXml(value)}"`);
491
+ }
492
+ return parts.length ? ` ${parts.join(" ")}` : "";
493
+ }
494
+
495
+ function escapeXml(value: string): string {
496
+ return value
497
+ .replace(/&/g, "&amp;")
498
+ .replace(/</g, "&lt;")
499
+ .replace(/>/g, "&gt;")
500
+ .replace(/"/g, "&quot;")
501
+ .replace(/'/g, "&apos;");
502
+ }