@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.
Files changed (38) hide show
  1. package/CHANGELOG.md +0 -1
  2. package/package.json +4 -4
  3. package/src/index.ts +4 -0
  4. package/src/issues/LanguageIssues.ts +147 -0
  5. package/src/issues/ReferenceIssues.ts +83 -0
  6. package/src/issues/SyntaxIssues.ts +84 -0
  7. package/src/issues/ValidationIssue.ts +29 -0
  8. package/src/issues/index.ts +4 -0
  9. package/src/languages/CompositeLionWebLanguageWrapper.ts +57 -0
  10. package/src/languages/LanguageRegistry.ts +44 -0
  11. package/src/languages/LanguageUtils.ts +63 -0
  12. package/src/languages/LionCore-M3.json +2356 -0
  13. package/src/languages/LionCore-builtins.json +372 -0
  14. package/src/languages/LionWebLanguageWrapper.ts +91 -0
  15. package/src/languages/MetaPointerMap.ts +41 -0
  16. package/src/languages/index.ts +2 -0
  17. package/src/runners/FileUtils.ts +59 -0
  18. package/src/runners/RunCheckFolder.ts +7 -0
  19. package/src/runners/RunCheckFolderWithLanguage.ts +45 -0
  20. package/src/runners/RunCheckOneFile.ts +7 -0
  21. package/src/runners/RunCheckOneFileWithLanguage.ts +35 -0
  22. package/src/runners/RunLioncoreDiff.ts +23 -0
  23. package/src/runners/Utils.ts +54 -0
  24. package/src/runners/index.ts +2 -0
  25. package/src/validators/LionWebChunkDefinitions.ts +104 -0
  26. package/src/validators/LionWebLanguageReferenceValidator.ts +201 -0
  27. package/src/validators/LionWebLanguageValidator.ts +79 -0
  28. package/src/validators/LionWebReferenceValidator.ts +225 -0
  29. package/src/validators/LionWebSyntaxValidator.ts +14 -0
  30. package/src/validators/LionWebValidator.ts +71 -0
  31. package/src/validators/ValidationFunctions.ts +129 -0
  32. package/src/validators/generic/SyntaxValidator.ts +164 -0
  33. package/src/validators/generic/ValidationResult.ts +17 -0
  34. package/src/validators/generic/index.ts +3 -0
  35. package/src/validators/generic/schema/DefinitionSchema.ts +52 -0
  36. package/src/validators/generic/schema/ValidationTypes.ts +134 -0
  37. package/src/validators/generic/schema/index.ts +2 -0
  38. 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,3 @@
1
+ export * from "./schema/index.js"
2
+ export * from "./ValidationResult.js"
3
+ export * from "./SyntaxValidator.js"
@@ -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
+ }