schema-markdown 1.3.1 → 1.3.3
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 +2 -0
- package/lib/parser.js +161 -158
- package/lib/schema.js +117 -103
- package/lib/schemaUtil.js +21 -3
- package/package.json +4 -4
package/lib/encode.js
CHANGED
|
@@ -106,6 +106,8 @@ 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
113
|
// Create the index for this key - check own properties only, and define "__proto__"
|
package/lib/parser.js
CHANGED
|
@@ -97,6 +97,9 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
|
|
|
97
97
|
lines.push(...text.split(rLineSplit));
|
|
98
98
|
} else {
|
|
99
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
|
+
}
|
|
100
103
|
lines.push(...textPart.split(rLineSplit));
|
|
101
104
|
}
|
|
102
105
|
}
|
|
@@ -109,66 +112,66 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
|
|
|
109
112
|
|
|
110
113
|
// Line continuation?
|
|
111
114
|
const linePartNoContinuation = linePart.includes('\\') ? linePart.replace(rLineCont, '') : linePart;
|
|
112
|
-
if (lineContinuation.length || linePartNoContinuation !== linePart) {
|
|
113
|
-
lineContinuation.push(linePartNoContinuation);
|
|
114
|
-
}
|
|
115
|
-
if (linePartNoContinuation !== linePart) {
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
115
|
let line;
|
|
119
|
-
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
|
+
}
|
|
120
123
|
line = lineContinuation.join('');
|
|
121
124
|
lineContinuation.length = 0;
|
|
122
|
-
} else {
|
|
123
|
-
line = linePart;
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
// 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
128
|
const lineTrimmed = line.trim();
|
|
129
129
|
let matchName = 'comment';
|
|
130
130
|
let match = (lineTrimmed === '' || lineTrimmed.startsWith('#')) ? line.match(rComment) : null;
|
|
131
|
+
|
|
132
|
+
// Not a comment or blank line? Try the remaining syntax
|
|
131
133
|
if (match === null) {
|
|
134
|
+
const lineIndented = rIndent.test(line);
|
|
132
135
|
matchName = 'group';
|
|
133
136
|
match = line.startsWith('group') ? line.match(rGroup) : null;
|
|
134
|
-
}
|
|
135
|
-
if (match === null) {
|
|
136
|
-
matchName = 'action';
|
|
137
|
-
match = line.startsWith('action') ? line.match(rAction) : null;
|
|
138
|
-
}
|
|
139
|
-
if (match === null) {
|
|
140
|
-
matchName = 'definition';
|
|
141
|
-
match = (line.startsWith('struct') || line.startsWith('union') || line.startsWith('enum')) ? line.match(rDefinition) : null;
|
|
142
|
-
}
|
|
143
|
-
if (match === null && action !== null && lineIndented) {
|
|
144
|
-
matchName = 'section';
|
|
145
|
-
match = line.match(rSection);
|
|
146
|
-
}
|
|
147
|
-
if (match === null && action !== null && lineIndented) {
|
|
148
|
-
matchName = 'sectionPlain';
|
|
149
|
-
match = line.match(rSectionPlain);
|
|
150
|
-
}
|
|
151
|
-
if (match === null && userType !== null && 'enum' in userType && lineIndented) {
|
|
152
|
-
matchName = 'value';
|
|
153
|
-
match = line.match(rValue);
|
|
154
137
|
if (match === null) {
|
|
155
|
-
|
|
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;
|
|
156
174
|
}
|
|
157
|
-
}
|
|
158
|
-
if (match === null && userType !== null && 'struct' in userType && lineIndented) {
|
|
159
|
-
matchName = 'member';
|
|
160
|
-
match = line.match(rMember);
|
|
161
|
-
}
|
|
162
|
-
if (match === null && urls !== null && lineIndented) {
|
|
163
|
-
matchName = 'urls';
|
|
164
|
-
match = line.match(rURL);
|
|
165
|
-
}
|
|
166
|
-
if (match === null) {
|
|
167
|
-
matchName = 'typedef';
|
|
168
|
-
match = line.startsWith('typedef') ? line.match(rTypedef) : null;
|
|
169
|
-
}
|
|
170
|
-
if (match === null) {
|
|
171
|
-
matchName = null;
|
|
172
175
|
}
|
|
173
176
|
|
|
174
177
|
// Comment?
|
|
@@ -178,39 +181,36 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
|
|
|
178
181
|
doc.push(!docString.startsWith(' ') ? docString : docString.slice(1));
|
|
179
182
|
}
|
|
180
183
|
|
|
181
|
-
//
|
|
182
|
-
} else if (matchName === '
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
docGroup = docGroup.trim();
|
|
186
|
-
} else {
|
|
187
|
-
docGroup = null;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Action?
|
|
191
|
-
} else if (matchName === 'action') {
|
|
192
|
-
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;
|
|
193
188
|
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
189
|
+
// Add the member
|
|
190
|
+
const {struct} = userType;
|
|
191
|
+
if (!('members' in struct)) {
|
|
192
|
+
struct.members = [];
|
|
197
193
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (actionDoc !== null) {
|
|
208
|
-
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;
|
|
209
203
|
}
|
|
210
|
-
if (
|
|
211
|
-
|
|
204
|
+
if (memberDoc !== null) {
|
|
205
|
+
member.doc = memberDoc;
|
|
206
|
+
}
|
|
207
|
+
if (optional) {
|
|
208
|
+
member.optional = true;
|
|
212
209
|
}
|
|
213
210
|
|
|
211
|
+
// Record the definition's line number
|
|
212
|
+
filepos[`${struct.name}.${memberName}`] = linenum;
|
|
213
|
+
|
|
214
214
|
// Definition?
|
|
215
215
|
} else if (matchName === 'definition') {
|
|
216
216
|
const definitionString = match.groups.type;
|
|
@@ -267,6 +267,93 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
|
|
|
267
267
|
// Record the definition's line number
|
|
268
268
|
filepos[definitionId] = linenum;
|
|
269
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
|
+
|
|
270
357
|
// Action section?
|
|
271
358
|
} else if (matchName === 'section') {
|
|
272
359
|
const sectionString = match.groups.type;
|
|
@@ -321,55 +408,6 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
|
|
|
321
408
|
action[sectionString] = urls;
|
|
322
409
|
}
|
|
323
410
|
|
|
324
|
-
// Enum value?
|
|
325
|
-
} else if (matchName === 'value') {
|
|
326
|
-
const valueString = match.groups.id;
|
|
327
|
-
|
|
328
|
-
// Add the enum value
|
|
329
|
-
const enum_ = userType.enum;
|
|
330
|
-
if (!('values' in enum_)) {
|
|
331
|
-
enum_.values = [];
|
|
332
|
-
}
|
|
333
|
-
const enumValue = {'name': valueString};
|
|
334
|
-
enum_.values.push(enumValue);
|
|
335
|
-
const enumValueDoc = getDoc();
|
|
336
|
-
if (enumValueDoc !== null) {
|
|
337
|
-
enumValue.doc = enumValueDoc;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Record the definition's line number
|
|
341
|
-
filepos[`${enum_.name}.${valueString}`] = linenum;
|
|
342
|
-
|
|
343
|
-
// Struct member?
|
|
344
|
-
} else if (matchName === 'member') {
|
|
345
|
-
const optional = typeof match.groups.optional !== 'undefined';
|
|
346
|
-
const memberName = match.groups.id;
|
|
347
|
-
|
|
348
|
-
// Add the member
|
|
349
|
-
const {struct} = userType;
|
|
350
|
-
if (!('members' in struct)) {
|
|
351
|
-
struct.members = [];
|
|
352
|
-
}
|
|
353
|
-
const [memberType, memberAttr] = parseTypedef(match);
|
|
354
|
-
const memberDoc = getDoc();
|
|
355
|
-
const member = {
|
|
356
|
-
'name': memberName,
|
|
357
|
-
'type': memberType
|
|
358
|
-
};
|
|
359
|
-
struct.members.push(member);
|
|
360
|
-
if (memberAttr !== null) {
|
|
361
|
-
member.attr = memberAttr;
|
|
362
|
-
}
|
|
363
|
-
if (memberDoc !== null) {
|
|
364
|
-
member.doc = memberDoc;
|
|
365
|
-
}
|
|
366
|
-
if (optional) {
|
|
367
|
-
member.optional = true;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Record the definition's line number
|
|
371
|
-
filepos[`${struct.name}.${memberName}`] = linenum;
|
|
372
|
-
|
|
373
411
|
// URL?
|
|
374
412
|
} else if (matchName === 'urls') {
|
|
375
413
|
const {method, path} = match.groups;
|
|
@@ -392,41 +430,6 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
|
|
|
392
430
|
// Add the URL
|
|
393
431
|
urls.push(actionUrl);
|
|
394
432
|
|
|
395
|
-
// Typedef?
|
|
396
|
-
} else if (matchName === 'typedef') {
|
|
397
|
-
const definitionId = match.groups.id;
|
|
398
|
-
|
|
399
|
-
// Type already defined?
|
|
400
|
-
if (builtinTypes.has(definitionId) || Object.hasOwn(builtinDeprecated, definitionId) || Object.hasOwn(types, definitionId)) {
|
|
401
|
-
addError(`Redefinition of type '${definitionId}'`, filename, linenum);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Clear parser state
|
|
405
|
-
action = null;
|
|
406
|
-
urls = null;
|
|
407
|
-
userType = null;
|
|
408
|
-
const typedefDoc = getDoc();
|
|
409
|
-
|
|
410
|
-
// Create the typedef
|
|
411
|
-
const [typedefType, typedefAttr] = parseTypedef(match);
|
|
412
|
-
const typedef = {
|
|
413
|
-
'name': definitionId,
|
|
414
|
-
'type': typedefType
|
|
415
|
-
};
|
|
416
|
-
types[definitionId] = {'typedef': typedef};
|
|
417
|
-
if (typedefAttr !== null) {
|
|
418
|
-
typedef.attr = typedefAttr;
|
|
419
|
-
}
|
|
420
|
-
if (typedefDoc !== null) {
|
|
421
|
-
typedef.doc = typedefDoc;
|
|
422
|
-
}
|
|
423
|
-
if (docGroup !== null) {
|
|
424
|
-
typedef.docGroup = docGroup;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// Record the definition's line number
|
|
428
|
-
filepos[definitionId] = linenum;
|
|
429
|
-
|
|
430
433
|
// Unrecognized line syntax
|
|
431
434
|
} else {
|
|
432
435
|
addError('Syntax error', filename, linenum);
|
package/lib/schema.js
CHANGED
|
@@ -159,8 +159,17 @@ export function validateType(types, typeName, value, memberFqn = null) {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
|
|
162
|
+
// Helper to create a local date (midnight) - unlike new Date, years 0 through 99 aren't 1900 through 1999
|
|
163
|
+
function newLocalDate(year, month, day) {
|
|
164
|
+
const date = new Date(0);
|
|
165
|
+
date.setFullYear(year, month - 1, day);
|
|
166
|
+
date.setHours(0, 0, 0, 0);
|
|
167
|
+
return date;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
162
171
|
// Regular expressions used by validateTypeHelper
|
|
163
|
-
const rDate = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})
|
|
172
|
+
const rDate = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})(?:T00:00(?::00(?:\.0+)?)?(?:Z|(?<tzhour>[+-]\d{2}):(?<tzminute>\d{2})))?$/;
|
|
164
173
|
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
174
|
const rUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
166
175
|
|
|
@@ -187,10 +196,10 @@ function validateTypeHelper(types, type, value, memberFqn) {
|
|
|
187
196
|
} else if (builtin === 'int' || builtin === 'float') {
|
|
188
197
|
// Convert string?
|
|
189
198
|
if (typeof value === 'string') {
|
|
190
|
-
|
|
199
|
+
valueNew = parseFloat(value);
|
|
200
|
+
if (isNaN(value) || !Number.isFinite(valueNew)) {
|
|
191
201
|
throwMemberError(type, value, memberFqn);
|
|
192
202
|
}
|
|
193
|
-
valueNew = parseFloat(value);
|
|
194
203
|
|
|
195
204
|
// Not a number?
|
|
196
205
|
} else if (typeof value !== 'number') {
|
|
@@ -221,26 +230,27 @@ function validateTypeHelper(types, type, value, memberFqn) {
|
|
|
221
230
|
|
|
222
231
|
// date?
|
|
223
232
|
} else if (builtin === 'date') {
|
|
224
|
-
// Convert string?
|
|
233
|
+
// Convert string? A date, or a datetime at midnight in its own offset - the date is the string's date.
|
|
225
234
|
if (typeof value === 'string') {
|
|
226
|
-
// Date-only?
|
|
227
235
|
const mDate = value.match(rDate);
|
|
228
236
|
if (mDate !== null) {
|
|
229
237
|
const year = Number.parseInt(mDate.groups.year, 10);
|
|
230
238
|
const month = Number.parseInt(mDate.groups.month, 10);
|
|
231
239
|
const day = Number.parseInt(mDate.groups.day, 10);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
240
|
+
const {tzhour = null, tzminute = null} = mDate.groups;
|
|
241
|
+
valueNew = newLocalDate(year, month, day);
|
|
242
|
+
// A date rolls an out-of-range month/day over (e.g. Feb 30 -> Mar 1); reject that, and an invalid offset
|
|
243
|
+
if (valueNew.getMonth() !== month - 1 ||
|
|
244
|
+
(tzhour !== null && (Math.abs(Number.parseInt(tzhour, 10)) > 23 || Number.parseInt(tzminute, 10) > 59))) {
|
|
235
245
|
throwMemberError(type, value, memberFqn);
|
|
236
246
|
}
|
|
237
247
|
} else {
|
|
238
248
|
throwMemberError(type, value, memberFqn);
|
|
239
249
|
}
|
|
240
250
|
|
|
241
|
-
// Not a date
|
|
242
|
-
|
|
243
|
-
|
|
251
|
+
// Not a date - a datetime at the start of its day? A day's start is its local midnight, or its first time when a
|
|
252
|
+
// timezone change skips midnight.
|
|
253
|
+
} else if (!(value instanceof Date) || new Date(value).setHours(0, 0, 0, 0) !== value.getTime()) {
|
|
244
254
|
throwMemberError(type, value, memberFqn);
|
|
245
255
|
}
|
|
246
256
|
|
|
@@ -254,8 +264,8 @@ function validateTypeHelper(types, type, value, memberFqn) {
|
|
|
254
264
|
const year = Number.parseInt(mDatetime.groups.year, 10);
|
|
255
265
|
const month = Number.parseInt(mDatetime.groups.month, 10);
|
|
256
266
|
const day = Number.parseInt(mDatetime.groups.day, 10);
|
|
257
|
-
//
|
|
258
|
-
if (Number.isNaN(valueNew.getTime()) ||
|
|
267
|
+
// A date rolls an out-of-range day over (e.g. Feb 30 -> Mar 1); reject that
|
|
268
|
+
if (Number.isNaN(valueNew.getTime()) || newLocalDate(year, month, day).getMonth() !== month - 1) {
|
|
259
269
|
throwMemberError(type, value, memberFqn);
|
|
260
270
|
}
|
|
261
271
|
} else {
|
|
@@ -277,91 +287,6 @@ function validateTypeHelper(types, type, value, memberFqn) {
|
|
|
277
287
|
}
|
|
278
288
|
}
|
|
279
289
|
|
|
280
|
-
// array?
|
|
281
|
-
} else if ('array' in type) {
|
|
282
|
-
// Valid value type?
|
|
283
|
-
const {array} = type;
|
|
284
|
-
const arrayType = array.type;
|
|
285
|
-
const arrayAttr = 'attr' in array ? array.attr : null;
|
|
286
|
-
if (value === '') {
|
|
287
|
-
valueNew = [];
|
|
288
|
-
} else if (!Array.isArray(value)) {
|
|
289
|
-
throwMemberError(type, value, memberFqn);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Validate the list contents
|
|
293
|
-
const valueCopy = [];
|
|
294
|
-
const arrayValueNullable = arrayAttr !== null && 'nullable' in arrayAttr && arrayAttr.nullable;
|
|
295
|
-
for (let ixArrayValue = 0; ixArrayValue < valueNew.length; ixArrayValue++) {
|
|
296
|
-
const memberFqnValue = [memberFqn, ixArrayValue];
|
|
297
|
-
let arrayValue = valueNew[ixArrayValue];
|
|
298
|
-
if (arrayValueNullable && (arrayValue === null || arrayValue === 'null')) {
|
|
299
|
-
arrayValue = null;
|
|
300
|
-
} else {
|
|
301
|
-
arrayValue = validateTypeHelper(types, arrayType, arrayValue, memberFqnValue);
|
|
302
|
-
if (arrayAttr !== null) {
|
|
303
|
-
validateAttr(arrayType, arrayAttr, arrayValue, memberFqnValue);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
valueCopy.push(arrayValue);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Return the validated, transformed copy
|
|
310
|
-
valueNew = valueCopy;
|
|
311
|
-
|
|
312
|
-
// dict?
|
|
313
|
-
} else if ('dict' in type) {
|
|
314
|
-
// Valid value type?
|
|
315
|
-
const {dict} = type;
|
|
316
|
-
const dictType = dict.type;
|
|
317
|
-
const dictAttr = 'attr' in dict ? dict.attr : null;
|
|
318
|
-
const dictKeyType = 'keyType' in dict ? dict.keyType : defaultDictKeyType;
|
|
319
|
-
const dictKeyAttr = 'keyAttr' in dict ? dict.keyAttr : null;
|
|
320
|
-
if (value === '') {
|
|
321
|
-
valueNew = {};
|
|
322
|
-
} else if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
323
|
-
throwMemberError(type, value, memberFqn);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Validate the dict key/value pairs
|
|
327
|
-
const isMap = valueNew instanceof Map;
|
|
328
|
-
const valueCopy = isMap ? new Map() : {};
|
|
329
|
-
const dictKeyNullable = dictKeyAttr !== null && 'nullable' in dictKeyAttr && dictKeyAttr.nullable;
|
|
330
|
-
const dictValueNullable = dictAttr !== null && 'nullable' in dictAttr && dictAttr.nullable;
|
|
331
|
-
for (let [dictKey, dictValue] of (isMap ? valueNew.entries() : Object.entries(valueNew))) {
|
|
332
|
-
const memberFqnKey = [memberFqn, dictKey];
|
|
333
|
-
|
|
334
|
-
// Validate the key
|
|
335
|
-
if (dictKeyNullable && (dictKey === null || dictKey === 'null')) {
|
|
336
|
-
dictKey = null;
|
|
337
|
-
} else {
|
|
338
|
-
dictKey = validateTypeHelper(types, dictKeyType, dictKey, memberFqn);
|
|
339
|
-
if (dictKeyAttr !== null) {
|
|
340
|
-
validateAttr(dictKeyType, dictKeyAttr, dictKey, memberFqn);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// Validate the value
|
|
345
|
-
if (dictValueNullable && (dictValue === null || dictValue === 'null')) {
|
|
346
|
-
dictValue = null;
|
|
347
|
-
} else {
|
|
348
|
-
dictValue = validateTypeHelper(types, dictType, dictValue, memberFqnKey);
|
|
349
|
-
if (dictAttr !== null) {
|
|
350
|
-
validateAttr(dictType, dictAttr, dictValue, memberFqnKey);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// Copy the key/value
|
|
355
|
-
if (isMap) {
|
|
356
|
-
valueCopy.set(dictKey, dictValue);
|
|
357
|
-
} else {
|
|
358
|
-
setValue(valueCopy, dictKey, dictValue);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Return the validated, transformed copy
|
|
363
|
-
valueNew = valueCopy;
|
|
364
|
-
|
|
365
290
|
// User type?
|
|
366
291
|
} else if ('user' in type) {
|
|
367
292
|
const userType = getUserType(types, type.user);
|
|
@@ -423,7 +348,6 @@ function validateTypeHelper(types, type, value, memberFqn) {
|
|
|
423
348
|
const memberFqnMember = [memberFqn, memberName];
|
|
424
349
|
const memberOptional = 'optional' in member && member.optional;
|
|
425
350
|
const memberAttr = 'attr' in member ? member.attr : null;
|
|
426
|
-
const memberNullable = memberAttr !== null && 'nullable' in memberAttr && memberAttr.nullable;
|
|
427
351
|
|
|
428
352
|
// Missing non-optional member? Check own properties only - "in" matches inherited
|
|
429
353
|
// Object.prototype properties for member names like "constructor"
|
|
@@ -434,13 +358,18 @@ function validateTypeHelper(types, type, value, memberFqn) {
|
|
|
434
358
|
} else {
|
|
435
359
|
// Validate the member value
|
|
436
360
|
let memberValue = isMap ? valueNew.get(memberName) : valueNew[memberName];
|
|
437
|
-
|
|
361
|
+
|
|
362
|
+
// No member attributes? Validate the member value directly
|
|
363
|
+
if (memberAttr === null) {
|
|
364
|
+
memberValue = validateTypeHelper(types, member.type, memberValue, memberFqnMember);
|
|
365
|
+
|
|
366
|
+
// Nullable null?
|
|
367
|
+
} else if ('nullable' in memberAttr && memberAttr.nullable &&
|
|
368
|
+
(memberValue === null || memberValue === 'null')) {
|
|
438
369
|
memberValue = null;
|
|
439
370
|
} else {
|
|
440
371
|
memberValue = validateTypeHelper(types, member.type, memberValue, memberFqnMember);
|
|
441
|
-
|
|
442
|
-
validateAttr(member.type, memberAttr, memberValue, memberFqnMember);
|
|
443
|
-
}
|
|
372
|
+
validateAttr(member.type, memberAttr, memberValue, memberFqnMember);
|
|
444
373
|
}
|
|
445
374
|
|
|
446
375
|
// Copy the validated member
|
|
@@ -466,6 +395,91 @@ function validateTypeHelper(types, type, value, memberFqn) {
|
|
|
466
395
|
// Return the validated, transformed copy
|
|
467
396
|
valueNew = valueCopy;
|
|
468
397
|
}
|
|
398
|
+
|
|
399
|
+
// array?
|
|
400
|
+
} else if ('array' in type) {
|
|
401
|
+
// Valid value type?
|
|
402
|
+
const {array} = type;
|
|
403
|
+
const arrayType = array.type;
|
|
404
|
+
const arrayAttr = 'attr' in array ? array.attr : null;
|
|
405
|
+
if (value === '') {
|
|
406
|
+
valueNew = [];
|
|
407
|
+
} else if (!Array.isArray(value)) {
|
|
408
|
+
throwMemberError(type, value, memberFqn);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Validate the list contents
|
|
412
|
+
const valueCopy = [];
|
|
413
|
+
const arrayValueNullable = arrayAttr !== null && 'nullable' in arrayAttr && arrayAttr.nullable;
|
|
414
|
+
for (let ixArrayValue = 0; ixArrayValue < valueNew.length; ixArrayValue++) {
|
|
415
|
+
const memberFqnValue = [memberFqn, ixArrayValue];
|
|
416
|
+
let arrayValue = valueNew[ixArrayValue];
|
|
417
|
+
if (arrayValueNullable && (arrayValue === null || arrayValue === 'null')) {
|
|
418
|
+
arrayValue = null;
|
|
419
|
+
} else {
|
|
420
|
+
arrayValue = validateTypeHelper(types, arrayType, arrayValue, memberFqnValue);
|
|
421
|
+
if (arrayAttr !== null) {
|
|
422
|
+
validateAttr(arrayType, arrayAttr, arrayValue, memberFqnValue);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
valueCopy.push(arrayValue);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Return the validated, transformed copy
|
|
429
|
+
valueNew = valueCopy;
|
|
430
|
+
|
|
431
|
+
// dict?
|
|
432
|
+
} else if ('dict' in type) {
|
|
433
|
+
// Valid value type?
|
|
434
|
+
const {dict} = type;
|
|
435
|
+
const dictType = dict.type;
|
|
436
|
+
const dictAttr = 'attr' in dict ? dict.attr : null;
|
|
437
|
+
const dictKeyType = 'keyType' in dict ? dict.keyType : defaultDictKeyType;
|
|
438
|
+
const dictKeyAttr = 'keyAttr' in dict ? dict.keyAttr : null;
|
|
439
|
+
if (value === '') {
|
|
440
|
+
valueNew = {};
|
|
441
|
+
} else if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
442
|
+
throwMemberError(type, value, memberFqn);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Validate the dict key/value pairs
|
|
446
|
+
const isMap = valueNew instanceof Map;
|
|
447
|
+
const valueCopy = isMap ? new Map() : {};
|
|
448
|
+
const dictKeyNullable = dictKeyAttr !== null && 'nullable' in dictKeyAttr && dictKeyAttr.nullable;
|
|
449
|
+
const dictValueNullable = dictAttr !== null && 'nullable' in dictAttr && dictAttr.nullable;
|
|
450
|
+
for (let [dictKey, dictValue] of (isMap ? valueNew.entries() : Object.entries(valueNew))) {
|
|
451
|
+
const memberFqnKey = [memberFqn, dictKey];
|
|
452
|
+
|
|
453
|
+
// Validate the key
|
|
454
|
+
if (dictKeyNullable && (dictKey === null || dictKey === 'null')) {
|
|
455
|
+
dictKey = null;
|
|
456
|
+
} else {
|
|
457
|
+
dictKey = validateTypeHelper(types, dictKeyType, dictKey, memberFqn);
|
|
458
|
+
if (dictKeyAttr !== null) {
|
|
459
|
+
validateAttr(dictKeyType, dictKeyAttr, dictKey, memberFqn);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Validate the value
|
|
464
|
+
if (dictValueNullable && (dictValue === null || dictValue === 'null')) {
|
|
465
|
+
dictValue = null;
|
|
466
|
+
} else {
|
|
467
|
+
dictValue = validateTypeHelper(types, dictType, dictValue, memberFqnKey);
|
|
468
|
+
if (dictAttr !== null) {
|
|
469
|
+
validateAttr(dictType, dictAttr, dictValue, memberFqnKey);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Copy the key/value
|
|
474
|
+
if (isMap) {
|
|
475
|
+
valueCopy.set(dictKey, dictValue);
|
|
476
|
+
} else {
|
|
477
|
+
setValue(valueCopy, dictKey, dictValue);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Return the validated, transformed copy
|
|
482
|
+
valueNew = valueCopy;
|
|
469
483
|
}
|
|
470
484
|
|
|
471
485
|
return valueNew;
|
package/lib/schemaUtil.js
CHANGED
|
@@ -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
|
|
|
@@ -170,11 +187,12 @@ export function validateTypeModelErrors(types) {
|
|
|
170
187
|
}
|
|
171
188
|
|
|
172
189
|
|
|
173
|
-
function getEffectiveType(types, type) {
|
|
174
|
-
if ('user' in type && Object.hasOwn(types, 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;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "schema-markdown",
|
|
4
|
-
"version": "1.3.
|
|
4
|
+
"version": "1.3.3",
|
|
5
5
|
"description": "A schema definition and validation library",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"schema",
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@eslint/js": "~10.0",
|
|
28
|
-
"c8": "~
|
|
29
|
-
"eslint": "~10.
|
|
30
|
-
"globals": "~17.
|
|
28
|
+
"c8": "~12.0",
|
|
29
|
+
"eslint": "~10.12",
|
|
30
|
+
"globals": "~17.13",
|
|
31
31
|
"jsdoc": "~4.0"
|
|
32
32
|
}
|
|
33
33
|
}
|