-
Notifications
You must be signed in to change notification settings - Fork 63
/
tmSchemaGenerator.js
482 lines (439 loc) · 16.2 KB
/
tmSchemaGenerator.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
/*
This script takes the TD Validation Schema (JSON Schema).
Its goal is to generate another JSON Schema that can be used to
validate Thing Model documents.
Generic Requirements:
- Not require any dependency
- Allow the inspection of the Schema at different iterations that add or remove functionalities
- Be recursive to be able to adapt to changes in the TD Schema
Changes to the TD Schema:
- Remove the term `required` from all levels
- Remove the term `enum` from all levels
- Maybe in the future: remove const but there is no use of it at the current state
- Remove format from a string type
- If a term is not of type string, allow also string
- Adding TM specific link validation (not fully clear yet)
Expectations:
- Required: There are currently 21 required in the TD Schema. There should be 2 left that are objects
- Enum: There are 29 enums, there should be 2 left.
- Format: There are 7 formats, there should be 3 left.
- anyOf: 1 anyOf in the td schema, should be 20 in the generated
*/
const fs = require("fs");
// some copied functions to manipulate objects
/**
* This function returns part of the object given in param with the value found when resolving the path. Similar to JSON Pointers.
* In case no path is found, the param defaultValue is echoed back
* Taken from https://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-and-arrays-by-string-path/6491621#6491621
* @param {object} object
* @param {string} path
* @param {any} defaultValue
* @return {object}
**/
const resolvePath = (object, path, defaultValue) =>
path
.split(/[\.\[\]\'\"]/)
.filter((p) => p)
.reduce((o, p) => (o ? o[p] : defaultValue), object);
/**
* This function replaces part of the object given in param with the value given as value at the location given in path.
* Taken from https://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-and-arrays-by-string-path/6491621#6491621
* @param {object} object
* @param {string} path
* @param {any} defaultValue
* @return {object}
**/
const setPath = (object, path, value) =>
path.split(".").reduce((o, p, i) => (o[p] = path.split(".").length === ++i ? value : o[p] || {}), object);
// regex/pattern to be used for strings when we want to enforce the {{PLACEHOLDER}} pattern
// ascii matching trick from https://stackoverflow.com/a/14608823/3806426
// tests available for now at https://regex101.com/r/Oxu9j2/1
const placeholderPattern = "^.*[{]{2}[ -~]+[}]{2}.*$";
// take the TD Schema
let tdSchema = JSON.parse(fs.readFileSync("validation/td-json-schema-validation.json"));
// do all the manipulation in order
let tmSchema = staticReplace(tdSchema);
tmSchema = removeRequired(tmSchema);
tmSchema = addPlaceholderRestrictionObjectNames(tmSchema);
tmSchema = replaceEnum(tmSchema);
tmSchema.definitions["placeholder-pattern"] = {
type: "string",
"pattern": placeholderPattern
};
tmSchema = removeFormat(tmSchema);
tmSchema = manualConvertString(tmSchema);
tmSchema = addTmTerms(tmSchema);
tmSchema = replaceSecurityOneOf(tmSchema);
tmSchema = postProcess(tmSchema);
// write a new file for the schema. Overwrites the existing one
// 2 spaces for easier reading
fs.writeFileSync("validation/tm-json-schema-validation.json", JSON.stringify(tmSchema, null, 2));
/**
* This function changes the values of terms in the schema in a static/deterministic way.
* These are title and description and `@type` in the root level
* @param {object} argObject
* @return {object}
**/
function staticReplace(argObject) {
argObject.title = "Thing Model";
argObject.description =
"JSON Schema for validating Thing Models. This is automatically generated from the WoT TD Schema.";
argObject.definitions.type_declaration = {
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
};
argObject.definitions.tm_type_declaration = {
"oneOf": [
{
"type": "string",
"const": "tm:ThingModel"
},
{
"type": "array",
"items": {
"type": "string"
},
"contains": {
"const": "tm:ThingModel"
}
}
]
};
argObject["$id"] =
"https://raw.githubusercontent.com/w3c/wot-thing-description/main/validation/tm-json-schema-validation.json";
return argObject;
}
/**
* if there is a required, remove that
* once that is done, find a sub item that is of object type, call recursively
* if there is no sub item with object, return the current scoped object
* @param {object} argObject
* @return {object}
**/
function removeRequired(argObject) {
// remove required if it exists and is of array type.
// check for array is needed since we also specify what a required is and that it is an object
if ("required" in argObject && Array.isArray(argObject.required)) {
// skip removal of required if the current object is a link_element subschema
// that requires a "sizes" or "rel" field. Otherwise it is not possible to use
// these two fields in link definitions
if (!(argObject["required"] == "sizes" || argObject["required"] == "rel")) {
// need to decide whether to delete or replace it with ""
// delete is "cleaner" but "" is more explicit
delete argObject.required;
} else if ("description" in argObject && argObject.description.includes(" or tm:extends")) {
argObject.description = argObject.description.replace(" or tm:extends", "");
argObject.properties.rel.enum = argObject.properties.rel.enum.filter((item) => item !== "tm:extends");
}
}
for (var key in argObject) {
let curValue = argObject[key];
// removal is done only in objects, other types are not JSON Schema points anyways
if (typeof curValue == "object") {
argObject[key] = removeRequired(curValue);
}
}
return argObject;
}
/**
* if there is an object, add the assertion that property names cannot follow the
* placeholder pattern, i.e. "{{asd}}": "mqtt://{{MQTT_BROKER_ADDRESS}}" is not allowed
* @param {object} argObject
* @return {object}
**/
function addPlaceholderRestrictionObjectNames(argObject) {
argObject["propertyNames"] = {
"not": {
"$ref": "#/definitions/placeholder-pattern"
}
};
for (var key in argObject) {
let curObject = argObject[key];
if (typeof curObject == "object") {
for (var key2 in curObject) {
curValue = curObject[key2];
if (curValue.type == "object") {
argObject[key][key2] = addPlaceholderRestrictionObjectNames(curValue);
}
}
}
}
return argObject;
}
/**
* if there is a enum, replace that with an oneOf of the same enum and a pattern for placeholder
* once that is done, remove find a sub item that is of object type, call recursively
* if there is no sub item with object, return the current scoped object
* This is done to allow putting placeholders for a string that would be limited with
* a list of allowed values in an enum
* @param {object} argObject
* @return {object}
**/
function replaceEnum(argObject) {
// this is created to have a custom array of the keys so that we don't call the function
// an infinite amount of times. Otherwise we would call replaceEnum on "oneOf"
var argObjectKeys = Object.keys(argObject);
// replace enum if it exists and is of array type.
// check for array is needed since we also specify what a enum is and that it is an object
if ("enum" in argObject && Array.isArray(argObject.enum)) {
// first the found enum is saved
// then it is deleted to be put in an oneOf
var newEnum = argObject.enum;
delete argObject.enum;
// the following will not work if somehow there is an enum and oneOf at the same time
// in the TD Schema. It will replace the oneOf with this
argObject.anyOf = [
{ enum: newEnum },
{
"$ref": "#/definitions/placeholder-pattern"
}
];
}
argObjectKeys.forEach((key) => {
let curValue = argObject[key];
// removal is done only in objects, other types are not JSON Schema points anyways
if (typeof curValue == "object") {
argObject[key] = replaceEnum(curValue);
} else if (typeof curValue == "array") {
curValue.forEach((item, x) => {
if (typeof item == "object") {
item = replaceEnum(item);
}
});
}
});
return argObject;
}
/**
* if there is a format for a string, remove that
* once that is done, find a sub item that is of object type, call recursively
* if there is no sub item with object, return the current scoped object
* This is done to allow putting placeholders for a string that will actually break the format
* @param {object} argObject
* @return {object}
**/
function removeFormat(argObject) {
// remove enum if it exists and is of array type.
// check for array is needed since we also specify what a enum is and that it is an object
if ("format" in argObject && typeof argObject.format == "string") {
// need to decide whether to delete or replace it with ""
// delete is "cleaner" but "" is more explicit
delete argObject.format;
}
for (var key in argObject) {
let curValue = argObject[key];
// removal is done only in objects, other types are not JSON Schema points anyways
if (typeof curValue == "object") {
argObject[key] = removeFormat(curValue);
}
}
return argObject;
}
/**
* This function changes the terms that have values of number, integer, boolean or array to anyOf with string and that term.
* Until a more recursive function works, this is its more manual version
* such types are found in: definitions/dataSchema minimum, maximum, minItems, maxItems, minLength, maxLength, multipleOf,
* writeOnly, readOnly and the exact same in definitions/property_element but there is also observable here
* safe, idempotent, synchronous in definitions/action_element
* @param {object} argObject
* @return {object}
**/
function manualConvertString(argObject) {
// the exact paths of the above mentioned locations of types
let paths = [
"definitions.multipleOfDefinition",
"definitions.dataSchema.properties.enum",
"definitions.dataSchema.properties.minimum",
"definitions.dataSchema.properties.maximum",
"definitions.dataSchema.properties.minItems",
"definitions.dataSchema.properties.maxItems",
"definitions.dataSchema.properties.minLength",
"definitions.dataSchema.properties.maxLength",
"definitions.dataSchema.properties.readOnly",
"definitions.dataSchema.properties.required",
"definitions.dataSchema.properties.writeOnly",
"definitions.property_element.properties.enum",
"definitions.property_element.properties.minimum",
"definitions.property_element.properties.maximum",
"definitions.property_element.properties.minItems",
"definitions.property_element.properties.maxItems",
"definitions.property_element.properties.minLength",
"definitions.property_element.properties.maxLength",
"definitions.property_element.properties.observable",
"definitions.property_element.properties.readOnly",
"definitions.property_element.properties.required",
"definitions.property_element.properties.writeOnly",
"definitions.action_element.properties.safe",
"definitions.action_element.properties.idempotent",
"definitions.action_element.properties.synchronous",
"properties.version"
];
//iterate over this array and replace for each
paths.forEach((element) => {
let curSchema = resolvePath(argObject, element, "NotFound");
if (curSchema == undefined || curSchema == "NotFound") {
console.log("The element " + element + " could not be found in the paths array");
process.exit(1);
}
let newSchema = changeToAnyOf(curSchema);
setPath(argObject, element, newSchema);
});
return argObject;
}
/**
* This function take a schema that has type:something and converts it into
* anyOf with string
* @param {object} argObject
* @return {object}
**/
function changeToAnyOf(argObject) {
if ("type" in argObject) {
let curSchema = argObject;
argObject = {};
argObject.anyOf = [
curSchema,
{
"$ref": "#/definitions/placeholder-pattern"
}
];
return argObject;
} else {
return argObject;
}
}
/**
* This function adds tm:optional, tm:ref and instanceName definitions
* Then these are referenced from the related locations, i.e.
* tm:optional is used only in the root level and tm:ref can be used anywhere
* It also add model into the version container and prohibits the use of instance
* @param {object} argObject
* @return {object}
**/
function addTmTerms(argObject) {
argObject.definitions["tm_optional"] = {
"type": "array",
"items": {
"$comment":
"this first checks for the general structure of /properties/myProp and then prohibits using / 3 times",
"allOf": [
{
"type": "string",
"pattern": "^((/properties/)|(/actions/)|(/events/))(([^/]))",
"$comment": "regex tests available at https://regex101.com/r/UgOzrJ/1"
},
{
"not": {
"type": "string",
"pattern": "(/)(.*/){2}",
"$comment": "regex tests available at https://regex101.com/r/r7vB0r/2"
}
}
]
}
};
argObject.properties["tm:optional"] = {
"$ref": "#/definitions/tm_optional"
};
argObject.definitions["tm_ref"] = {
"type": "string",
"format": "uri-reference"
};
let tmRefRef = {
"$ref": "#/definitions/tm_ref"
};
argObject.definitions["base_link_element"].properties["instanceName"] = {
"type": "string"
};
argObject.properties.version.anyOf[0].properties = {
"model": {
"type": "string"
}
};
argObject.properties.version.anyOf[0].not = {
"type": "object",
"properties": {
"instance": {
"type": "string"
}
},
"required": ["instance"]
};
// Note: this paths are statically defined
// please update the list if refactor the td schema
let paths = [
"definitions.dataSchema.properties",
"definitions.property_element.properties",
"definitions.action_element.properties",
"definitions.event_element.properties",
"definitions.form_element_property.properties",
"definitions.form_element_action.properties",
"definitions.form_element_event.properties",
"definitions.form_element_root.properties",
"definitions.noSecurityScheme.properties",
"definitions.comboSecurityScheme.oneOf.0.properties",
"definitions.comboSecurityScheme.oneOf.1.properties",
"definitions.basicSecurityScheme.properties",
"definitions.digestSecurityScheme.properties",
"definitions.apiKeySecurityScheme.properties",
"definitions.bearerSecurityScheme.properties",
"definitions.pskSecurityScheme.properties",
"definitions.oAuth2SecurityScheme.properties"
];
//iterate over this array and replace for each
paths.forEach((element) => {
let curSchema = resolvePath(argObject, element, "hey"); // this hey is just to have some argument for this copypaste function
if (curSchema == undefined) {
console.log("The element " + element + " could not be found in the paths array");
console.log("Did you forget to update the static paths above?");
process.exit(1);
}
curSchema["tm:ref"] = tmRefRef;
setPath(argObject, element, curSchema);
});
argObject.required = ["@context", "@type"];
argObject.properties["@type"]["$ref"] = "#/definitions/tm_type_declaration";
return argObject;
}
/**
* This very simple function changes the oneOf constraint of security definitions to
* anyOf since a placeholder used at scheme makes a TM validate all security schemes
* @param {object} argObject
* @return {object}
**/
function replaceSecurityOneOf(argObject) {
argObject.definitions.securityScheme.anyOf = argObject.definitions.securityScheme.oneOf;
delete argObject.definitions.securityScheme.oneOf;
return argObject;
}
/**
* Some custom logic to apply at the end to make the schema conform
* similar to staticReplace
* @param {object} argObject
* @return {object}
**/
function postProcess(argObject) {
argObject.definitions.security = {
"oneOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "string"
}
]
};
argObject.definitions.autoSecurityScheme.not = { "required": ["name"] };
return argObject;
}