@lionweb/utilities 0.7.0-beta.2 → 0.7.0-beta.20
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/CHANGELOG.md +3 -0
- package/dist/hashing.d.ts.map +1 -1
- package/dist/m3/dependencies.d.ts.map +1 -1
- package/dist/m3/dependencies.js +2 -2
- package/dist/m3/dependencies.js.map +1 -1
- package/dist/m3/diagrams/Mermaid-generator.d.ts.map +1 -1
- package/dist/m3/diagrams/Mermaid-generator.js.map +1 -1
- package/dist/m3/diagrams/PlantUML-generator.d.ts.map +1 -1
- package/dist/m3/infer-languages.d.ts.map +1 -1
- package/dist/m3/textualizer.d.ts.map +1 -1
- package/dist/m3/ts-generation/common.d.ts +5 -0
- package/dist/m3/ts-generation/common.d.ts.map +1 -0
- package/dist/m3/ts-generation/common.js +31 -0
- package/dist/m3/ts-generation/common.js.map +1 -0
- package/dist/m3/ts-generation/ts-types-generator.d.ts +1 -1
- package/dist/m3/ts-generation/ts-types-generator.d.ts.map +1 -1
- package/dist/m3/ts-generation/ts-types-generator.js +34 -55
- package/dist/m3/ts-generation/ts-types-generator.js.map +1 -1
- package/dist/m3/ts-generation/type-def.d.ts.map +1 -1
- package/dist/m3/ts-generation/type-def.js +0 -1
- package/dist/m3/ts-generation/type-def.js.map +1 -1
- package/dist/serialization/annotation-remover.d.ts.map +1 -1
- package/dist/serialization/chunk.d.ts.map +1 -1
- package/dist/serialization/measurer.d.ts.map +1 -1
- package/dist/serialization/ordering.d.ts.map +1 -1
- package/dist/serialization/sorting.d.ts.map +1 -1
- package/dist/serialization/textualizer.d.ts.map +1 -1
- package/dist/utils/json.d.ts.map +1 -1
- package/dist/utils/json.js +2 -2
- package/dist/utils/json.js.map +1 -1
- package/package.json +35 -35
- package/src/hashing.ts +148 -0
- package/src/index.ts +5 -0
- package/src/m1/reference-utils.ts +2 -0
- package/src/m3/dependencies.ts +62 -0
- package/src/m3/diagrams/Mermaid-generator.ts +129 -0
- package/src/m3/diagrams/PlantUML-generator.ts +147 -0
- package/src/m3/index.ts +6 -0
- package/src/m3/infer-languages.ts +147 -0
- package/src/m3/textualizer.ts +95 -0
- package/src/m3/ts-generation/common.ts +48 -0
- package/src/m3/ts-generation/ts-types-generator.ts +226 -0
- package/src/m3/ts-generation/type-def.ts +40 -0
- package/src/serialization/annotation-remover.ts +46 -0
- package/src/serialization/chunk.ts +105 -0
- package/src/serialization/index.ts +6 -0
- package/src/serialization/measurer.ts +115 -0
- package/src/serialization/metric-types.ts +43 -0
- package/src/serialization/ordering.ts +73 -0
- package/src/serialization/sorting.ts +33 -0
- package/src/serialization/textualizer.ts +93 -0
- package/src/utils/json.ts +8 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Annotation,
|
|
3
|
+
Classifier,
|
|
4
|
+
Concept,
|
|
5
|
+
Containment,
|
|
6
|
+
Enumeration,
|
|
7
|
+
EnumerationLiteral,
|
|
8
|
+
INamed,
|
|
9
|
+
Interface,
|
|
10
|
+
Language,
|
|
11
|
+
Link,
|
|
12
|
+
M3Node,
|
|
13
|
+
nameOf,
|
|
14
|
+
nameSorted,
|
|
15
|
+
PrimitiveType,
|
|
16
|
+
Property,
|
|
17
|
+
SingleRef,
|
|
18
|
+
unresolved
|
|
19
|
+
} from "@lionweb/core"
|
|
20
|
+
import { asString, indentWith, Template } from "littoral-templates"
|
|
21
|
+
|
|
22
|
+
const indented = indentWith(" ")(1)
|
|
23
|
+
|
|
24
|
+
const refAsText = <T extends INamed>(ref: SingleRef<T>): string => (ref === unresolved ? `???` : ref.name)
|
|
25
|
+
|
|
26
|
+
const recurse = <T extends M3Node>(ts: T[], header: string, func: (t: T) => Template = asText): Template =>
|
|
27
|
+
ts.length === 0 ? [] : indented([header, indented(ts.map(func))])
|
|
28
|
+
|
|
29
|
+
const featuresOf = (classifier: Classifier): Template => recurse(nameSorted(classifier.features), `features (↓name):`)
|
|
30
|
+
|
|
31
|
+
const asText = (node: M3Node): Template => {
|
|
32
|
+
if (node instanceof Annotation) {
|
|
33
|
+
return [
|
|
34
|
+
`annotation ${node.name}${node.extends === undefined ? `` : ` extends ${refAsText(node.extends)}`}${node.implements.length === 0 ? `` : ` implements ${nameSorted(node.implements).map(nameOf).join(", ")}`}`,
|
|
35
|
+
indented([`annotates: ${refAsText(node.annotates)}`]),
|
|
36
|
+
featuresOf(node)
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (node instanceof Concept) {
|
|
41
|
+
return [
|
|
42
|
+
`${node.partition ? `<<partition>> ` : ``}${node.abstract ? `abstract ` : ``}concept ${node.name}${node.extends === undefined ? `` : ` extends ${refAsText(node.extends)}`}${node.implements.length === 0 ? `` : ` implements ${nameSorted(node.implements).map(nameOf).join(", ")}`}`,
|
|
43
|
+
featuresOf(node)
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (node instanceof Interface) {
|
|
48
|
+
return [
|
|
49
|
+
`interface ${node.name}${node.extends.length === 0 ? `` : ` extends ${nameSorted(node.extends).map(nameOf).join(", ")}`}`,
|
|
50
|
+
featuresOf(node)
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (node instanceof Link) {
|
|
55
|
+
return `${node.name}${node instanceof Containment ? `:` : ` ->`} ${refAsText(node.type)}${node.multiple ? `[${node.optional ? `0` : `1`}..*]` : ``}${node.optional && !node.multiple ? `?` : ``}`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (node instanceof Enumeration) {
|
|
59
|
+
return [`enumeration ${node.name}`, recurse(node.literals, `literals:`)]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (node instanceof EnumerationLiteral) {
|
|
63
|
+
return `${node.name}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (node instanceof Language) {
|
|
67
|
+
return [
|
|
68
|
+
`language ${node.name}`,
|
|
69
|
+
indented([
|
|
70
|
+
`version: ${node.version}`,
|
|
71
|
+
node.dependsOn.length === 0
|
|
72
|
+
? []
|
|
73
|
+
: [`dependsOn:`, indented(node.dependsOn.map(language => `${language.name} (${language.version})`))],
|
|
74
|
+
`entities (↓name):`,
|
|
75
|
+
``,
|
|
76
|
+
indented(nameSorted(node.entities).map(entity => [asText(entity), ``]))
|
|
77
|
+
]),
|
|
78
|
+
``
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (node instanceof PrimitiveType) {
|
|
83
|
+
return `primitive type ${node.name}`
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (node instanceof Property) {
|
|
87
|
+
return `${node.name}: ${refAsText(node.type)}${node.optional ? `?` : ``}`
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return `node (key=${node.key}, ID=${node.id}) of class ${node.constructor.name} not handled`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const languageAsText = (language: Language) => asString(asText(language))
|
|
94
|
+
|
|
95
|
+
export const languagesAsText = (languages: Language[]): string => asString(nameSorted(languages).map(language => [asText(language), ``]))
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Annotation,
|
|
3
|
+
builtinClassifiers,
|
|
4
|
+
builtinPrimitives,
|
|
5
|
+
Concept,
|
|
6
|
+
Datatype,
|
|
7
|
+
Enumeration,
|
|
8
|
+
Interface,
|
|
9
|
+
LanguageEntity,
|
|
10
|
+
PrimitiveType,
|
|
11
|
+
SingleRef
|
|
12
|
+
} from "@lionweb/core"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
export const tsTypeFor = (datatype: SingleRef<Datatype>): string => {
|
|
16
|
+
if (datatype instanceof PrimitiveType) {
|
|
17
|
+
switch (datatype) {
|
|
18
|
+
case builtinPrimitives.booleanDatatype: return `boolean`
|
|
19
|
+
case builtinPrimitives.stringDatatype: return `string`
|
|
20
|
+
case builtinPrimitives.integerDatatype: return `number`
|
|
21
|
+
case builtinPrimitives.jsonDatatype: return `unknown`
|
|
22
|
+
default:
|
|
23
|
+
return `string`
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (datatype instanceof Enumeration) {
|
|
27
|
+
return datatype.name
|
|
28
|
+
}
|
|
29
|
+
return `unknown /* [ERROR] can't compute a TS type for this datatype: ${datatype} */`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const isINamed = (entity: LanguageEntity): boolean =>
|
|
33
|
+
entity === builtinClassifiers.inamed
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
export const usesINamedDirectly = (entity: LanguageEntity): boolean => {
|
|
37
|
+
if (entity instanceof Annotation) {
|
|
38
|
+
return entity.implements.some(isINamed)
|
|
39
|
+
}
|
|
40
|
+
if (entity instanceof Concept) {
|
|
41
|
+
return entity.implements.some(isINamed)
|
|
42
|
+
}
|
|
43
|
+
if (entity instanceof Interface) {
|
|
44
|
+
return entity.extends.some(isINamed)
|
|
45
|
+
}
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import {
|
|
2
|
+
allFeaturesOf,
|
|
3
|
+
Annotation,
|
|
4
|
+
Classifier,
|
|
5
|
+
Concept,
|
|
6
|
+
conceptsOf,
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
8
|
+
DynamicNode,
|
|
9
|
+
Enumeration,
|
|
10
|
+
Feature,
|
|
11
|
+
inheritsDirectlyFrom,
|
|
12
|
+
Interface,
|
|
13
|
+
isConcrete,
|
|
14
|
+
Language,
|
|
15
|
+
LanguageEntity,
|
|
16
|
+
Link,
|
|
17
|
+
lioncoreBuiltins,
|
|
18
|
+
nameOf,
|
|
19
|
+
nameSorted,
|
|
20
|
+
PrimitiveType,
|
|
21
|
+
Property,
|
|
22
|
+
unresolved
|
|
23
|
+
} from "@lionweb/core"
|
|
24
|
+
import { indent } from "@lionweb/textgen-utils"
|
|
25
|
+
import { groupBy, mapValues, uniquesAmong } from "@lionweb/ts-utils"
|
|
26
|
+
import { asString, Template, when } from "littoral-templates"
|
|
27
|
+
import { Field, tsFromTypeDef, TypeDefModifier } from "./type-def.js"
|
|
28
|
+
import { tsTypeFor, usesINamedDirectly } from "./common.js"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
const fieldForFeature = (feature: Feature) => {
|
|
32
|
+
if (feature instanceof Link) {
|
|
33
|
+
return fieldForLink(feature)
|
|
34
|
+
}
|
|
35
|
+
if (feature instanceof Property) {
|
|
36
|
+
return fieldForProperty(feature)
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
name: feature.name,
|
|
40
|
+
optional: false,
|
|
41
|
+
type: `unknown`
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
const fieldForLink = ({name, type, optional, multiple}: Link): Field =>
|
|
47
|
+
({
|
|
48
|
+
name,
|
|
49
|
+
optional: optional && !multiple,
|
|
50
|
+
type: `${type === unresolved ? `unknown` : type.name}${multiple ? `[]` : ``}`
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
const fieldForProperty = ({name, type, optional}: Property): Field =>
|
|
55
|
+
({
|
|
56
|
+
name,
|
|
57
|
+
optional,
|
|
58
|
+
type: tsTypeFor(type)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
const typeForEnumeration = (enumeration: Enumeration): Template =>
|
|
63
|
+
[
|
|
64
|
+
`enum ${enumeration.name} {`,
|
|
65
|
+
indent(enumeration.literals.map(nameOf).join(`, `)),
|
|
66
|
+
`}`,
|
|
67
|
+
``
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
const typeForPrimitiveType = (datatype: PrimitiveType): Template =>
|
|
72
|
+
[
|
|
73
|
+
`export type ${datatype.name} = ${tsTypeFor(datatype)};`,
|
|
74
|
+
``
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
export enum GenerationOptions {
|
|
79
|
+
assumeSealed
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @return string generated TypeScript source code that contains type definitions that match the given {@link Language language}
|
|
85
|
+
* in combination with using the {@link DynamicNode} base type and corresponding facades.
|
|
86
|
+
*/
|
|
87
|
+
export const tsTypeDefsForLanguage = (language: Language, ...generationOptions: GenerationOptions[]) => {
|
|
88
|
+
|
|
89
|
+
const fieldsForClassifier = (classifier: Classifier) => {
|
|
90
|
+
const map = mapValues<Feature[], [Feature, Field][]>(
|
|
91
|
+
groupBy(allFeaturesOf(classifier), nameOf),
|
|
92
|
+
(features) => features.map((feature) => ([feature, fieldForFeature(feature)]))
|
|
93
|
+
)
|
|
94
|
+
// ensure that features with duplicate names get a postfix indicating the classifier they originate from:
|
|
95
|
+
Object.values(map)
|
|
96
|
+
.filter((fieldsWithOrigin) => fieldsWithOrigin.length > 1)
|
|
97
|
+
.forEach((fieldsWithOrigin) => {
|
|
98
|
+
fieldsWithOrigin.forEach(([feature, field]) => {
|
|
99
|
+
field.name = `${field.name}_${feature.parent!.name}`
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
return Object.values(map)
|
|
103
|
+
.flatMap((fieldsWithOrigin) =>
|
|
104
|
+
fieldsWithOrigin.map(([_, field]) => field)
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const typeForAnnotation = (annotation: Annotation) => {
|
|
109
|
+
const superTypes = inheritsDirectlyFrom(annotation)
|
|
110
|
+
|
|
111
|
+
return tsFromTypeDef({
|
|
112
|
+
modifier: TypeDefModifier.none,
|
|
113
|
+
name: annotation.name,
|
|
114
|
+
mixinNames: superTypes.length === 0 ? [`DynamicNode`] : superTypes.map(nameOf),
|
|
115
|
+
fields: fieldsForClassifier(annotation)
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const typeForConcept = (concept: Concept) => {
|
|
120
|
+
const superTypes = inheritsDirectlyFrom(concept)
|
|
121
|
+
const subClassifiers =
|
|
122
|
+
concept.abstract
|
|
123
|
+
? (
|
|
124
|
+
generationOptions.indexOf(GenerationOptions.assumeSealed) > -1
|
|
125
|
+
? conceptsOf(language).filter((entity) => entity.extends === concept)
|
|
126
|
+
: []
|
|
127
|
+
)
|
|
128
|
+
: [concept]
|
|
129
|
+
|
|
130
|
+
return tsFromTypeDef({
|
|
131
|
+
modifier: concept.abstract ? TypeDefModifier.abstract : TypeDefModifier.none,
|
|
132
|
+
name: concept.name,
|
|
133
|
+
mixinNames: superTypes.length === 0 ? [`DynamicNode`] : superTypes.map(nameOf),
|
|
134
|
+
bodyComment: subClassifiers.length > 0 ? `classifier -> ${subClassifiers.map(nameOf).join(` | `)}` : undefined,
|
|
135
|
+
fields: fieldsForClassifier(concept)
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const typeForInterface = (intface: Interface) =>
|
|
140
|
+
tsFromTypeDef({
|
|
141
|
+
modifier: TypeDefModifier.interface,
|
|
142
|
+
name: intface.name,
|
|
143
|
+
mixinNames: intface.extends.length === 0 ? [`DynamicNode`] : intface.extends.map(nameOf),
|
|
144
|
+
fields: fieldsForClassifier(intface)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
const typeForLanguageEntity = (entity: LanguageEntity) => {
|
|
148
|
+
if (entity instanceof Annotation) {
|
|
149
|
+
return typeForAnnotation(entity)
|
|
150
|
+
}
|
|
151
|
+
if (entity instanceof Concept) {
|
|
152
|
+
return typeForConcept(entity)
|
|
153
|
+
}
|
|
154
|
+
if (entity instanceof Enumeration) {
|
|
155
|
+
return typeForEnumeration(entity)
|
|
156
|
+
}
|
|
157
|
+
if (entity instanceof Interface) {
|
|
158
|
+
return typeForInterface(entity)
|
|
159
|
+
}
|
|
160
|
+
if (entity instanceof PrimitiveType) {
|
|
161
|
+
return typeForPrimitiveType(entity)
|
|
162
|
+
}
|
|
163
|
+
return [
|
|
164
|
+
`// unhandled language entity <${entity.constructor.name}>"${entity.name}"`,
|
|
165
|
+
``
|
|
166
|
+
]
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const dependenciesOfClassifier = (classifier: Classifier): Classifier[] =>
|
|
170
|
+
[
|
|
171
|
+
...inheritsDirectlyFrom(classifier),
|
|
172
|
+
...allFeaturesOf(classifier)
|
|
173
|
+
.filter((feature) => feature instanceof Link)
|
|
174
|
+
.map((feature) => feature as Link)
|
|
175
|
+
.flatMap(({type}) => type)
|
|
176
|
+
.filter((type) => type instanceof Classifier)
|
|
177
|
+
.map((classifier) => classifier as Classifier)
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
const coreImports = [
|
|
181
|
+
...(language.entities.every(usesINamedDirectly) ? [] : [`DynamicNode`]),
|
|
182
|
+
...(language.entities.some(usesINamedDirectly) ? [`INamed`] : [])
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
const generatedDependencies = uniquesAmong(
|
|
186
|
+
language.entities
|
|
187
|
+
.filter((entity) => entity instanceof Classifier)
|
|
188
|
+
.flatMap((entity) => dependenciesOfClassifier(entity as Classifier))
|
|
189
|
+
)
|
|
190
|
+
.filter((classifier) => classifier.language !== language && classifier.language !== lioncoreBuiltins)
|
|
191
|
+
const importsPerPackage = groupBy(
|
|
192
|
+
generatedDependencies,
|
|
193
|
+
({language}) => language.name
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
const concreteClassifiers = language.entities.filter(isConcrete)
|
|
197
|
+
|
|
198
|
+
return asString(
|
|
199
|
+
[
|
|
200
|
+
`// Warning: this file is generated!`,
|
|
201
|
+
`// Modifying it by hand is useless at best, and sabotage at worst.`,
|
|
202
|
+
``,
|
|
203
|
+
`/*
|
|
204
|
+
* language's metadata:
|
|
205
|
+
* name: ${language.name}
|
|
206
|
+
* version: ${language.version}
|
|
207
|
+
*/`,
|
|
208
|
+
``,
|
|
209
|
+
when(coreImports.length > 0)(`import {${coreImports.join(`, `)}} from "@lionweb/core";`),
|
|
210
|
+
Object.keys(importsPerPackage)
|
|
211
|
+
.sort()
|
|
212
|
+
.map((packageName) => `import {${nameSorted(importsPerPackage[packageName]).map(nameOf).join(", ")}} from "./${packageName}.g.js";`),
|
|
213
|
+
``,
|
|
214
|
+
``,
|
|
215
|
+
nameSorted(language.entities).map(typeForLanguageEntity),
|
|
216
|
+
when(concreteClassifiers.length > 0)([
|
|
217
|
+
``,
|
|
218
|
+
`/** sum type of all types for all concrete classifiers of ${language.name}: */`,
|
|
219
|
+
`export type Nodes = ${nameSorted(concreteClassifiers).map(nameOf).join(` | `)};`,
|
|
220
|
+
``
|
|
221
|
+
])
|
|
222
|
+
]
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
}
|
|
226
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Template, when } from "littoral-templates"
|
|
2
|
+
import { indent } from "@lionweb/textgen-utils"
|
|
3
|
+
|
|
4
|
+
export enum TypeDefModifier {
|
|
5
|
+
none,
|
|
6
|
+
abstract,
|
|
7
|
+
interface
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type TypeDef = {
|
|
11
|
+
modifier: TypeDefModifier
|
|
12
|
+
name: string
|
|
13
|
+
mixinNames: string[]
|
|
14
|
+
bodyComment?: string
|
|
15
|
+
fields: Field[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type Field = {
|
|
19
|
+
name: string
|
|
20
|
+
optional: boolean
|
|
21
|
+
type: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const tsFromField = ({ name, optional, type }: Field): Template => `${name}${optional ? `?` : ``}: ${type};`
|
|
25
|
+
|
|
26
|
+
export const tsFromTypeDef = ({ modifier, name, mixinNames, bodyComment, fields }: TypeDef): Template => {
|
|
27
|
+
const hasBody = !!bodyComment || fields.length > 0
|
|
28
|
+
return [
|
|
29
|
+
`${modifier === TypeDefModifier.none ? `` : `/** ${TypeDefModifier[modifier]} */ `}export type ${name} = ${mixinNames.join(` & `)}${!hasBody ? `;` : ` & {`}`,
|
|
30
|
+
when(hasBody)([
|
|
31
|
+
indent([
|
|
32
|
+
when(!!bodyComment)(`// ${bodyComment}`),
|
|
33
|
+
when(fields.length > 0)([`settings: {`, indent(fields.map(tsFromField)), `};`])
|
|
34
|
+
]),
|
|
35
|
+
`};` // (`{` was already rendered as part of the header)
|
|
36
|
+
]),
|
|
37
|
+
``
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
3
|
+
Annotation
|
|
4
|
+
} from "@lionweb/core"
|
|
5
|
+
import { LionWebId, LionWebJsonChunk, LionWebJsonNode } from "@lionweb/json"
|
|
6
|
+
import { byIdMap } from "@lionweb/ts-utils"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Removes all annotations from the given {@link LionWebJsonChunk}, i.e.:
|
|
10
|
+
* * all annotations on nodes in the form of the {@code annotations} property,
|
|
11
|
+
* * all nodes that can be identified as instances of an {@link Annotation} by being referred to from {@code annotations}.
|
|
12
|
+
* It also removes all descendants of all annotations (insofar present in the given chunk).
|
|
13
|
+
* @param serializationChunk - The {@link LionWebJsonChunk}
|
|
14
|
+
*/
|
|
15
|
+
export const withoutAnnotations = (serializationChunk: LionWebJsonChunk) => {
|
|
16
|
+
const {serializationFormatVersion, languages, nodes} = serializationChunk
|
|
17
|
+
const id2node = byIdMap(nodes)
|
|
18
|
+
const childIds = (id: LionWebId) =>
|
|
19
|
+
(id in id2node)
|
|
20
|
+
? id2node[id].containments.flatMap((containment) => containment.children)
|
|
21
|
+
: []
|
|
22
|
+
const descendantIds = (id: LionWebId): LionWebId[] =>
|
|
23
|
+
[id, ...childIds(id).flatMap(descendantIds)]
|
|
24
|
+
const annotationIds = nodes.flatMap((node) => node.annotations) // (are unique, because of parent-child relation)
|
|
25
|
+
const idsOfNodesToDelete = [
|
|
26
|
+
...annotationIds,
|
|
27
|
+
...[...annotationIds].flatMap(descendantIds)
|
|
28
|
+
]
|
|
29
|
+
const withoutAnnotations = ({ id, classifier, properties, containments, references, parent }: LionWebJsonNode): LionWebJsonNode => ({
|
|
30
|
+
id,
|
|
31
|
+
classifier,
|
|
32
|
+
properties,
|
|
33
|
+
containments,
|
|
34
|
+
references,
|
|
35
|
+
annotations: [],
|
|
36
|
+
parent
|
|
37
|
+
})
|
|
38
|
+
return {
|
|
39
|
+
serializationFormatVersion,
|
|
40
|
+
languages,
|
|
41
|
+
nodes: nodes
|
|
42
|
+
.filter((node) => !idsOfNodesToDelete.includes(node.id)) // removes instances of annotations
|
|
43
|
+
.map(withoutAnnotations)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { deserializeLanguages, Language, lioncoreKey } from "@lionweb/core"
|
|
2
|
+
import { currentSerializationFormatVersion, LionWebJsonChunk, LionWebJsonUsedLanguage } from "@lionweb/json"
|
|
3
|
+
import { readFileAsJson } from "../utils/json.js"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Reads the file at the given path as a {@link LionWebJsonChunk serialization chunk}.
|
|
8
|
+
* **Note** that it's only checked that the file exists and can be parsed as JSON,
|
|
9
|
+
* _not_ whether it satisfies the specified serialization chunk format!
|
|
10
|
+
*/
|
|
11
|
+
export const readSerializationChunk = async (path: string) => {
|
|
12
|
+
try {
|
|
13
|
+
return readFileAsJson(path) as LionWebJsonChunk
|
|
14
|
+
} catch (e) {
|
|
15
|
+
console.error(`${path} is not a valid JSON file`)
|
|
16
|
+
throw e
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// TODO don't throw, but return some kind of error object (– possibly using Promise.reject)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
const isRecord = (json: unknown): json is Record<string, unknown> =>
|
|
23
|
+
typeof json === "object" && !Array.isArray(json)
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @return whether the given JSON looks like the serialization of languages.
|
|
27
|
+
*/
|
|
28
|
+
export const looksLikeSerializedLanguages = (json: unknown): boolean =>
|
|
29
|
+
isRecord(json)
|
|
30
|
+
&& json["serializationFormatVersion"] === currentSerializationFormatVersion
|
|
31
|
+
&& "languages" in json
|
|
32
|
+
&& Array.isArray(json["languages"])
|
|
33
|
+
&& json["languages"].some((language) => isRecord(language) && language["key"] === lioncoreKey)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Tries to read the given path as a JSON file containing the serialization of languages,
|
|
38
|
+
* and attempts to deserialize the serialization chunk when it is.
|
|
39
|
+
* If any of that fails, return an empty list.
|
|
40
|
+
*/
|
|
41
|
+
export const tryReadAsLanguages = async (path: string): Promise<Language[]> => {
|
|
42
|
+
const serializationChunk = await readSerializationChunk(path)
|
|
43
|
+
if (!looksLikeSerializedLanguages(serializationChunk)) {
|
|
44
|
+
console.error(`${path} is not a valid JSON serialization chunk of LionCore languages`)
|
|
45
|
+
return []
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
return deserializeLanguages(serializationChunk)
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
50
|
+
} catch (e: any) {
|
|
51
|
+
console.error(`${path} is not a valid JSON serialization chunk of LionCore languages: ${e.message}`)
|
|
52
|
+
return []
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
const flatMapDistinct = <T>(tss: (T[])[], equalFunc: (l: T, r: T) => boolean) =>
|
|
58
|
+
tss.reduce<T[]>(
|
|
59
|
+
(acc, ts) =>
|
|
60
|
+
[
|
|
61
|
+
...acc,
|
|
62
|
+
...(ts.filter((r) => !acc.some((l) => equalFunc(l, r))))
|
|
63
|
+
],
|
|
64
|
+
[]
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
const areEqual = (left: LionWebJsonUsedLanguage, right: LionWebJsonUsedLanguage): boolean =>
|
|
68
|
+
left.key === right.key && left.version === right.version
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @return the combination of the given {@link LionWebJsonChunk serialization chunks} into one.
|
|
73
|
+
*/
|
|
74
|
+
export const combinationOf = (serializationChunks: LionWebJsonChunk[]): LionWebJsonChunk =>
|
|
75
|
+
({
|
|
76
|
+
serializationFormatVersion: currentSerializationFormatVersion,
|
|
77
|
+
languages: flatMapDistinct(serializationChunks.map(({languages}) => languages), areEqual),
|
|
78
|
+
nodes: serializationChunks.flatMap(({nodes}) => nodes)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Tries to read all the given paths as JSON serialization chunks that are serializations of languages,
|
|
84
|
+
* and attempts to combine those chunks into one chunk, and deserializes that.
|
|
85
|
+
*/
|
|
86
|
+
export const tryReadAllAsLanguages = async (paths: string[]): Promise<Language[]> => {
|
|
87
|
+
const serializationChunks =
|
|
88
|
+
(await Promise.all(paths.map(readSerializationChunk)))
|
|
89
|
+
.filter((serializationChunk, index) => {
|
|
90
|
+
const ok = looksLikeSerializedLanguages(serializationChunk)
|
|
91
|
+
if (!ok) {
|
|
92
|
+
const path = paths[index]
|
|
93
|
+
console.error(`${path} is not a valid JSON serialization chunk of LionCore languages`)
|
|
94
|
+
}
|
|
95
|
+
return ok
|
|
96
|
+
})
|
|
97
|
+
try {
|
|
98
|
+
return deserializeLanguages(combinationOf(serializationChunks))
|
|
99
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
100
|
+
} catch (e: any) {
|
|
101
|
+
console.error(`couldn't deserialize combined JSON serialization chunk of LionCore languages: ${e.message}`)
|
|
102
|
+
return []
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export {withoutAnnotations} from "./annotation-remover.js"
|
|
2
|
+
export {looksLikeSerializedLanguages, readSerializationChunk, tryReadAllAsLanguages, tryReadAsLanguages} from "./chunk.js"
|
|
3
|
+
export {measure} from "./measurer.js"
|
|
4
|
+
export {orderedSerializationChunk} from "./ordering.js"
|
|
5
|
+
export {sortedSerializationChunk} from "./sorting.js"
|
|
6
|
+
export {genericAsTreeText} from "./textualizer.js"
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Annotation, Concept, instantiableClassifiersOf, Interface, Language, LanguageEntity, MemoisingSymbolTable } from "@lionweb/core"
|
|
2
|
+
import { LionWebJsonChunk, LionWebJsonMetaPointer, LionWebJsonNode, LionWebJsonUsedLanguage } from "@lionweb/json"
|
|
3
|
+
import { nested3Grouper, nested3Mapper, nestedFlatMap2, nestedFlatMap3 } from "@lionweb/ts-utils"
|
|
4
|
+
import { ClassifierMetaTypes, Metrics } from "./metric-types.js"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
type Info = {
|
|
8
|
+
classifier: LionWebJsonMetaPointer
|
|
9
|
+
instantiations: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Sum the given array of numbers.
|
|
15
|
+
*/
|
|
16
|
+
export const sumNumbers = (nums: number[]): number =>
|
|
17
|
+
nums.reduce((acc, cur) => acc + cur, 0)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Computes {@link Metrics metrics} on the given {@link LionWebJsonChunk serialization chunk}.
|
|
22
|
+
* Passing it {@link Language languages} make this language-aware:
|
|
23
|
+
* * language and classifier names are looked up,
|
|
24
|
+
* * unused instantiable classifiers and languages without instantiations are computed as well.
|
|
25
|
+
*/
|
|
26
|
+
export const measure = (serializationChunk: LionWebJsonChunk, languages: Language[]): Metrics => {
|
|
27
|
+
const symbolTable = new MemoisingSymbolTable(languages)
|
|
28
|
+
|
|
29
|
+
// group nodes by language key, version, and classifier key, mapped to the classifier meta-pointer and #instantiations:
|
|
30
|
+
const languageKey2version2classifierKey2info = nested3Mapper<LionWebJsonNode[], Info>(nodes => ({
|
|
31
|
+
classifier: nodes[0].classifier,
|
|
32
|
+
instantiations: nodes.length
|
|
33
|
+
}))(
|
|
34
|
+
nested3Grouper<LionWebJsonNode>(
|
|
35
|
+
({ classifier }) => classifier.language,
|
|
36
|
+
({ classifier }) => classifier.version,
|
|
37
|
+
({ classifier }) => classifier.key
|
|
38
|
+
)(serializationChunk.nodes)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const languagesWithInstantiations = nestedFlatMap2(
|
|
42
|
+
languageKey2version2classifierKey2info,
|
|
43
|
+
(classifierKey2info, languageKey, version) => ({
|
|
44
|
+
key: languageKey,
|
|
45
|
+
version,
|
|
46
|
+
name: symbolTable.languageMatching(languageKey, version)?.name,
|
|
47
|
+
instantiations: sumNumbers(Object.values(classifierKey2info).map(info => info.instantiations))
|
|
48
|
+
})
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
const metaTypeOf = (entity?: LanguageEntity): ClassifierMetaTypes | undefined => {
|
|
52
|
+
if (entity instanceof Annotation) {
|
|
53
|
+
return "annotation"
|
|
54
|
+
}
|
|
55
|
+
if (entity instanceof Concept) {
|
|
56
|
+
return "concept"
|
|
57
|
+
}
|
|
58
|
+
if (entity instanceof Interface) {
|
|
59
|
+
return "interface"
|
|
60
|
+
}
|
|
61
|
+
return undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// map grouped nodes to info including #instantiations:
|
|
65
|
+
const instantiatedClassifiers = nestedFlatMap3(languageKey2version2classifierKey2info, (info, languageKey, version, classifierKey) => {
|
|
66
|
+
const classifier = symbolTable.entityMatching(info.classifier)
|
|
67
|
+
return {
|
|
68
|
+
language: {
|
|
69
|
+
key: languageKey,
|
|
70
|
+
version,
|
|
71
|
+
name: symbolTable.languageMatching(languageKey, version)?.name
|
|
72
|
+
},
|
|
73
|
+
key: classifierKey,
|
|
74
|
+
name: classifier?.name,
|
|
75
|
+
metaType: metaTypeOf(classifier),
|
|
76
|
+
instantiations: info.instantiations
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const doesLanguageHaveInstantiations = (language: LionWebJsonUsedLanguage): boolean =>
|
|
81
|
+
language.key in languageKey2version2classifierKey2info && language.version in languageKey2version2classifierKey2info[language.key]
|
|
82
|
+
|
|
83
|
+
const languagesWithoutInstantiations = serializationChunk.languages
|
|
84
|
+
.filter(language => !doesLanguageHaveInstantiations(language))
|
|
85
|
+
.map(language => ({
|
|
86
|
+
...language,
|
|
87
|
+
name: symbolTable.languageMatching(language.key, language.version)?.name
|
|
88
|
+
}))
|
|
89
|
+
|
|
90
|
+
const isClassifierUsed = (metaPointer: LionWebJsonMetaPointer): boolean =>
|
|
91
|
+
metaPointer.language in languageKey2version2classifierKey2info &&
|
|
92
|
+
metaPointer.version in languageKey2version2classifierKey2info[metaPointer.language] &&
|
|
93
|
+
metaPointer.key in languageKey2version2classifierKey2info[metaPointer.language][metaPointer.version]
|
|
94
|
+
|
|
95
|
+
const uninstantiatedInstantiableClassifiers = languages
|
|
96
|
+
.flatMap(instantiableClassifiersOf)
|
|
97
|
+
.map(classifier => classifier.metaPointer())
|
|
98
|
+
.filter(metaPointer => !isClassifierUsed(metaPointer))
|
|
99
|
+
.map(metaPointer => ({
|
|
100
|
+
language: {
|
|
101
|
+
key: metaPointer.language,
|
|
102
|
+
version: metaPointer.version,
|
|
103
|
+
name: symbolTable.languageMatching(metaPointer.language, metaPointer.version)?.name
|
|
104
|
+
},
|
|
105
|
+
key: metaPointer.key,
|
|
106
|
+
name: symbolTable.entityMatching(metaPointer)?.name
|
|
107
|
+
}))
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
languagesWithInstantiations,
|
|
111
|
+
instantiatedClassifiers,
|
|
112
|
+
languagesWithoutInstantiations,
|
|
113
|
+
uninstantiatedInstantiableClassifiers
|
|
114
|
+
}
|
|
115
|
+
}
|