@lionweb/core 0.7.0-beta.18 → 0.7.0-beta.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,170 @@
1
+ import { asMinimalJsonString, StringsMapper } from "@lionweb/ts-utils"
2
+ import { PropertyValueDeserializer } from "../deserializer.js"
3
+ import { PropertyValueSerializer } from "../serializer.js"
4
+ import { currentReleaseVersion } from "../version.js"
5
+ import { LanguageFactory } from "./factory.js"
6
+ import { Classifier, Concept, DataType, lioncoreBuiltinsKey, Property } from "./types.js"
7
+
8
+ const lioncoreBuiltinsIdAndKeyGenerator: StringsMapper = (...names) => [lioncoreBuiltinsKey, ...names.slice(1)].join("-")
9
+
10
+ const factory = new LanguageFactory(
11
+ "LionCore_builtins",
12
+ currentReleaseVersion,
13
+ lioncoreBuiltinsIdAndKeyGenerator,
14
+ lioncoreBuiltinsIdAndKeyGenerator
15
+ )
16
+ /*
17
+ * ID == key: `LionCore-builtins-${qualified name _without_ "LionCore-builtins", dash-separated}`
18
+ */
19
+
20
+ /**
21
+ * Definition of a LionCore language that serves as a standard library of built-in primitive types.
22
+ */
23
+ const lioncoreBuiltins = factory.language
24
+
25
+ const stringDataType = factory.primitiveType("String")
26
+ const booleanDataType = factory.primitiveType("Boolean")
27
+ const integerDataType = factory.primitiveType("Integer")
28
+ const jsonDataType = factory.primitiveType("JSON")
29
+
30
+ const node = factory.concept("Node", true)
31
+
32
+ const isBuiltinNodeConcept = (classifier: Classifier) =>
33
+ classifier instanceof Concept &&
34
+ classifier.language.key === lioncoreBuiltinsKey &&
35
+ classifier.language.version === currentReleaseVersion &&
36
+ classifier.key === builtinClassifiers.node.key &&
37
+ (classifier as Concept).abstract
38
+
39
+ const inamed = factory.interface("INamed")
40
+
41
+ const inamed_name = factory.property(inamed, "name").ofType(stringDataType)
42
+
43
+ const builtinPrimitives = {
44
+ stringDataType,
45
+ booleanDataType,
46
+ integerDataType,
47
+ jsonDataType,
48
+ /**
49
+ * Misspelled alias of {@link stringDataType}, kept for backward compatibility, and to be deprecated and removed later.
50
+ */
51
+ stringDatatype: stringDataType,
52
+ /**
53
+ * Misspelled alias of {@link booleanDataType}, kept for backward compatibility, and to be deprecated and removed later.
54
+ */
55
+ booleanDatatype: booleanDataType,
56
+ /**
57
+ * Misspelled alias of {@link integerDataType}, kept for backward compatibility, and to be deprecated and removed later.
58
+ */
59
+ integerDatatype: integerDataType,
60
+ /**
61
+ * Misspelled alias of {@link jsonDataType}, kept for backward compatibility, and to be deprecated and removed later.
62
+ */
63
+ jsonDatatype: jsonDataType
64
+ }
65
+
66
+ const builtinClassifiers = {
67
+ node,
68
+ inamed
69
+ }
70
+
71
+ const builtinFeatures = {
72
+ inamed_name
73
+ }
74
+
75
+ /**
76
+ * Determines whether two data types should be structurally equal based on equality of: meta type, key, and language's key.
77
+ */
78
+ const shouldBeIdentical = (left: DataType, right: DataType): boolean =>
79
+ left.key === right.key && left.language.key === right.language.key && left.metaType() === right.metaType()
80
+
81
+ abstract class DataTypeRegister<T> {
82
+ private map = new Map<DataType, T>()
83
+
84
+ public register(dataType: DataType, t: T) {
85
+ this.map.set(dataType, t)
86
+ }
87
+
88
+ protected byType(targetDataType: DataType): T | undefined {
89
+ for (const [dataType, t] of this.map.entries()) {
90
+ if (shouldBeIdentical(targetDataType, dataType)) {
91
+ return t
92
+ }
93
+ }
94
+ return undefined
95
+ }
96
+ }
97
+
98
+ export class BuiltinPropertyValueDeserializer
99
+ extends DataTypeRegister<(value: string) => unknown>
100
+ implements PropertyValueDeserializer
101
+ {
102
+ constructor() {
103
+ super()
104
+ this.register(stringDataType, value => value)
105
+ this.register(booleanDataType, value => JSON.parse(value))
106
+ this.register(integerDataType, value => Number(value))
107
+ this.register(jsonDataType, value => JSON.parse(value as string))
108
+ }
109
+
110
+ deserializeValue(value: string | undefined, property: Property): unknown | undefined {
111
+ if (value === undefined) {
112
+ if (property.optional) {
113
+ return undefined
114
+ }
115
+ throw new Error(`can't deserialize undefined as the value of required property "${property.name}" (on classifier "${property.classifier.name}" in language "${property.classifier.language.name}")`)
116
+ }
117
+ const { type } = property
118
+ if (type == null) {
119
+ throw new Error(`can't deserialize property "${property.name}" (on classifier "${property.classifier.name}" in language "${property.classifier.language.name}") with unspecified type`)
120
+ }
121
+ const specificDeserializer = this.byType(type)
122
+ if (specificDeserializer != undefined) {
123
+ return specificDeserializer(value)
124
+ } else {
125
+ throw new Error(`can't deserialize value of property "${property.name}" (on classifier "${property.classifier.name}" in language "${property.classifier.language.name}") of type "${type!.name}": ${value}`)
126
+ }
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Misspelled alias of {@link BuiltinPropertyValueDeserializer}, kept for backward compatibility, and to be deprecated and removed later.
132
+ */
133
+ export class DefaultPrimitiveTypeDeserializer extends BuiltinPropertyValueDeserializer {}
134
+
135
+
136
+ export class BuiltinPropertyValueSerializer extends DataTypeRegister<(value: unknown) => string> implements PropertyValueSerializer {
137
+ constructor() {
138
+ super()
139
+ this.register(stringDataType, value => value as string)
140
+ this.register(booleanDataType, value => `${value as boolean}`)
141
+ this.register(integerDataType, value => `${value as number}`)
142
+ this.register(jsonDataType, value => asMinimalJsonString(value))
143
+ }
144
+
145
+ serializeValue(value: unknown | undefined, property: Property): string | null {
146
+ if (value === undefined) {
147
+ if (property.optional) {
148
+ return null
149
+ }
150
+ throw new Error(`can't serialize undefined as the value of required property "${property.name}" (on classifier "${property.classifier.name}" in language "${property.classifier.language.name}")`)
151
+ }
152
+ const { type } = property
153
+ if (type == null) {
154
+ throw new Error(`can't serialize property "${property.name}" (on classifier "${property.classifier.name}" in language "${property.classifier.language.name}") with unspecified type`)
155
+ }
156
+ const specificSerializer = this.byType(type)
157
+ if (specificSerializer != undefined) {
158
+ return specificSerializer(value)
159
+ } else {
160
+ throw new Error(`can't serialize value of property "${property.name}" (on classifier "${property.classifier.name}" in language "${property.classifier.language.name}") of type "${type!.name}": ${value}`)
161
+ }
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Misspelled alias of {@link BuiltinPropertyValueSerializer}, kept for backward compatibility, and to be deprecated and removed later.
167
+ */
168
+ export class DefaultPrimitiveTypeSerializer extends BuiltinPropertyValueSerializer {}
169
+
170
+ export { builtinPrimitives, builtinClassifiers, builtinFeatures, isBuiltinNodeConcept, lioncoreBuiltins, shouldBeIdentical }
@@ -0,0 +1,109 @@
1
+ import { duplicatesAmong } from "@lionweb/ts-utils"
2
+ import { idOf } from "../functions.js"
3
+ import { allContaineds, flatMap, inheritanceCycleWith, keyOf, namedsOf, qualifiedNameOf } from "./functions.js"
4
+ import { Classifier, isINamed, Language, M3Concept } from "./types.js"
5
+
6
+
7
+ /**
8
+ * Type definition for an issue corresponding
9
+ * to a violation of a constraint on a {@link M3Concept language object}.
10
+ */
11
+ export type Issue = {
12
+ location: M3Concept
13
+ message: string
14
+ secondaries: M3Concept[]
15
+ }
16
+ // TODO back this type with an M2
17
+
18
+
19
+ const base64urlRegex = /^[A-Za-z0-9_-]+$/
20
+
21
+ /**
22
+ * @return whether the given string is a valid identifier according to the LionWeb specification – see [here](https://github.com/LionWeb-io/specification/blob/main/2023.1/metametamodel/metametamodel.adoc#identifiers) for the relevant part.
23
+ * This is essentially whether the given string is a valid, non-empty [Base64url](https://en.wikipedia.org/wiki/Base64#Variants_summary_table) string.
24
+ */
25
+ const isValidIdentifier = (str: string): boolean =>
26
+ base64urlRegex.test(str)
27
+
28
+
29
+ /**
30
+ * Computes the {@link Issue issues} (i.e., constraint violations) for the given language.
31
+ * (This computation is resilient against e.g. inheritance cycles.)
32
+ */
33
+ export const issuesLanguage = (language: Language): Issue[] =>
34
+ [
35
+ ...flatMap(
36
+ language,
37
+ (t) => {
38
+
39
+ const issues: Issue[] = []
40
+ const issue = (message: string, secondaries?: M3Concept[]): void => {
41
+ issues.push({
42
+ location: t,
43
+ message,
44
+ secondaries: secondaries ?? []
45
+ })
46
+ }
47
+
48
+ // The id consists only of latin characters (upper/lowercase), numbers, underscores, and hyphens
49
+ const id = t.id.trim()
50
+ !isValidIdentifier(id) && issue(`An ID must consist only of latin characters (upper/lowercase), numbers, underscores, and hyphens`)
51
+
52
+ // The key consists only of latin characters (upper/lowercase), numbers, underscores, and hyphens
53
+ const key = t.key.trim()
54
+ !isValidIdentifier(key) && issue(`A KEY must consist only of latin characters (upper/lowercase), numbers, underscores, and hyphens`)
55
+
56
+ if(isINamed(t)) {
57
+ const trimmedName = t.name.trim()
58
+ // The name should be a non-empty string
59
+ trimmedName.length === 0 && issue(`A ${t.constructor.name}'s name must not be empty`)
60
+
61
+ // The name should not contain whitespace characters
62
+ trimmedName.includes(" ") && issue(`A ${t.constructor.name}'s name cannot contain whitespace characters`)
63
+ }
64
+
65
+ if (t instanceof Language) {
66
+ // The name should not start with a number
67
+ const name = t.name.trim()
68
+ !isNaN(parseInt(name[0])) && issue(`A Language's name cannot start with a number`)
69
+
70
+ // The version is a non-empty string
71
+ const version = t.version.trim();
72
+ version.length === 0 && issue(`A Language's version must be a non-empty string`)
73
+ }
74
+
75
+ // The classifier should not inherit from itself (directly or indirectly)
76
+ if (t instanceof Classifier) {
77
+ const cycle = inheritanceCycleWith(t);
78
+ (cycle.length > 0) && issue(`A ${t.constructor.name} can't inherit (directly or indirectly) from itself, but ${qualifiedNameOf(t)} does so through the following cycle: ${cycle.map((t) => qualifiedNameOf(t)).join(" -> ")}`)
79
+ // TODO check whether it needs to be "a" or "an", or just say "An instance of ..."
80
+ }
81
+
82
+ return issues
83
+ }
84
+ ),
85
+ ...Object.entries(duplicatesAmong(allContaineds(language), idOf))
86
+ .flatMap(
87
+ ([id, ts]) => ts.map(
88
+ (t) => ({ location: t, message: `Multiple (nested) language elements with the same ID "${id}" exist in this language`, secondaries: ts.filter((otherT) => t !== otherT) })
89
+ )
90
+ ),
91
+ ...Object.entries(duplicatesAmong(namedsOf(language), keyOf)) // all M3Concept-s that are INamed are also IKeyed
92
+ .flatMap(
93
+ ([key, ts]) => ts.map(
94
+ (t) => ({ location: t, message: `Multiple (nested) language elements with the same key "${key}" exist in this language`, secondaries: ts.filter((otherT) => t !== otherT) })
95
+ )
96
+ ),
97
+ ...Object.entries(duplicatesAmong(namedsOf(language), qualifiedNameOf))
98
+ .flatMap(
99
+ ([key, ts]) => ts.map(
100
+ (t) => ({ location: t, message: `Multiple (nested) language elements with the same key "${key}" exist in this language`, secondaries: ts.filter((otherT) => t !== otherT) })
101
+ )
102
+ )
103
+ ]
104
+
105
+
106
+ // not here: unresolved references are a problem on a lower level
107
+ // TODO (#8) check whether references are resolved
108
+ // Same goes for duplicate IDs, but for completeness' and symmetry's sake, we're also checking it here.
109
+
@@ -0,0 +1,38 @@
1
+ import { LionWebJsonChunk } from "@lionweb/json"
2
+ import { deserializeSerializationChunk } from "../deserializer.js"
3
+ import { nodesExtractorUsing } from "../extraction.js"
4
+ import { defaultSimplisticHandler, SimplisticHandler } from "../handler.js"
5
+ import { BuiltinPropertyValueDeserializer, lioncoreBuiltins } from "./builtins.js"
6
+ import { lioncoreReader, lioncoreWriter } from "./facade.js"
7
+ import { lioncore } from "./lioncore.js"
8
+ import { Language } from "./types.js"
9
+
10
+
11
+ /**
12
+ * Deserializes languages that have been serialized into the LionWeb serialization JSON format
13
+ * as an instance of the LionCore metametamodel, using {@link _M3Concept these type definitions}.
14
+ */
15
+ export const deserializeLanguages = (serializationChunk: LionWebJsonChunk, ...dependentLanguages: Language[]): Language[] =>
16
+ deserializeLanguagesWithHandler(serializationChunk, defaultSimplisticHandler, ...dependentLanguages)
17
+
18
+ /**
19
+ * Deserializes languages that have been serialized into the LionWeb serialization JSON format
20
+ * as an instance of the LionCore metametamodel, using {@link _M3Concept these type definitions}.
21
+ * This function takes a handler to be able to see what problems occurred.
22
+ */
23
+ export const deserializeLanguagesWithHandler = (
24
+ serializationChunk: LionWebJsonChunk,
25
+ handler: SimplisticHandler,
26
+ ...dependentLanguages: Language[]
27
+ ): Language[] =>
28
+ deserializeSerializationChunk(
29
+ serializationChunk,
30
+ lioncoreWriter,
31
+ [lioncore, ...dependentLanguages],
32
+ [lioncoreBuiltins, ...dependentLanguages].flatMap(nodesExtractorUsing(lioncoreReader)),
33
+ new BuiltinPropertyValueDeserializer(),
34
+ handler
35
+ )
36
+ .filter((rootNode) => rootNode instanceof Language)
37
+ .map((language) => (language as Language).dependingOn(...dependentLanguages))
38
+
@@ -0,0 +1,130 @@
1
+ import { builtinFeatures } from "./builtins.js"
2
+ import { metaTypedBasedClassifierDeducerFor, qualifiedNameOf } from "./functions.js"
3
+ import { lioncore, metaConcepts, metaFeatures } from "./lioncore.js"
4
+ import { Reader } from "../reading.js"
5
+ import {
6
+ Annotation,
7
+ Classifier,
8
+ Concept,
9
+ Containment,
10
+ Enumeration,
11
+ EnumerationLiteral,
12
+ Interface,
13
+ Language,
14
+ M3Concept,
15
+ PrimitiveType,
16
+ Property,
17
+ Reference
18
+ } from "./types.js"
19
+ import { updateSettingsNameBased, Writer } from "../writing.js"
20
+
21
+ const { inamed_name } = builtinFeatures
22
+ const { ikeyed_key } = metaFeatures
23
+
24
+ export const lioncoreReader: Reader<M3Concept> = {
25
+ classifierOf: metaTypedBasedClassifierDeducerFor(lioncore),
26
+ getFeatureValue: (node, feature) =>
27
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
+ (node as any)[feature.name], // (mirrors name-based update of settings)
29
+ enumerationLiteralFrom: (value, _) => value as EnumerationLiteral | null
30
+ }
31
+
32
+ /**
33
+ * Alias for {@link lioncoreReader}, kept for backward compatibility, and to be deprecated and removed later.
34
+ */
35
+ export const lioncoreExtractionFacade = lioncoreReader
36
+
37
+ /**
38
+ * @return An implementation of {@link Writer} for instances of the LionCore M3 (so M2s).
39
+ */
40
+ export const lioncoreWriter: Writer<M3Concept> = {
41
+ nodeFor: (parent, classifier, id, propertySettings) => {
42
+ switch (classifier.key) {
43
+ case metaConcepts.annotation.key:
44
+ return new Annotation(
45
+ parent as Language,
46
+ propertySettings[inamed_name.key] as string,
47
+ propertySettings[ikeyed_key.key] as string,
48
+ id
49
+ )
50
+ case metaConcepts.concept.key:
51
+ return new Concept(
52
+ parent as Language,
53
+ propertySettings[inamed_name.key] as string,
54
+ propertySettings[ikeyed_key.key] as string,
55
+ id,
56
+ propertySettings[metaFeatures.concept_abstract.key] as boolean
57
+ )
58
+ case metaConcepts.interface.key:
59
+ return new Interface(
60
+ parent as Language,
61
+ propertySettings[inamed_name.key] as string,
62
+ propertySettings[ikeyed_key.key] as string,
63
+ id
64
+ )
65
+ case metaConcepts.containment.key:
66
+ return new Containment(
67
+ parent as Classifier,
68
+ propertySettings[inamed_name.key] as string,
69
+ propertySettings[ikeyed_key.key] as string,
70
+ id
71
+ )
72
+ case metaConcepts.enumeration.key:
73
+ return new Enumeration(
74
+ parent as Language,
75
+ propertySettings[inamed_name.key] as string,
76
+ propertySettings[ikeyed_key.key] as string,
77
+ id
78
+ )
79
+ case metaConcepts.enumerationLiteral.key:
80
+ return new EnumerationLiteral(
81
+ parent as Enumeration,
82
+ propertySettings[inamed_name.key] as string,
83
+ propertySettings[ikeyed_key.key] as string,
84
+ id
85
+ )
86
+ case metaConcepts.language.key:
87
+ return new Language(
88
+ propertySettings[inamed_name.key] as string,
89
+ propertySettings[metaFeatures.language_version.key] as string,
90
+ id,
91
+ propertySettings[metaFeatures.ikeyed_key.key] as string
92
+ )
93
+ case metaConcepts.primitiveType.key:
94
+ return new PrimitiveType(
95
+ parent as Language,
96
+ propertySettings[inamed_name.key] as string,
97
+ propertySettings[ikeyed_key.key] as string,
98
+ id
99
+ )
100
+ case metaConcepts.property.key:
101
+ return new Property(
102
+ parent as Classifier,
103
+ propertySettings[inamed_name.key] as string,
104
+ propertySettings[ikeyed_key.key] as string,
105
+ id
106
+ )
107
+ case metaConcepts.reference.key:
108
+ return new Reference(
109
+ parent as Classifier,
110
+ propertySettings[inamed_name.key] as string,
111
+ propertySettings[ikeyed_key.key] as string,
112
+ id
113
+ )
114
+ default:
115
+ throw new Error(
116
+ `don't know a node of concept ${qualifiedNameOf(classifier)} with key ${classifier.key} that's not in LionCore M3`
117
+ )
118
+ }
119
+ },
120
+ setFeatureValue: (node, feature, value) => {
121
+ updateSettingsNameBased(node as unknown as Record<string, unknown>, feature, value)
122
+ },
123
+ encodingOf: literal => literal
124
+ }
125
+
126
+ /**
127
+ * Alias for {@link lioncoreWriter}, kept for backward compatibility, and to be deprecated and removed later.
128
+ */
129
+ export const lioncoreInstantationFacade = lioncoreWriter
130
+
@@ -0,0 +1,98 @@
1
+ import { StringsMapper } from "@lionweb/ts-utils"
2
+ import { SingleRef } from "../references.js"
3
+ import {
4
+ Annotation,
5
+ Classifier,
6
+ Concept,
7
+ Containment,
8
+ Enumeration,
9
+ EnumerationLiteral,
10
+ Interface,
11
+ Language,
12
+ PrimitiveType,
13
+ Property,
14
+ Reference
15
+ } from "./types.js"
16
+
17
+
18
+ /**
19
+ * A factory that produces a {@link Language} instance,
20
+ * as well as {@link LanguageEntity entities} contained by that instance
21
+ * and {@link Feature features} of {@link Classifier classifiers}
22
+ * and {@link EnumerationLiteral enumeration literals} of {@link Enumeration enumerations}.
23
+ *
24
+ * The factory methods take care of proper containment.
25
+ * *Note:* also calling `havingEntities`, `havingFeatures`, and `havingLiterals` doesn't produce duplicates.
26
+ * (This is to stay backward compatible.)
27
+ */
28
+ export class LanguageFactory {
29
+
30
+ readonly id: StringsMapper
31
+ readonly key: StringsMapper
32
+ readonly language: Language
33
+
34
+ constructor(name: string, version: string, id: StringsMapper, key: StringsMapper) {
35
+ this.id = id
36
+ this.key = key
37
+ this.language = new Language(name, version, this.id(name), this.key(name))
38
+ }
39
+
40
+
41
+ annotation(name: string, extends_?: SingleRef<Annotation>): Annotation {
42
+ const annotation = new Annotation(this.language, name, this.key(this.language.name, name), this.id(this.language.name, name), extends_)
43
+ this.language.havingEntities(annotation)
44
+ return annotation
45
+ }
46
+
47
+ concept(name: string, abstract: boolean, extends_?: SingleRef<Concept>): Concept {
48
+ const concept = new Concept(this.language, name, this.key(this.language.name, name), this.id(this.language.name, name), abstract, extends_)
49
+ this.language.havingEntities(concept)
50
+ return concept
51
+ }
52
+
53
+ interface(name: string): Interface {
54
+ const intface = new Interface(this.language, name, this.key(this.language.name, name), this.id(this.language.name, name))
55
+ this.language.havingEntities(intface)
56
+ return intface
57
+ }
58
+
59
+ enumeration(name: string): Enumeration {
60
+ const enumeration = new Enumeration(this.language, name, this.key(this.language.name, name), this.id(this.language.name, name))
61
+ this.language.havingEntities(enumeration)
62
+ return enumeration
63
+ }
64
+
65
+ primitiveType(name: string): PrimitiveType {
66
+ const primitiveType = new PrimitiveType(this.language, name, this.key(this.language.name, name), this.id(this.language.name, name))
67
+ this.language.havingEntities(primitiveType)
68
+ return primitiveType
69
+ }
70
+
71
+
72
+ containment(classifier: Classifier, name: string): Containment {
73
+ const containment = new Containment(classifier, name, this.key(this.language.name, classifier.name, name), this.id(this.language.name, classifier.name, name))
74
+ classifier.havingFeatures(containment)
75
+ return containment
76
+ }
77
+
78
+ property(classifier: Classifier, name: string): Property {
79
+ const property = new Property(classifier, name, this.key(this.language.name, classifier.name, name), this.id(this.language.name, classifier.name, name))
80
+ classifier.havingFeatures(property)
81
+ return property
82
+ }
83
+
84
+ reference(classifier: Classifier, name: string): Reference {
85
+ const reference = new Reference(classifier, name, this.key(this.language.name, classifier.name, name), this.id(this.language.name, classifier.name, name))
86
+ classifier.havingFeatures(reference)
87
+ return reference
88
+ }
89
+
90
+
91
+ enumerationLiteral(enumeration: Enumeration, name: string): EnumerationLiteral {
92
+ const enumerationLiteral = new EnumerationLiteral(enumeration, name, this.key(this.language.name, enumeration.name, name), this.id(this.language.name, enumeration.name, name))
93
+ enumeration.havingLiterals(enumerationLiteral)
94
+ return enumerationLiteral
95
+ }
96
+
97
+ }
98
+
@@ -0,0 +1,55 @@
1
+ import { LionWebJsonMetaPointer } from "@lionweb/json"
2
+
3
+ import { Classifier, Containment, Feature, Language, Property, Reference } from "./types.js"
4
+ import { MemoisingSymbolTable } from "./symbol-table.js"
5
+
6
+
7
+ /**
8
+ * Type def. for functions that resolve a {@link Feature feature} of the indicated sub-type
9
+ * from a feature's and a classifier's meta-pointers,
10
+ * throwing an {@link Error error} when the feature couldn't be resolved,
11
+ * or it isn't of the expected sub-type.
12
+ */
13
+ export type FeatureResolver<FT extends Feature> = (metaPointer: LionWebJsonMetaPointer, classifier: Classifier) => FT
14
+
15
+ /**
16
+ * Type def. for an object containing {@link FeatureResolver resolvers} for
17
+ * {@link Property properties}, {@link Containmnent containments}, and {@link Reference references}.
18
+ */
19
+ export type FeatureResolvers = {
20
+ resolvedPropertyFrom: FeatureResolver<Property>
21
+ resolvedContainmentFrom: FeatureResolver<Containment>
22
+ resolvedReferenceFrom: FeatureResolver<Reference>
23
+ }
24
+
25
+ /**
26
+ * @return an {@link FeatureResolvers object} for the given {@link Language languages}.
27
+ */
28
+ export const featureResolversFor = (languages: Language[]): FeatureResolvers => {
29
+ const symbolTable = new MemoisingSymbolTable(languages)
30
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
+ const featureResolverFor = <FT extends Feature>(featureClassConstructor: new (...args: any[]) => FT): FeatureResolver<FT> =>
32
+ (featureMetaPointer, classifier: Classifier) => {
33
+ const classifierMetaPointer = classifier.metaPointer()
34
+ const featureLocationMessage = () => `feature with meta-pointer ${JSON.stringify(featureMetaPointer)} on classifier with meta-pointer ${JSON.stringify(classifierMetaPointer)}`
35
+ const feature = symbolTable.featureMatching(classifierMetaPointer, featureMetaPointer)
36
+ if (feature === undefined) {
37
+ throw new Error(`couldn't resolve ${featureLocationMessage()}`) // fail early <== unrecoverable
38
+ }
39
+ if (feature.constructor !== featureClassConstructor) { // feature's type must match desired type *exactly* — cheaper to evaluate than "feature instanceof featureClassConstructor"
40
+ throw new Error(`${featureLocationMessage()} is not a ${featureClassConstructor.name} but a ${feature.constructor.name}`) // fail early <== unrecoverable
41
+ }
42
+ /*
43
+ * We could make this function memoising as well, to avoid having to perform these checks every resolution.
44
+ * That memoisation would involve a 6-deep lookup, in the feature's meta-pointer + container's classifier meta-pointer.
45
+ * The checks seem cheap enough to not be problematic performance-wise, though.
46
+ */
47
+ return feature as FT // valid <== feature.constructor === featureClassConstructor
48
+ }
49
+ return {
50
+ resolvedPropertyFrom: featureResolverFor(Property),
51
+ resolvedContainmentFrom: featureResolverFor(Containment),
52
+ resolvedReferenceFrom: featureResolverFor(Reference)
53
+ }
54
+ }
55
+