@lionweb/validation 0.5.0-beta.8
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/LICENSE +201 -0
- package/README.md +31 -0
- package/build.sh +6 -0
- package/package.json +28 -0
- package/src/diff/LionwebDiff.ts +193 -0
- package/src/issues/LanguageIssues.ts +91 -0
- package/src/issues/ReferenceIssues.ts +60 -0
- package/src/issues/SyntaxIssues.ts +68 -0
- package/src/issues/ValidationIssue.ts +46 -0
- package/src/json/ChunkUtils.ts +53 -0
- package/src/json/LanguageUtils.ts +12 -0
- package/src/json/LionWebJson.ts +77 -0
- package/src/json/LionWebJsonChunkWrapper.ts +74 -0
- package/src/json/LionWebLanguageDefinition.ts +141 -0
- package/src/json/NodeUtils.ts +72 -0
- package/src/json/std-builtins-copy.json +369 -0
- package/src/runners/RunCheckFolder.ts +36 -0
- package/src/runners/RunCheckFolderWithLanguage.ts +44 -0
- package/src/runners/RunCheckOneFile.ts +16 -0
- package/src/runners/RunCheckOneFileWithLanguage.ts +28 -0
- package/src/runners/RunLioncoreDiff.ts +23 -0
- package/src/runners/Utils.ts +21 -0
- package/src/validators/LionWebLanguageReferenceValidator.ts +135 -0
- package/src/validators/LionWebLanguageValidator.ts +40 -0
- package/src/validators/LionWebReferenceValidator.ts +213 -0
- package/src/validators/LionWebSyntaxValidator.ts +297 -0
- package/src/validators/LionWebValidator.ts +76 -0
- package/src/validators/SimpleFieldValidator.ts +90 -0
- package/src/validators/ValidationResult.ts +18 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { IncorrectLionCoreVersion_Issue, NotLionCoreLanguageKey_Issue, NumberOfLanguagesUsed_Issue } from "../issues/LanguageIssues";
|
|
2
|
+
import { JsonContext } from "../issues/ValidationIssue";
|
|
3
|
+
import { LionWebJsonChunk } from "../json/LionWebJson";
|
|
4
|
+
import { LionWebJsonChunkWrapper } from "../json/LionWebJsonChunkWrapper";
|
|
5
|
+
import { ValidationResult } from "./ValidationResult";
|
|
6
|
+
|
|
7
|
+
export class LionWebLanguageValidator {
|
|
8
|
+
validationResult: ValidationResult;
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
this.validationResult = new ValidationResult();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* check JSON as in `check`, but also check whether the metamoddel is a Language.
|
|
16
|
+
* @param obj
|
|
17
|
+
*/
|
|
18
|
+
checkLanguage(chunk: LionWebJsonChunk) {
|
|
19
|
+
// TODO Vaalidate syntax first via LionWebSyntaxValidator !!!
|
|
20
|
+
if (chunk.languages.length !== 1) {
|
|
21
|
+
// Check whether the LionCore M3 is the only used language, then this is a language model.
|
|
22
|
+
this.validationResult.issue(new NumberOfLanguagesUsed_Issue(new JsonContext(null, ["languages"]), chunk.languages.length))
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
const usedLanguage = chunk.languages[0];
|
|
26
|
+
if (usedLanguage.key !== "LionCore-M3") {
|
|
27
|
+
this.validationResult.issue(new NotLionCoreLanguageKey_Issue(new JsonContext(null, ["languages[0]"]), usedLanguage.key))
|
|
28
|
+
}
|
|
29
|
+
if (usedLanguage.version !== "1") {
|
|
30
|
+
this.validationResult.issue(new IncorrectLionCoreVersion_Issue(new JsonContext(null, ["languages[0]"]), usedLanguage.version))
|
|
31
|
+
}
|
|
32
|
+
const chunkWrapper = new LionWebJsonChunkWrapper(chunk);
|
|
33
|
+
const languageNodes = chunkWrapper.findNodesOfConcept("Language");
|
|
34
|
+
if (languageNodes.length !== 1) {
|
|
35
|
+
// TODO Better error handling.
|
|
36
|
+
console.error("Expected exactly one Language node, found " + languageNodes.length + " => " + JSON.stringify(languageNodes));
|
|
37
|
+
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Duplicates_Issue,
|
|
3
|
+
Reference_ChildMissingInParent_Issue,
|
|
4
|
+
Reference_CirculairParent_Issue,
|
|
5
|
+
Reference_DuplicateNodeId_Issue, Reference_LanguageUnknown_Issue,
|
|
6
|
+
Reference_ParentMissingInChild_Issue
|
|
7
|
+
} from "../issues/ReferenceIssues";
|
|
8
|
+
import { JsonContext } from "../issues/ValidationIssue";
|
|
9
|
+
import { ChunkUtils } from "../json/ChunkUtils";
|
|
10
|
+
import { LION_CORE_BUILTINS_KEY, LionWebJsonChild, LionWebJsonChunk, LionWebJsonMetaPointer, LionWebJsonNode, LwJsonUsedLanguage } from "../json/LionWebJson";
|
|
11
|
+
import { LionWebJsonChunkWrapper } from "../json/LionWebJsonChunkWrapper";
|
|
12
|
+
import { SimpleFieldValidator } from "./SimpleFieldValidator";
|
|
13
|
+
import { ValidationResult } from "./ValidationResult";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Assuming that the syntax is correct, check whether all LionWeb references are correct,
|
|
17
|
+
* as far as they do not need the used language definition.
|
|
18
|
+
*/
|
|
19
|
+
export class LionWebReferenceValidator {
|
|
20
|
+
validationResult: ValidationResult;
|
|
21
|
+
nodesIdMap: Map<string, LionWebJsonNode> = new Map<string, LionWebJsonNode>();
|
|
22
|
+
simpleFieldValidator: SimpleFieldValidator;
|
|
23
|
+
|
|
24
|
+
constructor(validationResult: ValidationResult) {
|
|
25
|
+
this.validationResult = validationResult;
|
|
26
|
+
this.simpleFieldValidator = new SimpleFieldValidator(this.validationResult);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
validateNodeIds(obj: LionWebJsonChunk, ctx: JsonContext): void {
|
|
30
|
+
// put all nodes in a map, validate that there are no two nodes with the same id.
|
|
31
|
+
obj.nodes.forEach((node, index) => {
|
|
32
|
+
// this.validationResult.check(this.nodesIdMap.get(node.id) === undefined, `Node number ${index} has duplicate id "${node.id}"`);
|
|
33
|
+
if (! (this.nodesIdMap.get(node.id) === undefined)) {
|
|
34
|
+
this.validationResult.issue(new Reference_DuplicateNodeId_Issue(ctx.concat("nodes", index), node.id));
|
|
35
|
+
}
|
|
36
|
+
this.nodesIdMap.set(node.id, node);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
validate(obj: LionWebJsonChunkWrapper): void {
|
|
41
|
+
const rootCtx = new JsonContext(null, ["$"]);
|
|
42
|
+
this.checkDuplicateUsedLanguage(obj.jsonChunk.languages, rootCtx);
|
|
43
|
+
this.validateNodeIds(obj.jsonChunk, rootCtx);
|
|
44
|
+
obj.jsonChunk.nodes.forEach((node, nodeIndex) => {
|
|
45
|
+
const context = rootCtx.concat(`node`, nodeIndex);
|
|
46
|
+
const parentNode = node.parent;
|
|
47
|
+
if (parentNode !== null) {
|
|
48
|
+
this.validateExistsAsChild(context, this.nodesIdMap.get(parentNode), node);
|
|
49
|
+
}
|
|
50
|
+
this.validateLanguageReference(obj, node.classifier, context);
|
|
51
|
+
this.checkParentCircular(node, context);
|
|
52
|
+
this.checkDuplicate(node.annotations, rootCtx.concat("node", nodeIndex, "annotations"));
|
|
53
|
+
this.validateChildrenHaveCorrectParent(node, rootCtx.concat("node", nodeIndex));
|
|
54
|
+
node.properties.forEach((prop, propertyIndex) => {
|
|
55
|
+
this.validateLanguageReference(obj, prop.property, rootCtx.concat("node", nodeIndex, "property", propertyIndex));
|
|
56
|
+
});
|
|
57
|
+
node.containments.forEach((containment, childIndex) => {
|
|
58
|
+
this.validateLanguageReference(obj, containment.containment, rootCtx.concat("node", nodeIndex, "containments", childIndex));
|
|
59
|
+
this.checkDuplicate(containment.children, rootCtx.concat("node", nodeIndex, "containments", childIndex));
|
|
60
|
+
containment.children.forEach((childId) => {
|
|
61
|
+
const childNode = this.nodesIdMap.get(childId);
|
|
62
|
+
if (childNode !== undefined) {
|
|
63
|
+
if (childNode.parent !== null && childNode.parent !== undefined && childNode.parent !== node.id) {
|
|
64
|
+
// TODO this.validationResult.error(`Child "${childId}" with parent "${childNode.parent}" is defined as child in node "${node.id}"`);
|
|
65
|
+
}
|
|
66
|
+
if (childNode.parent === null || childNode.parent === undefined) {
|
|
67
|
+
// TODO this.validationResult.error(`Child "${childId}" of node "${node.id}" has different parent "${childNode.parent}"`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
node.references.forEach((ref, refIndex) => {
|
|
73
|
+
this.validateLanguageReference(obj, ref.reference, rootCtx.concat("node", nodeIndex, "references", refIndex));
|
|
74
|
+
// TODO Check for duplicate targets?
|
|
75
|
+
// If so, what to check because there can be either or both a `resolveInfo` and a `reference`
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check whether the metapointer refers to a language defined in the usedLanguages of chunk.
|
|
82
|
+
* @param chunk
|
|
83
|
+
* @param metaPointer
|
|
84
|
+
* @param context
|
|
85
|
+
*/
|
|
86
|
+
validateLanguageReference(chunk: LionWebJsonChunkWrapper, metaPointer: LionWebJsonMetaPointer, context: JsonContext) {
|
|
87
|
+
const lang = ChunkUtils.findLwUsedLanguageWithVersion(chunk.jsonChunk, metaPointer.language, metaPointer.version);
|
|
88
|
+
if (metaPointer.language === LION_CORE_BUILTINS_KEY) {
|
|
89
|
+
// Ok, builtin
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (lang === undefined || lang === null) {
|
|
93
|
+
this.validationResult.issue(new Reference_LanguageUnknown_Issue(context, metaPointer))
|
|
94
|
+
} else {
|
|
95
|
+
if (lang.version !== metaPointer.version) {
|
|
96
|
+
this.validationResult.issue(new Reference_LanguageUnknown_Issue(context, metaPointer))
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check whether there are duplicate values in `strings`.
|
|
103
|
+
* @param strings
|
|
104
|
+
* @param context
|
|
105
|
+
*/
|
|
106
|
+
checkDuplicate(strings: string[], context: JsonContext) {
|
|
107
|
+
if (strings === null || strings === undefined) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const alreadySeen: Record<string, boolean> = {};
|
|
111
|
+
strings.forEach((str) => {
|
|
112
|
+
if (alreadySeen[str]) {
|
|
113
|
+
this.validationResult.issue(new Duplicates_Issue(context, str))
|
|
114
|
+
} else {
|
|
115
|
+
alreadySeen[str] = true;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Checks whether there are duplicate usedLanguages in `usedLanguages`.
|
|
122
|
+
* usedLanguages are considered equal when bith their `key` and `version` are identical.
|
|
123
|
+
* @param usedLanguages
|
|
124
|
+
* @param context
|
|
125
|
+
*/
|
|
126
|
+
checkDuplicateUsedLanguage(usedLanguages: LwJsonUsedLanguage[], context: JsonContext) {
|
|
127
|
+
if (usedLanguages === null || usedLanguages === undefined) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const alreadySeen = new Map<string, string[]>();
|
|
131
|
+
usedLanguages.forEach((usedLanguage, index) => {
|
|
132
|
+
const seenKeys = alreadySeen.get(usedLanguage.key);
|
|
133
|
+
if (seenKeys !== null && seenKeys !== undefined) {
|
|
134
|
+
if (seenKeys.includes(usedLanguage.version)) {
|
|
135
|
+
this.validationResult.issue(new Duplicates_Issue(context.concat("language", index, "version"), usedLanguage.version));
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
alreadySeen.set(usedLanguage.key, [usedLanguage.version]);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Checks whether the parent of node recursively points to `node` itself.
|
|
145
|
+
* @param node
|
|
146
|
+
*/
|
|
147
|
+
checkParentCircular(node: LionWebJsonNode, context: JsonContext) {
|
|
148
|
+
if (node === null || node === undefined) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (node.parent === null || node.parent === undefined) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
let current: LionWebJsonNode | undefined = node;
|
|
155
|
+
const seenParents = [node.id];
|
|
156
|
+
while (current !== null && current !== undefined && current.parent !== null && current.parent !== undefined) {
|
|
157
|
+
const nextParent = current.parent;
|
|
158
|
+
if (nextParent !== null && nextParent !== undefined && seenParents.includes(nextParent)) {
|
|
159
|
+
this.validationResult.issue(new Reference_CirculairParent_Issue(context.concat("???"), this.nodesIdMap.get(nextParent), seenParents));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
seenParents.push(nextParent);
|
|
163
|
+
current = this.nodesIdMap.get(nextParent);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
validateExistsAsChild(context: JsonContext, parent: LionWebJsonNode | undefined, child: LionWebJsonNode) {
|
|
168
|
+
if (parent === undefined || parent === null) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
for (const containment of parent.containments) {
|
|
172
|
+
if (containment.children.includes(child.id)) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (parent.annotations.includes(child.id)) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
this.validationResult.issue(new Reference_ChildMissingInParent_Issue(context, child, parent));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
validateChildrenHaveCorrectParent(node: LionWebJsonNode, context: JsonContext) {
|
|
183
|
+
node.containments.forEach((child: LionWebJsonChild) => {
|
|
184
|
+
child.children.forEach((childId: string, index: number) => {
|
|
185
|
+
const childNode = this.nodesIdMap.get(childId);
|
|
186
|
+
if (childNode !== undefined) {
|
|
187
|
+
if (childNode.parent !== node.id) {
|
|
188
|
+
// TODO Check that this is already tested from the child in vaidateExistsAsChild().
|
|
189
|
+
}
|
|
190
|
+
if (childNode.parent === null || childNode.parent === undefined) {
|
|
191
|
+
this.validationResult.issue(new Reference_ParentMissingInChild_Issue(context.concat("child", "containment", "key", index), node, childNode));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
node.annotations.forEach((annotationId: string, annotationIndex: number) => {
|
|
197
|
+
const childNode = this.nodesIdMap.get(annotationId);
|
|
198
|
+
if (childNode !== undefined) {
|
|
199
|
+
if (childNode.parent === null || childNode.parent === undefined) {
|
|
200
|
+
this.validationResult.issue(new Reference_ParentMissingInChild_Issue(context.concat("annotations", annotationIndex), node, childNode));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
// for (const childId of NodeUtils.allChildren(node)) {
|
|
205
|
+
// const childNode = this.nodesIdMap.get(childId);
|
|
206
|
+
// if (childNode !== undefined) {
|
|
207
|
+
// if (childNode.parent !== node.id) {
|
|
208
|
+
// this.validationResult.error(`QQ Parent of child "${childId}" is "${childNode.parent}", but should be "${node.id}" in ${context}`);
|
|
209
|
+
// }
|
|
210
|
+
// }
|
|
211
|
+
// }
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Syntax_ArrayContainsNull_Issue,
|
|
3
|
+
Syntax_PropertyMissingIssue,
|
|
4
|
+
Syntax_PropertyNullIssue,
|
|
5
|
+
Syntax_PropertyTypeIssue,
|
|
6
|
+
Syntax_PropertyUnknownIssue
|
|
7
|
+
} from "../issues/SyntaxIssues";
|
|
8
|
+
import { SimpleFieldValidator, ValidatorFunction } from "./SimpleFieldValidator";
|
|
9
|
+
import { JsonContext } from "../issues/ValidationIssue";
|
|
10
|
+
import { ValidationResult } from "./ValidationResult";
|
|
11
|
+
|
|
12
|
+
export type UnknownObjectType = { [key: string]: unknown };
|
|
13
|
+
|
|
14
|
+
export type PropertyType =
|
|
15
|
+
"string"
|
|
16
|
+
| "number"
|
|
17
|
+
| "bigint"
|
|
18
|
+
| "boolean"
|
|
19
|
+
| "symbol"
|
|
20
|
+
| "undefined"
|
|
21
|
+
| "object"
|
|
22
|
+
| "function"
|
|
23
|
+
| "array";
|
|
24
|
+
|
|
25
|
+
export type PropertyDefinition = {
|
|
26
|
+
/**
|
|
27
|
+
* The property name
|
|
28
|
+
*/
|
|
29
|
+
property: string;
|
|
30
|
+
/**
|
|
31
|
+
* The expected type of the property value
|
|
32
|
+
*/
|
|
33
|
+
expectedType: PropertyType;
|
|
34
|
+
/**
|
|
35
|
+
* Whether the property value is allowed to be null
|
|
36
|
+
*/
|
|
37
|
+
mayBeNull: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* If the property type is correct, check its value further with this function.
|
|
40
|
+
* Will, only be called if `this.recursive === true`.
|
|
41
|
+
* If the property value is an Array, the `checkValue` will be called on each element in the array.
|
|
42
|
+
* @param obj
|
|
43
|
+
* @param ctx
|
|
44
|
+
*/
|
|
45
|
+
validateValue?: ValidatorFunction;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Make boolean argument more readable.
|
|
49
|
+
const MAY_BE_NULL = true;
|
|
50
|
+
const NOT_NULL = false;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* LionWebCheck can chack whether objects are LionWeb JSON objects.
|
|
54
|
+
* The check can be on a single object, or recursively on an object and its children.
|
|
55
|
+
*/
|
|
56
|
+
export class LionWebSyntaxValidator {
|
|
57
|
+
validationResult: ValidationResult;
|
|
58
|
+
simpleFieldValidator: SimpleFieldValidator;
|
|
59
|
+
/**
|
|
60
|
+
* When true, each function will work recursively on the given object.
|
|
61
|
+
* When false, will only check the given object.
|
|
62
|
+
* Metapointers are always checked as part of the object, disregarding the va;lue of `recursive`.
|
|
63
|
+
*/
|
|
64
|
+
recursive: boolean = true;
|
|
65
|
+
|
|
66
|
+
constructor(validationResult: ValidationResult) {
|
|
67
|
+
this.validationResult = validationResult;
|
|
68
|
+
this.simpleFieldValidator = new SimpleFieldValidator(this.validationResult);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check whether `obj` is a JSON object that conforms to the serialization syntax of LionCore.
|
|
73
|
+
* All errors found will be pushed into the `errors` array, if its length is not 0, the check has failed.
|
|
74
|
+
* @param obj
|
|
75
|
+
*/
|
|
76
|
+
validate(obj: unknown) {
|
|
77
|
+
this.validateLwChunk(obj, new JsonContext(null, ["$"]));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
validateLwChunk = (obj: unknown, ctx: JsonContext): void => {
|
|
81
|
+
const expected: PropertyDefinition[] = [
|
|
82
|
+
{ property: "serializationFormatVersion", expectedType: "string", mayBeNull: NOT_NULL, validateValue: this.simpleFieldValidator.validateSerializationFormatVersion },
|
|
83
|
+
{ property: "languages", expectedType: "array", mayBeNull: NOT_NULL, validateValue: this.validateLwUsedLanguage },
|
|
84
|
+
{ property: "nodes", expectedType: "array", mayBeNull: NOT_NULL, validateValue: this.validateLwNode },
|
|
85
|
+
];
|
|
86
|
+
this.propertyChecks(obj, expected, ctx);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
validateLwUsedLanguage = (obj: unknown, ctx: JsonContext): void => {
|
|
90
|
+
const expected: PropertyDefinition[] = [
|
|
91
|
+
{ property: "key", expectedType: "string", mayBeNull: NOT_NULL, validateValue: this.simpleFieldValidator.validateKey },
|
|
92
|
+
{ property: "version", expectedType: "string", mayBeNull: NOT_NULL, validateValue: this.simpleFieldValidator.validateVersion }
|
|
93
|
+
];
|
|
94
|
+
this.propertyChecks(obj, expected, ctx);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
validateLwNode = (obj: unknown, ctx: JsonContext): void => {
|
|
98
|
+
const expected: PropertyDefinition[] = [
|
|
99
|
+
{ property: "id", expectedType: "string", mayBeNull: NOT_NULL, validateValue: this.simpleFieldValidator.validateId },
|
|
100
|
+
{ property: "classifier", expectedType: "object", mayBeNull: NOT_NULL, validateValue: this.validateLwMetaPointer },
|
|
101
|
+
{ property: "properties", expectedType: "array", mayBeNull: NOT_NULL, validateValue: this.validateLwProperty },
|
|
102
|
+
{ property: "containments", expectedType: "array", mayBeNull: NOT_NULL, validateValue: this.validateLwChild },
|
|
103
|
+
{ property: "references", expectedType: "array", mayBeNull: NOT_NULL, validateValue: this.validateLwReference },
|
|
104
|
+
{ property: "annotations", expectedType: "array", mayBeNull: NOT_NULL, validateValue: this.validateLwAnnotation },
|
|
105
|
+
{ property: "parent", expectedType: "string", mayBeNull: MAY_BE_NULL, validateValue: this.simpleFieldValidator.validateId },
|
|
106
|
+
];
|
|
107
|
+
this.propertyChecks(obj, expected, ctx);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
validateLwAnnotation = (obj: unknown, context: JsonContext) => {
|
|
111
|
+
if (this.checkType(obj, "string", context)) {
|
|
112
|
+
this.simpleFieldValidator.validateId(obj as string, context);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
validateLwProperty = (obj: unknown, ctx: JsonContext): void => {
|
|
117
|
+
const expected: PropertyDefinition[] = [
|
|
118
|
+
{ property: "property", expectedType: "object", mayBeNull: NOT_NULL, validateValue: this.validateLwMetaPointer },
|
|
119
|
+
{ property: "value", expectedType: "string", mayBeNull: MAY_BE_NULL },
|
|
120
|
+
];
|
|
121
|
+
this.propertyChecks(obj, expected, ctx);
|
|
122
|
+
// TODO: hack for keys in M2 models
|
|
123
|
+
// if ((obj as any)["property"].key === "IKeyed-key") {
|
|
124
|
+
// // console.log("CHECKING KEY");
|
|
125
|
+
// this.simpleFieldValidator.validateKey((obj as any)["value"], ctx);
|
|
126
|
+
// }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
validateLwMetaPointer = (obj: unknown, ctx: JsonContext): void => {
|
|
130
|
+
const expected: PropertyDefinition[] = [
|
|
131
|
+
{ property: "key", expectedType: "string", mayBeNull: NOT_NULL, validateValue: this.simpleFieldValidator.validateKey },
|
|
132
|
+
{ property: "version", expectedType: "string", mayBeNull: MAY_BE_NULL, validateValue: this.simpleFieldValidator.validateVersion },
|
|
133
|
+
{ property: "language", expectedType: "string", mayBeNull: MAY_BE_NULL, validateValue: this.simpleFieldValidator.validateKey },
|
|
134
|
+
];
|
|
135
|
+
this.propertyChecks(obj, expected, ctx);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
validateLwChild = (obj: unknown, ctx: JsonContext): void => {
|
|
139
|
+
const expected: PropertyDefinition[] = [
|
|
140
|
+
{ property: "containment", expectedType: "object", mayBeNull: NOT_NULL, validateValue: this.validateLwMetaPointer },
|
|
141
|
+
{ property: "children", expectedType: "array", mayBeNull: NOT_NULL, validateValue: this.checkChild }
|
|
142
|
+
];
|
|
143
|
+
this.propertyChecks(obj, expected, ctx);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private checkChild = (obj: unknown, context: JsonContext) => {
|
|
147
|
+
if (this.checkType(obj, "string", context)) {
|
|
148
|
+
this.simpleFieldValidator.validateId(obj as string, context);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Checks whether `obj` is not null or defined and has the correct type.
|
|
153
|
+
*/
|
|
154
|
+
private checkType = (obj: unknown, expectedType: PropertyType, context: JsonContext): boolean => {
|
|
155
|
+
if (obj === null || obj === undefined) {
|
|
156
|
+
this.validationResult.issue(new Syntax_PropertyTypeIssue(context, "obj", expectedType, typeof obj));
|
|
157
|
+
return false;
|
|
158
|
+
} else if (typeof obj !== expectedType) {
|
|
159
|
+
// TODO Better context: where does obj come from
|
|
160
|
+
this.validationResult.issue(new Syntax_PropertyTypeIssue(context, "obj", expectedType, typeof obj));
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
validateLwReference = (obj: unknown, ctx: JsonContext): void => {
|
|
167
|
+
const expected: PropertyDefinition[] = [
|
|
168
|
+
{ property: "reference", expectedType: "object", mayBeNull: NOT_NULL, validateValue: this.validateLwMetaPointer },
|
|
169
|
+
{ property: "targets", expectedType: "array", mayBeNull: NOT_NULL, validateValue: this.validateLwReferenceTarget }
|
|
170
|
+
];
|
|
171
|
+
this.propertyChecks(obj, expected, ctx);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
validateLwReferenceTarget = (obj: unknown, ctx: JsonContext): void => {
|
|
175
|
+
const expected: PropertyDefinition[] = [
|
|
176
|
+
{ property: "resolveInfo", expectedType: "string", mayBeNull: MAY_BE_NULL },
|
|
177
|
+
{ property: "reference", expectedType: "string", mayBeNull: MAY_BE_NULL, validateValue: this.simpleFieldValidator.validateId }
|
|
178
|
+
];
|
|
179
|
+
this.propertyChecks(obj, expected, ctx);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Check whether all property definitions in `propDef` are correct and check that there are
|
|
184
|
+
* no iother properties in `obj`.
|
|
185
|
+
* @param obj
|
|
186
|
+
* @param propDefs
|
|
187
|
+
* @param context
|
|
188
|
+
*/
|
|
189
|
+
propertyChecks(obj: unknown, propDefs: PropertyDefinition[], context: JsonContext): void {
|
|
190
|
+
if (!this.checkType(obj, "object", context)) {
|
|
191
|
+
// console.log("UNEXPECTED NULL OBJECT");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const object = obj as UnknownObjectType;
|
|
195
|
+
const allProperties: string[] = [];
|
|
196
|
+
propDefs.forEach( (propDef) => {
|
|
197
|
+
if (propDef.property === "key") {
|
|
198
|
+
// console.log("CHECKING KEY of " + JSON.stringify(obj))
|
|
199
|
+
}
|
|
200
|
+
if (this.checkPropertyType(object, propDef.property, propDef.expectedType, propDef.mayBeNull, context.concat(propDef.property))) {
|
|
201
|
+
const propValue = object[propDef.property];
|
|
202
|
+
if (this.recursive && propDef.expectedType === "array" && Array.isArray(propValue) && !!propDef.validateValue) {
|
|
203
|
+
propValue.forEach((arrayItem: unknown, index: number) => {
|
|
204
|
+
if (arrayItem === null) {
|
|
205
|
+
this.validationResult.issue(new Syntax_ArrayContainsNull_Issue(context.concat(propDef.property, index), propDef.property, index));
|
|
206
|
+
} else {
|
|
207
|
+
if (propDef.validateValue !== null && propDef.validateValue !== undefined ) {
|
|
208
|
+
propDef.validateValue(arrayItem, context.concat(propDef.property, index));
|
|
209
|
+
} else {
|
|
210
|
+
// TODO: give an error, whih ine?
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
} else if (propDef.validateValue !== null && propDef.validateValue !== undefined) {
|
|
215
|
+
// propValue is niot an array, so it should be aa string
|
|
216
|
+
propDef.validateValue(propValue as string, context.concat(propDef.property));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
allProperties.push(propDef.property);
|
|
220
|
+
});
|
|
221
|
+
this.checkStrayProperties(object, allProperties, context);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Check whether there are extra properties that should not be there.
|
|
226
|
+
* @param obj
|
|
227
|
+
* @param properties
|
|
228
|
+
* @param context
|
|
229
|
+
*/
|
|
230
|
+
checkStrayProperties(obj: UnknownObjectType, properties: string[], context: JsonContext) {
|
|
231
|
+
|
|
232
|
+
const own = Object.getOwnPropertyNames(obj);
|
|
233
|
+
own.forEach((ownProp) => {
|
|
234
|
+
if (!properties.includes(ownProp)) {
|
|
235
|
+
this.validationResult.issue(new Syntax_PropertyUnknownIssue(context, ownProp));
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
properties.forEach((prop) => {
|
|
239
|
+
if (!own.includes(prop)) {
|
|
240
|
+
this.validationResult.issue(new Syntax_PropertyMissingIssue(context, prop));
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Check whether the value of property `prop` of `obj` has type `expectedType`.
|
|
247
|
+
* @param obj
|
|
248
|
+
* @param prop
|
|
249
|
+
* @param expectedType
|
|
250
|
+
* @param context
|
|
251
|
+
*/
|
|
252
|
+
checkPropertyType = (obj: UnknownObjectType, prop: string, expectedType: PropertyType, mayBeNull: boolean, context: JsonContext): boolean => {
|
|
253
|
+
if (prop === "key") {
|
|
254
|
+
// console.log(" checking type of key " + JSON.stringify(obj));
|
|
255
|
+
}
|
|
256
|
+
if (obj[prop] === undefined || obj[prop] === null) {
|
|
257
|
+
if (!mayBeNull) {
|
|
258
|
+
this.validationResult.issue(new Syntax_PropertyNullIssue(context, prop));
|
|
259
|
+
return false;
|
|
260
|
+
} else {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
const actualType = typeof obj[prop];
|
|
265
|
+
if (expectedType !== actualType) {
|
|
266
|
+
if (expectedType === "array" && actualType === "object") {
|
|
267
|
+
// typeof returns an object for an array, so we need to check this separately.
|
|
268
|
+
if (!Array.isArray(obj[prop])) {
|
|
269
|
+
this.validationResult.issue(new Syntax_PropertyTypeIssue(context, prop, "array", typeof obj[prop]));
|
|
270
|
+
return false;
|
|
271
|
+
} else {
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
this.validationResult.issue(new Syntax_PropertyTypeIssue(context, prop, expectedType, actualType));
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
if (expectedType === "object") {
|
|
280
|
+
// typeof returns an object for an array, so we need to check this separately.
|
|
281
|
+
if (Array.isArray(obj[prop])) {
|
|
282
|
+
this.validationResult.issue(new Syntax_PropertyTypeIssue(context, prop, expectedType, "array"));
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function SyntaxValidator(jsonChunk:unknown): ValidationResult {
|
|
293
|
+
const validationResult = new ValidationResult();
|
|
294
|
+
const syntaxValidator = new LionWebSyntaxValidator(validationResult);
|
|
295
|
+
syntaxValidator.validate(jsonChunk);
|
|
296
|
+
return validationResult;
|
|
297
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { LionWebJsonChunk } from "../json/LionWebJson";
|
|
2
|
+
import { LionWebJsonChunkWrapper } from "../json/LionWebJsonChunkWrapper";
|
|
3
|
+
import { LionWebLanguageDefinition } from "../json/LionWebLanguageDefinition";
|
|
4
|
+
import { LionWebLanguageReferenceValidator } from "./LionWebLanguageReferenceValidator";
|
|
5
|
+
import { LionWebReferenceValidator } from "./LionWebReferenceValidator";
|
|
6
|
+
import { LionWebSyntaxValidator } from "./LionWebSyntaxValidator";
|
|
7
|
+
import { ValidationResult } from "./ValidationResult";
|
|
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
|
+
language: LionWebLanguageDefinition | null = null;
|
|
16
|
+
|
|
17
|
+
chunk: unknown;
|
|
18
|
+
validationResult: ValidationResult;
|
|
19
|
+
syntaxValidator: LionWebSyntaxValidator;
|
|
20
|
+
referenceValidator: LionWebReferenceValidator;
|
|
21
|
+
syntaxCorrect: boolean = false;
|
|
22
|
+
referencesCorrect: boolean = false;
|
|
23
|
+
|
|
24
|
+
constructor(json: unknown, lang: LionWebLanguageDefinition | null) {
|
|
25
|
+
this.object = json;
|
|
26
|
+
this.language =lang;
|
|
27
|
+
this.validationResult = new ValidationResult();
|
|
28
|
+
this.syntaxValidator = new LionWebSyntaxValidator(this.validationResult);
|
|
29
|
+
this.referenceValidator = new LionWebReferenceValidator(this.validationResult);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
validateAll() {
|
|
33
|
+
this.validateSyntax();
|
|
34
|
+
this.validateReferences();
|
|
35
|
+
this.validateForLanguage();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
validateSyntax() {
|
|
39
|
+
this.syntaxValidator.recursive = true;
|
|
40
|
+
this.syntaxValidator.validate(this.object);
|
|
41
|
+
this.syntaxCorrect = !this.validationResult.hasErrors();
|
|
42
|
+
if (this.syntaxCorrect) {
|
|
43
|
+
this.chunk = new LionWebJsonChunkWrapper(this.object as LionWebJsonChunk);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
validateReferences(): void {
|
|
48
|
+
if (!this.syntaxCorrect) {
|
|
49
|
+
// console.log("validateReferences not executed because there are syntax errors.")
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// when syntax is correct we know the chunk is actually a chunk!
|
|
53
|
+
this.referenceValidator.validate(this.chunk as LionWebJsonChunkWrapper);
|
|
54
|
+
this.referencesCorrect = !this.validationResult.hasErrors()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
validateForLanguage(): void {
|
|
58
|
+
if (!this.syntaxCorrect) {
|
|
59
|
+
// console.log("validateForLanguage not executed because there are syntax errors.")
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!this.referencesCorrect) {
|
|
63
|
+
// console.log("validateForLanguage not executed because there are reference errors.")
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (this.language !== null) {
|
|
67
|
+
const languageReferenceValidator = new LionWebLanguageReferenceValidator(this.validationResult, this.language);
|
|
68
|
+
// when syntax is correct we know the chunk is actually a chunk!
|
|
69
|
+
languageReferenceValidator.validate(this.chunk as LionWebJsonChunkWrapper);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// setLanguage(json: LionwebLanguageDefinition) {
|
|
74
|
+
// this.language = json;
|
|
75
|
+
// }
|
|
76
|
+
}
|