schema-markdown 1.2.13 → 1.3.1
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/README.md +8 -2
- package/lib/encode.js +8 -3
- package/lib/parser.js +36 -22
- package/lib/schema.js +183 -77
- package/lib/schemaUtil.js +38 -38
- package/lib/typeModel.js +4 -4
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ Validation fails if the object does not match the schema:
|
|
|
63
63
|
try {
|
|
64
64
|
validateType(modelTypes, 'Aggregation', {'numbers': [1, 2, 'asdf', 4]});
|
|
65
65
|
} catch ({message}) {
|
|
66
|
-
console.assert(message ===
|
|
66
|
+
console.assert(message === 'Invalid value "asdf" (type "string") for member "numbers.2", expected type "int"', message);
|
|
67
67
|
}
|
|
68
68
|
~~~
|
|
69
69
|
|
|
@@ -73,10 +73,16 @@ Validation also fails if a member constraint is violated:
|
|
|
73
73
|
try {
|
|
74
74
|
validateType(modelTypes, 'Aggregation', {'numbers': []});
|
|
75
75
|
} catch ({message}) {
|
|
76
|
-
console.assert(message ===
|
|
76
|
+
console.assert(message === 'Invalid value [] (type "object") for member "numbers", expected type "array" [len > 0]', message);
|
|
77
77
|
}
|
|
78
78
|
~~~
|
|
79
79
|
|
|
80
|
+
`validateType` validates JSON-like values. In this JavaScript implementation, date values are
|
|
81
|
+
`Date` objects (time-of-day cleared), datetime values are `Date` objects, and uuid strings remain
|
|
82
|
+
strings. A `Date` input is left as a `Date` (date values have time-of-day cleared). See
|
|
83
|
+
[The Schema Markdown Language - Built-in Types](https://craigahobbs.github.io/schema-markdown-js/language/#built-in-types)
|
|
84
|
+
for accepted JSON input formats and host-runtime behavior.
|
|
85
|
+
|
|
80
86
|
|
|
81
87
|
## Schema Documentation
|
|
82
88
|
|
package/lib/encode.js
CHANGED
|
@@ -108,9 +108,14 @@ export function decodeQueryString(paramStr) {
|
|
|
108
108
|
parent[keyParent] = obj;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
// Create the index for this key
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
// Create the index for this key - check own properties only, and define "__proto__"
|
|
112
|
+
// directly since assignment would invoke the prototype setter
|
|
113
|
+
if (!Object.hasOwn(obj, key)) {
|
|
114
|
+
if (key === '__proto__') {
|
|
115
|
+
Object.defineProperty(obj, key, {'value': null, 'enumerable': true, 'writable': true, 'configurable': true});
|
|
116
|
+
} else {
|
|
117
|
+
obj[key] = null;
|
|
118
|
+
}
|
|
114
119
|
}
|
|
115
120
|
}
|
|
116
121
|
|
package/lib/parser.js
CHANGED
|
@@ -24,7 +24,7 @@ const rFindAttrs = new RegExp(`${rPartAttr}(?:\\s*,\\s*|\\s*$)`, 'g');
|
|
|
24
24
|
const rLineCont = /\\\s*$/;
|
|
25
25
|
const rComment = /^\s*(?:#-.*|#(?<doc>.*))?$/;
|
|
26
26
|
const rGroup = /^group(?:\s+"(?<group>.+?)")?\s*$/;
|
|
27
|
-
const rAction = new RegExp(`^action\\s+(?<id>${rPartId})
|
|
27
|
+
const rAction = new RegExp(`^action\\s+(?<id>${rPartId})\\s*$`);
|
|
28
28
|
const rPartBaseIds = `(?:\\s*\\(\\s*(?<baseIds>${rPartId}(?:\\s*,\\s*${rPartId})*)\\s*\\)\\s*)`;
|
|
29
29
|
const rBaseIdsSplit = /\s*,\s*/;
|
|
30
30
|
const rDefinition = new RegExp(`^(?<type>struct|union|enum)\\s+(?<id>${rPartId})${rPartBaseIds}?\\s*$`);
|
|
@@ -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
|
/**
|
|
@@ -107,7 +108,7 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
|
|
|
107
108
|
linenum += 1;
|
|
108
109
|
|
|
109
110
|
// Line continuation?
|
|
110
|
-
const linePartNoContinuation = linePart.replace(rLineCont, '');
|
|
111
|
+
const linePartNoContinuation = linePart.includes('\\') ? linePart.replace(rLineCont, '') : linePart;
|
|
111
112
|
if (lineContinuation.length || linePartNoContinuation !== linePart) {
|
|
112
113
|
lineContinuation.push(linePartNoContinuation);
|
|
113
114
|
}
|
|
@@ -122,47 +123,49 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
|
|
|
122
123
|
line = linePart;
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
// Match syntax
|
|
126
|
+
// Match syntax - each regex is guarded by a cheap string test that only skips it when it cannot match
|
|
127
|
+
const lineIndented = rIndent.test(line);
|
|
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;
|
|
128
131
|
if (match === null) {
|
|
129
132
|
matchName = 'group';
|
|
130
|
-
match = line.match(rGroup);
|
|
133
|
+
match = line.startsWith('group') ? line.match(rGroup) : null;
|
|
131
134
|
}
|
|
132
135
|
if (match === null) {
|
|
133
136
|
matchName = 'action';
|
|
134
|
-
match = line.match(rAction);
|
|
137
|
+
match = line.startsWith('action') ? line.match(rAction) : null;
|
|
135
138
|
}
|
|
136
139
|
if (match === null) {
|
|
137
140
|
matchName = 'definition';
|
|
138
|
-
match = line.match(rDefinition);
|
|
141
|
+
match = (line.startsWith('struct') || line.startsWith('union') || line.startsWith('enum')) ? line.match(rDefinition) : null;
|
|
139
142
|
}
|
|
140
|
-
if (match === null && action !== null) {
|
|
143
|
+
if (match === null && action !== null && lineIndented) {
|
|
141
144
|
matchName = 'section';
|
|
142
145
|
match = line.match(rSection);
|
|
143
146
|
}
|
|
144
|
-
if (match === null && action !== null) {
|
|
147
|
+
if (match === null && action !== null && lineIndented) {
|
|
145
148
|
matchName = 'sectionPlain';
|
|
146
149
|
match = line.match(rSectionPlain);
|
|
147
150
|
}
|
|
148
|
-
if (match === null && userType !== null && 'enum' in userType) {
|
|
151
|
+
if (match === null && userType !== null && 'enum' in userType && lineIndented) {
|
|
149
152
|
matchName = 'value';
|
|
150
153
|
match = line.match(rValue);
|
|
151
154
|
if (match === null) {
|
|
152
155
|
match = line.match(rValueQuoted);
|
|
153
156
|
}
|
|
154
157
|
}
|
|
155
|
-
if (match === null && userType !== null && 'struct' in userType) {
|
|
158
|
+
if (match === null && userType !== null && 'struct' in userType && lineIndented) {
|
|
156
159
|
matchName = 'member';
|
|
157
160
|
match = line.match(rMember);
|
|
158
161
|
}
|
|
159
|
-
if (match === null && urls !== null) {
|
|
162
|
+
if (match === null && urls !== null && lineIndented) {
|
|
160
163
|
matchName = 'urls';
|
|
161
164
|
match = line.match(rURL);
|
|
162
165
|
}
|
|
163
166
|
if (match === null) {
|
|
164
167
|
matchName = 'typedef';
|
|
165
|
-
match = line.match(rTypedef);
|
|
168
|
+
match = line.startsWith('typedef') ? line.match(rTypedef) : null;
|
|
166
169
|
}
|
|
167
170
|
if (match === null) {
|
|
168
171
|
matchName = null;
|
|
@@ -189,7 +192,7 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
|
|
|
189
192
|
const actionId = match.groups.id;
|
|
190
193
|
|
|
191
194
|
// Action already defined?
|
|
192
|
-
if (actionId
|
|
195
|
+
if (Object.hasOwn(types, actionId)) {
|
|
193
196
|
addError(`Redefinition of action '${actionId}'`, filename, linenum);
|
|
194
197
|
}
|
|
195
198
|
|
|
@@ -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
|
|
|
@@ -311,8 +314,12 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
|
|
|
311
314
|
// Clear parser state
|
|
312
315
|
userType = null;
|
|
313
316
|
|
|
314
|
-
// Update the parser state
|
|
317
|
+
// Update the parser state - attach urls immediately so a second empty
|
|
318
|
+
// "urls" section is detected as a redefinition (even with no URL lines)
|
|
315
319
|
urls = [];
|
|
320
|
+
if (!(sectionString in action)) {
|
|
321
|
+
action[sectionString] = urls;
|
|
322
|
+
}
|
|
316
323
|
|
|
317
324
|
// Enum value?
|
|
318
325
|
} else if (matchName === 'value') {
|
|
@@ -383,9 +390,6 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
|
|
|
383
390
|
}
|
|
384
391
|
|
|
385
392
|
// Add the URL
|
|
386
|
-
if (!('urls' in action)) {
|
|
387
|
-
action.urls = urls;
|
|
388
|
-
}
|
|
389
393
|
urls.push(actionUrl);
|
|
390
394
|
|
|
391
395
|
// Typedef?
|
|
@@ -393,7 +397,7 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
|
|
|
393
397
|
const definitionId = match.groups.id;
|
|
394
398
|
|
|
395
399
|
// Type already defined?
|
|
396
|
-
if (builtinTypes.has(definitionId) ||
|
|
400
|
+
if (builtinTypes.has(definitionId) || Object.hasOwn(builtinDeprecated, definitionId) || Object.hasOwn(types, definitionId)) {
|
|
397
401
|
addError(`Redefinition of type '${definitionId}'`, filename, linenum);
|
|
398
402
|
}
|
|
399
403
|
|
|
@@ -429,6 +433,16 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
|
|
|
429
433
|
}
|
|
430
434
|
}
|
|
431
435
|
|
|
436
|
+
// Drop empty action urls arrays (type model requires len > 0 when present)
|
|
437
|
+
for (const userTypeIter of Object.values(types)) {
|
|
438
|
+
if ('action' in userTypeIter) {
|
|
439
|
+
const actionType = userTypeIter.action;
|
|
440
|
+
if ('urls' in actionType && actionType.urls.length === 0) {
|
|
441
|
+
delete actionType.urls;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
432
446
|
// Validate the type model, if requested
|
|
433
447
|
if (validate) {
|
|
434
448
|
for (const [typeName, memberName, errorMsg] of validateTypeModelErrors(types)) {
|
|
@@ -438,7 +452,7 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
|
|
|
438
452
|
errorLinenum = filepos[`${typeName}.${memberName}`] ?? null;
|
|
439
453
|
}
|
|
440
454
|
if (errorLinenum === null) {
|
|
441
|
-
errorLinenum = filepos[typeName]
|
|
455
|
+
errorLinenum = Object.hasOwn(filepos, typeName) ? filepos[typeName] : null;
|
|
442
456
|
}
|
|
443
457
|
if (errorLinenum === null) {
|
|
444
458
|
errorFilename = '';
|
|
@@ -516,7 +530,7 @@ function parseTypedef(matchTypedef) {
|
|
|
516
530
|
|
|
517
531
|
// Helper function to create a type model
|
|
518
532
|
function createType(typeName) {
|
|
519
|
-
if (typeName
|
|
533
|
+
if (Object.hasOwn(builtinDeprecated, typeName)) {
|
|
520
534
|
return {'builtin': builtinDeprecated[typeName]};
|
|
521
535
|
}
|
|
522
536
|
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,10 +116,37 @@ 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.
|
|
122
143
|
*
|
|
144
|
+
* Validates JSON-like values. In this implementation, date values are `Date` objects (time-of-day
|
|
145
|
+
* cleared), datetime values are `Date` objects, and uuid strings remain strings. A `Date` input is
|
|
146
|
+
* left as a `Date` (date values have time-of-day cleared). See the language documentation on
|
|
147
|
+
* [built-in types]{@link https://craigahobbs.github.io/schema-markdown-js/language/#built-in-types}
|
|
148
|
+
* for accepted JSON input formats and host-runtime behavior.
|
|
149
|
+
*
|
|
123
150
|
* @param {Object} types - The [type model]{@link https://craigahobbs.github.io/bare-script/model/#var.vURL=''&var.vName='Types'}
|
|
124
151
|
* @param {string} typeName - The type name
|
|
125
152
|
* @param {Object} value - The value object to validate
|
|
@@ -128,16 +155,18 @@ export class ValidationError extends Error {
|
|
|
128
155
|
* @throws [ValidationError]{@link module:lib/schema.ValidationError}
|
|
129
156
|
*/
|
|
130
157
|
export function validateType(types, typeName, value, memberFqn = null) {
|
|
131
|
-
if (!(typeName in types)) {
|
|
132
|
-
throw new ValidationError(`Unknown type '${typeName}'`);
|
|
133
|
-
}
|
|
134
158
|
return validateTypeHelper(types, {'user': typeName}, value, memberFqn);
|
|
135
159
|
}
|
|
136
160
|
|
|
137
161
|
|
|
138
162
|
// Regular expressions used by validateTypeHelper
|
|
139
163
|
const rDate = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/;
|
|
140
|
-
const rDatetime =
|
|
164
|
+
const rDatetime = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
165
|
+
const rUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
// Default dict key type used by validateTypeHelper
|
|
169
|
+
const defaultDictKeyType = {'builtin': 'string'};
|
|
141
170
|
|
|
142
171
|
|
|
143
172
|
function validateTypeHelper(types, type, value, memberFqn) {
|
|
@@ -147,18 +176,13 @@ function validateTypeHelper(types, type, value, memberFqn) {
|
|
|
147
176
|
if ('builtin' in type) {
|
|
148
177
|
const {builtin} = type;
|
|
149
178
|
|
|
150
|
-
// string
|
|
151
|
-
if (builtin === 'string'
|
|
179
|
+
// string?
|
|
180
|
+
if (builtin === 'string') {
|
|
152
181
|
// Not a string?
|
|
153
182
|
if (typeof value !== 'string') {
|
|
154
183
|
throwMemberError(type, value, memberFqn);
|
|
155
184
|
}
|
|
156
185
|
|
|
157
|
-
// Not a valid UUID?
|
|
158
|
-
if (builtin === 'uuid' && !value.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i)) {
|
|
159
|
-
throwMemberError(type, value, memberFqn);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
186
|
// int or float?
|
|
163
187
|
} else if (builtin === 'int' || builtin === 'float') {
|
|
164
188
|
// Convert string?
|
|
@@ -195,31 +219,61 @@ function validateTypeHelper(types, type, value, memberFqn) {
|
|
|
195
219
|
throwMemberError(type, value, memberFqn);
|
|
196
220
|
}
|
|
197
221
|
|
|
198
|
-
// date
|
|
199
|
-
} else if (builtin === 'date'
|
|
222
|
+
// date?
|
|
223
|
+
} else if (builtin === 'date') {
|
|
200
224
|
// Convert string?
|
|
201
225
|
if (typeof value === 'string') {
|
|
202
|
-
//
|
|
226
|
+
// Date-only?
|
|
203
227
|
const mDate = value.match(rDate);
|
|
204
228
|
if (mDate !== null) {
|
|
205
229
|
const year = Number.parseInt(mDate.groups.year, 10);
|
|
206
230
|
const month = Number.parseInt(mDate.groups.month, 10);
|
|
207
231
|
const day = Number.parseInt(mDate.groups.day, 10);
|
|
208
232
|
valueNew = new Date(year, month - 1, day);
|
|
209
|
-
|
|
233
|
+
// new Date rolls an out-of-range month/day over (e.g. Feb 30 -> Mar 1); reject that
|
|
234
|
+
if (valueNew.getMonth() !== month - 1) {
|
|
235
|
+
throwMemberError(type, value, memberFqn);
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
throwMemberError(type, value, memberFqn);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Not a date, or a date with a time-of-day?
|
|
242
|
+
} else if (!(value instanceof Date) ||
|
|
243
|
+
value.getHours() + value.getMinutes() + value.getSeconds() + value.getMilliseconds() !== 0) {
|
|
244
|
+
throwMemberError(type, value, memberFqn);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// datetime?
|
|
248
|
+
} else if (builtin === 'datetime') {
|
|
249
|
+
// Convert string?
|
|
250
|
+
if (typeof value === 'string') {
|
|
251
|
+
const mDatetime = value.match(rDatetime);
|
|
252
|
+
if (mDatetime !== null) {
|
|
210
253
|
valueNew = new Date(value);
|
|
254
|
+
const year = Number.parseInt(mDatetime.groups.year, 10);
|
|
255
|
+
const month = Number.parseInt(mDatetime.groups.month, 10);
|
|
256
|
+
const day = Number.parseInt(mDatetime.groups.day, 10);
|
|
257
|
+
// new Date rolls an out-of-range day over (e.g. Feb 30 -> Mar 1); reject that
|
|
258
|
+
if (Number.isNaN(valueNew.getTime()) || new Date(year, month - 1, day).getMonth() !== month - 1) {
|
|
259
|
+
throwMemberError(type, value, memberFqn);
|
|
260
|
+
}
|
|
211
261
|
} else {
|
|
212
262
|
throwMemberError(type, value, memberFqn);
|
|
213
263
|
}
|
|
214
264
|
|
|
215
|
-
// Not a
|
|
265
|
+
// Not a datetime?
|
|
216
266
|
} else if (!(value instanceof Date)) {
|
|
217
267
|
throwMemberError(type, value, memberFqn);
|
|
218
268
|
}
|
|
219
269
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
270
|
+
// uuid?
|
|
271
|
+
} else if (builtin === 'uuid') {
|
|
272
|
+
if (typeof value !== 'string') {
|
|
273
|
+
throwMemberError(type, value, memberFqn);
|
|
274
|
+
}
|
|
275
|
+
if (!rUuid.test(value)) {
|
|
276
|
+
throwMemberError(type, value, memberFqn);
|
|
223
277
|
}
|
|
224
278
|
}
|
|
225
279
|
|
|
@@ -239,13 +293,15 @@ function validateTypeHelper(types, type, value, memberFqn) {
|
|
|
239
293
|
const valueCopy = [];
|
|
240
294
|
const arrayValueNullable = arrayAttr !== null && 'nullable' in arrayAttr && arrayAttr.nullable;
|
|
241
295
|
for (let ixArrayValue = 0; ixArrayValue < valueNew.length; ixArrayValue++) {
|
|
242
|
-
const memberFqnValue = memberFqn
|
|
296
|
+
const memberFqnValue = [memberFqn, ixArrayValue];
|
|
243
297
|
let arrayValue = valueNew[ixArrayValue];
|
|
244
298
|
if (arrayValueNullable && (arrayValue === null || arrayValue === 'null')) {
|
|
245
299
|
arrayValue = null;
|
|
246
300
|
} else {
|
|
247
301
|
arrayValue = validateTypeHelper(types, arrayType, arrayValue, memberFqnValue);
|
|
248
|
-
|
|
302
|
+
if (arrayAttr !== null) {
|
|
303
|
+
validateAttr(arrayType, arrayAttr, arrayValue, memberFqnValue);
|
|
304
|
+
}
|
|
249
305
|
}
|
|
250
306
|
valueCopy.push(arrayValue);
|
|
251
307
|
}
|
|
@@ -259,27 +315,30 @@ function validateTypeHelper(types, type, value, memberFqn) {
|
|
|
259
315
|
const {dict} = type;
|
|
260
316
|
const dictType = dict.type;
|
|
261
317
|
const dictAttr = 'attr' in dict ? dict.attr : null;
|
|
262
|
-
const dictKeyType = 'keyType' in dict ? dict.keyType :
|
|
318
|
+
const dictKeyType = 'keyType' in dict ? dict.keyType : defaultDictKeyType;
|
|
263
319
|
const dictKeyAttr = 'keyAttr' in dict ? dict.keyAttr : null;
|
|
264
320
|
if (value === '') {
|
|
265
321
|
valueNew = {};
|
|
266
|
-
} else if (value === null || typeof value !== 'object') {
|
|
322
|
+
} else if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
267
323
|
throwMemberError(type, value, memberFqn);
|
|
268
324
|
}
|
|
269
325
|
|
|
270
326
|
// Validate the dict key/value pairs
|
|
271
|
-
const
|
|
327
|
+
const isMap = valueNew instanceof Map;
|
|
328
|
+
const valueCopy = isMap ? new Map() : {};
|
|
272
329
|
const dictKeyNullable = dictKeyAttr !== null && 'nullable' in dictKeyAttr && dictKeyAttr.nullable;
|
|
273
330
|
const dictValueNullable = dictAttr !== null && 'nullable' in dictAttr && dictAttr.nullable;
|
|
274
|
-
for (let [dictKey, dictValue] of (
|
|
275
|
-
const memberFqnKey = memberFqn
|
|
331
|
+
for (let [dictKey, dictValue] of (isMap ? valueNew.entries() : Object.entries(valueNew))) {
|
|
332
|
+
const memberFqnKey = [memberFqn, dictKey];
|
|
276
333
|
|
|
277
334
|
// Validate the key
|
|
278
335
|
if (dictKeyNullable && (dictKey === null || dictKey === 'null')) {
|
|
279
336
|
dictKey = null;
|
|
280
337
|
} else {
|
|
281
338
|
dictKey = validateTypeHelper(types, dictKeyType, dictKey, memberFqn);
|
|
282
|
-
|
|
339
|
+
if (dictKeyAttr !== null) {
|
|
340
|
+
validateAttr(dictKeyType, dictKeyAttr, dictKey, memberFqn);
|
|
341
|
+
}
|
|
283
342
|
}
|
|
284
343
|
|
|
285
344
|
// Validate the value
|
|
@@ -287,14 +346,16 @@ function validateTypeHelper(types, type, value, memberFqn) {
|
|
|
287
346
|
dictValue = null;
|
|
288
347
|
} else {
|
|
289
348
|
dictValue = validateTypeHelper(types, dictType, dictValue, memberFqnKey);
|
|
290
|
-
|
|
349
|
+
if (dictAttr !== null) {
|
|
350
|
+
validateAttr(dictType, dictAttr, dictValue, memberFqnKey);
|
|
351
|
+
}
|
|
291
352
|
}
|
|
292
353
|
|
|
293
354
|
// Copy the key/value
|
|
294
|
-
if (
|
|
355
|
+
if (isMap) {
|
|
295
356
|
valueCopy.set(dictKey, dictValue);
|
|
296
357
|
} else {
|
|
297
|
-
valueCopy
|
|
358
|
+
setValue(valueCopy, dictKey, dictValue);
|
|
298
359
|
}
|
|
299
360
|
}
|
|
300
361
|
|
|
@@ -303,7 +364,7 @@ function validateTypeHelper(types, type, value, memberFqn) {
|
|
|
303
364
|
|
|
304
365
|
// User type?
|
|
305
366
|
} else if ('user' in type) {
|
|
306
|
-
const userType = types
|
|
367
|
+
const userType = getUserType(types, type.user);
|
|
307
368
|
|
|
308
369
|
// action?
|
|
309
370
|
if ('action' in userType) {
|
|
@@ -321,7 +382,9 @@ function validateTypeHelper(types, type, value, memberFqn) {
|
|
|
321
382
|
valueNew = null;
|
|
322
383
|
} else {
|
|
323
384
|
valueNew = validateTypeHelper(types, typedef.type, value, memberFqn);
|
|
324
|
-
|
|
385
|
+
if (typedefAttr !== null) {
|
|
386
|
+
validateAttr(type, typedefAttr, valueNew, memberFqn);
|
|
387
|
+
}
|
|
325
388
|
}
|
|
326
389
|
|
|
327
390
|
// enum?
|
|
@@ -340,58 +403,64 @@ function validateTypeHelper(types, type, value, memberFqn) {
|
|
|
340
403
|
// Valid value type?
|
|
341
404
|
if (value === '') {
|
|
342
405
|
valueNew = {};
|
|
343
|
-
} else if (value === null || typeof value !== 'object') {
|
|
406
|
+
} else if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
344
407
|
throwMemberError({'user': struct.name}, value, memberFqn);
|
|
345
408
|
}
|
|
346
409
|
|
|
347
410
|
// Valid union?
|
|
411
|
+
const isMap = valueNew instanceof Map;
|
|
348
412
|
const isUnion = 'union' in struct ? struct.union : false;
|
|
349
413
|
if (isUnion) {
|
|
350
|
-
if (Object.keys(
|
|
414
|
+
if ((isMap ? valueNew.size : Object.keys(valueNew).length) !== 1) {
|
|
351
415
|
throwMemberError({'user': struct.name}, value, memberFqn);
|
|
352
416
|
}
|
|
353
417
|
}
|
|
354
418
|
|
|
355
419
|
// Validate the struct members
|
|
356
|
-
const valueCopy =
|
|
420
|
+
const valueCopy = isMap ? new Map() : {};
|
|
357
421
|
for (const member of getStructMembers(types, struct)) {
|
|
358
422
|
const memberName = member.name;
|
|
359
|
-
const memberFqnMember = memberFqn
|
|
423
|
+
const memberFqnMember = [memberFqn, memberName];
|
|
360
424
|
const memberOptional = 'optional' in member && member.optional;
|
|
361
|
-
const
|
|
425
|
+
const memberAttr = 'attr' in member ? member.attr : null;
|
|
426
|
+
const memberNullable = memberAttr !== null && 'nullable' in memberAttr && memberAttr.nullable;
|
|
362
427
|
|
|
363
|
-
// Missing non-optional member?
|
|
364
|
-
|
|
428
|
+
// Missing non-optional member? Check own properties only - "in" matches inherited
|
|
429
|
+
// Object.prototype properties for member names like "constructor"
|
|
430
|
+
if (!(isMap ? valueNew.has(memberName) : Object.hasOwn(valueNew, memberName))) {
|
|
365
431
|
if (!memberOptional && !isUnion) {
|
|
366
|
-
throw new ValidationError(`Required member
|
|
432
|
+
throw new ValidationError(`Required member "${memberFqnStr(memberFqnMember)}" missing`);
|
|
367
433
|
}
|
|
368
434
|
} else {
|
|
369
435
|
// Validate the member value
|
|
370
|
-
let memberValue =
|
|
436
|
+
let memberValue = isMap ? valueNew.get(memberName) : valueNew[memberName];
|
|
371
437
|
if (memberNullable && (memberValue === null || memberValue === 'null')) {
|
|
372
438
|
memberValue = null;
|
|
373
439
|
} else {
|
|
374
440
|
memberValue = validateTypeHelper(types, member.type, memberValue, memberFqnMember);
|
|
375
|
-
|
|
441
|
+
if (memberAttr !== null) {
|
|
442
|
+
validateAttr(member.type, memberAttr, memberValue, memberFqnMember);
|
|
443
|
+
}
|
|
376
444
|
}
|
|
377
445
|
|
|
378
446
|
// Copy the validated member
|
|
379
|
-
if (
|
|
447
|
+
if (isMap) {
|
|
380
448
|
valueCopy.set(memberName, memberValue);
|
|
381
449
|
} else {
|
|
382
|
-
valueCopy
|
|
450
|
+
setValue(valueCopy, memberName, memberValue);
|
|
383
451
|
}
|
|
384
452
|
}
|
|
385
453
|
}
|
|
386
454
|
|
|
387
455
|
// Any unknown members?
|
|
388
|
-
const
|
|
389
|
-
const
|
|
390
|
-
if (
|
|
456
|
+
const valueCopyCount = isMap ? valueCopy.size : Object.keys(valueCopy).length;
|
|
457
|
+
const valueNewCount = isMap ? valueNew.size : Object.keys(valueNew).length;
|
|
458
|
+
if (valueCopyCount !== valueNewCount) {
|
|
391
459
|
const memberSet = new Set(getStructMembers(types, struct).map((member) => member.name));
|
|
460
|
+
const valueNewKeys = isMap ? Array.from(valueNew.keys()) : Object.keys(valueNew);
|
|
392
461
|
const [unknownKey] = valueNewKeys.filter((key) => !memberSet.has(key));
|
|
393
|
-
const unknownFqn = memberFqn
|
|
394
|
-
throw new ValidationError(`Unknown member
|
|
462
|
+
const unknownFqn = memberFqnStr([memberFqn, unknownKey]);
|
|
463
|
+
throw new ValidationError(`Unknown member "${unknownFqn.slice(0, 100)}"`);
|
|
395
464
|
}
|
|
396
465
|
|
|
397
466
|
// Return the validated, transformed copy
|
|
@@ -403,36 +472,56 @@ function validateTypeHelper(types, type, value, memberFqn) {
|
|
|
403
472
|
}
|
|
404
473
|
|
|
405
474
|
|
|
475
|
+
// Helper to materialize a member's fully-qualified name string. For performance, member FQNs are
|
|
476
|
+
// built lazily as [parent, key] pairs and only converted to strings on error.
|
|
477
|
+
function memberFqnStr(memberFqn) {
|
|
478
|
+
if (memberFqn === null || typeof memberFqn === 'string') {
|
|
479
|
+
return memberFqn;
|
|
480
|
+
}
|
|
481
|
+
const parts = [];
|
|
482
|
+
let fqnPart = memberFqn;
|
|
483
|
+
while (Array.isArray(fqnPart)) {
|
|
484
|
+
parts.push(`${fqnPart[1]}`);
|
|
485
|
+
[fqnPart] = fqnPart;
|
|
486
|
+
}
|
|
487
|
+
if (fqnPart !== null) {
|
|
488
|
+
parts.push(`${fqnPart}`);
|
|
489
|
+
}
|
|
490
|
+
parts.reverse();
|
|
491
|
+
return parts.join('.');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
|
|
406
495
|
function throwMemberError(type, value, memberFqn, attr = null) {
|
|
407
|
-
const
|
|
496
|
+
const memberFqnValue = memberFqnStr(memberFqn);
|
|
497
|
+
const memberPart = memberFqnValue ? ` for member "${memberFqnValue}"` : '';
|
|
408
498
|
const typeName = 'builtin' in type ? type.builtin : ('array' in type ? 'array' : ('dict' in type ? 'dict' : type.user));
|
|
409
499
|
const attrPart = attr !== null ? ` [${attr}]` : '';
|
|
410
500
|
const valueStr = `${JSON.stringify(value)}`;
|
|
411
|
-
const msg = `Invalid value ${valueStr.slice(0, 100)} (type
|
|
412
|
-
throw new ValidationError(msg,
|
|
501
|
+
const msg = `Invalid value ${valueStr.slice(0, 100)} (type "${typeof value}")${memberPart}, expected type "${typeName}"${attrPart}`;
|
|
502
|
+
throw new ValidationError(msg, memberFqnValue);
|
|
413
503
|
}
|
|
414
504
|
|
|
415
505
|
|
|
416
506
|
function validateAttr(type, attr, value, memberFqn) {
|
|
417
|
-
if (attr
|
|
507
|
+
if ('eq' in attr && !(value === attr.eq)) {
|
|
508
|
+
throwMemberError(type, value, memberFqn, `== ${attr.eq}`);
|
|
509
|
+
}
|
|
510
|
+
if ('lt' in attr && !(value < attr.lt)) {
|
|
511
|
+
throwMemberError(type, value, memberFqn, `< ${attr.lt}`);
|
|
512
|
+
}
|
|
513
|
+
if ('lte' in attr && !(value <= attr.lte)) {
|
|
514
|
+
throwMemberError(type, value, memberFqn, `<= ${attr.lte}`);
|
|
515
|
+
}
|
|
516
|
+
if ('gt' in attr && !(value > attr.gt)) {
|
|
517
|
+
throwMemberError(type, value, memberFqn, `> ${attr.gt}`);
|
|
518
|
+
}
|
|
519
|
+
if ('gte' in attr && !(value >= attr.gte)) {
|
|
520
|
+
throwMemberError(type, value, memberFqn, `>= ${attr.gte}`);
|
|
521
|
+
}
|
|
522
|
+
if ('lenEq' in attr || 'lenLT' in attr || 'lenLTE' in attr || 'lenGT' in attr || 'lenGTE' in attr) {
|
|
418
523
|
const length = Array.isArray(value) || typeof value === 'string' ? value.length
|
|
419
|
-
: (typeof value === 'object' ? Object.keys(value).length : null);
|
|
420
|
-
|
|
421
|
-
if ('eq' in attr && !(value === attr.eq)) {
|
|
422
|
-
throwMemberError(type, value, memberFqn, `== ${attr.eq}`);
|
|
423
|
-
}
|
|
424
|
-
if ('lt' in attr && !(value < attr.lt)) {
|
|
425
|
-
throwMemberError(type, value, memberFqn, `< ${attr.lt}`);
|
|
426
|
-
}
|
|
427
|
-
if ('lte' in attr && !(value <= attr.lte)) {
|
|
428
|
-
throwMemberError(type, value, memberFqn, `<= ${attr.lte}`);
|
|
429
|
-
}
|
|
430
|
-
if ('gt' in attr && !(value > attr.gt)) {
|
|
431
|
-
throwMemberError(type, value, memberFqn, `> ${attr.gt}`);
|
|
432
|
-
}
|
|
433
|
-
if ('gte' in attr && !(value >= attr.gte)) {
|
|
434
|
-
throwMemberError(type, value, memberFqn, `>= ${attr.gte}`);
|
|
435
|
-
}
|
|
524
|
+
: (value instanceof Map ? value.size : (typeof value === 'object' ? Object.keys(value).length : null));
|
|
436
525
|
if ('lenEq' in attr && !(length === attr.lenEq)) {
|
|
437
526
|
throwMemberError(type, value, memberFqn, `len == ${attr.lenEq}`);
|
|
438
527
|
}
|
|
@@ -469,9 +558,9 @@ export function getStructMembers(types, struct) {
|
|
|
469
558
|
// Get base struct members
|
|
470
559
|
const members = [];
|
|
471
560
|
for (const base of struct.bases) {
|
|
472
|
-
let baseUserType = types
|
|
561
|
+
let baseUserType = getUserType(types, base);
|
|
473
562
|
while ('typedef' in baseUserType) {
|
|
474
|
-
baseUserType = types
|
|
563
|
+
baseUserType = getUserType(types, baseUserType.typedef.type.user);
|
|
475
564
|
}
|
|
476
565
|
members.push(...getStructMembers(types, baseUserType.struct));
|
|
477
566
|
}
|
|
@@ -502,9 +591,9 @@ export function getEnumValues(types, enum_) {
|
|
|
502
591
|
// Get base enum values
|
|
503
592
|
const values = [];
|
|
504
593
|
for (const base of enum_.bases) {
|
|
505
|
-
let baseUserType = types
|
|
594
|
+
let baseUserType = getUserType(types, base);
|
|
506
595
|
while ('typedef' in baseUserType) {
|
|
507
|
-
baseUserType = types
|
|
596
|
+
baseUserType = getUserType(types, baseUserType.typedef.type.user);
|
|
508
597
|
}
|
|
509
598
|
values.push(...getEnumValues(types, baseUserType.enum));
|
|
510
599
|
}
|
|
@@ -532,6 +621,23 @@ export function validateTypeModel(types) {
|
|
|
532
621
|
// Do additional type model validation
|
|
533
622
|
const errors = validateTypeModelErrors(validatedTypes);
|
|
534
623
|
if (errors.length) {
|
|
624
|
+
// Sort by (typeName, memberName, message); null memberName as ''
|
|
625
|
+
errors.sort((errorA, errorB) => {
|
|
626
|
+
let result = 0;
|
|
627
|
+
for (let ixPart = 0; ixPart < 3; ixPart++) {
|
|
628
|
+
const partA = errorA[ixPart] ?? '';
|
|
629
|
+
const partB = errorB[ixPart] ?? '';
|
|
630
|
+
if (partA < partB) {
|
|
631
|
+
result = -1;
|
|
632
|
+
break;
|
|
633
|
+
}
|
|
634
|
+
if (partA > partB) {
|
|
635
|
+
result = 1;
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return result;
|
|
640
|
+
});
|
|
535
641
|
throw new ValidationError(errors.map(([,, message]) => message).join('\n'));
|
|
536
642
|
}
|
|
537
643
|
|
package/lib/schemaUtil.js
CHANGED
|
@@ -21,7 +21,7 @@ export function validateTypeModelErrors(types) {
|
|
|
21
21
|
|
|
22
22
|
// Inconsistent type name?
|
|
23
23
|
if (typeName !== struct.name) {
|
|
24
|
-
errors.push([typeName, null, `Inconsistent type name
|
|
24
|
+
errors.push([typeName, null, `Inconsistent type name "${struct.name}" for "${typeName}"`]);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
// Check base types
|
|
@@ -36,29 +36,29 @@ export function validateTypeModelErrors(types) {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
if (invalidBase) {
|
|
39
|
-
errors.push([typeName, null, `Invalid struct base type
|
|
39
|
+
errors.push([typeName, null, `Invalid struct base type "${baseName}"`]);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
}
|
|
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
|
-
errors.push([typeName, memberName, `Redefinition of
|
|
54
|
+
errors.push([typeName, memberName, `Redefinition of "${typeName}" member "${memberName}"`]);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
// Check member type and attributes
|
|
58
58
|
validateTypeModelType(errors, types, member.type, 'attr' in member ? member.attr : null, struct.name, member.name);
|
|
59
59
|
}
|
|
60
60
|
} catch {
|
|
61
|
-
errors.push([typeName, null, `Circular base type detected for type
|
|
61
|
+
errors.push([typeName, null, `Circular base type detected for type "${typeName}"`]);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
// Enum?
|
|
@@ -67,7 +67,7 @@ export function validateTypeModelErrors(types) {
|
|
|
67
67
|
|
|
68
68
|
// Inconsistent type name?
|
|
69
69
|
if (typeName !== enum_.name) {
|
|
70
|
-
errors.push([typeName, null, `Inconsistent type name
|
|
70
|
+
errors.push([typeName, null, `Inconsistent type name "${enum_.name}" for "${typeName}"`]);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
// Check base types
|
|
@@ -75,26 +75,26 @@ export function validateTypeModelErrors(types) {
|
|
|
75
75
|
for (const baseName of enum_.bases) {
|
|
76
76
|
const baseUserType = getEffectiveUserType(types, baseName);
|
|
77
77
|
if (baseUserType === null || !('enum' in baseUserType)) {
|
|
78
|
-
errors.push([typeName, null, `Invalid enum base type
|
|
78
|
+
errors.push([typeName, null, `Invalid enum base type "${baseName}"`]);
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
}
|
|
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
|
-
errors.push([typeName, valueName, `Redefinition of
|
|
93
|
+
errors.push([typeName, valueName, `Redefinition of "${typeName}" value "${valueName}"`]);
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
} catch {
|
|
97
|
-
errors.push([typeName, null, `Circular base type detected for type
|
|
97
|
+
errors.push([typeName, null, `Circular base type detected for type "${typeName}"`]);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
// Typedef?
|
|
@@ -103,7 +103,7 @@ export function validateTypeModelErrors(types) {
|
|
|
103
103
|
|
|
104
104
|
// Inconsistent type name?
|
|
105
105
|
if (typeName !== typedef.name) {
|
|
106
|
-
errors.push([typeName, null, `Inconsistent type name
|
|
106
|
+
errors.push([typeName, null, `Inconsistent type name "${typedef.name}" for "${typeName}"`]);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
// Check the type and its attributes
|
|
@@ -115,7 +115,7 @@ export function validateTypeModelErrors(types) {
|
|
|
115
115
|
|
|
116
116
|
// Inconsistent type name?
|
|
117
117
|
if (typeName !== action.name) {
|
|
118
|
-
errors.push([typeName, null, `Inconsistent type name
|
|
118
|
+
errors.push([typeName, null, `Inconsistent type name "${action.name}" for "${typeName}"`]);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
// Check action section types
|
|
@@ -129,11 +129,11 @@ export function validateTypeModelErrors(types) {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
// Compute effective input member counts
|
|
132
|
-
const memberSections =
|
|
132
|
+
const memberSections = new Map();
|
|
133
133
|
for (const section of ['path', 'query', 'input']) {
|
|
134
134
|
if (section in action) {
|
|
135
135
|
const sectionTypeName = action[section];
|
|
136
|
-
if (sectionTypeName
|
|
136
|
+
if (Object.hasOwn(types, sectionTypeName)) {
|
|
137
137
|
const sectionUserType = getEffectiveUserType(types, sectionTypeName);
|
|
138
138
|
if (sectionUserType !== null && 'struct' in sectionUserType) {
|
|
139
139
|
const sectionStruct = sectionUserType.struct;
|
|
@@ -142,10 +142,10 @@ export function validateTypeModelErrors(types) {
|
|
|
142
142
|
try {
|
|
143
143
|
for (const member of getStructMembers(types, sectionStruct)) {
|
|
144
144
|
const memberName = member.name;
|
|
145
|
-
if (!(memberName
|
|
146
|
-
memberSections
|
|
145
|
+
if (!memberSections.has(memberName)) {
|
|
146
|
+
memberSections.set(memberName, []);
|
|
147
147
|
}
|
|
148
|
-
memberSections
|
|
148
|
+
memberSections.get(memberName).push(sectionStruct.name);
|
|
149
149
|
}
|
|
150
150
|
} catch {
|
|
151
151
|
// Do nothing
|
|
@@ -156,10 +156,10 @@ export function validateTypeModelErrors(types) {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
// Check for duplicate input members
|
|
159
|
-
for (const [memberName, memberSectionNames] of
|
|
159
|
+
for (const [memberName, memberSectionNames] of memberSections.entries()) {
|
|
160
160
|
if (memberSectionNames.length > 1) {
|
|
161
161
|
for (const sectionType of memberSectionNames.sort()) {
|
|
162
|
-
errors.push([sectionType, memberName, `Redefinition of
|
|
162
|
+
errors.push([sectionType, memberName, `Redefinition of "${sectionType}" member "${memberName}"`]);
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
165
|
}
|
|
@@ -171,7 +171,7 @@ export function validateTypeModelErrors(types) {
|
|
|
171
171
|
|
|
172
172
|
|
|
173
173
|
function getEffectiveType(types, type) {
|
|
174
|
-
if ('user' in type && type.user
|
|
174
|
+
if ('user' in type && Object.hasOwn(types, type.user)) {
|
|
175
175
|
const userType = types[type.user];
|
|
176
176
|
if ('typedef' in userType) {
|
|
177
177
|
return getEffectiveType(types, userType.typedef.type);
|
|
@@ -182,24 +182,24 @@ function getEffectiveType(types, type) {
|
|
|
182
182
|
|
|
183
183
|
|
|
184
184
|
function getEffectiveUserType(types, userTypeName) {
|
|
185
|
-
const userType = userTypeName
|
|
185
|
+
const userType = Object.hasOwn(types, userTypeName) ? types[userTypeName] : null;
|
|
186
186
|
if (userType !== null && 'typedef' in userType) {
|
|
187
187
|
const typeEffective = getEffectiveType(types, userType.typedef.type);
|
|
188
188
|
if (!('user' in typeEffective)) {
|
|
189
189
|
return null;
|
|
190
190
|
}
|
|
191
|
-
return typeEffective.user
|
|
191
|
+
return Object.hasOwn(types, typeEffective.user) ? types[typeEffective.user] : null;
|
|
192
192
|
}
|
|
193
193
|
return userType;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
|
|
197
|
-
function getStructMembers(types, struct, visited =
|
|
197
|
+
function getStructMembers(types, struct, visited = new Set()) {
|
|
198
198
|
return getTypeItems(types, struct, visited, 'struct', 'members');
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
|
|
202
|
-
function getEnumValues(types, enum_, visited =
|
|
202
|
+
function getEnumValues(types, enum_, visited = new Set()) {
|
|
203
203
|
return getTypeItems(types, enum_, visited, 'enum', 'values');
|
|
204
204
|
}
|
|
205
205
|
|
|
@@ -211,8 +211,8 @@ function getTypeItems(types, type, visited, defName, memberName) {
|
|
|
211
211
|
const userType = getEffectiveUserType(types, base);
|
|
212
212
|
if (userType !== null && defName in userType) {
|
|
213
213
|
const userTypeName = userType[defName].name;
|
|
214
|
-
if (!(userTypeName
|
|
215
|
-
visited
|
|
214
|
+
if (!visited.has(userTypeName)) {
|
|
215
|
+
visited.add(userTypeName);
|
|
216
216
|
items.push(...getTypeItems(types, userType[defName], visited, defName, memberName));
|
|
217
217
|
} else {
|
|
218
218
|
throw new TypeError();
|
|
@@ -256,9 +256,9 @@ function validateTypeModelType(errors, types, type, attr, typeName, memberName)
|
|
|
256
256
|
// Helper function to push an error tuple
|
|
257
257
|
const error = (message) => {
|
|
258
258
|
if (memberName !== null) {
|
|
259
|
-
errors.push([typeName, memberName, `${message} from
|
|
259
|
+
errors.push([typeName, memberName, `${message} from "${typeName}" member "${memberName}"`]);
|
|
260
260
|
} else {
|
|
261
|
-
errors.push([typeName, null, `${message} from
|
|
261
|
+
errors.push([typeName, null, `${message} from "${typeName}"`]);
|
|
262
262
|
}
|
|
263
263
|
};
|
|
264
264
|
|
|
@@ -285,7 +285,7 @@ function validateTypeModelType(errors, types, type, attr, typeName, memberName)
|
|
|
285
285
|
|
|
286
286
|
// Valid dict key type (string or enum)
|
|
287
287
|
if (!('builtin' in dictKeyType && dictKeyType.builtin === 'string') &&
|
|
288
|
-
!('user' in dictKeyType && dictKeyType.user
|
|
288
|
+
!('user' in dictKeyType && Object.hasOwn(types, dictKeyType.user) && 'enum' in types[dictKeyType.user])) {
|
|
289
289
|
error('Invalid dictionary key type');
|
|
290
290
|
}
|
|
291
291
|
}
|
|
@@ -295,14 +295,14 @@ function validateTypeModelType(errors, types, type, attr, typeName, memberName)
|
|
|
295
295
|
const userTypeName = type.user;
|
|
296
296
|
|
|
297
297
|
// Unknown user type?
|
|
298
|
-
if (!(userTypeName
|
|
299
|
-
error(`Unknown type
|
|
298
|
+
if (!Object.hasOwn(types, userTypeName)) {
|
|
299
|
+
error(`Unknown type "${userTypeName}"`);
|
|
300
300
|
} else {
|
|
301
301
|
const userType = types[userTypeName];
|
|
302
302
|
|
|
303
303
|
// Action type references not allowed
|
|
304
304
|
if ('action' in userType) {
|
|
305
|
-
error(`Invalid reference to action
|
|
305
|
+
error(`Invalid reference to action "${userTypeName}"`);
|
|
306
306
|
}
|
|
307
307
|
}
|
|
308
308
|
}
|
|
@@ -312,7 +312,7 @@ function validateTypeModelType(errors, types, type, attr, typeName, memberName)
|
|
|
312
312
|
const typeEffective = getEffectiveType(types, type);
|
|
313
313
|
const [typeKey] = Object.keys(typeEffective);
|
|
314
314
|
const typeAttrKey = typeKey === 'builtin' ? typeEffective[typeKey] : typeKey;
|
|
315
|
-
const allowedAttr = typeAttrKey
|
|
315
|
+
const allowedAttr = Object.hasOwn(typeToAllowedAttr, typeAttrKey) ? typeToAllowedAttr[typeAttrKey] : null;
|
|
316
316
|
const disallowedAttr = Object.fromEntries(Object.keys(attr).map((attrKey) => [attrKey, null]));
|
|
317
317
|
delete disallowedAttr.nullable;
|
|
318
318
|
if (allowedAttr !== null) {
|
|
@@ -324,7 +324,7 @@ function validateTypeModelType(errors, types, type, attr, typeName, memberName)
|
|
|
324
324
|
for (const attrKey of Object.keys(disallowedAttr)) {
|
|
325
325
|
const attrValue = `${attr[attrKey]}`;
|
|
326
326
|
const attrText = `${attrToText[attrKey]} ${attrValue}`;
|
|
327
|
-
error(`Invalid attribute
|
|
327
|
+
error(`Invalid attribute "${attrText}"`);
|
|
328
328
|
}
|
|
329
329
|
}
|
|
330
330
|
}
|
package/lib/typeModel.js
CHANGED
|
@@ -108,16 +108,16 @@ struct Array
|
|
|
108
108
|
# A dictionary type
|
|
109
109
|
struct Dict
|
|
110
110
|
|
|
111
|
-
# The contained
|
|
111
|
+
# The contained value type
|
|
112
112
|
Type type
|
|
113
113
|
|
|
114
|
-
# The contained
|
|
114
|
+
# The contained value type's attributes
|
|
115
115
|
optional Attributes attr
|
|
116
116
|
|
|
117
|
-
# The contained
|
|
117
|
+
# The contained key type
|
|
118
118
|
optional Type keyType
|
|
119
119
|
|
|
120
|
-
# The contained
|
|
120
|
+
# The contained key type's attributes
|
|
121
121
|
optional Attributes keyAttr
|
|
122
122
|
|
|
123
123
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "schema-markdown",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.3.1",
|
|
5
5
|
"description": "A schema definition and validation library",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"schema",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@eslint/js": "~10.0",
|
|
28
28
|
"c8": "~11.0",
|
|
29
|
-
"eslint": "~10.
|
|
30
|
-
"globals": "~17.
|
|
29
|
+
"eslint": "~10.6",
|
|
30
|
+
"globals": "~17.7",
|
|
31
31
|
"jsdoc": "~4.0"
|
|
32
32
|
}
|
|
33
33
|
}
|