@lionweb/utilities 0.7.0-beta.2 → 0.7.0-beta.21
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
package/src/hashing.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { LionWebId } from "@lionweb/json"
|
|
2
|
+
import { createHash } from "crypto"
|
|
3
|
+
import { nanoid } from "nanoid"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Type def. for a hashing function string → string.
|
|
8
|
+
*/
|
|
9
|
+
export type StringHasher = (str: string) => string
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Hasher based on the {@link https://zelark.github.io/nano-id-cc/ `nanoid` NPM package}.
|
|
14
|
+
*/
|
|
15
|
+
export const nanoIdGen = (): StringHasher =>
|
|
16
|
+
(_) => nanoid()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
const defaultHashAlgorithm = "SHA256"
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Type definition for objects that configure a
|
|
23
|
+
* {@link StringHasher hasher}.
|
|
24
|
+
*/
|
|
25
|
+
export type StringHasherConfig = {
|
|
26
|
+
algorithm?: typeof defaultHashAlgorithm | string
|
|
27
|
+
salt?: string
|
|
28
|
+
encoding?: "base64url" | "base64"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates a {@link StringHasher hasher},
|
|
33
|
+
* optionally using a {@link StringHasherConfig configuration object}.
|
|
34
|
+
* The default is:
|
|
35
|
+
* - uses the *SHA256* hashing algorithm
|
|
36
|
+
* - without salt prefix string
|
|
37
|
+
*/
|
|
38
|
+
export const hasher = (config?: StringHasherConfig): StringHasher => {
|
|
39
|
+
const algorithm = config?.algorithm ?? defaultHashAlgorithm
|
|
40
|
+
const salt = config?.salt ?? ""
|
|
41
|
+
const encoding = config?.encoding ?? "base64url"
|
|
42
|
+
|
|
43
|
+
return (data) => {
|
|
44
|
+
if (data === undefined) {
|
|
45
|
+
throw new Error(`expected data for hashing`)
|
|
46
|
+
}
|
|
47
|
+
return createHash(algorithm)
|
|
48
|
+
.update(salt + data)
|
|
49
|
+
.digest(encoding)
|
|
50
|
+
.toString()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Augments the given {@link StringHasher hasher} by checking
|
|
57
|
+
* whether it's been given unique strings,
|
|
58
|
+
* throwing an error when not.
|
|
59
|
+
*/
|
|
60
|
+
export const checkUniqueData = (hasher: StringHasher): StringHasher => {
|
|
61
|
+
const datas: string[] = []
|
|
62
|
+
return (data) => {
|
|
63
|
+
if (datas.indexOf(data) > -1) {
|
|
64
|
+
throw new Error(`duplicate data encountered: "${data}"`)
|
|
65
|
+
}
|
|
66
|
+
datas.push(data)
|
|
67
|
+
return hasher(data)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Augments the given {@link StringHasher hasher} by checking
|
|
74
|
+
* whether it's been given defined ({@code !== undefined}) data,
|
|
75
|
+
* throwing an error when not.
|
|
76
|
+
*/
|
|
77
|
+
export const checkDefinedData = (hasher: StringHasher): StringHasher =>
|
|
78
|
+
(data) => {
|
|
79
|
+
if (data === undefined) {
|
|
80
|
+
throw new Error(`expected data`)
|
|
81
|
+
}
|
|
82
|
+
return hasher(data)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Augments the given {@link StringHasher hasher} by checking
|
|
88
|
+
* whether that returns unique IDs, throwing an error when not.
|
|
89
|
+
*/
|
|
90
|
+
export const checkUniqueId = (hasher: StringHasher): StringHasher => {
|
|
91
|
+
const ids: LionWebId[] = []
|
|
92
|
+
|
|
93
|
+
return (data) => {
|
|
94
|
+
const id = hasher(data)
|
|
95
|
+
if (ids.indexOf(id) > -1) {
|
|
96
|
+
throw new Error(`duplicate ID generated: "${id}"`)
|
|
97
|
+
}
|
|
98
|
+
ids.push(id)
|
|
99
|
+
return id
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Augments the given {@link StringHasher hasher} by checking
|
|
106
|
+
* whether it returns valid IDs, meaning [Base64URL](https://www.base64url.com/).
|
|
107
|
+
* (See also [Wikipedia](https://en.wikipedia.org/wiki/Base64#Variants_summary_table).)
|
|
108
|
+
* If a generated ID is not valid, an error is thrown.
|
|
109
|
+
*/
|
|
110
|
+
export const checkValidId = (hasher: StringHasher): StringHasher =>
|
|
111
|
+
(data) => {
|
|
112
|
+
const id = hasher(data)
|
|
113
|
+
if (!id.match(/^[A-Za-z0-9_-]+$/)) {
|
|
114
|
+
throw new Error(`generated ID is not valid: ${id}`)
|
|
115
|
+
}
|
|
116
|
+
return id
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Type definition for transformers of {@link StringHasher hashers}.
|
|
122
|
+
*/
|
|
123
|
+
export type StringHasherTransformer = (hasher: StringHasher) => StringHasher
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Wraps the given ("initial") {@link StringHasher hasher}
|
|
127
|
+
* with the given {@link HasherTransformer hasher transfomers}.
|
|
128
|
+
* In other words:
|
|
129
|
+
*
|
|
130
|
+
* chain(hasher, trafo1, trafo2, ..., trafoN) === trafoN(...trafo2(trafo1(hasher)))
|
|
131
|
+
*/
|
|
132
|
+
export const chain = (hasher: StringHasher, ...hasherTransformers: StringHasherTransformer[]): StringHasher =>
|
|
133
|
+
hasherTransformers.reduce((acc, current) => current(acc), hasher)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Wraps the given ("initial") {@link StringHasher hasher} with all
|
|
138
|
+
* {@link HasherTransformer hasher transfomers} defined above.
|
|
139
|
+
*/
|
|
140
|
+
export const checkAll = (hasher: StringHasher): StringHasher =>
|
|
141
|
+
chain(
|
|
142
|
+
hasher,
|
|
143
|
+
checkDefinedData,
|
|
144
|
+
checkUniqueData,
|
|
145
|
+
checkValidId,
|
|
146
|
+
checkUniqueId
|
|
147
|
+
)
|
|
148
|
+
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Copyright 2025 TRUMPF Laser SE and other contributors
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License")
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
//
|
|
15
|
+
// SPDX-FileCopyrightText: 2025 TRUMPF Laser SE and other contributors
|
|
16
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
17
|
+
|
|
18
|
+
import { Classifier, inheritsDirectlyFrom, Language, nameOf, nameSorted } from "@lionweb/core"
|
|
19
|
+
import { indent } from "@lionweb/textgen-utils"
|
|
20
|
+
import { cycleWith, uniquesAmong } from "@lionweb/ts-utils"
|
|
21
|
+
import { asString, when } from "littoral-templates"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @return all languages that any {@link Classifier classifier} of the given {@link Language language} depends on through direct inheritance.
|
|
26
|
+
*/
|
|
27
|
+
export const dependenciesThroughDirectInheritanceOf = (language: Language) =>
|
|
28
|
+
uniquesAmong(
|
|
29
|
+
language.entities
|
|
30
|
+
.filter((entity) => entity instanceof Classifier)
|
|
31
|
+
.flatMap((entity) => inheritsDirectlyFrom(entity as Classifier))
|
|
32
|
+
.map((classifier) => classifier.language)
|
|
33
|
+
.filter((depLanguage) => depLanguage !== language)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @return the – currently only those arising from direct inheritance on classifier-level – dependencies of the given {@link Language languages},
|
|
39
|
+
* as a human-readable text
|
|
40
|
+
*/
|
|
41
|
+
export const verboseDependencies = (languages: Language[]): string => {
|
|
42
|
+
const nameSortedLanguages = nameSorted(languages)
|
|
43
|
+
return asString([
|
|
44
|
+
nameSortedLanguages.map((language) => {
|
|
45
|
+
const deps = dependenciesThroughDirectInheritanceOf(language)
|
|
46
|
+
const what = `direct type-wise dependencies (through inheritance)`
|
|
47
|
+
const cycle = cycleWith(language, dependenciesThroughDirectInheritanceOf)
|
|
48
|
+
return deps.length === 0
|
|
49
|
+
? `language ${language.name} has no ${what}`
|
|
50
|
+
: [
|
|
51
|
+
`language ${language.name} has the following ${what}:`,
|
|
52
|
+
indent([
|
|
53
|
+
nameSorted(dependenciesThroughDirectInheritanceOf(language)).map(nameOf),
|
|
54
|
+
when(cycle.length > 0)(
|
|
55
|
+
`⚠ language "${language.name}" is part of the following cycle through type-wise (through direct inheritance): ${cycle.map(nameOf).join(" -> ")}`
|
|
56
|
+
)
|
|
57
|
+
])
|
|
58
|
+
]
|
|
59
|
+
})
|
|
60
|
+
])
|
|
61
|
+
}
|
|
62
|
+
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Annotation,
|
|
3
|
+
Concept,
|
|
4
|
+
Containment,
|
|
5
|
+
Enumeration,
|
|
6
|
+
Feature,
|
|
7
|
+
Interface,
|
|
8
|
+
isBuiltinNodeConcept,
|
|
9
|
+
isRef,
|
|
10
|
+
Language,
|
|
11
|
+
LanguageEntity,
|
|
12
|
+
Link,
|
|
13
|
+
nameSorted,
|
|
14
|
+
nonRelationalFeatures,
|
|
15
|
+
PrimitiveType,
|
|
16
|
+
relationsOf,
|
|
17
|
+
type,
|
|
18
|
+
unresolved
|
|
19
|
+
} from "@lionweb/core"
|
|
20
|
+
import { asString, indentWith, Template } from "littoral-templates"
|
|
21
|
+
|
|
22
|
+
// define some layouting basics/building algebra:
|
|
23
|
+
|
|
24
|
+
const indented = indentWith(` `)(1)
|
|
25
|
+
|
|
26
|
+
const block = (header: Template, elements: Template[]): Template =>
|
|
27
|
+
elements.length === 0 ? header : [`${header} {`, indented(elements), `}`]
|
|
28
|
+
|
|
29
|
+
const withNewLine = (content: Template): Template => [content, ``]
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generates a string with a Mermaid class diagram
|
|
33
|
+
* representing the given {@link Language LionCore instance}.
|
|
34
|
+
*/
|
|
35
|
+
export const generateMermaidForLanguage = ({ entities }: Language) =>
|
|
36
|
+
asString([
|
|
37
|
+
"```mermaid",
|
|
38
|
+
`classDiagram
|
|
39
|
+
`,
|
|
40
|
+
indented(nameSorted(entities).map(generateForEntity)),
|
|
41
|
+
``,
|
|
42
|
+
indented(nameSorted(entities).map(generateForRelationsOf)),
|
|
43
|
+
``,
|
|
44
|
+
"```"
|
|
45
|
+
])
|
|
46
|
+
|
|
47
|
+
const generateForEnumeration = ({ name, literals }: Enumeration) =>
|
|
48
|
+
withNewLine(block(`class ${name}`, [`<<enumeration>>`, literals.map(({name}) => name)]))
|
|
49
|
+
|
|
50
|
+
const generateForAnnotation = ({ name, features, extends: extends_, implements: implements_, annotates }: Annotation) => [
|
|
51
|
+
block(
|
|
52
|
+
[`class ${name}`, `<<Annotation>> ${name}`, isRef(annotates) ? `${name} ..> ${annotates.name}` : []],
|
|
53
|
+
nonRelationalFeatures(features).map(generateForNonRelationalFeature)
|
|
54
|
+
),
|
|
55
|
+
isRef(extends_) && !isBuiltinNodeConcept(extends_) ? `${extends_.name} <|-- ${name}` : [],
|
|
56
|
+
implements_.filter(isRef).map(interface_ => `${interface_.name} <|.. ${name}`),
|
|
57
|
+
``
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
const generateForConcept = ({
|
|
61
|
+
name,
|
|
62
|
+
features,
|
|
63
|
+
abstract: abstract_,
|
|
64
|
+
extends: extends_ /*, implements: implements_*/,
|
|
65
|
+
partition
|
|
66
|
+
}: Concept) => [
|
|
67
|
+
block(`class ${partition ? `<<partition>> ` : ``}${name}`, nonRelationalFeatures(features).map(generateForNonRelationalFeature)),
|
|
68
|
+
abstract_ ? `<<Abstract>> ${name}` : [],
|
|
69
|
+
isRef(extends_) && !isBuiltinNodeConcept(extends_) ? `${extends_.name} <|-- ${name}` : [],
|
|
70
|
+
``
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
const generateForInterface = ({ name, features, extends: extends_ }: Interface) => [
|
|
74
|
+
block(`class ${name}`, nonRelationalFeatures(features).map(generateForNonRelationalFeature)),
|
|
75
|
+
`<<Interface>> ${name}`,
|
|
76
|
+
extends_.map(({ name: extendsName }) => `${extendsName} <|-- ${name}`),
|
|
77
|
+
``
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
const generateForNonRelationalFeature = (feature: Feature) => {
|
|
81
|
+
const { name, optional } = feature
|
|
82
|
+
const multiple = feature instanceof Link && feature.multiple
|
|
83
|
+
const type_ = type(feature)
|
|
84
|
+
const typeText = `${multiple ? `List~` : ``}${type_ === unresolved ? `???` : type_.name}${multiple ? `~` : ``}${optional ? `?` : ``}`
|
|
85
|
+
return `+${typeText} ${name}`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const generateForPrimitiveType = ({ name }: PrimitiveType) =>
|
|
89
|
+
`class ${name}
|
|
90
|
+
<<PrimitiveType>> ${name}
|
|
91
|
+
`
|
|
92
|
+
|
|
93
|
+
const generateForEntity = (entity: LanguageEntity) => {
|
|
94
|
+
if (entity instanceof Annotation) {
|
|
95
|
+
return generateForAnnotation(entity)
|
|
96
|
+
}
|
|
97
|
+
if (entity instanceof Concept) {
|
|
98
|
+
return generateForConcept(entity)
|
|
99
|
+
}
|
|
100
|
+
if (entity instanceof Enumeration) {
|
|
101
|
+
return generateForEnumeration(entity)
|
|
102
|
+
}
|
|
103
|
+
if (entity instanceof Interface) {
|
|
104
|
+
return generateForInterface(entity)
|
|
105
|
+
}
|
|
106
|
+
if (entity instanceof PrimitiveType) {
|
|
107
|
+
return generateForPrimitiveType(entity)
|
|
108
|
+
}
|
|
109
|
+
return `// unhandled language entity: <${entity.constructor.name}>${entity.name}`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const generateForRelationsOf = (entity: LanguageEntity) => {
|
|
113
|
+
const relations = relationsOf(entity)
|
|
114
|
+
return relations.length === 0 ? `` : relations.map(relation => generateForRelation(entity, relation))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const generateForRelation = ({ name: leftName }: LanguageEntity, relation: Link) => {
|
|
118
|
+
const { name: relationName, optional, multiple, type } = relation
|
|
119
|
+
const rightName = isRef(type) ? type.name : type === unresolved ? `<unresolved>` : `<null>`
|
|
120
|
+
const isContainment = relation instanceof Containment
|
|
121
|
+
const leftMultiplicity = isContainment ? `1` : `*`
|
|
122
|
+
const rightMultiplicity = (() => {
|
|
123
|
+
if (multiple) {
|
|
124
|
+
return "*"
|
|
125
|
+
}
|
|
126
|
+
return optional ? "0..1" : "1"
|
|
127
|
+
})()
|
|
128
|
+
return `${leftName} "${leftMultiplicity}" ${isContainment ? `o` : ``}--> "${rightMultiplicity}" ${rightName}: ${relationName}`
|
|
129
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Annotation,
|
|
3
|
+
Concept,
|
|
4
|
+
Containment,
|
|
5
|
+
Enumeration,
|
|
6
|
+
Feature,
|
|
7
|
+
Interface,
|
|
8
|
+
isBuiltinNodeConcept,
|
|
9
|
+
isRef,
|
|
10
|
+
Language,
|
|
11
|
+
LanguageEntity,
|
|
12
|
+
Link,
|
|
13
|
+
nameOf,
|
|
14
|
+
nameSorted,
|
|
15
|
+
nonRelationalFeatures,
|
|
16
|
+
PrimitiveType,
|
|
17
|
+
relationsOf,
|
|
18
|
+
type,
|
|
19
|
+
unresolved
|
|
20
|
+
} from "@lionweb/core"
|
|
21
|
+
import { asString, indentWith } from "littoral-templates"
|
|
22
|
+
|
|
23
|
+
const indented = indentWith(` `)(1)
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Generates a string with a PlantUML class diagram
|
|
27
|
+
* representing the given {@link Language LionCore instance}.
|
|
28
|
+
*/
|
|
29
|
+
export const generatePlantUmlForLanguage = ({ name, entities }: Language) =>
|
|
30
|
+
asString([
|
|
31
|
+
`@startuml
|
|
32
|
+
hide empty members
|
|
33
|
+
|
|
34
|
+
' qualified name: "${name}"
|
|
35
|
+
|
|
36
|
+
`,
|
|
37
|
+
nameSorted(entities).map(generateForEntity),
|
|
38
|
+
`
|
|
39
|
+
|
|
40
|
+
' relations:
|
|
41
|
+
`,
|
|
42
|
+
nameSorted(entities).map(generateForRelationsOf),
|
|
43
|
+
`
|
|
44
|
+
@enduml`
|
|
45
|
+
])
|
|
46
|
+
|
|
47
|
+
const generateForEnumeration = ({ name, literals }: Enumeration) => [
|
|
48
|
+
`enum ${name} {`,
|
|
49
|
+
indented(literals.map(({name}) => name)),
|
|
50
|
+
`}
|
|
51
|
+
`
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
const generateForAnnotation = ({ name, features, extends: extends_, implements: implements_, annotates }: Annotation) => {
|
|
55
|
+
const fragments: string[] = []
|
|
56
|
+
fragments.push(`annotation`, name)
|
|
57
|
+
if (isRef(extends_) && !isBuiltinNodeConcept(extends_)) {
|
|
58
|
+
fragments.push(`extends`, extends_.name)
|
|
59
|
+
}
|
|
60
|
+
if (implements_.length > 0) {
|
|
61
|
+
fragments.push(`implements`, implements_.map(nameOf).sort().join(", "))
|
|
62
|
+
}
|
|
63
|
+
const nonRelationalFeatures_ = nonRelationalFeatures(features)
|
|
64
|
+
return nonRelationalFeatures_.length === 0
|
|
65
|
+
? [`${fragments.join(" ")}`, isRef(annotates) ? `${name} ..> ${annotates.name}` : [], ``]
|
|
66
|
+
: [`${fragments.join(" ")} {`, indented(nonRelationalFeatures_.map(generateForNonRelationalFeature)), `}`, ``]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const generateForConcept = ({ name, features, abstract: abstract_, extends: extends_, implements: implements_, partition }: Concept) => {
|
|
70
|
+
const fragments: string[] = []
|
|
71
|
+
if (abstract_) {
|
|
72
|
+
fragments.push(`abstract`)
|
|
73
|
+
}
|
|
74
|
+
fragments.push(`class`, name)
|
|
75
|
+
if (partition) {
|
|
76
|
+
fragments.push(`<<partition>>`)
|
|
77
|
+
}
|
|
78
|
+
if (isRef(extends_) && !isBuiltinNodeConcept(extends_)) {
|
|
79
|
+
fragments.push(`extends`, extends_.name)
|
|
80
|
+
}
|
|
81
|
+
if (implements_.length > 0) {
|
|
82
|
+
fragments.push(`implements`, implements_.map(nameOf).sort().join(", "))
|
|
83
|
+
}
|
|
84
|
+
const nonRelationalFeatures_ = nonRelationalFeatures(features)
|
|
85
|
+
return nonRelationalFeatures_.length === 0
|
|
86
|
+
? [`${fragments.join(" ")}`, ``]
|
|
87
|
+
: [`${fragments.join(" ")} {`, indented(nonRelationalFeatures_.map(generateForNonRelationalFeature)), `}`, ``]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const generateForInterface = ({ name, extends: extends_, features }: Interface) => {
|
|
91
|
+
const fragments: string[] = [`interface`, name]
|
|
92
|
+
if (extends_.length > 0) {
|
|
93
|
+
fragments.push(`extends`, extends_.map(superInterface => superInterface.name).join(", "))
|
|
94
|
+
}
|
|
95
|
+
const nonRelationalFeatures_ = nonRelationalFeatures(features)
|
|
96
|
+
return nonRelationalFeatures_.length === 0
|
|
97
|
+
? `${fragments.join(" ")}`
|
|
98
|
+
: [`${fragments.join(" ")} {`, indented(nonRelationalFeatures_.map(generateForNonRelationalFeature)), `}`, ``]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const generateForNonRelationalFeature = (feature: Feature) => {
|
|
102
|
+
const { name, optional } = feature
|
|
103
|
+
const multiple = feature instanceof Link && feature.multiple
|
|
104
|
+
const type_ = type(feature)
|
|
105
|
+
return `${name}: ${multiple ? `List<` : ``}${type_ === unresolved ? `???` : type_.name}${optional && !multiple ? `?` : ``}${multiple ? `>` : ``}`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const generateForPrimitiveType = ({ name }: PrimitiveType) => `class "${name}" <<primitive type>>`
|
|
109
|
+
|
|
110
|
+
const generateForEntity = (entity: LanguageEntity) => {
|
|
111
|
+
if (entity instanceof Annotation) {
|
|
112
|
+
return generateForAnnotation(entity)
|
|
113
|
+
}
|
|
114
|
+
if (entity instanceof Enumeration) {
|
|
115
|
+
return generateForEnumeration(entity)
|
|
116
|
+
}
|
|
117
|
+
if (entity instanceof Concept) {
|
|
118
|
+
return generateForConcept(entity)
|
|
119
|
+
}
|
|
120
|
+
if (entity instanceof Interface) {
|
|
121
|
+
return generateForInterface(entity)
|
|
122
|
+
}
|
|
123
|
+
if (entity instanceof PrimitiveType) {
|
|
124
|
+
return generateForPrimitiveType(entity)
|
|
125
|
+
}
|
|
126
|
+
return `' unhandled language entity: <${entity.constructor.name}>${entity.name}
|
|
127
|
+
`
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const generateForRelationsOf = (entity: LanguageEntity) => {
|
|
131
|
+
const relations = relationsOf(entity)
|
|
132
|
+
return relations.length === 0 ? `` : relations.map(relation => generateForRelation(entity, relation))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const generateForRelation = ({ name: leftName }: LanguageEntity, relation: Link) => {
|
|
136
|
+
const { name: relationName, type, optional, multiple } = relation
|
|
137
|
+
const rightName = isRef(type) ? type.name : type === unresolved ? `<unresolved>` : `<null>`
|
|
138
|
+
const isContainment = relation instanceof Containment
|
|
139
|
+
const leftMultiplicity = isContainment ? `1` : `*`
|
|
140
|
+
const rightMultiplicity = multiple ? "*" : optional ? "0..1" : "1"
|
|
141
|
+
return `${leftName} "${leftMultiplicity}" ${isContainment ? `o` : ``}--> "${rightMultiplicity}" ${rightName}: ${relationName}`
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/*
|
|
145
|
+
Notes:
|
|
146
|
+
1. No construct for PrimitiveType in PlantUML.
|
|
147
|
+
*/
|
package/src/m3/index.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import {
|
|
2
|
+
builtinPrimitives,
|
|
3
|
+
Concept,
|
|
4
|
+
Containment,
|
|
5
|
+
Language,
|
|
6
|
+
Link,
|
|
7
|
+
Property,
|
|
8
|
+
Reference
|
|
9
|
+
} from "@lionweb/core"
|
|
10
|
+
import { LionWebId, LionWebJsonChunk, LionWebKey } from "@lionweb/json"
|
|
11
|
+
import { asArray, chain, concatenator, lastOf } from "@lionweb/ts-utils"
|
|
12
|
+
import { hasher } from "../hashing.js"
|
|
13
|
+
|
|
14
|
+
const possibleKeySeparators = ["-", "_"]
|
|
15
|
+
|
|
16
|
+
const id = chain(concatenator("-"), hasher())
|
|
17
|
+
const key = lastOf
|
|
18
|
+
|
|
19
|
+
const { stringDataType, booleanDataType, integerDataType } = builtinPrimitives
|
|
20
|
+
|
|
21
|
+
export const inferLanguagesFromSerializationChunk = (chunk: LionWebJsonChunk): Language[] => {
|
|
22
|
+
const languages = new Map<string, Language>()
|
|
23
|
+
const concepts = new Map<string, Concept>()
|
|
24
|
+
const links = new Array<{ link: Link; conceptId: LionWebId }>()
|
|
25
|
+
|
|
26
|
+
for (const chunkLanguage of chunk.languages) {
|
|
27
|
+
const languageName = chunkLanguage.key
|
|
28
|
+
const language = new Language(languageName, chunkLanguage.version, id(languageName), key(languageName))
|
|
29
|
+
languages.set(languageName, language)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const node of chunk.nodes) {
|
|
33
|
+
const languageName = node.classifier.language
|
|
34
|
+
const entityName = node.classifier.key
|
|
35
|
+
|
|
36
|
+
const language = findLanguage(languages, languageName)
|
|
37
|
+
if (language.entities.filter(entity => entity.key === entityName).length) {
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const concept = new Concept(language, entityName, key(language.name, entityName), id(language.name, entityName), false)
|
|
42
|
+
language.havingEntities(concept)
|
|
43
|
+
concepts.set(node.id, concept)
|
|
44
|
+
|
|
45
|
+
for (const property of node.properties) {
|
|
46
|
+
const propertyName = deriveLikelyPropertyName(property.property.key)
|
|
47
|
+
if (concept.features.filter(feature => feature.key === propertyName).length) {
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const feature = new Property(
|
|
52
|
+
concept,
|
|
53
|
+
propertyName,
|
|
54
|
+
key(languageName, concept.name, propertyName),
|
|
55
|
+
id(languageName, concept.name, propertyName)
|
|
56
|
+
).havingKey(property.property.key)
|
|
57
|
+
|
|
58
|
+
if (property.value === null) {
|
|
59
|
+
feature.isOptional()
|
|
60
|
+
} else {
|
|
61
|
+
if (isBoolean(property.value)) {
|
|
62
|
+
feature.ofType(booleanDataType)
|
|
63
|
+
} else if (isNumeric(property.value)) {
|
|
64
|
+
feature.ofType(integerDataType)
|
|
65
|
+
} else {
|
|
66
|
+
feature.ofType(stringDataType)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
concept.havingFeatures(feature)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const containment of node.containments) {
|
|
74
|
+
const containmentName = containment.containment.key
|
|
75
|
+
|
|
76
|
+
if (concept.features.filter(feature => feature.key === containmentName).length) {
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const children = asArray(containment.children)
|
|
81
|
+
const feature = new Containment(
|
|
82
|
+
concept,
|
|
83
|
+
containmentName,
|
|
84
|
+
key(languageName, concept.name, containmentName),
|
|
85
|
+
id(languageName, concept.name, containmentName)
|
|
86
|
+
)
|
|
87
|
+
if (children.length) {
|
|
88
|
+
feature.isMultiple()
|
|
89
|
+
}
|
|
90
|
+
concept.havingFeatures(feature)
|
|
91
|
+
|
|
92
|
+
links.push({ link: feature, conceptId: children[0] })
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const reference of node.references) {
|
|
96
|
+
const referenceName = reference.reference.key
|
|
97
|
+
if (concept.features.filter(feature => feature.key === referenceName).length) {
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const feature = new Reference(
|
|
102
|
+
concept,
|
|
103
|
+
referenceName,
|
|
104
|
+
key(languageName, concept.name, referenceName),
|
|
105
|
+
id(languageName, concept.name, referenceName)
|
|
106
|
+
)
|
|
107
|
+
concept.havingFeatures(feature)
|
|
108
|
+
|
|
109
|
+
const value = reference.targets[0].reference
|
|
110
|
+
links.push({ link: feature, conceptId: value })
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const link of links) {
|
|
115
|
+
const linkedConcept = concepts.get(link.conceptId)
|
|
116
|
+
if (linkedConcept) {
|
|
117
|
+
link.link.ofType(linkedConcept)
|
|
118
|
+
} else {
|
|
119
|
+
// Containment of primitive types??
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return Array.from(languages.values())
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const findLanguage = (languages: Map<string, Language>, languageName: string) => {
|
|
127
|
+
const language = languages.get(languageName)
|
|
128
|
+
if (language === undefined) {
|
|
129
|
+
throw new Error(`Language '${languageName} does not exist in the languages section`)
|
|
130
|
+
}
|
|
131
|
+
return language
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const deriveLikelyPropertyName = (key: LionWebKey) => {
|
|
135
|
+
for (const separator of possibleKeySeparators) {
|
|
136
|
+
const name = key.split(separator)[2]
|
|
137
|
+
if (name) {
|
|
138
|
+
return name
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return key
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const isBoolean = (value: string) => value === "true" || value === "false"
|
|
146
|
+
|
|
147
|
+
const isNumeric = (value: string) => !isNaN(parseFloat(value))
|