@lionweb/validation 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.
- package/CHANGELOG.md +0 -1
- package/package.json +4 -4
- package/src/index.ts +4 -0
- package/src/issues/LanguageIssues.ts +147 -0
- package/src/issues/ReferenceIssues.ts +83 -0
- package/src/issues/SyntaxIssues.ts +84 -0
- package/src/issues/ValidationIssue.ts +29 -0
- package/src/issues/index.ts +4 -0
- package/src/languages/CompositeLionWebLanguageWrapper.ts +57 -0
- package/src/languages/LanguageRegistry.ts +44 -0
- package/src/languages/LanguageUtils.ts +63 -0
- package/src/languages/LionCore-M3.json +2356 -0
- package/src/languages/LionCore-builtins.json +372 -0
- package/src/languages/LionWebLanguageWrapper.ts +91 -0
- package/src/languages/MetaPointerMap.ts +41 -0
- package/src/languages/index.ts +2 -0
- package/src/runners/FileUtils.ts +59 -0
- package/src/runners/RunCheckFolder.ts +7 -0
- package/src/runners/RunCheckFolderWithLanguage.ts +45 -0
- package/src/runners/RunCheckOneFile.ts +7 -0
- package/src/runners/RunCheckOneFileWithLanguage.ts +35 -0
- package/src/runners/RunLioncoreDiff.ts +23 -0
- package/src/runners/Utils.ts +54 -0
- package/src/runners/index.ts +2 -0
- package/src/validators/LionWebChunkDefinitions.ts +104 -0
- package/src/validators/LionWebLanguageReferenceValidator.ts +201 -0
- package/src/validators/LionWebLanguageValidator.ts +79 -0
- package/src/validators/LionWebReferenceValidator.ts +225 -0
- package/src/validators/LionWebSyntaxValidator.ts +14 -0
- package/src/validators/LionWebValidator.ts +71 -0
- package/src/validators/ValidationFunctions.ts +129 -0
- package/src/validators/generic/SyntaxValidator.ts +164 -0
- package/src/validators/generic/ValidationResult.ts +17 -0
- package/src/validators/generic/index.ts +3 -0
- package/src/validators/generic/schema/DefinitionSchema.ts +52 -0
- package/src/validators/generic/schema/ValidationTypes.ts +134 -0
- package/src/validators/generic/schema/index.ts +2 -0
- package/src/validators/index.ts +8 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { LionWebId, LionWebJsonChunk, LionWebJsonContainment, LionWebJsonMetaPointer, LionWebJsonNode, LionWebJsonUsedLanguage } from "@lionweb/json"
|
|
2
|
+
import { ChunkUtils, JsonContext, LionWebJsonChunkWrapper } from "@lionweb/json-utils"
|
|
3
|
+
import {
|
|
4
|
+
Duplicates_Issue,
|
|
5
|
+
Reference_ChildMissingInParent_Issue,
|
|
6
|
+
Reference_CirculairParent_Issue,
|
|
7
|
+
Reference_DuplicateNodeId_Issue,
|
|
8
|
+
Reference_LanguageUnknown_Issue,
|
|
9
|
+
Reference_ParentMissingInChild_Issue
|
|
10
|
+
} from "../issues/ReferenceIssues.js"
|
|
11
|
+
import { ValidationResult } from "./generic/ValidationResult.js"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Assuming that the syntax is correct, check whether all LionWeb references are correct,
|
|
15
|
+
* as far as they do not need the used language definition.
|
|
16
|
+
*/
|
|
17
|
+
export class LionWebReferenceValidator {
|
|
18
|
+
validationResult: ValidationResult
|
|
19
|
+
nodesIdMap: Map<string, LionWebJsonNode> = new Map<string, LionWebJsonNode>()
|
|
20
|
+
|
|
21
|
+
constructor(validationResult: ValidationResult) {
|
|
22
|
+
this.validationResult = validationResult
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
validateNodeIds(obj: LionWebJsonChunk, ctx: JsonContext): void {
|
|
26
|
+
// put all nodes in a map, validate that there are no two nodes with the same id.
|
|
27
|
+
obj.nodes.forEach((node, index) => {
|
|
28
|
+
// this.validationResult.check(this.nodesIdMap.get(node.id) === undefined, `Node number ${index} has duplicate id "${node.id}"`);
|
|
29
|
+
if (!(this.nodesIdMap.get(node.id) === undefined)) {
|
|
30
|
+
this.validationResult.issue(new Reference_DuplicateNodeId_Issue(ctx.concat("nodes", index), node.id))
|
|
31
|
+
}
|
|
32
|
+
this.nodesIdMap.set(node.id, node)
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
validate(obj: LionWebJsonChunkWrapper): void {
|
|
37
|
+
const rootCtx = new JsonContext(null, ["$"])
|
|
38
|
+
this.checkDuplicateUsedLanguage(obj.jsonChunk.languages, rootCtx)
|
|
39
|
+
this.validateNodeIds(obj.jsonChunk, rootCtx)
|
|
40
|
+
obj.jsonChunk.nodes.forEach((node, nodeIndex) => {
|
|
41
|
+
const context = rootCtx.concat(`node`, nodeIndex)
|
|
42
|
+
const parentNode = node.parent
|
|
43
|
+
if (parentNode !== null) {
|
|
44
|
+
this.validateExistsAsChild(context, this.nodesIdMap.get(parentNode), node)
|
|
45
|
+
}
|
|
46
|
+
this.validateLanguageReference(obj, node.classifier, context)
|
|
47
|
+
this.checkParentCircular(node, context)
|
|
48
|
+
this.checkDuplicate(node.annotations, rootCtx.concat("node", nodeIndex, "annotations"))
|
|
49
|
+
this.validateChildrenHaveCorrectParent(node, rootCtx.concat("node", nodeIndex))
|
|
50
|
+
node.properties.forEach((prop, propertyIndex) => {
|
|
51
|
+
this.validateLanguageReference(obj, prop.property, rootCtx.concat("node", nodeIndex, "property", propertyIndex))
|
|
52
|
+
})
|
|
53
|
+
node.containments.forEach((containment, childIndex) => {
|
|
54
|
+
this.validateLanguageReference(obj, containment.containment, rootCtx.concat("node", nodeIndex, "containments", childIndex))
|
|
55
|
+
this.checkDuplicate(containment.children, rootCtx.concat("node", nodeIndex, "containments", childIndex))
|
|
56
|
+
containment.children.forEach(childId => {
|
|
57
|
+
const childNode = this.nodesIdMap.get(childId)
|
|
58
|
+
if (childNode !== undefined) {
|
|
59
|
+
if (childNode.parent !== null && childNode.parent !== undefined && childNode.parent !== node.id) {
|
|
60
|
+
this.validationResult.issue(new Reference_ParentMissingInChild_Issue(context, node, childNode))
|
|
61
|
+
// TODO this.validationResult.error(`Child "${childId}" with parent "${childNode.parent}" is defined as child in node "${node.id}"`);
|
|
62
|
+
}
|
|
63
|
+
if (childNode.parent === null || childNode.parent === undefined) {
|
|
64
|
+
// TODO this.validationResult.error(`Child "${childId}" of node "${node.id}" has different parent "${childNode.parent}"`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
node.annotations.forEach(annotationId => {
|
|
70
|
+
const annotation = this.nodesIdMap.get(annotationId)
|
|
71
|
+
if (annotation !== undefined) {
|
|
72
|
+
if (annotation.parent !== null && annotation.parent !== undefined && annotation.parent !== node.id) {
|
|
73
|
+
this.validationResult.issue(new Reference_ParentMissingInChild_Issue(context, node, annotation))
|
|
74
|
+
// TODO this.validationResult.error(`Child "${annotationId}" with parent "${childNode.parent}" is defined as child in node "${node.id}"`);
|
|
75
|
+
}
|
|
76
|
+
if (annotation.parent === null || annotation.parent === undefined) {
|
|
77
|
+
// TODO this.validationResult.error(`Child "${annotationId}" of node "${node.id}" has different parent "${childNode.parent}"`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
node.references.forEach((ref, refIndex) => {
|
|
82
|
+
this.validateLanguageReference(obj, ref.reference, rootCtx.concat("node", nodeIndex, "references", refIndex))
|
|
83
|
+
// TODO Check for duplicate targets?
|
|
84
|
+
// If so, what to check because there can be either or both a `resolveInfo` and a `reference`
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check whether the metapointer refers to a language defined in the usedLanguages of chunk.
|
|
91
|
+
* @param chunk
|
|
92
|
+
* @param metaPointer
|
|
93
|
+
* @param context
|
|
94
|
+
*/
|
|
95
|
+
validateLanguageReference(chunk: LionWebJsonChunkWrapper, metaPointer: LionWebJsonMetaPointer, context: JsonContext) {
|
|
96
|
+
const lang = ChunkUtils.findLwUsedLanguageWithVersion(chunk.jsonChunk, metaPointer.language, metaPointer.version)
|
|
97
|
+
if (lang === undefined || lang === null) {
|
|
98
|
+
this.validationResult.issue(new Reference_LanguageUnknown_Issue(context, metaPointer))
|
|
99
|
+
} else {
|
|
100
|
+
if (lang.version !== metaPointer.version) {
|
|
101
|
+
this.validationResult.issue(new Reference_LanguageUnknown_Issue(context, metaPointer))
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check whether there are duplicate values in `strings`.
|
|
108
|
+
* @param strings
|
|
109
|
+
* @param context
|
|
110
|
+
*/
|
|
111
|
+
checkDuplicate(strings: string[], context: JsonContext) {
|
|
112
|
+
if (strings === null || strings === undefined) {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
const alreadySeen: Record<string, boolean> = {}
|
|
116
|
+
strings.forEach(str => {
|
|
117
|
+
if (alreadySeen[str]) {
|
|
118
|
+
this.validationResult.issue(new Duplicates_Issue(context, str))
|
|
119
|
+
} else {
|
|
120
|
+
alreadySeen[str] = true
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Checks whether there are duplicate usedLanguages in `usedLanguages`.
|
|
127
|
+
* usedLanguages are considered equal when bith their `key` and `version` are identical.
|
|
128
|
+
* @param usedLanguages
|
|
129
|
+
* @param context
|
|
130
|
+
*/
|
|
131
|
+
checkDuplicateUsedLanguage(usedLanguages: LionWebJsonUsedLanguage[], context: JsonContext) {
|
|
132
|
+
if (usedLanguages === null || usedLanguages === undefined) {
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
const alreadySeen = new Map<string, string[]>()
|
|
136
|
+
usedLanguages.forEach((usedLanguage, index) => {
|
|
137
|
+
const seenKeys = alreadySeen.get(usedLanguage.key)
|
|
138
|
+
if (seenKeys !== null && seenKeys !== undefined) {
|
|
139
|
+
if (seenKeys.includes(usedLanguage.version)) {
|
|
140
|
+
this.validationResult.issue(new Duplicates_Issue(context.concat("language", index, "version"), usedLanguage.version))
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
alreadySeen.set(usedLanguage.key, [usedLanguage.version])
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Checks whether the parent of node recursively points to `node` itself.
|
|
150
|
+
* @param node The noide being checked
|
|
151
|
+
* @param context The location in the JSON
|
|
152
|
+
*/
|
|
153
|
+
checkParentCircular(node: LionWebJsonNode, context: JsonContext) {
|
|
154
|
+
if (node === null || node === undefined) {
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
if (node.parent === null || node.parent === undefined) {
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
let current: LionWebJsonNode | undefined = node
|
|
161
|
+
const seenParents = [node.id]
|
|
162
|
+
while (current !== null && current !== undefined && current.parent !== null && current.parent !== undefined) {
|
|
163
|
+
const nextParent = current.parent
|
|
164
|
+
if (nextParent !== null && nextParent !== undefined && seenParents.includes(nextParent)) {
|
|
165
|
+
this.validationResult.issue(
|
|
166
|
+
new Reference_CirculairParent_Issue(context.concat("???"), this.nodesIdMap.get(nextParent), seenParents),
|
|
167
|
+
)
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
seenParents.push(nextParent)
|
|
171
|
+
current = this.nodesIdMap.get(nextParent)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
validateExistsAsChild(context: JsonContext, parent: LionWebJsonNode | undefined, child: LionWebJsonNode) {
|
|
176
|
+
if (parent === undefined || parent === null) {
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
for (const containment of parent.containments) {
|
|
180
|
+
if (containment.children.includes(child.id)) {
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (parent.annotations.includes(child.id)) {
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
this.validationResult.issue(new Reference_ChildMissingInParent_Issue(context, child, parent))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
validateChildrenHaveCorrectParent(node: LionWebJsonNode, context: JsonContext) {
|
|
191
|
+
node.containments.forEach((child: LionWebJsonContainment) => {
|
|
192
|
+
child.children.forEach((childId: LionWebId, index: number) => {
|
|
193
|
+
const childNode = this.nodesIdMap.get(childId)
|
|
194
|
+
if (childNode !== undefined) {
|
|
195
|
+
if (childNode.parent !== node.id) {
|
|
196
|
+
// TODO Check that this is already tested from the child in vaidateExistsAsChild().
|
|
197
|
+
}
|
|
198
|
+
if (childNode.parent === null || childNode.parent === undefined) {
|
|
199
|
+
this.validationResult.issue(
|
|
200
|
+
new Reference_ParentMissingInChild_Issue(context.concat("child", "containment", "key", index), node, childNode),
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
node.annotations.forEach((annotationId: LionWebId, annotationIndex: number) => {
|
|
207
|
+
const childNode = this.nodesIdMap.get(annotationId)
|
|
208
|
+
if (childNode !== undefined) {
|
|
209
|
+
if (childNode.parent === null || childNode.parent === undefined) {
|
|
210
|
+
this.validationResult.issue(
|
|
211
|
+
new Reference_ParentMissingInChild_Issue(context.concat("annotations", annotationIndex), node, childNode),
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
// for (const childId of NodeUtils.allChildren(node)) {
|
|
217
|
+
// const childNode = this.nodesIdMap.get(childId);
|
|
218
|
+
// if (childNode !== undefined) {
|
|
219
|
+
// if (childNode.parent !== node.id) {
|
|
220
|
+
// this.validationResult.error(`QQ Parent of child "${childId}" is "${childNode.parent}", but should be "${node.id}" in ${context}`);
|
|
221
|
+
// }
|
|
222
|
+
// }
|
|
223
|
+
// }
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SyntaxValidator } from "./generic/SyntaxValidator.js"
|
|
2
|
+
import { ValidationResult } from "./generic/ValidationResult.js"
|
|
3
|
+
import { LionWebSchema } from "./LionWebChunkDefinitions.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* LionWebSyntaxValidator can check whether objects are structurally LionWeb objects.
|
|
7
|
+
*/
|
|
8
|
+
export class LionWebSyntaxValidator extends SyntaxValidator {
|
|
9
|
+
|
|
10
|
+
constructor(validationResult: ValidationResult) {
|
|
11
|
+
super(validationResult, LionWebSchema)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { LionWebJsonChunk } from "@lionweb/json"
|
|
2
|
+
import { LionWebJsonChunkWrapper } from "@lionweb/json-utils"
|
|
3
|
+
import { LanguageRegistry } from "../languages/index.js"
|
|
4
|
+
import { ValidationResult } from "./generic/ValidationResult.js"
|
|
5
|
+
import { LionWebLanguageReferenceValidator } from "./LionWebLanguageReferenceValidator.js"
|
|
6
|
+
import { LionWebReferenceValidator } from "./LionWebReferenceValidator.js"
|
|
7
|
+
import { LionWebSyntaxValidator } from "./LionWebSyntaxValidator.js"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Combined validator that calls all available validators.
|
|
11
|
+
* Will stop when one validator fails.
|
|
12
|
+
*/
|
|
13
|
+
export class LionWebValidator {
|
|
14
|
+
object: unknown
|
|
15
|
+
|
|
16
|
+
chunk: unknown
|
|
17
|
+
validationResult: ValidationResult
|
|
18
|
+
syntaxValidator: LionWebSyntaxValidator
|
|
19
|
+
referenceValidator: LionWebReferenceValidator
|
|
20
|
+
syntaxCorrect: boolean = false
|
|
21
|
+
referencesCorrect: boolean = false
|
|
22
|
+
|
|
23
|
+
constructor(json: unknown, private registry: LanguageRegistry) {
|
|
24
|
+
this.object = json
|
|
25
|
+
this.validationResult = new ValidationResult()
|
|
26
|
+
this.syntaxValidator = new LionWebSyntaxValidator(this.validationResult)
|
|
27
|
+
this.referenceValidator = new LionWebReferenceValidator(this.validationResult)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
validateAll() {
|
|
31
|
+
this.validateSyntax()
|
|
32
|
+
this.validateReferences()
|
|
33
|
+
this.validateForLanguage()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
validateSyntax() {
|
|
37
|
+
this.syntaxValidator.validate(this.object, "LionWebJsonChunk")
|
|
38
|
+
this.syntaxCorrect = !this.validationResult.hasErrors()
|
|
39
|
+
if (this.syntaxCorrect) {
|
|
40
|
+
this.chunk = new LionWebJsonChunkWrapper(this.object as LionWebJsonChunk)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
validateReferences(): void {
|
|
45
|
+
if (!this.syntaxCorrect) {
|
|
46
|
+
// console.log("validateReferences not executed because there are syntax errors.")
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
// when syntax is correct we know the chunk is actually a chunk!
|
|
50
|
+
this.referenceValidator.validate(this.chunk as LionWebJsonChunkWrapper)
|
|
51
|
+
this.referencesCorrect = !this.validationResult.hasErrors()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
validateForLanguage(): void {
|
|
55
|
+
if (!this.syntaxCorrect) {
|
|
56
|
+
// console.log("validateForLanguage not executed because there are syntax errors.")
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
if (!this.referencesCorrect) {
|
|
60
|
+
// console.log("validateForLanguage not executed because there are reference errors.")
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
const languageReferenceValidator = new LionWebLanguageReferenceValidator(this.validationResult, this.registry)
|
|
64
|
+
// when syntax is correct we know the chunk is actually a chunk!
|
|
65
|
+
languageReferenceValidator.validate(this.chunk as LionWebJsonChunkWrapper)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// setLanguage(json: LionwebLanguageDefinition) {
|
|
69
|
+
// this.language = json;
|
|
70
|
+
// }
|
|
71
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A list of functions that are used to validate primitive fields for LionWeb conformance.
|
|
3
|
+
* Used in the LionWebSyntaxValidator.
|
|
4
|
+
*/
|
|
5
|
+
import { JsonContext } from "@lionweb/json-utils"
|
|
6
|
+
import { asMinimalJsonString } from "@lionweb/ts-utils"
|
|
7
|
+
import { Language_PropertyValue_Issue } from "../issues/LanguageIssues.js"
|
|
8
|
+
import {
|
|
9
|
+
Syntax_IdFormat_Issue,
|
|
10
|
+
Syntax_KeyFormat_Issue,
|
|
11
|
+
Syntax_PropertyNullIssue,
|
|
12
|
+
Syntax_SerializationFormatVersion_Issue,
|
|
13
|
+
Syntax_VersionFormat_Issue
|
|
14
|
+
} from "../issues/SyntaxIssues.js"
|
|
15
|
+
import { ValidationResult } from "./generic/ValidationResult.js"
|
|
16
|
+
import { PropertyDefinition } from "./generic/schema/ValidationTypes.js"
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check whether `id` is a valid LionWeb id.
|
|
20
|
+
* @param value The `value` to be checked.
|
|
21
|
+
* @param result Any validation issues found will be put into this object.
|
|
22
|
+
* @param context The context for the error message in errors.
|
|
23
|
+
*/
|
|
24
|
+
export function validateId<String>(value: String, result: ValidationResult, context: JsonContext): void {
|
|
25
|
+
const idString: string = "" + value
|
|
26
|
+
const regexp = /^[a-zA-Z0-9_-][a-zA-Z0-9_-]*$/
|
|
27
|
+
if (!regexp.test(idString)) {
|
|
28
|
+
result.issue(new Syntax_IdFormat_Issue(context, idString))
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check whether `key` is a valid LionWeb key.
|
|
34
|
+
* @param value The `key` to be checked.
|
|
35
|
+
* @param result Any validation issues found will be put into this object.
|
|
36
|
+
* @param context The context for the error message in errors.
|
|
37
|
+
*/
|
|
38
|
+
export function validateKey<String>(value: String, result: ValidationResult, context: JsonContext): void {
|
|
39
|
+
const keyString: string = "" + value
|
|
40
|
+
const regexp = /^[a-zA-Z0-9_-][a-zA-Z0-9_-]*$/
|
|
41
|
+
if (!regexp.test(keyString)) {
|
|
42
|
+
result.issue(new Syntax_KeyFormat_Issue(context, keyString))
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check whether `version` is a valid LionWeb version.
|
|
48
|
+
* @param value The version to be checked
|
|
49
|
+
* @param result Any validation issues found will be put into this object.
|
|
50
|
+
* @param context The location in the overall JSON.
|
|
51
|
+
*/
|
|
52
|
+
export function validateVersion<String>(value: String, result: ValidationResult, context: JsonContext): void {
|
|
53
|
+
const versionString: string = "" + value
|
|
54
|
+
if (versionString.length === 0) {
|
|
55
|
+
result.issue(new Syntax_VersionFormat_Issue(context, versionString))
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check whether the string `value` represents a LionWeb boolean, its value should be "true" or "false".
|
|
61
|
+
* @param value The value to be checked
|
|
62
|
+
* @param result Any validation issues found will be put into this object.
|
|
63
|
+
* @param context The location in the overall JSON.
|
|
64
|
+
* @param propDef The PropertyDefinition for this value
|
|
65
|
+
*/
|
|
66
|
+
export function validateBoolean<String>(value: String, result: ValidationResult, context: JsonContext, propDef?: PropertyDefinition): void {
|
|
67
|
+
const valueAsPrimitive = "" + value
|
|
68
|
+
if (valueAsPrimitive !== "true" && valueAsPrimitive !== "false") {
|
|
69
|
+
result.issue(
|
|
70
|
+
new Language_PropertyValue_Issue(
|
|
71
|
+
context,
|
|
72
|
+
propDef ? propDef.name : "unknown",
|
|
73
|
+
valueAsPrimitive,
|
|
74
|
+
"boolean " + asMinimalJsonString(value)
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check whether the string `value` represents a LionWeb integer
|
|
82
|
+
* @param value The value to be checked
|
|
83
|
+
* @param result Any validation issues found will be put into this object.
|
|
84
|
+
* @param context The location in the overall JSON.
|
|
85
|
+
* @param propDef The PropertyDefinition for this value
|
|
86
|
+
*/
|
|
87
|
+
export function validateInteger<String>(value: String, result: ValidationResult, context: JsonContext, propDef?: PropertyDefinition): void {
|
|
88
|
+
const valueAsPrimitive = "" + value
|
|
89
|
+
const regexp = /^[+-]?(0|[1-9][0-9]*)$/
|
|
90
|
+
if (valueAsPrimitive === null || !regexp.test(valueAsPrimitive)) {
|
|
91
|
+
result.issue(new Language_PropertyValue_Issue(context, propDef ? propDef.name : "unknown", valueAsPrimitive, "integer"))
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check whether the string `value` represents a LionWeb Json.
|
|
97
|
+
* @param value The value to be checked
|
|
98
|
+
* @param result Any validation issues found will be put into this object.
|
|
99
|
+
* @param context The location in the overall JSON.
|
|
100
|
+
* @param propDef The PropertyDefinition for this value
|
|
101
|
+
*/
|
|
102
|
+
export function validateJSON<String>(value: String, result: ValidationResult, context: JsonContext, propDef?: PropertyDefinition): void {
|
|
103
|
+
const valueAsPrimitive = "" + value
|
|
104
|
+
if (value === null) {
|
|
105
|
+
result.issue(new Syntax_PropertyNullIssue(context, propDef!.name!))
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
JSON.parse(valueAsPrimitive)
|
|
109
|
+
} catch (_) {
|
|
110
|
+
result.issue(new Language_PropertyValue_Issue(context, propDef ? propDef.name : "unknown", valueAsPrimitive, "JSON"))
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check whether the string `value` is a correct LionWeb serializationFormatVersion.
|
|
116
|
+
* @param value
|
|
117
|
+
* @param result
|
|
118
|
+
* @param context
|
|
119
|
+
*/
|
|
120
|
+
export function validateSerializationFormatVersion<String>(value: String, result: ValidationResult, context: JsonContext): void {
|
|
121
|
+
if (typeof value !== "string") {
|
|
122
|
+
result.issue(new Syntax_SerializationFormatVersion_Issue(context, asMinimalJsonString(value)))
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
if (value.length === 0) {
|
|
126
|
+
result.issue(new Syntax_SerializationFormatVersion_Issue(context, value))
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { JsonContext } from "@lionweb/json-utils"
|
|
2
|
+
import {
|
|
3
|
+
Syntax_ArrayContainsNull_Issue,
|
|
4
|
+
Syntax_PropertyMissingIssue,
|
|
5
|
+
Syntax_PropertyNullIssue,
|
|
6
|
+
Syntax_PropertyTypeIssue,
|
|
7
|
+
Syntax_PropertyUnknownIssue
|
|
8
|
+
} from "../../issues/index.js"
|
|
9
|
+
import { ValidationResult } from "./ValidationResult.js"
|
|
10
|
+
import {
|
|
11
|
+
isObjectDefinition,
|
|
12
|
+
isPrimitiveDefinition, ObjectDefinition, PrimitiveDefinition, DefinitionSchema,
|
|
13
|
+
UnknownObjectType
|
|
14
|
+
} from "./schema/index.js"
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Syntax Validator checks whether objects are structurally conforming to the
|
|
18
|
+
* definitions given in `schema`.
|
|
19
|
+
*/
|
|
20
|
+
export class SyntaxValidator {
|
|
21
|
+
validationResult: ValidationResult
|
|
22
|
+
schema: DefinitionSchema
|
|
23
|
+
|
|
24
|
+
constructor(validationResult: ValidationResult, schema: DefinitionSchema) {
|
|
25
|
+
this.validationResult = validationResult
|
|
26
|
+
this.schema = schema
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check whether `obj` is a JSON object that conforms to the definition of `expectedType`.
|
|
31
|
+
* All errors found will be added to the `validationResult` object.
|
|
32
|
+
* @param obj The object to validate.
|
|
33
|
+
* @param expectedType The expected type of the object.
|
|
34
|
+
*/
|
|
35
|
+
validate(obj: unknown, expectedType: string) {
|
|
36
|
+
const object = obj as UnknownObjectType
|
|
37
|
+
const typeDef = this.schema.getDefinition(expectedType)
|
|
38
|
+
|
|
39
|
+
if (typeDef === undefined) {
|
|
40
|
+
throw new Error(`SyntaxValidator.validate: cannot find definition for ${expectedType}`)
|
|
41
|
+
} else if (isObjectDefinition(typeDef)) {
|
|
42
|
+
this.validateObjectProperties(expectedType, typeDef, object, new JsonContext(null, ["$"]))
|
|
43
|
+
} else if (isPrimitiveDefinition(typeDef)) {
|
|
44
|
+
this.validatePrimitiveValue("$", typeDef, object, new JsonContext(null, ["$"]))
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Validate whether `object` is structured conform the properties in `propertyDef`
|
|
50
|
+
* @param originalProperty The property of which `object` it the value
|
|
51
|
+
* @param typeDef The property definitions that are being validated
|
|
52
|
+
* @param object The object being validated
|
|
53
|
+
* @param jsonContext The location in the JSON
|
|
54
|
+
* @private
|
|
55
|
+
*/
|
|
56
|
+
validateObjectProperties(originalProperty: string, typeDef: ObjectDefinition, object: UnknownObjectType, jsonContext: JsonContext) {
|
|
57
|
+
if (typeDef === null || typeDef === undefined) {
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
if (typeof object !== "object") {
|
|
61
|
+
this.validationResult.issue(new Syntax_PropertyTypeIssue(jsonContext, originalProperty, "object", typeof object))
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
for (const propertyDef of typeDef.properties) {
|
|
65
|
+
const expectedTypeDef = this.schema.getDefinition(propertyDef.type)
|
|
66
|
+
const validator = propertyDef.validate!
|
|
67
|
+
const propertyValue = object[propertyDef.name]
|
|
68
|
+
if (propertyValue === undefined) {
|
|
69
|
+
if (!propertyDef.isOptional) {
|
|
70
|
+
this.validationResult.issue(new Syntax_PropertyMissingIssue(jsonContext, propertyDef.name))
|
|
71
|
+
}
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
if (!propertyDef.mayBeNull && propertyValue === null) {
|
|
75
|
+
this.validationResult.issue(new Syntax_PropertyNullIssue(jsonContext, propertyDef.name))
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
if (propertyDef.mayBeNull && propertyValue === null) {
|
|
79
|
+
// Ok, stop checking, continue with next property def
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
if (propertyDef.isList) {
|
|
83
|
+
// Check whether value is an array
|
|
84
|
+
if (!Array.isArray(propertyValue)) {
|
|
85
|
+
const newContext = jsonContext.concat(propertyDef.name)
|
|
86
|
+
this.validationResult.issue(new Syntax_PropertyTypeIssue(newContext, propertyDef.name, "array", typeof propertyValue))
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
// If an array, validate every item in the array
|
|
90
|
+
(propertyValue as UnknownObjectType[]).forEach((item, index) => {
|
|
91
|
+
const newContext = jsonContext.concat(propertyDef.name, index)
|
|
92
|
+
if (item === null) {
|
|
93
|
+
this.validationResult.issue(new Syntax_ArrayContainsNull_Issue(newContext, propertyDef.name, index))
|
|
94
|
+
} else {
|
|
95
|
+
if (expectedTypeDef !== undefined) {
|
|
96
|
+
if (isPrimitiveDefinition(expectedTypeDef)) {
|
|
97
|
+
if (this.validatePrimitiveValue(propertyDef.name, expectedTypeDef, item, jsonContext)) {
|
|
98
|
+
validator.apply(null, [item, this.validationResult, newContext, propertyDef])
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
// propertyValue should be an object, validate its properties
|
|
102
|
+
this.validateObjectProperties(propertyDef.name, expectedTypeDef, item as UnknownObjectType, newContext)
|
|
103
|
+
validator.apply(null, [item, this.validationResult, newContext, propertyDef])
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
throw new Error(`Expected type '${propertyDef.type} has neither property defs, nor a validator.`)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
} else {
|
|
111
|
+
const newContext = jsonContext.concat(propertyDef.name)
|
|
112
|
+
if (Array.isArray(propertyValue)) {
|
|
113
|
+
this.validationResult.issue(new Syntax_PropertyTypeIssue(newContext, propertyDef.name, propertyDef.type, "array"))
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
// Single valued property, validate it
|
|
117
|
+
if (expectedTypeDef !== undefined) {
|
|
118
|
+
if (isPrimitiveDefinition(expectedTypeDef)) {
|
|
119
|
+
// propertyValue should be a primitive as it has no property definitions
|
|
120
|
+
if (this.validatePrimitiveValue(propertyDef.name, expectedTypeDef, propertyValue, jsonContext)) {
|
|
121
|
+
validator.apply(null, [propertyValue, this.validationResult, newContext, propertyDef])
|
|
122
|
+
}
|
|
123
|
+
} else if (isObjectDefinition(expectedTypeDef)) {
|
|
124
|
+
// propertyValue should be an object, validate its properties
|
|
125
|
+
this.validateObjectProperties(propertyDef.name, expectedTypeDef, propertyValue as UnknownObjectType, newContext)
|
|
126
|
+
validator.apply(null, [propertyValue, this.validationResult, newContext, propertyDef])
|
|
127
|
+
} else {
|
|
128
|
+
throw new Error("EXPECTING ObjectDefinition or PrimitiveDefinition, but got something else")
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Expected single type '${propertyDef.type}' for '${propertyDef.name}' at ${newContext.toString()} has neither property defs, nor a validator.`
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
this.checkStrayProperties(object, typeDef, jsonContext)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
validatePrimitiveValue(propertyName: string, propDef: PrimitiveDefinition, object: unknown, jsonContext: JsonContext): boolean {
|
|
141
|
+
if (typeof object !== propDef.primitiveType) {
|
|
142
|
+
this.validationResult.issue(new Syntax_PropertyTypeIssue(jsonContext, propertyName, propDef.primitiveType, typeof object))
|
|
143
|
+
return false
|
|
144
|
+
}
|
|
145
|
+
propDef.validate!(object, this.validationResult, jsonContext)
|
|
146
|
+
return true
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check whether there are extra properties that should not be there.
|
|
151
|
+
* @param obj Object to be validated
|
|
152
|
+
* @param properties The names of the expected properties
|
|
153
|
+
* @param context Location in JSON
|
|
154
|
+
*/
|
|
155
|
+
checkStrayProperties(obj: UnknownObjectType, def: ObjectDefinition, context: JsonContext) {
|
|
156
|
+
const own = Object.getOwnPropertyNames(obj)
|
|
157
|
+
const defined = def.properties.map(pdef => pdef.name)
|
|
158
|
+
own.forEach(ownProp => {
|
|
159
|
+
if (!defined.includes(ownProp)) {
|
|
160
|
+
this.validationResult.issue(new Syntax_PropertyUnknownIssue(context, ownProp))
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ValidationIssue } from "../../issues/ValidationIssue.js"
|
|
2
|
+
|
|
3
|
+
export class ValidationResult {
|
|
4
|
+
issues: ValidationIssue[] = []
|
|
5
|
+
|
|
6
|
+
issue(issue: ValidationIssue) {
|
|
7
|
+
this.issues.push(issue)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
reset() {
|
|
11
|
+
this.issues = []
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
hasErrors(): boolean {
|
|
15
|
+
return this.issues.length !== 0
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Definition, PrimitiveDefinition, TaggedUnionDefinition } from "./ValidationTypes.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A collection of object and primitive definitions describing JSON objects.
|
|
5
|
+
* Used to
|
|
6
|
+
* - validate an incoming JSON object
|
|
7
|
+
* - generate the corresponding TypeScript type definitions
|
|
8
|
+
* - generate handlers for the JSOn objects (in @lionweb/server)
|
|
9
|
+
*/
|
|
10
|
+
export class DefinitionSchema {
|
|
11
|
+
unionDefinition: TaggedUnionDefinition | undefined
|
|
12
|
+
/**
|
|
13
|
+
* Mapping from extenden object type name to list of extending Object Definitions
|
|
14
|
+
*/
|
|
15
|
+
definitionsMap: Map<string, Definition> = new Map<string, Definition>()
|
|
16
|
+
|
|
17
|
+
constructor(definitions: Definition[], taggedUnion?: TaggedUnionDefinition) {
|
|
18
|
+
this.add(definitions)
|
|
19
|
+
this.unionDefinition = taggedUnion
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
getDefinition(name: string): Definition | undefined {
|
|
23
|
+
return this.definitionsMap.get(name)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
add(definitions :Definition[] | Definition) {
|
|
27
|
+
if (!Array.isArray(definitions)) {
|
|
28
|
+
definitions = [definitions]
|
|
29
|
+
}
|
|
30
|
+
for(const def of definitions) {
|
|
31
|
+
this.definitionsMap.set(def.name, def)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
isTagProperty(propertyName: string): boolean {
|
|
36
|
+
return this.unionDefinition?.unionProperty === propertyName
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
definitions(): Definition[] {
|
|
40
|
+
return Array.from(this.definitionsMap.values())
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
isUnionDiscriminator(propDef: PrimitiveDefinition): boolean {
|
|
44
|
+
return this.unionDefinition?.unionDiscriminator === propDef.name
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
joinDefinitions(...schema: DefinitionSchema[]): void {
|
|
48
|
+
schema.forEach(sch => {
|
|
49
|
+
this.add(sch.definitions())
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
}
|