schema-markdown 1.3.0 → 1.3.2
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/lib/encode.js +10 -3
- package/lib/parser.js +170 -164
- package/lib/schema.js +195 -134
- package/lib/schemaUtil.js +42 -24
- package/package.json +1 -1
package/lib/encode.js
CHANGED
|
@@ -106,11 +106,18 @@ export function decodeQueryString(paramStr) {
|
|
|
106
106
|
if (obj === null) {
|
|
107
107
|
obj = {};
|
|
108
108
|
parent[keyParent] = obj;
|
|
109
|
+
} else if (typeof obj !== 'object') {
|
|
110
|
+
throw new Error(`Invalid key '${keyFqn.slice(0, 100)}'`);
|
|
109
111
|
}
|
|
110
112
|
|
|
111
|
-
// Create the index for this key
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
// Create the index for this key - check own properties only, and define "__proto__"
|
|
114
|
+
// directly since assignment would invoke the prototype setter
|
|
115
|
+
if (!Object.hasOwn(obj, key)) {
|
|
116
|
+
if (key === '__proto__') {
|
|
117
|
+
Object.defineProperty(obj, key, {'value': null, 'enumerable': true, 'writable': true, 'configurable': true});
|
|
118
|
+
} else {
|
|
119
|
+
obj[key] = null;
|
|
120
|
+
}
|
|
114
121
|
}
|
|
115
122
|
}
|
|
116
123
|
|
package/lib/parser.js
CHANGED
|
@@ -49,6 +49,7 @@ const rValue = new RegExp(`^\\s+(?<id>${rPartId})\\s*$`);
|
|
|
49
49
|
const rValueQuoted = /^\s+"(?<id>.*?)"\s*$/;
|
|
50
50
|
const rURL = /^\s+(?<method>[A-Za-z]+|\*)(?:\s+(?<path>\/\S*))?/;
|
|
51
51
|
const rLineSplit = /\r?\n/;
|
|
52
|
+
const rIndent = /^\s/;
|
|
52
53
|
|
|
53
54
|
|
|
54
55
|
/**
|
|
@@ -96,6 +97,9 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
|
|
|
96
97
|
lines.push(...text.split(rLineSplit));
|
|
97
98
|
} else {
|
|
98
99
|
for (const textPart of text) {
|
|
100
|
+
if (typeof textPart !== 'string') {
|
|
101
|
+
throw new TypeError(`Invalid Schema Markdown text ${JSON.stringify(textPart)}`.slice(0, 100));
|
|
102
|
+
}
|
|
99
103
|
lines.push(...textPart.split(rLineSplit));
|
|
100
104
|
}
|
|
101
105
|
}
|
|
@@ -107,65 +111,67 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
|
|
|
107
111
|
linenum += 1;
|
|
108
112
|
|
|
109
113
|
// Line continuation?
|
|
110
|
-
const linePartNoContinuation = linePart.replace(rLineCont, '');
|
|
111
|
-
if (lineContinuation.length || linePartNoContinuation !== linePart) {
|
|
112
|
-
lineContinuation.push(linePartNoContinuation);
|
|
113
|
-
}
|
|
114
|
-
if (linePartNoContinuation !== linePart) {
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
114
|
+
const linePartNoContinuation = linePart.includes('\\') ? linePart.replace(rLineCont, '') : linePart;
|
|
117
115
|
let line;
|
|
118
|
-
if (lineContinuation.length) {
|
|
116
|
+
if (linePartNoContinuation === linePart && !lineContinuation.length) {
|
|
117
|
+
line = linePart;
|
|
118
|
+
} else {
|
|
119
|
+
lineContinuation.push(linePartNoContinuation);
|
|
120
|
+
if (linePartNoContinuation !== linePart) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
119
123
|
line = lineContinuation.join('');
|
|
120
124
|
lineContinuation.length = 0;
|
|
121
|
-
} else {
|
|
122
|
-
line = linePart;
|
|
123
125
|
}
|
|
124
126
|
|
|
125
|
-
// Match syntax
|
|
127
|
+
// Match syntax - each regex is guarded by a cheap string test that only skips it when it cannot match
|
|
128
|
+
const lineTrimmed = line.trim();
|
|
126
129
|
let matchName = 'comment';
|
|
127
|
-
let match = line.match(rComment);
|
|
130
|
+
let match = (lineTrimmed === '' || lineTrimmed.startsWith('#')) ? line.match(rComment) : null;
|
|
131
|
+
|
|
132
|
+
// Not a comment or blank line? Try the remaining syntax
|
|
128
133
|
if (match === null) {
|
|
134
|
+
const lineIndented = rIndent.test(line);
|
|
129
135
|
matchName = 'group';
|
|
130
|
-
match = line.match(rGroup);
|
|
131
|
-
}
|
|
132
|
-
if (match === null) {
|
|
133
|
-
matchName = 'action';
|
|
134
|
-
match = line.match(rAction);
|
|
135
|
-
}
|
|
136
|
-
if (match === null) {
|
|
137
|
-
matchName = 'definition';
|
|
138
|
-
match = line.match(rDefinition);
|
|
139
|
-
}
|
|
140
|
-
if (match === null && action !== null) {
|
|
141
|
-
matchName = 'section';
|
|
142
|
-
match = line.match(rSection);
|
|
143
|
-
}
|
|
144
|
-
if (match === null && action !== null) {
|
|
145
|
-
matchName = 'sectionPlain';
|
|
146
|
-
match = line.match(rSectionPlain);
|
|
147
|
-
}
|
|
148
|
-
if (match === null && userType !== null && 'enum' in userType) {
|
|
149
|
-
matchName = 'value';
|
|
150
|
-
match = line.match(rValue);
|
|
136
|
+
match = line.startsWith('group') ? line.match(rGroup) : null;
|
|
151
137
|
if (match === null) {
|
|
152
|
-
|
|
138
|
+
matchName = 'action';
|
|
139
|
+
match = line.startsWith('action') ? line.match(rAction) : null;
|
|
140
|
+
}
|
|
141
|
+
if (match === null) {
|
|
142
|
+
matchName = 'definition';
|
|
143
|
+
match = (line.startsWith('struct') || line.startsWith('union') || line.startsWith('enum')) ? line.match(rDefinition) : null;
|
|
144
|
+
}
|
|
145
|
+
if (match === null && action !== null && lineIndented) {
|
|
146
|
+
matchName = 'section';
|
|
147
|
+
match = line.match(rSection);
|
|
148
|
+
}
|
|
149
|
+
if (match === null && action !== null && lineIndented) {
|
|
150
|
+
matchName = 'sectionPlain';
|
|
151
|
+
match = line.match(rSectionPlain);
|
|
152
|
+
}
|
|
153
|
+
if (match === null && userType !== null && 'enum' in userType && lineIndented) {
|
|
154
|
+
matchName = 'value';
|
|
155
|
+
match = line.match(rValue);
|
|
156
|
+
if (match === null) {
|
|
157
|
+
match = line.match(rValueQuoted);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (match === null && userType !== null && 'struct' in userType && lineIndented) {
|
|
161
|
+
matchName = 'member';
|
|
162
|
+
match = line.match(rMember);
|
|
163
|
+
}
|
|
164
|
+
if (match === null && urls !== null && lineIndented) {
|
|
165
|
+
matchName = 'urls';
|
|
166
|
+
match = line.match(rURL);
|
|
167
|
+
}
|
|
168
|
+
if (match === null) {
|
|
169
|
+
matchName = 'typedef';
|
|
170
|
+
match = line.startsWith('typedef') ? line.match(rTypedef) : null;
|
|
171
|
+
}
|
|
172
|
+
if (match === null) {
|
|
173
|
+
matchName = null;
|
|
153
174
|
}
|
|
154
|
-
}
|
|
155
|
-
if (match === null && userType !== null && 'struct' in userType) {
|
|
156
|
-
matchName = 'member';
|
|
157
|
-
match = line.match(rMember);
|
|
158
|
-
}
|
|
159
|
-
if (match === null && urls !== null) {
|
|
160
|
-
matchName = 'urls';
|
|
161
|
-
match = line.match(rURL);
|
|
162
|
-
}
|
|
163
|
-
if (match === null) {
|
|
164
|
-
matchName = 'typedef';
|
|
165
|
-
match = line.match(rTypedef);
|
|
166
|
-
}
|
|
167
|
-
if (match === null) {
|
|
168
|
-
matchName = null;
|
|
169
175
|
}
|
|
170
176
|
|
|
171
177
|
// Comment?
|
|
@@ -175,39 +181,36 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
|
|
|
175
181
|
doc.push(!docString.startsWith(' ') ? docString : docString.slice(1));
|
|
176
182
|
}
|
|
177
183
|
|
|
178
|
-
//
|
|
179
|
-
} else if (matchName === '
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
docGroup = docGroup.trim();
|
|
183
|
-
} else {
|
|
184
|
-
docGroup = null;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Action?
|
|
188
|
-
} else if (matchName === 'action') {
|
|
189
|
-
const actionId = match.groups.id;
|
|
184
|
+
// Struct member?
|
|
185
|
+
} else if (matchName === 'member') {
|
|
186
|
+
const optional = typeof match.groups.optional !== 'undefined';
|
|
187
|
+
const memberName = match.groups.id;
|
|
190
188
|
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
189
|
+
// Add the member
|
|
190
|
+
const {struct} = userType;
|
|
191
|
+
if (!('members' in struct)) {
|
|
192
|
+
struct.members = [];
|
|
194
193
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (actionDoc !== null) {
|
|
205
|
-
action.doc = actionDoc;
|
|
194
|
+
const [memberType, memberAttr] = parseTypedef(match);
|
|
195
|
+
const memberDoc = getDoc();
|
|
196
|
+
const member = {
|
|
197
|
+
'name': memberName,
|
|
198
|
+
'type': memberType
|
|
199
|
+
};
|
|
200
|
+
struct.members.push(member);
|
|
201
|
+
if (memberAttr !== null) {
|
|
202
|
+
member.attr = memberAttr;
|
|
206
203
|
}
|
|
207
|
-
if (
|
|
208
|
-
|
|
204
|
+
if (memberDoc !== null) {
|
|
205
|
+
member.doc = memberDoc;
|
|
206
|
+
}
|
|
207
|
+
if (optional) {
|
|
208
|
+
member.optional = true;
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
// Record the definition's line number
|
|
212
|
+
filepos[`${struct.name}.${memberName}`] = linenum;
|
|
213
|
+
|
|
211
214
|
// Definition?
|
|
212
215
|
} else if (matchName === 'definition') {
|
|
213
216
|
const definitionString = match.groups.type;
|
|
@@ -215,7 +218,7 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
|
|
|
215
218
|
const definitionBaseIds = match.groups.baseIds;
|
|
216
219
|
|
|
217
220
|
// Type already defined?
|
|
218
|
-
if (builtinTypes.has(definitionId) ||
|
|
221
|
+
if (builtinTypes.has(definitionId) || Object.hasOwn(builtinDeprecated, definitionId) || Object.hasOwn(types, definitionId)) {
|
|
219
222
|
addError(`Redefinition of type '${definitionId}'`, filename, linenum);
|
|
220
223
|
}
|
|
221
224
|
|
|
@@ -264,6 +267,93 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
|
|
|
264
267
|
// Record the definition's line number
|
|
265
268
|
filepos[definitionId] = linenum;
|
|
266
269
|
|
|
270
|
+
// Enum value?
|
|
271
|
+
} else if (matchName === 'value') {
|
|
272
|
+
const valueString = match.groups.id;
|
|
273
|
+
|
|
274
|
+
// Add the enum value
|
|
275
|
+
const enum_ = userType.enum;
|
|
276
|
+
if (!('values' in enum_)) {
|
|
277
|
+
enum_.values = [];
|
|
278
|
+
}
|
|
279
|
+
const enumValue = {'name': valueString};
|
|
280
|
+
enum_.values.push(enumValue);
|
|
281
|
+
const enumValueDoc = getDoc();
|
|
282
|
+
if (enumValueDoc !== null) {
|
|
283
|
+
enumValue.doc = enumValueDoc;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Record the definition's line number
|
|
287
|
+
filepos[`${enum_.name}.${valueString}`] = linenum;
|
|
288
|
+
|
|
289
|
+
// Typedef?
|
|
290
|
+
} else if (matchName === 'typedef') {
|
|
291
|
+
const definitionId = match.groups.id;
|
|
292
|
+
|
|
293
|
+
// Type already defined?
|
|
294
|
+
if (builtinTypes.has(definitionId) || Object.hasOwn(builtinDeprecated, definitionId) || Object.hasOwn(types, definitionId)) {
|
|
295
|
+
addError(`Redefinition of type '${definitionId}'`, filename, linenum);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Clear parser state
|
|
299
|
+
action = null;
|
|
300
|
+
urls = null;
|
|
301
|
+
userType = null;
|
|
302
|
+
const typedefDoc = getDoc();
|
|
303
|
+
|
|
304
|
+
// Create the typedef
|
|
305
|
+
const [typedefType, typedefAttr] = parseTypedef(match);
|
|
306
|
+
const typedef = {
|
|
307
|
+
'name': definitionId,
|
|
308
|
+
'type': typedefType
|
|
309
|
+
};
|
|
310
|
+
types[definitionId] = {'typedef': typedef};
|
|
311
|
+
if (typedefAttr !== null) {
|
|
312
|
+
typedef.attr = typedefAttr;
|
|
313
|
+
}
|
|
314
|
+
if (typedefDoc !== null) {
|
|
315
|
+
typedef.doc = typedefDoc;
|
|
316
|
+
}
|
|
317
|
+
if (docGroup !== null) {
|
|
318
|
+
typedef.docGroup = docGroup;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Record the definition's line number
|
|
322
|
+
filepos[definitionId] = linenum;
|
|
323
|
+
|
|
324
|
+
// Documentation group?
|
|
325
|
+
} else if (matchName === 'group') {
|
|
326
|
+
docGroup = match.groups.group;
|
|
327
|
+
if (typeof docGroup !== 'undefined') {
|
|
328
|
+
docGroup = docGroup.trim();
|
|
329
|
+
} else {
|
|
330
|
+
docGroup = null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Action?
|
|
334
|
+
} else if (matchName === 'action') {
|
|
335
|
+
const actionId = match.groups.id;
|
|
336
|
+
|
|
337
|
+
// Action already defined?
|
|
338
|
+
if (Object.hasOwn(types, actionId)) {
|
|
339
|
+
addError(`Redefinition of action '${actionId}'`, filename, linenum);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Clear parser state
|
|
343
|
+
urls = null;
|
|
344
|
+
userType = null;
|
|
345
|
+
const actionDoc = getDoc();
|
|
346
|
+
|
|
347
|
+
// Create the new action
|
|
348
|
+
action = {'name': actionId};
|
|
349
|
+
types[actionId] = {'action': action};
|
|
350
|
+
if (actionDoc !== null) {
|
|
351
|
+
action.doc = actionDoc;
|
|
352
|
+
}
|
|
353
|
+
if (docGroup !== null) {
|
|
354
|
+
action.docGroup = docGroup;
|
|
355
|
+
}
|
|
356
|
+
|
|
267
357
|
// Action section?
|
|
268
358
|
} else if (matchName === 'section') {
|
|
269
359
|
const sectionString = match.groups.type;
|
|
@@ -318,55 +408,6 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
|
|
|
318
408
|
action[sectionString] = urls;
|
|
319
409
|
}
|
|
320
410
|
|
|
321
|
-
// Enum value?
|
|
322
|
-
} else if (matchName === 'value') {
|
|
323
|
-
const valueString = match.groups.id;
|
|
324
|
-
|
|
325
|
-
// Add the enum value
|
|
326
|
-
const enum_ = userType.enum;
|
|
327
|
-
if (!('values' in enum_)) {
|
|
328
|
-
enum_.values = [];
|
|
329
|
-
}
|
|
330
|
-
const enumValue = {'name': valueString};
|
|
331
|
-
enum_.values.push(enumValue);
|
|
332
|
-
const enumValueDoc = getDoc();
|
|
333
|
-
if (enumValueDoc !== null) {
|
|
334
|
-
enumValue.doc = enumValueDoc;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Record the definition's line number
|
|
338
|
-
filepos[`${enum_.name}.${valueString}`] = linenum;
|
|
339
|
-
|
|
340
|
-
// Struct member?
|
|
341
|
-
} else if (matchName === 'member') {
|
|
342
|
-
const optional = typeof match.groups.optional !== 'undefined';
|
|
343
|
-
const memberName = match.groups.id;
|
|
344
|
-
|
|
345
|
-
// Add the member
|
|
346
|
-
const {struct} = userType;
|
|
347
|
-
if (!('members' in struct)) {
|
|
348
|
-
struct.members = [];
|
|
349
|
-
}
|
|
350
|
-
const [memberType, memberAttr] = parseTypedef(match);
|
|
351
|
-
const memberDoc = getDoc();
|
|
352
|
-
const member = {
|
|
353
|
-
'name': memberName,
|
|
354
|
-
'type': memberType
|
|
355
|
-
};
|
|
356
|
-
struct.members.push(member);
|
|
357
|
-
if (memberAttr !== null) {
|
|
358
|
-
member.attr = memberAttr;
|
|
359
|
-
}
|
|
360
|
-
if (memberDoc !== null) {
|
|
361
|
-
member.doc = memberDoc;
|
|
362
|
-
}
|
|
363
|
-
if (optional) {
|
|
364
|
-
member.optional = true;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Record the definition's line number
|
|
368
|
-
filepos[`${struct.name}.${memberName}`] = linenum;
|
|
369
|
-
|
|
370
411
|
// URL?
|
|
371
412
|
} else if (matchName === 'urls') {
|
|
372
413
|
const {method, path} = match.groups;
|
|
@@ -389,41 +430,6 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
|
|
|
389
430
|
// Add the URL
|
|
390
431
|
urls.push(actionUrl);
|
|
391
432
|
|
|
392
|
-
// Typedef?
|
|
393
|
-
} else if (matchName === 'typedef') {
|
|
394
|
-
const definitionId = match.groups.id;
|
|
395
|
-
|
|
396
|
-
// Type already defined?
|
|
397
|
-
if (builtinTypes.has(definitionId) || definitionId in builtinDeprecated || definitionId in types) {
|
|
398
|
-
addError(`Redefinition of type '${definitionId}'`, filename, linenum);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Clear parser state
|
|
402
|
-
action = null;
|
|
403
|
-
urls = null;
|
|
404
|
-
userType = null;
|
|
405
|
-
const typedefDoc = getDoc();
|
|
406
|
-
|
|
407
|
-
// Create the typedef
|
|
408
|
-
const [typedefType, typedefAttr] = parseTypedef(match);
|
|
409
|
-
const typedef = {
|
|
410
|
-
'name': definitionId,
|
|
411
|
-
'type': typedefType
|
|
412
|
-
};
|
|
413
|
-
types[definitionId] = {'typedef': typedef};
|
|
414
|
-
if (typedefAttr !== null) {
|
|
415
|
-
typedef.attr = typedefAttr;
|
|
416
|
-
}
|
|
417
|
-
if (typedefDoc !== null) {
|
|
418
|
-
typedef.doc = typedefDoc;
|
|
419
|
-
}
|
|
420
|
-
if (docGroup !== null) {
|
|
421
|
-
typedef.docGroup = docGroup;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Record the definition's line number
|
|
425
|
-
filepos[definitionId] = linenum;
|
|
426
|
-
|
|
427
433
|
// Unrecognized line syntax
|
|
428
434
|
} else {
|
|
429
435
|
addError('Syntax error', filename, linenum);
|
|
@@ -449,7 +455,7 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
|
|
|
449
455
|
errorLinenum = filepos[`${typeName}.${memberName}`] ?? null;
|
|
450
456
|
}
|
|
451
457
|
if (errorLinenum === null) {
|
|
452
|
-
errorLinenum = filepos[typeName]
|
|
458
|
+
errorLinenum = Object.hasOwn(filepos, typeName) ? filepos[typeName] : null;
|
|
453
459
|
}
|
|
454
460
|
if (errorLinenum === null) {
|
|
455
461
|
errorFilename = '';
|
|
@@ -527,7 +533,7 @@ function parseTypedef(matchTypedef) {
|
|
|
527
533
|
|
|
528
534
|
// Helper function to create a type model
|
|
529
535
|
function createType(typeName) {
|
|
530
|
-
if (typeName
|
|
536
|
+
if (Object.hasOwn(builtinDeprecated, typeName)) {
|
|
531
537
|
return {'builtin': builtinDeprecated[typeName]};
|
|
532
538
|
}
|
|
533
539
|
if (builtinTypes.has(typeName)) {
|
package/lib/schema.js
CHANGED
|
@@ -39,9 +39,9 @@ function getReferencedTypesHelper(types, type, referencedTypes) {
|
|
|
39
39
|
const typeName = type.user;
|
|
40
40
|
|
|
41
41
|
// Already encountered?
|
|
42
|
-
if (!(typeName
|
|
43
|
-
const userType = types
|
|
44
|
-
referencedTypes
|
|
42
|
+
if (!Object.hasOwn(referencedTypes, typeName)) {
|
|
43
|
+
const userType = getUserType(types, typeName);
|
|
44
|
+
setValue(referencedTypes, typeName, userType);
|
|
45
45
|
|
|
46
46
|
// Struct?
|
|
47
47
|
if ('struct' in userType) {
|
|
@@ -116,6 +116,27 @@ export class ValidationError extends Error {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
|
|
119
|
+
// Helper to look up a user type by name - throws ValidationError for unknown type names
|
|
120
|
+
function getUserType(types, typeName) {
|
|
121
|
+
const userType = Object.hasOwn(types, typeName) ? types[typeName] : null;
|
|
122
|
+
if (userType === null) {
|
|
123
|
+
throw new ValidationError(`Unknown type "${typeName}"`);
|
|
124
|
+
}
|
|
125
|
+
return userType;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
// Helper to set an own property on a plain-object copy - the "__proto__" key is defined directly
|
|
130
|
+
// since assignment would invoke the inherited prototype setter instead of creating an own property
|
|
131
|
+
function setValue(obj, key, value) {
|
|
132
|
+
if (key === '__proto__') {
|
|
133
|
+
Object.defineProperty(obj, key, {'value': value, 'enumerable': true, 'writable': true, 'configurable': true});
|
|
134
|
+
} else {
|
|
135
|
+
obj[key] = value;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
119
140
|
/**
|
|
120
141
|
* Type-validate a value using a user type model. Container values are duplicated since some member types are
|
|
121
142
|
* transformed during validation.
|
|
@@ -134,9 +155,6 @@ export class ValidationError extends Error {
|
|
|
134
155
|
* @throws [ValidationError]{@link module:lib/schema.ValidationError}
|
|
135
156
|
*/
|
|
136
157
|
export function validateType(types, typeName, value, memberFqn = null) {
|
|
137
|
-
if (!(typeName in types)) {
|
|
138
|
-
throw new ValidationError(`Unknown type "${typeName}"`);
|
|
139
|
-
}
|
|
140
158
|
return validateTypeHelper(types, {'user': typeName}, value, memberFqn);
|
|
141
159
|
}
|
|
142
160
|
|
|
@@ -147,6 +165,10 @@ const rDatetime = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})T\d{2}:\d{2}(?::
|
|
|
147
165
|
const rUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
148
166
|
|
|
149
167
|
|
|
168
|
+
// Default dict key type used by validateTypeHelper
|
|
169
|
+
const defaultDictKeyType = {'builtin': 'string'};
|
|
170
|
+
|
|
171
|
+
|
|
150
172
|
function validateTypeHelper(types, type, value, memberFqn) {
|
|
151
173
|
let valueNew = value;
|
|
152
174
|
|
|
@@ -161,23 +183,14 @@ function validateTypeHelper(types, type, value, memberFqn) {
|
|
|
161
183
|
throwMemberError(type, value, memberFqn);
|
|
162
184
|
}
|
|
163
185
|
|
|
164
|
-
// uuid?
|
|
165
|
-
} else if (builtin === 'uuid') {
|
|
166
|
-
if (typeof value !== 'string') {
|
|
167
|
-
throwMemberError(type, value, memberFqn);
|
|
168
|
-
}
|
|
169
|
-
if (!rUuid.test(value)) {
|
|
170
|
-
throwMemberError(type, value, memberFqn);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
186
|
// int or float?
|
|
174
187
|
} else if (builtin === 'int' || builtin === 'float') {
|
|
175
188
|
// Convert string?
|
|
176
189
|
if (typeof value === 'string') {
|
|
177
|
-
|
|
190
|
+
valueNew = parseFloat(value);
|
|
191
|
+
if (isNaN(value) || !Number.isFinite(valueNew)) {
|
|
178
192
|
throwMemberError(type, value, memberFqn);
|
|
179
193
|
}
|
|
180
|
-
valueNew = parseFloat(value);
|
|
181
194
|
|
|
182
195
|
// Not a number?
|
|
183
196
|
} else if (typeof value !== 'number') {
|
|
@@ -253,89 +266,20 @@ function validateTypeHelper(types, type, value, memberFqn) {
|
|
|
253
266
|
} else if (!(value instanceof Date)) {
|
|
254
267
|
throwMemberError(type, value, memberFqn);
|
|
255
268
|
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// array?
|
|
259
|
-
} else if ('array' in type) {
|
|
260
|
-
// Valid value type?
|
|
261
|
-
const {array} = type;
|
|
262
|
-
const arrayType = array.type;
|
|
263
|
-
const arrayAttr = 'attr' in array ? array.attr : null;
|
|
264
|
-
if (value === '') {
|
|
265
|
-
valueNew = [];
|
|
266
|
-
} else if (!Array.isArray(value)) {
|
|
267
|
-
throwMemberError(type, value, memberFqn);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Validate the list contents
|
|
271
|
-
const valueCopy = [];
|
|
272
|
-
const arrayValueNullable = arrayAttr !== null && 'nullable' in arrayAttr && arrayAttr.nullable;
|
|
273
|
-
for (let ixArrayValue = 0; ixArrayValue < valueNew.length; ixArrayValue++) {
|
|
274
|
-
const memberFqnValue = memberFqn !== null ? `${memberFqn}.${ixArrayValue}` : `${ixArrayValue}`;
|
|
275
|
-
let arrayValue = valueNew[ixArrayValue];
|
|
276
|
-
if (arrayValueNullable && (arrayValue === null || arrayValue === 'null')) {
|
|
277
|
-
arrayValue = null;
|
|
278
|
-
} else {
|
|
279
|
-
arrayValue = validateTypeHelper(types, arrayType, arrayValue, memberFqnValue);
|
|
280
|
-
validateAttr(arrayType, arrayAttr, arrayValue, memberFqnValue);
|
|
281
|
-
}
|
|
282
|
-
valueCopy.push(arrayValue);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Return the validated, transformed copy
|
|
286
|
-
valueNew = valueCopy;
|
|
287
|
-
|
|
288
|
-
// dict?
|
|
289
|
-
} else if ('dict' in type) {
|
|
290
|
-
// Valid value type?
|
|
291
|
-
const {dict} = type;
|
|
292
|
-
const dictType = dict.type;
|
|
293
|
-
const dictAttr = 'attr' in dict ? dict.attr : null;
|
|
294
|
-
const dictKeyType = 'keyType' in dict ? dict.keyType : {'builtin': 'string'};
|
|
295
|
-
const dictKeyAttr = 'keyAttr' in dict ? dict.keyAttr : null;
|
|
296
|
-
if (value === '') {
|
|
297
|
-
valueNew = {};
|
|
298
|
-
} else if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
299
|
-
throwMemberError(type, value, memberFqn);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Validate the dict key/value pairs
|
|
303
|
-
const valueCopy = valueNew instanceof Map ? new Map() : {};
|
|
304
|
-
const dictKeyNullable = dictKeyAttr !== null && 'nullable' in dictKeyAttr && dictKeyAttr.nullable;
|
|
305
|
-
const dictValueNullable = dictAttr !== null && 'nullable' in dictAttr && dictAttr.nullable;
|
|
306
|
-
for (let [dictKey, dictValue] of (valueNew instanceof Map ? valueNew.entries() : Object.entries(valueNew))) {
|
|
307
|
-
const memberFqnKey = memberFqn !== null ? `${memberFqn}.${dictKey}` : `${dictKey}`;
|
|
308
269
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
dictKey = validateTypeHelper(types, dictKeyType, dictKey, memberFqn);
|
|
314
|
-
validateAttr(dictKeyType, dictKeyAttr, dictKey, memberFqn);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Validate the value
|
|
318
|
-
if (dictValueNullable && (dictValue === null || dictValue === 'null')) {
|
|
319
|
-
dictValue = null;
|
|
320
|
-
} else {
|
|
321
|
-
dictValue = validateTypeHelper(types, dictType, dictValue, memberFqnKey);
|
|
322
|
-
validateAttr(dictType, dictAttr, dictValue, memberFqnKey);
|
|
270
|
+
// uuid?
|
|
271
|
+
} else if (builtin === 'uuid') {
|
|
272
|
+
if (typeof value !== 'string') {
|
|
273
|
+
throwMemberError(type, value, memberFqn);
|
|
323
274
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (valueCopy instanceof Map) {
|
|
327
|
-
valueCopy.set(dictKey, dictValue);
|
|
328
|
-
} else {
|
|
329
|
-
valueCopy[dictKey] = dictValue;
|
|
275
|
+
if (!rUuid.test(value)) {
|
|
276
|
+
throwMemberError(type, value, memberFqn);
|
|
330
277
|
}
|
|
331
278
|
}
|
|
332
279
|
|
|
333
|
-
// Return the validated, transformed copy
|
|
334
|
-
valueNew = valueCopy;
|
|
335
|
-
|
|
336
280
|
// User type?
|
|
337
281
|
} else if ('user' in type) {
|
|
338
|
-
const userType = types
|
|
282
|
+
const userType = getUserType(types, type.user);
|
|
339
283
|
|
|
340
284
|
// action?
|
|
341
285
|
if ('action' in userType) {
|
|
@@ -353,7 +297,9 @@ function validateTypeHelper(types, type, value, memberFqn) {
|
|
|
353
297
|
valueNew = null;
|
|
354
298
|
} else {
|
|
355
299
|
valueNew = validateTypeHelper(types, typedef.type, value, memberFqn);
|
|
356
|
-
|
|
300
|
+
if (typedefAttr !== null) {
|
|
301
|
+
validateAttr(type, typedefAttr, valueNew, memberFqn);
|
|
302
|
+
}
|
|
357
303
|
}
|
|
358
304
|
|
|
359
305
|
// enum?
|
|
@@ -377,94 +323,209 @@ function validateTypeHelper(types, type, value, memberFqn) {
|
|
|
377
323
|
}
|
|
378
324
|
|
|
379
325
|
// Valid union?
|
|
326
|
+
const isMap = valueNew instanceof Map;
|
|
380
327
|
const isUnion = 'union' in struct ? struct.union : false;
|
|
381
328
|
if (isUnion) {
|
|
382
|
-
if (Object.keys(
|
|
329
|
+
if ((isMap ? valueNew.size : Object.keys(valueNew).length) !== 1) {
|
|
383
330
|
throwMemberError({'user': struct.name}, value, memberFqn);
|
|
384
331
|
}
|
|
385
332
|
}
|
|
386
333
|
|
|
387
334
|
// Validate the struct members
|
|
388
|
-
const valueCopy =
|
|
335
|
+
const valueCopy = isMap ? new Map() : {};
|
|
389
336
|
for (const member of getStructMembers(types, struct)) {
|
|
390
337
|
const memberName = member.name;
|
|
391
|
-
const memberFqnMember = memberFqn
|
|
338
|
+
const memberFqnMember = [memberFqn, memberName];
|
|
392
339
|
const memberOptional = 'optional' in member && member.optional;
|
|
393
|
-
const
|
|
340
|
+
const memberAttr = 'attr' in member ? member.attr : null;
|
|
394
341
|
|
|
395
|
-
// Missing non-optional member?
|
|
396
|
-
|
|
342
|
+
// Missing non-optional member? Check own properties only - "in" matches inherited
|
|
343
|
+
// Object.prototype properties for member names like "constructor"
|
|
344
|
+
if (!(isMap ? valueNew.has(memberName) : Object.hasOwn(valueNew, memberName))) {
|
|
397
345
|
if (!memberOptional && !isUnion) {
|
|
398
|
-
throw new ValidationError(`Required member "${memberFqnMember}" missing`);
|
|
346
|
+
throw new ValidationError(`Required member "${memberFqnStr(memberFqnMember)}" missing`);
|
|
399
347
|
}
|
|
400
348
|
} else {
|
|
401
349
|
// Validate the member value
|
|
402
|
-
let memberValue =
|
|
403
|
-
|
|
350
|
+
let memberValue = isMap ? valueNew.get(memberName) : valueNew[memberName];
|
|
351
|
+
|
|
352
|
+
// No member attributes? Validate the member value directly
|
|
353
|
+
if (memberAttr === null) {
|
|
354
|
+
memberValue = validateTypeHelper(types, member.type, memberValue, memberFqnMember);
|
|
355
|
+
|
|
356
|
+
// Nullable null?
|
|
357
|
+
} else if ('nullable' in memberAttr && memberAttr.nullable &&
|
|
358
|
+
(memberValue === null || memberValue === 'null')) {
|
|
404
359
|
memberValue = null;
|
|
405
360
|
} else {
|
|
406
361
|
memberValue = validateTypeHelper(types, member.type, memberValue, memberFqnMember);
|
|
407
|
-
validateAttr(member.type,
|
|
362
|
+
validateAttr(member.type, memberAttr, memberValue, memberFqnMember);
|
|
408
363
|
}
|
|
409
364
|
|
|
410
365
|
// Copy the validated member
|
|
411
|
-
if (
|
|
366
|
+
if (isMap) {
|
|
412
367
|
valueCopy.set(memberName, memberValue);
|
|
413
368
|
} else {
|
|
414
|
-
valueCopy
|
|
369
|
+
setValue(valueCopy, memberName, memberValue);
|
|
415
370
|
}
|
|
416
371
|
}
|
|
417
372
|
}
|
|
418
373
|
|
|
419
374
|
// Any unknown members?
|
|
420
|
-
const
|
|
421
|
-
const
|
|
422
|
-
if (
|
|
375
|
+
const valueCopyCount = isMap ? valueCopy.size : Object.keys(valueCopy).length;
|
|
376
|
+
const valueNewCount = isMap ? valueNew.size : Object.keys(valueNew).length;
|
|
377
|
+
if (valueCopyCount !== valueNewCount) {
|
|
423
378
|
const memberSet = new Set(getStructMembers(types, struct).map((member) => member.name));
|
|
379
|
+
const valueNewKeys = isMap ? Array.from(valueNew.keys()) : Object.keys(valueNew);
|
|
424
380
|
const [unknownKey] = valueNewKeys.filter((key) => !memberSet.has(key));
|
|
425
|
-
const unknownFqn = memberFqn
|
|
381
|
+
const unknownFqn = memberFqnStr([memberFqn, unknownKey]);
|
|
426
382
|
throw new ValidationError(`Unknown member "${unknownFqn.slice(0, 100)}"`);
|
|
427
383
|
}
|
|
428
384
|
|
|
429
385
|
// Return the validated, transformed copy
|
|
430
386
|
valueNew = valueCopy;
|
|
431
387
|
}
|
|
388
|
+
|
|
389
|
+
// array?
|
|
390
|
+
} else if ('array' in type) {
|
|
391
|
+
// Valid value type?
|
|
392
|
+
const {array} = type;
|
|
393
|
+
const arrayType = array.type;
|
|
394
|
+
const arrayAttr = 'attr' in array ? array.attr : null;
|
|
395
|
+
if (value === '') {
|
|
396
|
+
valueNew = [];
|
|
397
|
+
} else if (!Array.isArray(value)) {
|
|
398
|
+
throwMemberError(type, value, memberFqn);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Validate the list contents
|
|
402
|
+
const valueCopy = [];
|
|
403
|
+
const arrayValueNullable = arrayAttr !== null && 'nullable' in arrayAttr && arrayAttr.nullable;
|
|
404
|
+
for (let ixArrayValue = 0; ixArrayValue < valueNew.length; ixArrayValue++) {
|
|
405
|
+
const memberFqnValue = [memberFqn, ixArrayValue];
|
|
406
|
+
let arrayValue = valueNew[ixArrayValue];
|
|
407
|
+
if (arrayValueNullable && (arrayValue === null || arrayValue === 'null')) {
|
|
408
|
+
arrayValue = null;
|
|
409
|
+
} else {
|
|
410
|
+
arrayValue = validateTypeHelper(types, arrayType, arrayValue, memberFqnValue);
|
|
411
|
+
if (arrayAttr !== null) {
|
|
412
|
+
validateAttr(arrayType, arrayAttr, arrayValue, memberFqnValue);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
valueCopy.push(arrayValue);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Return the validated, transformed copy
|
|
419
|
+
valueNew = valueCopy;
|
|
420
|
+
|
|
421
|
+
// dict?
|
|
422
|
+
} else if ('dict' in type) {
|
|
423
|
+
// Valid value type?
|
|
424
|
+
const {dict} = type;
|
|
425
|
+
const dictType = dict.type;
|
|
426
|
+
const dictAttr = 'attr' in dict ? dict.attr : null;
|
|
427
|
+
const dictKeyType = 'keyType' in dict ? dict.keyType : defaultDictKeyType;
|
|
428
|
+
const dictKeyAttr = 'keyAttr' in dict ? dict.keyAttr : null;
|
|
429
|
+
if (value === '') {
|
|
430
|
+
valueNew = {};
|
|
431
|
+
} else if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
432
|
+
throwMemberError(type, value, memberFqn);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Validate the dict key/value pairs
|
|
436
|
+
const isMap = valueNew instanceof Map;
|
|
437
|
+
const valueCopy = isMap ? new Map() : {};
|
|
438
|
+
const dictKeyNullable = dictKeyAttr !== null && 'nullable' in dictKeyAttr && dictKeyAttr.nullable;
|
|
439
|
+
const dictValueNullable = dictAttr !== null && 'nullable' in dictAttr && dictAttr.nullable;
|
|
440
|
+
for (let [dictKey, dictValue] of (isMap ? valueNew.entries() : Object.entries(valueNew))) {
|
|
441
|
+
const memberFqnKey = [memberFqn, dictKey];
|
|
442
|
+
|
|
443
|
+
// Validate the key
|
|
444
|
+
if (dictKeyNullable && (dictKey === null || dictKey === 'null')) {
|
|
445
|
+
dictKey = null;
|
|
446
|
+
} else {
|
|
447
|
+
dictKey = validateTypeHelper(types, dictKeyType, dictKey, memberFqn);
|
|
448
|
+
if (dictKeyAttr !== null) {
|
|
449
|
+
validateAttr(dictKeyType, dictKeyAttr, dictKey, memberFqn);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Validate the value
|
|
454
|
+
if (dictValueNullable && (dictValue === null || dictValue === 'null')) {
|
|
455
|
+
dictValue = null;
|
|
456
|
+
} else {
|
|
457
|
+
dictValue = validateTypeHelper(types, dictType, dictValue, memberFqnKey);
|
|
458
|
+
if (dictAttr !== null) {
|
|
459
|
+
validateAttr(dictType, dictAttr, dictValue, memberFqnKey);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Copy the key/value
|
|
464
|
+
if (isMap) {
|
|
465
|
+
valueCopy.set(dictKey, dictValue);
|
|
466
|
+
} else {
|
|
467
|
+
setValue(valueCopy, dictKey, dictValue);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Return the validated, transformed copy
|
|
472
|
+
valueNew = valueCopy;
|
|
432
473
|
}
|
|
433
474
|
|
|
434
475
|
return valueNew;
|
|
435
476
|
}
|
|
436
477
|
|
|
437
478
|
|
|
479
|
+
// Helper to materialize a member's fully-qualified name string. For performance, member FQNs are
|
|
480
|
+
// built lazily as [parent, key] pairs and only converted to strings on error.
|
|
481
|
+
function memberFqnStr(memberFqn) {
|
|
482
|
+
if (memberFqn === null || typeof memberFqn === 'string') {
|
|
483
|
+
return memberFqn;
|
|
484
|
+
}
|
|
485
|
+
const parts = [];
|
|
486
|
+
let fqnPart = memberFqn;
|
|
487
|
+
while (Array.isArray(fqnPart)) {
|
|
488
|
+
parts.push(`${fqnPart[1]}`);
|
|
489
|
+
[fqnPart] = fqnPart;
|
|
490
|
+
}
|
|
491
|
+
if (fqnPart !== null) {
|
|
492
|
+
parts.push(`${fqnPart}`);
|
|
493
|
+
}
|
|
494
|
+
parts.reverse();
|
|
495
|
+
return parts.join('.');
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
|
|
438
499
|
function throwMemberError(type, value, memberFqn, attr = null) {
|
|
439
|
-
const
|
|
500
|
+
const memberFqnValue = memberFqnStr(memberFqn);
|
|
501
|
+
const memberPart = memberFqnValue ? ` for member "${memberFqnValue}"` : '';
|
|
440
502
|
const typeName = 'builtin' in type ? type.builtin : ('array' in type ? 'array' : ('dict' in type ? 'dict' : type.user));
|
|
441
503
|
const attrPart = attr !== null ? ` [${attr}]` : '';
|
|
442
504
|
const valueStr = `${JSON.stringify(value)}`;
|
|
443
505
|
const msg = `Invalid value ${valueStr.slice(0, 100)} (type "${typeof value}")${memberPart}, expected type "${typeName}"${attrPart}`;
|
|
444
|
-
throw new ValidationError(msg,
|
|
506
|
+
throw new ValidationError(msg, memberFqnValue);
|
|
445
507
|
}
|
|
446
508
|
|
|
447
509
|
|
|
448
510
|
function validateAttr(type, attr, value, memberFqn) {
|
|
449
|
-
if (attr
|
|
511
|
+
if ('eq' in attr && !(value === attr.eq)) {
|
|
512
|
+
throwMemberError(type, value, memberFqn, `== ${attr.eq}`);
|
|
513
|
+
}
|
|
514
|
+
if ('lt' in attr && !(value < attr.lt)) {
|
|
515
|
+
throwMemberError(type, value, memberFqn, `< ${attr.lt}`);
|
|
516
|
+
}
|
|
517
|
+
if ('lte' in attr && !(value <= attr.lte)) {
|
|
518
|
+
throwMemberError(type, value, memberFqn, `<= ${attr.lte}`);
|
|
519
|
+
}
|
|
520
|
+
if ('gt' in attr && !(value > attr.gt)) {
|
|
521
|
+
throwMemberError(type, value, memberFqn, `> ${attr.gt}`);
|
|
522
|
+
}
|
|
523
|
+
if ('gte' in attr && !(value >= attr.gte)) {
|
|
524
|
+
throwMemberError(type, value, memberFqn, `>= ${attr.gte}`);
|
|
525
|
+
}
|
|
526
|
+
if ('lenEq' in attr || 'lenLT' in attr || 'lenLTE' in attr || 'lenGT' in attr || 'lenGTE' in attr) {
|
|
450
527
|
const length = Array.isArray(value) || typeof value === 'string' ? value.length
|
|
451
|
-
: (typeof value === 'object' ? Object.keys(value).length : null);
|
|
452
|
-
|
|
453
|
-
if ('eq' in attr && !(value === attr.eq)) {
|
|
454
|
-
throwMemberError(type, value, memberFqn, `== ${attr.eq}`);
|
|
455
|
-
}
|
|
456
|
-
if ('lt' in attr && !(value < attr.lt)) {
|
|
457
|
-
throwMemberError(type, value, memberFqn, `< ${attr.lt}`);
|
|
458
|
-
}
|
|
459
|
-
if ('lte' in attr && !(value <= attr.lte)) {
|
|
460
|
-
throwMemberError(type, value, memberFqn, `<= ${attr.lte}`);
|
|
461
|
-
}
|
|
462
|
-
if ('gt' in attr && !(value > attr.gt)) {
|
|
463
|
-
throwMemberError(type, value, memberFqn, `> ${attr.gt}`);
|
|
464
|
-
}
|
|
465
|
-
if ('gte' in attr && !(value >= attr.gte)) {
|
|
466
|
-
throwMemberError(type, value, memberFqn, `>= ${attr.gte}`);
|
|
467
|
-
}
|
|
528
|
+
: (value instanceof Map ? value.size : (typeof value === 'object' ? Object.keys(value).length : null));
|
|
468
529
|
if ('lenEq' in attr && !(length === attr.lenEq)) {
|
|
469
530
|
throwMemberError(type, value, memberFqn, `len == ${attr.lenEq}`);
|
|
470
531
|
}
|
|
@@ -501,9 +562,9 @@ export function getStructMembers(types, struct) {
|
|
|
501
562
|
// Get base struct members
|
|
502
563
|
const members = [];
|
|
503
564
|
for (const base of struct.bases) {
|
|
504
|
-
let baseUserType = types
|
|
565
|
+
let baseUserType = getUserType(types, base);
|
|
505
566
|
while ('typedef' in baseUserType) {
|
|
506
|
-
baseUserType = types
|
|
567
|
+
baseUserType = getUserType(types, baseUserType.typedef.type.user);
|
|
507
568
|
}
|
|
508
569
|
members.push(...getStructMembers(types, baseUserType.struct));
|
|
509
570
|
}
|
|
@@ -534,9 +595,9 @@ export function getEnumValues(types, enum_) {
|
|
|
534
595
|
// Get base enum values
|
|
535
596
|
const values = [];
|
|
536
597
|
for (const base of enum_.bases) {
|
|
537
|
-
let baseUserType = types
|
|
598
|
+
let baseUserType = getUserType(types, base);
|
|
538
599
|
while ('typedef' in baseUserType) {
|
|
539
|
-
baseUserType = types
|
|
600
|
+
baseUserType = getUserType(types, baseUserType.typedef.type.user);
|
|
540
601
|
}
|
|
541
602
|
values.push(...getEnumValues(types, baseUserType.enum));
|
|
542
603
|
}
|
package/lib/schemaUtil.js
CHANGED
|
@@ -43,13 +43,13 @@ export function validateTypeModelErrors(types) {
|
|
|
43
43
|
|
|
44
44
|
// Iterate the members
|
|
45
45
|
try {
|
|
46
|
-
const members =
|
|
46
|
+
const members = new Set();
|
|
47
47
|
for (const member of getStructMembers(types, struct)) {
|
|
48
48
|
const memberName = member.name;
|
|
49
49
|
|
|
50
50
|
// Duplicate member?
|
|
51
|
-
if (!(memberName
|
|
52
|
-
members
|
|
51
|
+
if (!members.has(memberName)) {
|
|
52
|
+
members.add(memberName);
|
|
53
53
|
} else {
|
|
54
54
|
errors.push([typeName, memberName, `Redefinition of "${typeName}" member "${memberName}"`]);
|
|
55
55
|
}
|
|
@@ -82,13 +82,13 @@ export function validateTypeModelErrors(types) {
|
|
|
82
82
|
|
|
83
83
|
// Get the enumeration values
|
|
84
84
|
try {
|
|
85
|
-
const values =
|
|
85
|
+
const values = new Set();
|
|
86
86
|
for (const value of getEnumValues(types, enum_)) {
|
|
87
87
|
const valueName = value.name;
|
|
88
88
|
|
|
89
89
|
// Duplicate value?
|
|
90
|
-
if (!(valueName
|
|
91
|
-
values
|
|
90
|
+
if (!values.has(valueName)) {
|
|
91
|
+
values.add(valueName);
|
|
92
92
|
} else {
|
|
93
93
|
errors.push([typeName, valueName, `Redefinition of "${typeName}" value "${valueName}"`]);
|
|
94
94
|
}
|
|
@@ -106,6 +106,23 @@ export function validateTypeModelErrors(types) {
|
|
|
106
106
|
errors.push([typeName, null, `Inconsistent type name "${typedef.name}" for "${typeName}"`]);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
// Circular typedef?
|
|
110
|
+
const visited = new Set([typeName]);
|
|
111
|
+
let typedefType = typedef.type;
|
|
112
|
+
while ('user' in typedefType) {
|
|
113
|
+
const typedefUserName = typedefType.user;
|
|
114
|
+
const typedefUserType = Object.hasOwn(types, typedefUserName) ? types[typedefUserName] : null;
|
|
115
|
+
if (typedefUserType === null || !('typedef' in typedefUserType)) {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
if (visited.has(typedefUserName)) {
|
|
119
|
+
errors.push([typeName, null, `Circular typedef detected for type "${typeName}"`]);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
visited.add(typedefUserName);
|
|
123
|
+
typedefType = typedefUserType.typedef.type;
|
|
124
|
+
}
|
|
125
|
+
|
|
109
126
|
// Check the type and its attributes
|
|
110
127
|
validateTypeModelType(errors, types, typedef.type, 'attr' in typedef ? typedef.attr : null, typeName, null);
|
|
111
128
|
|
|
@@ -129,11 +146,11 @@ export function validateTypeModelErrors(types) {
|
|
|
129
146
|
}
|
|
130
147
|
|
|
131
148
|
// Compute effective input member counts
|
|
132
|
-
const memberSections =
|
|
149
|
+
const memberSections = new Map();
|
|
133
150
|
for (const section of ['path', 'query', 'input']) {
|
|
134
151
|
if (section in action) {
|
|
135
152
|
const sectionTypeName = action[section];
|
|
136
|
-
if (sectionTypeName
|
|
153
|
+
if (Object.hasOwn(types, sectionTypeName)) {
|
|
137
154
|
const sectionUserType = getEffectiveUserType(types, sectionTypeName);
|
|
138
155
|
if (sectionUserType !== null && 'struct' in sectionUserType) {
|
|
139
156
|
const sectionStruct = sectionUserType.struct;
|
|
@@ -142,10 +159,10 @@ export function validateTypeModelErrors(types) {
|
|
|
142
159
|
try {
|
|
143
160
|
for (const member of getStructMembers(types, sectionStruct)) {
|
|
144
161
|
const memberName = member.name;
|
|
145
|
-
if (!(memberName
|
|
146
|
-
memberSections
|
|
162
|
+
if (!memberSections.has(memberName)) {
|
|
163
|
+
memberSections.set(memberName, []);
|
|
147
164
|
}
|
|
148
|
-
memberSections
|
|
165
|
+
memberSections.get(memberName).push(sectionStruct.name);
|
|
149
166
|
}
|
|
150
167
|
} catch {
|
|
151
168
|
// Do nothing
|
|
@@ -156,7 +173,7 @@ export function validateTypeModelErrors(types) {
|
|
|
156
173
|
}
|
|
157
174
|
|
|
158
175
|
// Check for duplicate input members
|
|
159
|
-
for (const [memberName, memberSectionNames] of
|
|
176
|
+
for (const [memberName, memberSectionNames] of memberSections.entries()) {
|
|
160
177
|
if (memberSectionNames.length > 1) {
|
|
161
178
|
for (const sectionType of memberSectionNames.sort()) {
|
|
162
179
|
errors.push([sectionType, memberName, `Redefinition of "${sectionType}" member "${memberName}"`]);
|
|
@@ -170,11 +187,12 @@ export function validateTypeModelErrors(types) {
|
|
|
170
187
|
}
|
|
171
188
|
|
|
172
189
|
|
|
173
|
-
function getEffectiveType(types, type) {
|
|
174
|
-
if ('user' in type && type.user
|
|
190
|
+
function getEffectiveType(types, type, visited = new Set()) {
|
|
191
|
+
if ('user' in type && Object.hasOwn(types, type.user) && !visited.has(type.user)) {
|
|
175
192
|
const userType = types[type.user];
|
|
176
193
|
if ('typedef' in userType) {
|
|
177
|
-
|
|
194
|
+
visited.add(type.user);
|
|
195
|
+
return getEffectiveType(types, userType.typedef.type, visited);
|
|
178
196
|
}
|
|
179
197
|
}
|
|
180
198
|
return type;
|
|
@@ -182,24 +200,24 @@ function getEffectiveType(types, type) {
|
|
|
182
200
|
|
|
183
201
|
|
|
184
202
|
function getEffectiveUserType(types, userTypeName) {
|
|
185
|
-
const userType = userTypeName
|
|
203
|
+
const userType = Object.hasOwn(types, userTypeName) ? types[userTypeName] : null;
|
|
186
204
|
if (userType !== null && 'typedef' in userType) {
|
|
187
205
|
const typeEffective = getEffectiveType(types, userType.typedef.type);
|
|
188
206
|
if (!('user' in typeEffective)) {
|
|
189
207
|
return null;
|
|
190
208
|
}
|
|
191
|
-
return typeEffective.user
|
|
209
|
+
return Object.hasOwn(types, typeEffective.user) ? types[typeEffective.user] : null;
|
|
192
210
|
}
|
|
193
211
|
return userType;
|
|
194
212
|
}
|
|
195
213
|
|
|
196
214
|
|
|
197
|
-
function getStructMembers(types, struct, visited =
|
|
215
|
+
function getStructMembers(types, struct, visited = new Set()) {
|
|
198
216
|
return getTypeItems(types, struct, visited, 'struct', 'members');
|
|
199
217
|
}
|
|
200
218
|
|
|
201
219
|
|
|
202
|
-
function getEnumValues(types, enum_, visited =
|
|
220
|
+
function getEnumValues(types, enum_, visited = new Set()) {
|
|
203
221
|
return getTypeItems(types, enum_, visited, 'enum', 'values');
|
|
204
222
|
}
|
|
205
223
|
|
|
@@ -211,8 +229,8 @@ function getTypeItems(types, type, visited, defName, memberName) {
|
|
|
211
229
|
const userType = getEffectiveUserType(types, base);
|
|
212
230
|
if (userType !== null && defName in userType) {
|
|
213
231
|
const userTypeName = userType[defName].name;
|
|
214
|
-
if (!(userTypeName
|
|
215
|
-
visited
|
|
232
|
+
if (!visited.has(userTypeName)) {
|
|
233
|
+
visited.add(userTypeName);
|
|
216
234
|
items.push(...getTypeItems(types, userType[defName], visited, defName, memberName));
|
|
217
235
|
} else {
|
|
218
236
|
throw new TypeError();
|
|
@@ -285,7 +303,7 @@ function validateTypeModelType(errors, types, type, attr, typeName, memberName)
|
|
|
285
303
|
|
|
286
304
|
// Valid dict key type (string or enum)
|
|
287
305
|
if (!('builtin' in dictKeyType && dictKeyType.builtin === 'string') &&
|
|
288
|
-
!('user' in dictKeyType && dictKeyType.user
|
|
306
|
+
!('user' in dictKeyType && Object.hasOwn(types, dictKeyType.user) && 'enum' in types[dictKeyType.user])) {
|
|
289
307
|
error('Invalid dictionary key type');
|
|
290
308
|
}
|
|
291
309
|
}
|
|
@@ -295,7 +313,7 @@ function validateTypeModelType(errors, types, type, attr, typeName, memberName)
|
|
|
295
313
|
const userTypeName = type.user;
|
|
296
314
|
|
|
297
315
|
// Unknown user type?
|
|
298
|
-
if (!(userTypeName
|
|
316
|
+
if (!Object.hasOwn(types, userTypeName)) {
|
|
299
317
|
error(`Unknown type "${userTypeName}"`);
|
|
300
318
|
} else {
|
|
301
319
|
const userType = types[userTypeName];
|
|
@@ -312,7 +330,7 @@ function validateTypeModelType(errors, types, type, attr, typeName, memberName)
|
|
|
312
330
|
const typeEffective = getEffectiveType(types, type);
|
|
313
331
|
const [typeKey] = Object.keys(typeEffective);
|
|
314
332
|
const typeAttrKey = typeKey === 'builtin' ? typeEffective[typeKey] : typeKey;
|
|
315
|
-
const allowedAttr = typeAttrKey
|
|
333
|
+
const allowedAttr = Object.hasOwn(typeToAllowedAttr, typeAttrKey) ? typeToAllowedAttr[typeAttrKey] : null;
|
|
316
334
|
const disallowedAttr = Object.fromEntries(Object.keys(attr).map((attrKey) => [attrKey, null]));
|
|
317
335
|
delete disallowedAttr.nullable;
|
|
318
336
|
if (allowedAttr !== null) {
|