@openmbee/opensysml 0.9.1 → 0.10.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 +355 -33
- package/dist/browser/index.d.ts +9 -0
- package/dist/browser/index.js +16 -0
- package/dist/browser/wasm-worker.d.ts +1 -0
- package/dist/browser/wasm-worker.js +2 -0
- package/dist/browser/wasm.d.ts +17 -0
- package/dist/browser/wasm.js +94 -0
- package/dist/core/arguments.d.ts +4 -0
- package/dist/core/arguments.js +19 -0
- package/dist/core/capabilities.d.ts +90 -9
- package/dist/core/capabilities.js +111 -10
- package/dist/core/connection.d.ts +203 -2
- package/dist/core/connection.js +810 -17
- package/dist/core/conversion.d.ts +153 -0
- package/dist/core/conversion.js +191 -0
- package/dist/core/document.d.ts +188 -0
- package/dist/core/document.js +457 -0
- package/dist/core/edit.d.ts +446 -0
- package/dist/core/edit.js +1994 -0
- package/dist/core/engines.d.ts +64 -0
- package/dist/core/engines.js +74 -0
- package/dist/core/errors.d.ts +164 -2
- package/dist/core/errors.js +209 -3
- package/dist/core/exploration.d.ts +71 -0
- package/dist/core/exploration.js +116 -0
- package/dist/core/graphs.d.ts +15 -0
- package/dist/core/graphs.js +3 -0
- package/dist/core/index.d.ts +30 -9
- package/dist/core/index.js +14 -5
- package/dist/core/model.d.ts +168 -8
- package/dist/core/model.js +258 -31
- package/dist/core/package.d.ts +2 -0
- package/dist/core/package.js +2 -0
- package/dist/core/query.d.ts +50 -0
- package/dist/core/query.js +197 -0
- package/dist/core/render-view.d.ts +97 -0
- package/dist/core/render-view.js +120 -0
- package/dist/core/sources.d.ts +34 -0
- package/dist/core/sources.js +134 -0
- package/dist/core/status.d.ts +9 -2
- package/dist/core/status.js +24 -8
- package/dist/core/typed.d.ts +71 -0
- package/dist/core/typed.js +245 -0
- package/dist/core/values.d.ts +127 -8
- package/dist/core/values.js +536 -30
- package/dist/core/verdict.d.ts +271 -0
- package/dist/core/verdict.js +567 -0
- package/dist/core/wasm.d.ts +114 -0
- package/dist/core/wasm.js +514 -0
- package/dist/generated/sysml_pb.d.ts +1080 -22
- package/dist/generated/sysml_pb.js +200 -100
- package/dist/node/binary.d.ts +2 -0
- package/dist/node/binary.js +8 -6
- package/dist/node/generate-cli.d.ts +2 -0
- package/dist/node/generate-cli.js +10 -0
- package/dist/node/generate.d.ts +99 -0
- package/dist/node/generate.js +757 -0
- package/dist/node/index.d.ts +29 -2
- package/dist/node/index.js +211 -10
- package/dist/node/service.d.ts +5 -3
- package/dist/node/service.js +32 -26
- package/dist/node/wasm-source.d.ts +18 -0
- package/dist/node/wasm-source.js +103 -0
- package/dist/node/wasm-worker.d.ts +1 -0
- package/dist/node/wasm-worker.js +10 -0
- package/dist/node/wasm.d.ts +17 -0
- package/dist/node/wasm.js +63 -0
- package/package.json +19 -7
- package/release-digests.json +105 -0
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
// Generate statically typed TypeScript classes from a SysML model.
|
|
2
|
+
//
|
|
3
|
+
// Usage: `opensysml-generate model.sysml -o model_types.ts`
|
|
4
|
+
//
|
|
5
|
+
// The generated module contains one class per SysML definition, deriving from
|
|
6
|
+
// TypedObject, with one getter per feature. Emission is deterministic —
|
|
7
|
+
// symbols are ordered by fully-qualified name and nothing environment-
|
|
8
|
+
// dependent is written — so the output can be committed as a golden file, and
|
|
9
|
+
// --check can gate that it is current.
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { CAPABILITY_TYPE_FACTS, MissingCapabilityError, requireCapability, } from "../core/capabilities.js";
|
|
13
|
+
import { PACKAGE_NAME } from "../core/package.js";
|
|
14
|
+
import { connect } from "./index.js";
|
|
15
|
+
export const GENERATED_HEADER = "// Generated by opensysml-generate. Do not edit.";
|
|
16
|
+
// Decoder for a feature whose SysML type has no sound TypeScript type.
|
|
17
|
+
const OBJECT_DECODER = "_t.asObject";
|
|
18
|
+
// Emission schema, bumped when this generator's output changes for an
|
|
19
|
+
// unchanged model. Not the opensysml release version, which would churn
|
|
20
|
+
// every module.
|
|
21
|
+
export const GENERATOR_VERSION = "5";
|
|
22
|
+
// The stamp hashes the model source client-side. A path, a timestamp or the
|
|
23
|
+
// service version would churn without the module's content changing.
|
|
24
|
+
export const MODEL_HASH_ALGORITHM = "sha256";
|
|
25
|
+
// What a module rendered without the model source says, so it cannot be
|
|
26
|
+
// mistaken for one stamped with a real hash.
|
|
27
|
+
export const UNSTAMPED = "unstamped";
|
|
28
|
+
// Definition kinds get a class; these usage kinds become properties on the
|
|
29
|
+
// enclosing class. Behavioral and connector usages (action, state, calc,
|
|
30
|
+
// constraint, requirement, connection, flow, interface, allocation, case) are
|
|
31
|
+
// not instance feature values and are skipped.
|
|
32
|
+
const FEATURE_KINDS = new Set([
|
|
33
|
+
"attributeusage",
|
|
34
|
+
"referenceusage",
|
|
35
|
+
"partusage",
|
|
36
|
+
"itemusage",
|
|
37
|
+
"occurrenceusage",
|
|
38
|
+
"individualusage",
|
|
39
|
+
"portusage",
|
|
40
|
+
"enumusage",
|
|
41
|
+
]);
|
|
42
|
+
// The generalization edges that make one element a kind of another. A typing
|
|
43
|
+
// edge is the element's type, not a supertype of it, so it is not one of these.
|
|
44
|
+
const GENERALIZATION_KINDS = new Set(["specializes", "subsets", "redefines"]);
|
|
45
|
+
// How far a generalization chain is followed for a fact a declaration leaves out.
|
|
46
|
+
const MAX_GENERALIZATION_DEPTH = 16;
|
|
47
|
+
const PRIMITIVE_TYPES = new Map([
|
|
48
|
+
["Boolean", { annotation: "boolean", decoder: "_t.asBoolean" }],
|
|
49
|
+
["String", { annotation: "string", decoder: "_t.asString" }],
|
|
50
|
+
["Natural", { annotation: "bigint", decoder: "_t.asInt" }],
|
|
51
|
+
["Integer", { annotation: "bigint", decoder: "_t.asInt" }],
|
|
52
|
+
["Rational", { annotation: "_t.RationalValue", decoder: "_t.asRational" }],
|
|
53
|
+
["Real", { annotation: "number", decoder: "_t.asReal" }],
|
|
54
|
+
["Complex", { annotation: "_t.ComplexValue", decoder: "_t.asComplex" }],
|
|
55
|
+
]);
|
|
56
|
+
// Library scalars with no sound TypeScript counterpart, mapped to object:
|
|
57
|
+
// Number is the supertype of every numeric scalar, so a Number slot may hold
|
|
58
|
+
// any of them.
|
|
59
|
+
const UNMAPPED_PRIMITIVES = new Set(["Number"]);
|
|
60
|
+
// Members TypedObject itself provides; a getter of the same name would shadow
|
|
61
|
+
// the machinery the accessors use, so such a feature is renamed.
|
|
62
|
+
const RESERVED_MEMBERS = new Set([
|
|
63
|
+
"instance",
|
|
64
|
+
"fromInstance",
|
|
65
|
+
"unchecked",
|
|
66
|
+
"sysmlId",
|
|
67
|
+
"resolver",
|
|
68
|
+
]);
|
|
69
|
+
// Words TypeScript and JavaScript reserve; a getter or class may not be one.
|
|
70
|
+
const RESERVED_WORDS = new Set([
|
|
71
|
+
"break", "case", "catch", "class", "const", "continue", "debugger",
|
|
72
|
+
"default", "delete", "do", "else", "enum", "export", "extends", "false",
|
|
73
|
+
"finally", "for", "function", "if", "implements", "import", "in",
|
|
74
|
+
"instanceof", "interface", "let", "new", "null", "package", "private",
|
|
75
|
+
"protected", "public", "return", "static", "super", "switch", "this",
|
|
76
|
+
"throw", "true", "try", "typeof", "undefined", "var", "void", "while",
|
|
77
|
+
"with", "yield",
|
|
78
|
+
]);
|
|
79
|
+
/** Whether a symbol kind declares a type, e.g. `partDef` or `metaclass`. */
|
|
80
|
+
export function isDefinitionKind(kind) {
|
|
81
|
+
const lowered = kind.toLowerCase();
|
|
82
|
+
return lowered.endsWith("def") || lowered === "metaclass";
|
|
83
|
+
}
|
|
84
|
+
/** Whether a symbol kind is a usage exposed as an instance feature. */
|
|
85
|
+
export function isFeatureKind(kind) {
|
|
86
|
+
return FEATURE_KINDS.has(kind.toLowerCase());
|
|
87
|
+
}
|
|
88
|
+
/** One SysML definition to emit as a class. */
|
|
89
|
+
export class Definition {
|
|
90
|
+
id;
|
|
91
|
+
name;
|
|
92
|
+
kind;
|
|
93
|
+
specializations;
|
|
94
|
+
features;
|
|
95
|
+
constructor(init) {
|
|
96
|
+
this.id = init.id;
|
|
97
|
+
this.name = init.name;
|
|
98
|
+
this.kind = init.kind;
|
|
99
|
+
this.specializations = init.specializations ?? [];
|
|
100
|
+
this.features = init.features ?? [];
|
|
101
|
+
}
|
|
102
|
+
/** FQNs of what this definition specializes, subsets or redefines, in
|
|
103
|
+
* declaration order, without repetition. */
|
|
104
|
+
get baseIds() {
|
|
105
|
+
const ordered = [];
|
|
106
|
+
for (const spec of this.specializations) {
|
|
107
|
+
if (GENERALIZATION_KINDS.has(spec.kind) && spec.targetId !== "" && !ordered.includes(spec.targetId)) {
|
|
108
|
+
ordered.push(spec.targetId);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return ordered;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** Collect the definitions in one or more symbol trees, sorted by FQN, each
|
|
115
|
+
* feature taking the type and multiplicity it inherits. */
|
|
116
|
+
export async function collectDefinitions(...roots) {
|
|
117
|
+
const definitions = [];
|
|
118
|
+
const factsById = new Map();
|
|
119
|
+
await Promise.all(roots.map((root) => collect(root, definitions, factsById)));
|
|
120
|
+
const resolved = definitions.map((definition) => withInheritedFacts(definition, factsById));
|
|
121
|
+
resolved.sort((left, right) => left.id.localeCompare(right.id));
|
|
122
|
+
return resolved;
|
|
123
|
+
}
|
|
124
|
+
async function collect(symbol, out, factsById) {
|
|
125
|
+
const children = await symbol.children();
|
|
126
|
+
factsById.set(symbol.id, {
|
|
127
|
+
id: symbol.id,
|
|
128
|
+
name: symbol.name,
|
|
129
|
+
kind: symbol.kind,
|
|
130
|
+
type: symbol.type,
|
|
131
|
+
multiplicity: symbol.multiplicity,
|
|
132
|
+
specializations: symbol.specializations ?? [],
|
|
133
|
+
});
|
|
134
|
+
if (isDefinitionKind(symbol.kind)) {
|
|
135
|
+
const featureChildren = children
|
|
136
|
+
.filter((child) => isFeatureKind(child.kind))
|
|
137
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
138
|
+
out.push(new Definition({
|
|
139
|
+
id: symbol.id,
|
|
140
|
+
name: symbol.name,
|
|
141
|
+
kind: symbol.kind,
|
|
142
|
+
specializations: symbol.specializations,
|
|
143
|
+
features: featureChildren.map((child) => ({
|
|
144
|
+
id: child.id,
|
|
145
|
+
name: child.name,
|
|
146
|
+
kind: child.kind,
|
|
147
|
+
type: child.type,
|
|
148
|
+
multiplicity: child.multiplicity,
|
|
149
|
+
})),
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
await Promise.all(children.map((child) => collect(child, out, factsById)));
|
|
153
|
+
}
|
|
154
|
+
function withInheritedFacts(definition, factsById) {
|
|
155
|
+
return new Definition({
|
|
156
|
+
id: definition.id,
|
|
157
|
+
name: definition.name,
|
|
158
|
+
kind: definition.kind,
|
|
159
|
+
specializations: definition.specializations,
|
|
160
|
+
features: definition.features.map((feature) => resolvedFeature(feature, factsById)),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
function resolvedFeature(feature, factsById) {
|
|
164
|
+
const owner = factsById.get(feature.id);
|
|
165
|
+
const specializations = owner?.specializations ?? [];
|
|
166
|
+
let typeFacts = feature.type;
|
|
167
|
+
if (!namesAType(typeFacts)) {
|
|
168
|
+
const inherited = inheritedFact({ specializations }, factsById, (other) => (namesAType(other.type) ? other.type : undefined));
|
|
169
|
+
if (inherited !== undefined) {
|
|
170
|
+
typeFacts = mergedType(typeFacts, inherited);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const multiplicity = feature.multiplicity ?? inheritedFact({ specializations }, factsById, (other) => other.multiplicity);
|
|
174
|
+
if (typeFacts === feature.type && multiplicity === feature.multiplicity) {
|
|
175
|
+
return feature;
|
|
176
|
+
}
|
|
177
|
+
return { ...feature, type: typeFacts, multiplicity };
|
|
178
|
+
}
|
|
179
|
+
function inheritedFact(facts, factsById, pick, depth = 0) {
|
|
180
|
+
if (depth >= MAX_GENERALIZATION_DEPTH) {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
for (const spec of facts.specializations) {
|
|
184
|
+
if (!GENERALIZATION_KINDS.has(spec.kind) || spec.targetId === "") {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const target = factsById.get(spec.targetId);
|
|
188
|
+
if (target === undefined) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const found = pick(target);
|
|
192
|
+
if (found !== undefined) {
|
|
193
|
+
return found;
|
|
194
|
+
}
|
|
195
|
+
const deeper = inheritedFact(target, factsById, pick, depth + 1);
|
|
196
|
+
if (deeper !== undefined) {
|
|
197
|
+
return deeper;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
function namesAType(typeFacts) {
|
|
203
|
+
return typeFacts !== undefined && (typeFacts.resolvedId !== "" || typeFacts.declared !== "");
|
|
204
|
+
}
|
|
205
|
+
function mergedType(own, inherited) {
|
|
206
|
+
if (own === undefined) {
|
|
207
|
+
return inherited;
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
declared: inherited.declared,
|
|
211
|
+
resolvedId: inherited.resolvedId,
|
|
212
|
+
resolvedKind: inherited.resolvedKind,
|
|
213
|
+
primitive: own.primitive !== "" ? own.primitive : inherited.primitive,
|
|
214
|
+
primitiveSource: own.primitiveSource !== "" ? own.primitiveSource : inherited.primitiveSource,
|
|
215
|
+
quantity: own.quantity || inherited.quantity,
|
|
216
|
+
unit: own.unit !== "" ? own.unit : inherited.unit,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/** Map definition FQN → TypeScript class name.
|
|
220
|
+
*
|
|
221
|
+
* A simple name declared by more than one definition is qualified with its
|
|
222
|
+
* owning path so that every generated class name is unique and stable. */
|
|
223
|
+
export function classNames(definitions) {
|
|
224
|
+
const byName = new Map();
|
|
225
|
+
for (const definition of definitions) {
|
|
226
|
+
const simple = identifier(definition.name);
|
|
227
|
+
const group = byName.get(simple) ?? [];
|
|
228
|
+
group.push(definition);
|
|
229
|
+
byName.set(simple, group);
|
|
230
|
+
}
|
|
231
|
+
const names = new Map();
|
|
232
|
+
for (const [simple, group] of byName) {
|
|
233
|
+
if (group.length === 1) {
|
|
234
|
+
names.set(group[0].id, simple);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
for (const definition of group) {
|
|
238
|
+
names.set(definition.id, definition.id.split("::").map(identifier).join("_"));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return names;
|
|
242
|
+
}
|
|
243
|
+
function identifier(name) {
|
|
244
|
+
let cleaned = Array.from(name)
|
|
245
|
+
.map((char) => (/[\p{L}\p{N}_]/u.test(char) ? char : "_"))
|
|
246
|
+
.join("");
|
|
247
|
+
if (cleaned === "" || /^\d/.test(cleaned)) {
|
|
248
|
+
cleaned = `_${cleaned}`;
|
|
249
|
+
}
|
|
250
|
+
if (RESERVED_WORDS.has(cleaned)) {
|
|
251
|
+
cleaned += "_";
|
|
252
|
+
}
|
|
253
|
+
return cleaned;
|
|
254
|
+
}
|
|
255
|
+
function literal(text) {
|
|
256
|
+
return JSON.stringify(text);
|
|
257
|
+
}
|
|
258
|
+
/** TypeScript name of the getter for a feature; the SysML feature name is unchanged. */
|
|
259
|
+
export function propertyName(name) {
|
|
260
|
+
let ident = identifier(name);
|
|
261
|
+
if (RESERVED_MEMBERS.has(ident)) {
|
|
262
|
+
ident += "_";
|
|
263
|
+
}
|
|
264
|
+
return ident;
|
|
265
|
+
}
|
|
266
|
+
/** Map the type of a single value to a TypeScript annotation and decoder. */
|
|
267
|
+
export function elementType(typeFacts, names) {
|
|
268
|
+
if (typeFacts === undefined) {
|
|
269
|
+
return { annotation: "_t.SysMLValue", decoder: OBJECT_DECODER, comment: "untyped feature" };
|
|
270
|
+
}
|
|
271
|
+
// A value written with a unit is a magnitude and that unit, whatever library
|
|
272
|
+
// scalar the magnitude reduces to, so it is typed before the primitive below.
|
|
273
|
+
if (typeFacts.quantity && typeFacts.unit !== "") {
|
|
274
|
+
return {
|
|
275
|
+
annotation: "_t.Quantity",
|
|
276
|
+
decoder: "_t.asQuantity",
|
|
277
|
+
comment: `quantity in [${typeFacts.unit}]`,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
if (typeFacts.quantity) {
|
|
281
|
+
// A quantity value type constrains the type, not what is written for it.
|
|
282
|
+
return {
|
|
283
|
+
annotation: "_t.SysMLValue",
|
|
284
|
+
decoder: OBJECT_DECODER,
|
|
285
|
+
comment: "quantity value type, no unit written",
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
// A definition reducing to a library scalar holds that scalar, not an
|
|
289
|
+
// instance, even when it has a generated class of its own.
|
|
290
|
+
const primitive = PRIMITIVE_TYPES.get(typeFacts.primitive);
|
|
291
|
+
if (primitive !== undefined) {
|
|
292
|
+
return { annotation: primitive.annotation, decoder: primitive.decoder, comment: "" };
|
|
293
|
+
}
|
|
294
|
+
// An enumeration-typed feature holds one of the enumeration's literals, a
|
|
295
|
+
// value of its own rather than an instance of the generated class.
|
|
296
|
+
if (typeFacts.resolvedKind === "enumDef") {
|
|
297
|
+
return { annotation: "_t.EnumValue", decoder: "_t.asEnum", comment: "" };
|
|
298
|
+
}
|
|
299
|
+
const generated = names.get(typeFacts.resolvedId);
|
|
300
|
+
if (typeFacts.resolvedId !== "" && generated !== undefined) {
|
|
301
|
+
return { annotation: generated, decoder: `_t.asTyped(${generated})`, comment: "" };
|
|
302
|
+
}
|
|
303
|
+
if (UNMAPPED_PRIMITIVES.has(typeFacts.primitive)) {
|
|
304
|
+
return {
|
|
305
|
+
annotation: "_t.SysMLValue",
|
|
306
|
+
decoder: OBJECT_DECODER,
|
|
307
|
+
comment: `SysML ${typeFacts.primitive} has no TypeScript counterpart`,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
if (typeFacts.resolvedId !== "") {
|
|
311
|
+
return {
|
|
312
|
+
annotation: "_t.SysMLValue",
|
|
313
|
+
decoder: OBJECT_DECODER,
|
|
314
|
+
comment: `SysML type ${typeFacts.resolvedId} has no generated class`,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
if (typeFacts.declared !== "") {
|
|
318
|
+
return {
|
|
319
|
+
annotation: "_t.SysMLValue",
|
|
320
|
+
decoder: OBJECT_DECODER,
|
|
321
|
+
comment: `unresolved SysML type ${typeFacts.declared}`,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
return { annotation: "_t.SysMLValue", decoder: OBJECT_DECODER, comment: "untyped feature" };
|
|
325
|
+
}
|
|
326
|
+
const EMPTY_MULTIPLICITY = { lower: "", upper: "" };
|
|
327
|
+
function isCollection(multiplicity) {
|
|
328
|
+
if (multiplicity.upper === "") {
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
if (multiplicity.upper === "*") {
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
const upper = Number.parseInt(multiplicity.upper, 10);
|
|
335
|
+
return Number.isNaN(upper) ? undefined : upper > 1;
|
|
336
|
+
}
|
|
337
|
+
function isOptional(multiplicity) {
|
|
338
|
+
if (multiplicity.lower === "") {
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
const lower = Number.parseInt(multiplicity.lower, 10);
|
|
342
|
+
return Number.isNaN(lower) ? undefined : lower === 0;
|
|
343
|
+
}
|
|
344
|
+
/** Map a feature to its TypeScript annotation, decoder and accessor, honoring multiplicity. */
|
|
345
|
+
export function featureType(feature, names) {
|
|
346
|
+
const element = elementType(feature.type, names);
|
|
347
|
+
const multiplicity = feature.multiplicity ?? EMPTY_MULTIPLICITY;
|
|
348
|
+
if (isCollection(multiplicity)) {
|
|
349
|
+
return { ...element, annotation: `readonly ${element.annotation}[]` };
|
|
350
|
+
}
|
|
351
|
+
if (isOptional(multiplicity)) {
|
|
352
|
+
return { ...element, annotation: `${element.annotation} | undefined` };
|
|
353
|
+
}
|
|
354
|
+
return element;
|
|
355
|
+
}
|
|
356
|
+
function accessor(feature) {
|
|
357
|
+
const multiplicity = feature.multiplicity ?? EMPTY_MULTIPLICITY;
|
|
358
|
+
if (isCollection(multiplicity)) {
|
|
359
|
+
return "_t.listFeatureValue";
|
|
360
|
+
}
|
|
361
|
+
if (isOptional(multiplicity)) {
|
|
362
|
+
return "_t.optionalFeatureValue";
|
|
363
|
+
}
|
|
364
|
+
return "_t.featureValue";
|
|
365
|
+
}
|
|
366
|
+
/** Hash of the model source a generated module was produced from.
|
|
367
|
+
*
|
|
368
|
+
* Newlines are normalized so the same model checked out with CRLF endings
|
|
369
|
+
* stamps identically; nothing else about the text is interpreted. */
|
|
370
|
+
export function modelStamp(sourceText) {
|
|
371
|
+
const normalized = sourceText.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
|
|
372
|
+
return createHash(MODEL_HASH_ALGORITHM).update(normalized, "utf8").digest("hex");
|
|
373
|
+
}
|
|
374
|
+
function hashLiteral(stamp) {
|
|
375
|
+
return stamp !== undefined ? `${MODEL_HASH_ALGORITHM}:${stamp}` : UNSTAMPED;
|
|
376
|
+
}
|
|
377
|
+
/** Render a complete, importable TypeScript module for `definitions`.
|
|
378
|
+
*
|
|
379
|
+
* `stamp` is the {@link modelStamp} of the model source, recorded in the
|
|
380
|
+
* module so a reader — and the `--check` gate — can tell what it was generated
|
|
381
|
+
* from. Without one the module says `unstamped` rather than claim a hash it
|
|
382
|
+
* does not have. */
|
|
383
|
+
export function renderModule(definitions, stamp) {
|
|
384
|
+
const names = classNames(definitions);
|
|
385
|
+
const lines = [
|
|
386
|
+
GENERATED_HEADER,
|
|
387
|
+
"// Typed views over SysML definitions, generated from a parsed model.",
|
|
388
|
+
"",
|
|
389
|
+
`import * as _t from ${literal(PACKAGE_NAME)};`,
|
|
390
|
+
"",
|
|
391
|
+
`export const SYSML_GENERATOR_VERSION = ${literal(GENERATOR_VERSION)};`,
|
|
392
|
+
`export const SYSML_MODEL_HASH = ${literal(hashLiteral(stamp))};`,
|
|
393
|
+
];
|
|
394
|
+
if (definitions.length === 0) {
|
|
395
|
+
lines.push("");
|
|
396
|
+
return lines.join("\n");
|
|
397
|
+
}
|
|
398
|
+
const plan = basePlan(definitions);
|
|
399
|
+
const linearizations = plan.linearizations;
|
|
400
|
+
for (const definition of dependencyOrder(definitions)) {
|
|
401
|
+
lines.push("", "", ...renderClass(definition, names, plan.plan.get(definition.id), linearizations, definitions));
|
|
402
|
+
}
|
|
403
|
+
lines.push("");
|
|
404
|
+
return lines.join("\n");
|
|
405
|
+
}
|
|
406
|
+
function basePlan(definitions) {
|
|
407
|
+
const generated = new Set(definitions.map((definition) => definition.id));
|
|
408
|
+
const linearizations = new Map();
|
|
409
|
+
const plan = new Map();
|
|
410
|
+
for (const definition of dependencyOrder(definitions)) {
|
|
411
|
+
const kept = [];
|
|
412
|
+
const declared = [...new Set(definition.baseIds)].filter((baseId) => generated.has(baseId));
|
|
413
|
+
const candidates = declared.filter((baseId) => linearizations.has(baseId));
|
|
414
|
+
for (const baseId of withoutImplied(candidates, linearizations)) {
|
|
415
|
+
if (linearize(definition.id, [...kept, baseId], linearizations) === undefined) {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
kept.push(baseId);
|
|
419
|
+
}
|
|
420
|
+
const line = linearize(definition.id, kept, linearizations) ?? [definition.id];
|
|
421
|
+
linearizations.set(definition.id, line);
|
|
422
|
+
const dropped = declared.filter((baseId) => !line.includes(baseId));
|
|
423
|
+
plan.set(definition.id, { kept, dropped });
|
|
424
|
+
}
|
|
425
|
+
return { plan, linearizations };
|
|
426
|
+
}
|
|
427
|
+
function withoutImplied(bases, linearizations) {
|
|
428
|
+
const unique = [...new Set(bases)];
|
|
429
|
+
return unique.filter((baseId) => !unique.some((other) => other !== baseId && (linearizations.get(other) ?? []).slice(1).includes(baseId)));
|
|
430
|
+
}
|
|
431
|
+
function linearize(target, bases, linearizations) {
|
|
432
|
+
const sequences = bases.map((base) => [...(linearizations.get(base) ?? [base])]);
|
|
433
|
+
sequences.push([...bases]);
|
|
434
|
+
const result = [target];
|
|
435
|
+
for (;;) {
|
|
436
|
+
const remaining = sequences.filter((sequence) => sequence.length > 0);
|
|
437
|
+
if (remaining.length === 0) {
|
|
438
|
+
return result;
|
|
439
|
+
}
|
|
440
|
+
const head = remaining.find((sequence) => !remaining.some((other) => other.slice(1).includes(sequence[0])))?.[0];
|
|
441
|
+
if (head === undefined) {
|
|
442
|
+
return undefined;
|
|
443
|
+
}
|
|
444
|
+
result.push(head);
|
|
445
|
+
for (const sequence of remaining) {
|
|
446
|
+
if (sequence[0] === head) {
|
|
447
|
+
sequence.shift();
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
sequences.length = 0;
|
|
451
|
+
sequences.push(...remaining);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function dependencyOrder(definitions) {
|
|
455
|
+
const byId = new Map(definitions.map((definition) => [definition.id, definition]));
|
|
456
|
+
const ordered = [];
|
|
457
|
+
const emitted = new Set();
|
|
458
|
+
const visiting = new Set();
|
|
459
|
+
const visit = (definition) => {
|
|
460
|
+
if (emitted.has(definition.id) || visiting.has(definition.id)) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
visiting.add(definition.id);
|
|
464
|
+
for (const baseId of definition.baseIds) {
|
|
465
|
+
const base = byId.get(baseId);
|
|
466
|
+
if (base !== undefined) {
|
|
467
|
+
visit(base);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
visiting.delete(definition.id);
|
|
471
|
+
emitted.add(definition.id);
|
|
472
|
+
ordered.push(definition);
|
|
473
|
+
};
|
|
474
|
+
for (const definition of definitions) {
|
|
475
|
+
visit(definition);
|
|
476
|
+
}
|
|
477
|
+
return ordered;
|
|
478
|
+
}
|
|
479
|
+
function edgeKinds(definition) {
|
|
480
|
+
const edges = new Map();
|
|
481
|
+
for (const spec of definition.specializations) {
|
|
482
|
+
if (GENERALIZATION_KINDS.has(spec.kind) && spec.targetId !== "") {
|
|
483
|
+
edges.set(spec.targetId, spec.kind);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return edges;
|
|
487
|
+
}
|
|
488
|
+
function featuresOf(line, definitions) {
|
|
489
|
+
const byId = new Map(definitions.map((definition) => [definition.id, definition]));
|
|
490
|
+
const seen = new Set();
|
|
491
|
+
const features = [];
|
|
492
|
+
for (const id of line) {
|
|
493
|
+
for (const feature of byId.get(id)?.features ?? []) {
|
|
494
|
+
if (!seen.has(feature.name)) {
|
|
495
|
+
seen.add(feature.name);
|
|
496
|
+
features.push(feature);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return features;
|
|
501
|
+
}
|
|
502
|
+
function renderClass(definition, names, plan, linearizations, definitions) {
|
|
503
|
+
const kept = plan?.kept ?? definition.baseIds;
|
|
504
|
+
const dropped = plan?.dropped ?? [];
|
|
505
|
+
const bases = kept.filter((baseId) => names.has(baseId)).map((baseId) => names.get(baseId) ?? "");
|
|
506
|
+
const unmapped = definition.baseIds.filter((baseId) => !names.has(baseId));
|
|
507
|
+
const edges = edgeKinds(definition);
|
|
508
|
+
const header = `export class ${names.get(definition.id) ?? identifier(definition.name)} extends ${bases[0] ?? "_t.TypedObject"} {`;
|
|
509
|
+
const lines = [
|
|
510
|
+
`/** ${definition.kind} ${definition.id}. */`,
|
|
511
|
+
header,
|
|
512
|
+
...unmapped.map((baseId) => ` // ${edges.get(baseId) ?? "specializes"} ${baseId}, which has no generated class`),
|
|
513
|
+
...dropped.map((baseId) => ` // ${edges.get(baseId) ?? "specializes"} ${baseId}, left out: ` +
|
|
514
|
+
"the generated hierarchy cannot linearize it with the bases above"),
|
|
515
|
+
` static readonly sysmlId: string = ${literal(definition.id)};`,
|
|
516
|
+
" static { _t.registerTyped(this); }",
|
|
517
|
+
];
|
|
518
|
+
// TypeScript has single inheritance: the first kept base is the superclass,
|
|
519
|
+
// and every other base Python would have carried is re-declared as getters,
|
|
520
|
+
// so every feature its class exposes is reachable here.
|
|
521
|
+
const inheritedFeatures = new Map();
|
|
522
|
+
const ownNames = new Set(definition.features.map((feature) => feature.name));
|
|
523
|
+
for (const baseId of [...kept.slice(1), ...dropped]) {
|
|
524
|
+
for (const feature of featuresOf(linearizations.get(baseId) ?? [baseId], definitions)) {
|
|
525
|
+
if (!ownNames.has(feature.name) && !inheritedFeatures.has(feature.name)) {
|
|
526
|
+
inheritedFeatures.set(feature.name, feature);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
for (const feature of [...definition.features, ...inheritedFeatures.values()]) {
|
|
531
|
+
lines.push("", ...renderGetter(feature, names));
|
|
532
|
+
}
|
|
533
|
+
lines.push("}");
|
|
534
|
+
return lines;
|
|
535
|
+
}
|
|
536
|
+
function renderGetter(feature, names) {
|
|
537
|
+
const tsType = featureType(feature, names);
|
|
538
|
+
const ident = propertyName(feature.name);
|
|
539
|
+
let doc = `${feature.kind} ${feature.id}.`;
|
|
540
|
+
if (RESERVED_MEMBERS.has(identifier(feature.name))) {
|
|
541
|
+
doc += ` Renamed from ${JSON.stringify(feature.name)}, which TypedObject already defines.`;
|
|
542
|
+
}
|
|
543
|
+
if (tsType.comment !== "") {
|
|
544
|
+
doc += ` Mapped to ${tsType.annotation}: ${tsType.comment}.`;
|
|
545
|
+
}
|
|
546
|
+
return [
|
|
547
|
+
` /** ${doc} */`,
|
|
548
|
+
` get ${ident}(): ${tsType.annotation} {`,
|
|
549
|
+
` return ${accessor(feature)}(this, ${literal(feature.name)}, ${tsType.decoder});`,
|
|
550
|
+
" }",
|
|
551
|
+
];
|
|
552
|
+
}
|
|
553
|
+
/** Render the typed module for a loaded model.
|
|
554
|
+
*
|
|
555
|
+
* `sourceText` is the model source the module is stamped with; it is required
|
|
556
|
+
* so a module cannot silently claim the hash of nothing. A model of several
|
|
557
|
+
* documents (`Connection.parseSources`) is generated whole, from every
|
|
558
|
+
* document's root. */
|
|
559
|
+
export async function generateSource(model, sourceText) {
|
|
560
|
+
return renderModule(await collectDefinitions(...model.roots), modelStamp(sourceText));
|
|
561
|
+
}
|
|
562
|
+
// Named in the failure message below: which binary is at fault, and what to do.
|
|
563
|
+
const TYPE_FACTS_REMEDY = "install a sysml-grpc that reports the 'type_facts' capability. Build it from " +
|
|
564
|
+
"this repository with `make build-grpc` and run `bin/sysml-grpc`, or remove the " +
|
|
565
|
+
"cached binary at ~/.opensysml/bin/sysml-grpc and set $OPENSYSML_GRPC_VERSION to a " +
|
|
566
|
+
"release that has it. Generating against a service without type facts would emit " +
|
|
567
|
+
"a module in which every feature is typed `SysMLValue`, indistinguishable from a " +
|
|
568
|
+
"feature that is genuinely untyped, so generation stops instead.";
|
|
569
|
+
/** Fail unless `connection`'s service can supply static type facts. */
|
|
570
|
+
export function requireTypeFacts(connection) {
|
|
571
|
+
requireCapability(connection.info, CAPABILITY_TYPE_FACTS, TYPE_FACTS_REMEDY);
|
|
572
|
+
}
|
|
573
|
+
/** The externally managed service `--host`/`--port` names, or undefined when
|
|
574
|
+
* neither names one and the connection starts a private child — or honors
|
|
575
|
+
* $OPENSYSML_SERVICE — the way `connect()` does with no address. */
|
|
576
|
+
export function serviceAddress(host, port) {
|
|
577
|
+
if (port === undefined && host === "localhost") {
|
|
578
|
+
return undefined;
|
|
579
|
+
}
|
|
580
|
+
return splitTarget(host, port);
|
|
581
|
+
}
|
|
582
|
+
function splitTarget(host, port) {
|
|
583
|
+
const fallback = () => `${host}:${port ?? 50051}`;
|
|
584
|
+
if (!host.includes(":")) {
|
|
585
|
+
return fallback();
|
|
586
|
+
}
|
|
587
|
+
let name;
|
|
588
|
+
let written;
|
|
589
|
+
if (host.startsWith("[")) {
|
|
590
|
+
const closing = host.indexOf("]");
|
|
591
|
+
if (closing === -1 || !host.slice(closing + 1).startsWith(":")) {
|
|
592
|
+
return fallback();
|
|
593
|
+
}
|
|
594
|
+
name = host.slice(0, closing + 1);
|
|
595
|
+
written = host.slice(closing + 2);
|
|
596
|
+
}
|
|
597
|
+
else if (host.indexOf(":") !== host.lastIndexOf(":")) {
|
|
598
|
+
return fallback();
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
const parts = host.split(":");
|
|
602
|
+
name = parts[0] ?? "";
|
|
603
|
+
written = parts[1] ?? "";
|
|
604
|
+
}
|
|
605
|
+
if (!/^\d+$/.test(written)) {
|
|
606
|
+
throw new RangeError(`host=${JSON.stringify(host)} names no port this client can read; pass ` +
|
|
607
|
+
`host and port separately, as --host ${JSON.stringify(name)} --port <port>`);
|
|
608
|
+
}
|
|
609
|
+
const embedded = Number.parseInt(written, 10);
|
|
610
|
+
if (port !== undefined && port !== embedded) {
|
|
611
|
+
throw new RangeError(`host=${JSON.stringify(host)} and port=${port} name different ports; give the port once`);
|
|
612
|
+
}
|
|
613
|
+
return `${name}:${String(embedded)}`;
|
|
614
|
+
}
|
|
615
|
+
function parseArgs(argv) {
|
|
616
|
+
const args = { host: "localhost", check: false };
|
|
617
|
+
const rest = [];
|
|
618
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
619
|
+
const arg = argv[index];
|
|
620
|
+
if (arg === "-o" || arg === "--output") {
|
|
621
|
+
args.output = argv[++index];
|
|
622
|
+
}
|
|
623
|
+
else if (arg.startsWith("--output=")) {
|
|
624
|
+
args.output = arg.slice("--output=".length);
|
|
625
|
+
}
|
|
626
|
+
else if (arg === "--host") {
|
|
627
|
+
args.host = argv[++index];
|
|
628
|
+
}
|
|
629
|
+
else if (arg.startsWith("--host=")) {
|
|
630
|
+
args.host = arg.slice("--host=".length);
|
|
631
|
+
}
|
|
632
|
+
else if (arg === "--port") {
|
|
633
|
+
args.port = Number.parseInt(argv[++index], 10);
|
|
634
|
+
}
|
|
635
|
+
else if (arg.startsWith("--port=")) {
|
|
636
|
+
args.port = Number.parseInt(arg.slice("--port=".length), 10);
|
|
637
|
+
}
|
|
638
|
+
else if (arg === "--check") {
|
|
639
|
+
args.check = true;
|
|
640
|
+
}
|
|
641
|
+
else if (arg === "-h" || arg === "--help") {
|
|
642
|
+
return { error: help(), code: 0 };
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
rest.push(arg);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (args.output === "" || args.host === "" || Number.isNaN(args.port)) {
|
|
649
|
+
return { error: "error: an option is missing its value or is invalid", code: 2 };
|
|
650
|
+
}
|
|
651
|
+
if (rest.length !== 1) {
|
|
652
|
+
return { error: "error: expected exactly one source path", code: 2 };
|
|
653
|
+
}
|
|
654
|
+
return { source: rest[0], output: args.output, host: args.host ?? "localhost", port: args.port, check: args.check ?? false };
|
|
655
|
+
}
|
|
656
|
+
function help() {
|
|
657
|
+
return [
|
|
658
|
+
"usage: opensysml-generate [-h] [-o OUTPUT] [--host HOST] [--port PORT] [--check] source",
|
|
659
|
+
"",
|
|
660
|
+
"Generate typed TypeScript classes from a SysML model.",
|
|
661
|
+
"",
|
|
662
|
+
"positional arguments:",
|
|
663
|
+
" source path to a .sysml file",
|
|
664
|
+
"",
|
|
665
|
+
"options:",
|
|
666
|
+
" -h, --help show this help message and exit",
|
|
667
|
+
" -o, --output output .ts path (default: stdout)",
|
|
668
|
+
" --host HOST sysml-grpc host (default: localhost)",
|
|
669
|
+
" --port PORT sysml-grpc port (default: 50051)",
|
|
670
|
+
" --check do not write; exit non-zero if regenerating would change --output",
|
|
671
|
+
].join("\n");
|
|
672
|
+
}
|
|
673
|
+
/** Entry point for the `opensysml-generate` bin. */
|
|
674
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
675
|
+
const parsed = parseArgs(argv);
|
|
676
|
+
if ("error" in parsed) {
|
|
677
|
+
const out = parsed.code === 0 ? console.log : console.error;
|
|
678
|
+
out(parsed.error);
|
|
679
|
+
return parsed.code;
|
|
680
|
+
}
|
|
681
|
+
if (parsed.check && parsed.output === undefined) {
|
|
682
|
+
console.error("error: --check requires -o/--output");
|
|
683
|
+
return 2;
|
|
684
|
+
}
|
|
685
|
+
let sourceText;
|
|
686
|
+
try {
|
|
687
|
+
sourceText = readFileSync(parsed.source, "utf8");
|
|
688
|
+
}
|
|
689
|
+
catch (error) {
|
|
690
|
+
console.error(`error: cannot read ${parsed.source}: ${error.message}`);
|
|
691
|
+
return 2;
|
|
692
|
+
}
|
|
693
|
+
let address;
|
|
694
|
+
try {
|
|
695
|
+
address = serviceAddress(parsed.host, parsed.port);
|
|
696
|
+
}
|
|
697
|
+
catch (error) {
|
|
698
|
+
console.error(`error: ${error.message}`);
|
|
699
|
+
return 2;
|
|
700
|
+
}
|
|
701
|
+
let connection;
|
|
702
|
+
try {
|
|
703
|
+
connection = address === undefined ? await connect() : await connect({ address });
|
|
704
|
+
}
|
|
705
|
+
catch (error) {
|
|
706
|
+
console.error(`error: ${error.message}`);
|
|
707
|
+
return 2;
|
|
708
|
+
}
|
|
709
|
+
try {
|
|
710
|
+
const model = await connection.load(parsed.source);
|
|
711
|
+
try {
|
|
712
|
+
requireTypeFacts(connection);
|
|
713
|
+
}
|
|
714
|
+
catch (error) {
|
|
715
|
+
if (error instanceof MissingCapabilityError) {
|
|
716
|
+
console.error(`error: ${error.message}`);
|
|
717
|
+
return 1;
|
|
718
|
+
}
|
|
719
|
+
throw error;
|
|
720
|
+
}
|
|
721
|
+
const errors = model.errors;
|
|
722
|
+
if (errors.length > 0) {
|
|
723
|
+
for (const diagnostic of errors) {
|
|
724
|
+
console.error(`error: ${diagnostic.message}`);
|
|
725
|
+
}
|
|
726
|
+
return 1;
|
|
727
|
+
}
|
|
728
|
+
const source = await generateSource(model, sourceText);
|
|
729
|
+
const output = parsed.output;
|
|
730
|
+
if (parsed.check) {
|
|
731
|
+
let existing;
|
|
732
|
+
try {
|
|
733
|
+
existing = readFileSync(output, "utf8");
|
|
734
|
+
}
|
|
735
|
+
catch {
|
|
736
|
+
existing = undefined;
|
|
737
|
+
}
|
|
738
|
+
if (existing === source) {
|
|
739
|
+
return 0;
|
|
740
|
+
}
|
|
741
|
+
const reason = existing === undefined ? "does not exist" : "is out of date";
|
|
742
|
+
console.error(`error: ${output} ${reason}; regenerate with: ` +
|
|
743
|
+
`opensysml-generate ${parsed.source} -o ${output}`);
|
|
744
|
+
return 1;
|
|
745
|
+
}
|
|
746
|
+
if (parsed.output !== undefined) {
|
|
747
|
+
writeFileSync(parsed.output, source, "utf8");
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
process.stdout.write(source);
|
|
751
|
+
}
|
|
752
|
+
return 0;
|
|
753
|
+
}
|
|
754
|
+
finally {
|
|
755
|
+
await connection.close();
|
|
756
|
+
}
|
|
757
|
+
}
|