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 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 === "Invalid value \"asdf\" (type 'string') for member 'numbers.2', expected type 'int'", 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 === "Invalid value [] (type 'object') for member 'numbers', expected type 'array' [len > 0]", 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
- if (!(key in obj)) {
113
- obj[key] = null;
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 in types) {
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) || 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
 
@@ -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) || definitionId in builtinDeprecated || definitionId in types) {
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] ?? null;
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 in builtinDeprecated) {
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 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,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 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?(?:Z|[+-]\d{2}:\d{2})$/;
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 or uuid?
151
- if (builtin === 'string' || builtin === 'uuid') {
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 or datetime?
199
- } else if (builtin === 'date' || builtin === 'datetime') {
222
+ // date?
223
+ } else if (builtin === 'date') {
200
224
  // Convert string?
201
225
  if (typeof value === 'string') {
202
- // Valid date format?
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
- } else if (rDatetime.test(value)) {
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 date?
265
+ // Not a datetime?
216
266
  } else if (!(value instanceof Date)) {
217
267
  throwMemberError(type, value, memberFqn);
218
268
  }
219
269
 
220
- // For date type, clear hours, minutes, seconds, and milliseconds
221
- if (builtin === 'date') {
222
- valueNew = new Date(valueNew.getFullYear(), valueNew.getMonth(), valueNew.getDate());
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 !== null ? `${memberFqn}.${ixArrayValue}` : `${ixArrayValue}`;
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
- validateAttr(arrayType, arrayAttr, arrayValue, memberFqnValue);
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 : {'builtin': 'string'};
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 valueCopy = valueNew instanceof Map ? new Map() : {};
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 (valueNew instanceof Map ? valueNew.entries() : Object.entries(valueNew))) {
275
- const memberFqnKey = memberFqn !== null ? `${memberFqn}.${dictKey}` : `${dictKey}`;
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
- validateAttr(dictKeyType, dictKeyAttr, dictKey, memberFqn);
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
- validateAttr(dictType, dictAttr, dictValue, memberFqnKey);
349
+ if (dictAttr !== null) {
350
+ validateAttr(dictType, dictAttr, dictValue, memberFqnKey);
351
+ }
291
352
  }
292
353
 
293
354
  // Copy the key/value
294
- if (valueCopy instanceof Map) {
355
+ if (isMap) {
295
356
  valueCopy.set(dictKey, dictValue);
296
357
  } else {
297
- valueCopy[dictKey] = dictValue;
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[type.user];
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
- validateAttr(type, typedefAttr, valueNew, memberFqn);
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(value).length !== 1) {
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 = valueNew instanceof Map ? new Map() : {};
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 !== null ? `${memberFqn}.${memberName}` : `${memberName}`;
423
+ const memberFqnMember = [memberFqn, memberName];
360
424
  const memberOptional = 'optional' in member && member.optional;
361
- const memberNullable = 'attr' in member && 'nullable' in member.attr && member.attr.nullable;
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
- if (!(valueNew instanceof Map ? valueNew.has(memberName) : memberName in valueNew)) {
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 '${memberFqnMember}' missing`);
432
+ throw new ValidationError(`Required member "${memberFqnStr(memberFqnMember)}" missing`);
367
433
  }
368
434
  } else {
369
435
  // Validate the member value
370
- let memberValue = valueNew instanceof Map ? valueNew.get(memberName) : valueNew[memberName];
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
- validateAttr(member.type, 'attr' in member ? member.attr : null, memberValue, memberFqnMember);
441
+ if (memberAttr !== null) {
442
+ validateAttr(member.type, memberAttr, memberValue, memberFqnMember);
443
+ }
376
444
  }
377
445
 
378
446
  // Copy the validated member
379
- if (valueCopy instanceof Map) {
447
+ if (isMap) {
380
448
  valueCopy.set(memberName, memberValue);
381
449
  } else {
382
- valueCopy[memberName] = memberValue;
450
+ setValue(valueCopy, memberName, memberValue);
383
451
  }
384
452
  }
385
453
  }
386
454
 
387
455
  // Any unknown members?
388
- const valueCopyKeys = valueCopy instanceof Map ? Array.from(valueCopy.keys()) : Object.keys(valueCopy);
389
- const valueNewKeys = valueNew instanceof Map ? Array.from(valueNew.keys()) : Object.keys(valueNew);
390
- if (valueCopyKeys.length !== valueNewKeys.length) {
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 !== null ? `${memberFqn}.${unknownKey}` : `${unknownKey}`;
394
- throw new ValidationError(`Unknown member '${unknownFqn.slice(0, 100)}'`);
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 memberPart = memberFqn !== null ? ` for member '${memberFqn}'` : '';
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 '${typeof value}')${memberPart}, expected type '${typeName}'${attrPart}`;
412
- throw new ValidationError(msg, memberFqn);
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 !== null) {
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[base];
561
+ let baseUserType = getUserType(types, base);
473
562
  while ('typedef' in baseUserType) {
474
- baseUserType = types[baseUserType.typedef.type.user];
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[base];
594
+ let baseUserType = getUserType(types, base);
506
595
  while ('typedef' in baseUserType) {
507
- baseUserType = types[baseUserType.typedef.type.user];
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 '${struct.name}' for '${typeName}'`]);
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 '${baseName}'`]);
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 in members)) {
52
- members[memberName] = true;
51
+ if (!members.has(memberName)) {
52
+ members.add(memberName);
53
53
  } else {
54
- errors.push([typeName, memberName, `Redefinition of '${typeName}' member '${memberName}'`]);
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 '${typeName}'`]);
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 '${enum_.name}' for '${typeName}'`]);
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 '${baseName}'`]);
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 in values)) {
91
- values[valueName] = true;
90
+ if (!values.has(valueName)) {
91
+ values.add(valueName);
92
92
  } else {
93
- errors.push([typeName, valueName, `Redefinition of '${typeName}' value '${valueName}'`]);
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 '${typeName}'`]);
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 '${typedef.name}' for '${typeName}'`]);
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 '${action.name}' for '${typeName}'`]);
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 in types) {
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 in memberSections)) {
146
- memberSections[memberName] = [];
145
+ if (!memberSections.has(memberName)) {
146
+ memberSections.set(memberName, []);
147
147
  }
148
- memberSections[memberName].push(sectionStruct.name);
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 Object.entries(memberSections)) {
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 '${sectionType}' member '${memberName}'`]);
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 in types) {
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 in types ? types[userTypeName] : null;
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 in types ? types[typeEffective.user] : null;
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 in visited)) {
215
- visited[userTypeName] = true;
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 '${typeName}' member '${memberName}'`]);
259
+ errors.push([typeName, memberName, `${message} from "${typeName}" member "${memberName}"`]);
260
260
  } else {
261
- errors.push([typeName, null, `${message} from '${typeName}'`]);
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 in types && 'enum' in types[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 in types)) {
299
- error(`Unknown type '${userTypeName}'`);
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 '${userTypeName}'`);
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 in typeToAllowedAttr ? typeToAllowedAttr[typeAttrKey] : null;
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 '${attrText}'`);
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 key type
111
+ # The contained value type
112
112
  Type type
113
113
 
114
- # The contained key type's attributes
114
+ # The contained value type's attributes
115
115
  optional Attributes attr
116
116
 
117
- # The contained value type
117
+ # The contained key type
118
118
  optional Type keyType
119
119
 
120
- # The contained value type's attributes
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.2.13",
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.1",
30
- "globals": "~17.4",
29
+ "eslint": "~10.6",
30
+ "globals": "~17.7",
31
31
  "jsdoc": "~4.0"
32
32
  }
33
33
  }