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 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
- if (!(key in obj)) {
113
- obj[key] = null;
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
- match = line.match(rValueQuoted);
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
- // Documentation group?
179
- } else if (matchName === 'group') {
180
- docGroup = match.groups.group;
181
- if (typeof docGroup !== 'undefined') {
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
- // Action already defined?
192
- if (actionId in types) {
193
- addError(`Redefinition of action '${actionId}'`, filename, linenum);
189
+ // Add the member
190
+ const {struct} = userType;
191
+ if (!('members' in struct)) {
192
+ struct.members = [];
194
193
  }
195
-
196
- // Clear parser state
197
- urls = null;
198
- userType = null;
199
- const actionDoc = getDoc();
200
-
201
- // Create the new action
202
- action = {'name': actionId};
203
- types[actionId] = {'action': action};
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 (docGroup !== null) {
208
- action.docGroup = docGroup;
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) || definitionId in builtinDeprecated || definitionId in types) {
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] ?? null;
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 in builtinDeprecated) {
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 in referencedTypes)) {
43
- const userType = types[typeName];
44
- referencedTypes[typeName] = userType;
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
- if (isNaN(value)) {
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
- // Validate the key
310
- if (dictKeyNullable && (dictKey === null || dictKey === 'null')) {
311
- dictKey = null;
312
- } else {
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
- // Copy the key/value
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[type.user];
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
- validateAttr(type, typedefAttr, valueNew, memberFqn);
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(value).length !== 1) {
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 = valueNew instanceof Map ? new Map() : {};
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 !== null ? `${memberFqn}.${memberName}` : `${memberName}`;
338
+ const memberFqnMember = [memberFqn, memberName];
392
339
  const memberOptional = 'optional' in member && member.optional;
393
- const memberNullable = 'attr' in member && 'nullable' in member.attr && member.attr.nullable;
340
+ const memberAttr = 'attr' in member ? member.attr : null;
394
341
 
395
- // Missing non-optional member?
396
- if (!(valueNew instanceof Map ? valueNew.has(memberName) : memberName in valueNew)) {
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 = valueNew instanceof Map ? valueNew.get(memberName) : valueNew[memberName];
403
- if (memberNullable && (memberValue === null || memberValue === 'null')) {
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, 'attr' in member ? member.attr : null, memberValue, memberFqnMember);
362
+ validateAttr(member.type, memberAttr, memberValue, memberFqnMember);
408
363
  }
409
364
 
410
365
  // Copy the validated member
411
- if (valueCopy instanceof Map) {
366
+ if (isMap) {
412
367
  valueCopy.set(memberName, memberValue);
413
368
  } else {
414
- valueCopy[memberName] = memberValue;
369
+ setValue(valueCopy, memberName, memberValue);
415
370
  }
416
371
  }
417
372
  }
418
373
 
419
374
  // Any unknown members?
420
- const valueCopyKeys = valueCopy instanceof Map ? Array.from(valueCopy.keys()) : Object.keys(valueCopy);
421
- const valueNewKeys = valueNew instanceof Map ? Array.from(valueNew.keys()) : Object.keys(valueNew);
422
- if (valueCopyKeys.length !== valueNewKeys.length) {
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 !== null ? `${memberFqn}.${unknownKey}` : `${unknownKey}`;
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 memberPart = memberFqn !== null ? ` for member "${memberFqn}"` : '';
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, memberFqn);
506
+ throw new ValidationError(msg, memberFqnValue);
445
507
  }
446
508
 
447
509
 
448
510
  function validateAttr(type, attr, value, memberFqn) {
449
- if (attr !== null) {
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[base];
565
+ let baseUserType = getUserType(types, base);
505
566
  while ('typedef' in baseUserType) {
506
- baseUserType = types[baseUserType.typedef.type.user];
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[base];
598
+ let baseUserType = getUserType(types, base);
538
599
  while ('typedef' in baseUserType) {
539
- baseUserType = types[baseUserType.typedef.type.user];
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 in members)) {
52
- members[memberName] = true;
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 in values)) {
91
- values[valueName] = true;
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 in types) {
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 in memberSections)) {
146
- memberSections[memberName] = [];
162
+ if (!memberSections.has(memberName)) {
163
+ memberSections.set(memberName, []);
147
164
  }
148
- memberSections[memberName].push(sectionStruct.name);
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 Object.entries(memberSections)) {
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 in types) {
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
- return getEffectiveType(types, userType.typedef.type);
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 in types ? types[userTypeName] : null;
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 in types ? types[typeEffective.user] : null;
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 in visited)) {
215
- visited[userTypeName] = true;
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 in types && 'enum' in types[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 in types)) {
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 in typeToAllowedAttr ? typeToAllowedAttr[typeAttrKey] : null;
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) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "schema-markdown",
4
- "version": "1.3.0",
4
+ "version": "1.3.2",
5
5
  "description": "A schema definition and validation library",
6
6
  "keywords": [
7
7
  "schema",