schema-markdown 1.2.13 → 1.3.0

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/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*$`);
@@ -311,8 +311,12 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
311
311
  // Clear parser state
312
312
  userType = null;
313
313
 
314
- // Update the parser state
314
+ // Update the parser state - attach urls immediately so a second empty
315
+ // "urls" section is detected as a redefinition (even with no URL lines)
315
316
  urls = [];
317
+ if (!(sectionString in action)) {
318
+ action[sectionString] = urls;
319
+ }
316
320
 
317
321
  // Enum value?
318
322
  } else if (matchName === 'value') {
@@ -383,9 +387,6 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
383
387
  }
384
388
 
385
389
  // Add the URL
386
- if (!('urls' in action)) {
387
- action.urls = urls;
388
- }
389
390
  urls.push(actionUrl);
390
391
 
391
392
  // Typedef?
@@ -429,6 +430,16 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
429
430
  }
430
431
  }
431
432
 
433
+ // Drop empty action urls arrays (type model requires len > 0 when present)
434
+ for (const userTypeIter of Object.values(types)) {
435
+ if ('action' in userTypeIter) {
436
+ const actionType = userTypeIter.action;
437
+ if ('urls' in actionType && actionType.urls.length === 0) {
438
+ delete actionType.urls;
439
+ }
440
+ }
441
+ }
442
+
432
443
  // Validate the type model, if requested
433
444
  if (validate) {
434
445
  for (const [typeName, memberName, errorMsg] of validateTypeModelErrors(types)) {
package/lib/schema.js CHANGED
@@ -120,6 +120,12 @@ export class ValidationError extends Error {
120
120
  * Type-validate a value using a user type model. Container values are duplicated since some member types are
121
121
  * transformed during validation.
122
122
  *
123
+ * Validates JSON-like values. In this implementation, date values are `Date` objects (time-of-day
124
+ * cleared), datetime values are `Date` objects, and uuid strings remain strings. A `Date` input is
125
+ * left as a `Date` (date values have time-of-day cleared). See the language documentation on
126
+ * [built-in types]{@link https://craigahobbs.github.io/schema-markdown-js/language/#built-in-types}
127
+ * for accepted JSON input formats and host-runtime behavior.
128
+ *
123
129
  * @param {Object} types - The [type model]{@link https://craigahobbs.github.io/bare-script/model/#var.vURL=''&var.vName='Types'}
124
130
  * @param {string} typeName - The type name
125
131
  * @param {Object} value - The value object to validate
@@ -129,7 +135,7 @@ export class ValidationError extends Error {
129
135
  */
130
136
  export function validateType(types, typeName, value, memberFqn = null) {
131
137
  if (!(typeName in types)) {
132
- throw new ValidationError(`Unknown type '${typeName}'`);
138
+ throw new ValidationError(`Unknown type "${typeName}"`);
133
139
  }
134
140
  return validateTypeHelper(types, {'user': typeName}, value, memberFqn);
135
141
  }
@@ -137,7 +143,8 @@ export function validateType(types, typeName, value, memberFqn = null) {
137
143
 
138
144
  // Regular expressions used by validateTypeHelper
139
145
  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})$/;
146
+ const rDatetime = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/;
147
+ const rUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
141
148
 
142
149
 
143
150
  function validateTypeHelper(types, type, value, memberFqn) {
@@ -147,15 +154,19 @@ function validateTypeHelper(types, type, value, memberFqn) {
147
154
  if ('builtin' in type) {
148
155
  const {builtin} = type;
149
156
 
150
- // string or uuid?
151
- if (builtin === 'string' || builtin === 'uuid') {
157
+ // string?
158
+ if (builtin === 'string') {
152
159
  // Not a string?
153
160
  if (typeof value !== 'string') {
154
161
  throwMemberError(type, value, memberFqn);
155
162
  }
156
163
 
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)) {
164
+ // uuid?
165
+ } else if (builtin === 'uuid') {
166
+ if (typeof value !== 'string') {
167
+ throwMemberError(type, value, memberFqn);
168
+ }
169
+ if (!rUuid.test(value)) {
159
170
  throwMemberError(type, value, memberFqn);
160
171
  }
161
172
 
@@ -195,31 +206,52 @@ function validateTypeHelper(types, type, value, memberFqn) {
195
206
  throwMemberError(type, value, memberFqn);
196
207
  }
197
208
 
198
- // date or datetime?
199
- } else if (builtin === 'date' || builtin === 'datetime') {
209
+ // date?
210
+ } else if (builtin === 'date') {
200
211
  // Convert string?
201
212
  if (typeof value === 'string') {
202
- // Valid date format?
213
+ // Date-only?
203
214
  const mDate = value.match(rDate);
204
215
  if (mDate !== null) {
205
216
  const year = Number.parseInt(mDate.groups.year, 10);
206
217
  const month = Number.parseInt(mDate.groups.month, 10);
207
218
  const day = Number.parseInt(mDate.groups.day, 10);
208
219
  valueNew = new Date(year, month - 1, day);
209
- } else if (rDatetime.test(value)) {
210
- valueNew = new Date(value);
220
+ // new Date rolls an out-of-range month/day over (e.g. Feb 30 -> Mar 1); reject that
221
+ if (valueNew.getMonth() !== month - 1) {
222
+ throwMemberError(type, value, memberFqn);
223
+ }
211
224
  } else {
212
225
  throwMemberError(type, value, memberFqn);
213
226
  }
214
227
 
215
- // Not a date?
216
- } else if (!(value instanceof Date)) {
228
+ // Not a date, or a date with a time-of-day?
229
+ } else if (!(value instanceof Date) ||
230
+ value.getHours() + value.getMinutes() + value.getSeconds() + value.getMilliseconds() !== 0) {
217
231
  throwMemberError(type, value, memberFqn);
218
232
  }
219
233
 
220
- // For date type, clear hours, minutes, seconds, and milliseconds
221
- if (builtin === 'date') {
222
- valueNew = new Date(valueNew.getFullYear(), valueNew.getMonth(), valueNew.getDate());
234
+ // datetime?
235
+ } else if (builtin === 'datetime') {
236
+ // Convert string?
237
+ if (typeof value === 'string') {
238
+ const mDatetime = value.match(rDatetime);
239
+ if (mDatetime !== null) {
240
+ valueNew = new Date(value);
241
+ const year = Number.parseInt(mDatetime.groups.year, 10);
242
+ const month = Number.parseInt(mDatetime.groups.month, 10);
243
+ const day = Number.parseInt(mDatetime.groups.day, 10);
244
+ // new Date rolls an out-of-range day over (e.g. Feb 30 -> Mar 1); reject that
245
+ if (Number.isNaN(valueNew.getTime()) || new Date(year, month - 1, day).getMonth() !== month - 1) {
246
+ throwMemberError(type, value, memberFqn);
247
+ }
248
+ } else {
249
+ throwMemberError(type, value, memberFqn);
250
+ }
251
+
252
+ // Not a datetime?
253
+ } else if (!(value instanceof Date)) {
254
+ throwMemberError(type, value, memberFqn);
223
255
  }
224
256
  }
225
257
 
@@ -263,7 +295,7 @@ function validateTypeHelper(types, type, value, memberFqn) {
263
295
  const dictKeyAttr = 'keyAttr' in dict ? dict.keyAttr : null;
264
296
  if (value === '') {
265
297
  valueNew = {};
266
- } else if (value === null || typeof value !== 'object') {
298
+ } else if (value === null || typeof value !== 'object' || Array.isArray(value)) {
267
299
  throwMemberError(type, value, memberFqn);
268
300
  }
269
301
 
@@ -340,7 +372,7 @@ function validateTypeHelper(types, type, value, memberFqn) {
340
372
  // Valid value type?
341
373
  if (value === '') {
342
374
  valueNew = {};
343
- } else if (value === null || typeof value !== 'object') {
375
+ } else if (value === null || typeof value !== 'object' || Array.isArray(value)) {
344
376
  throwMemberError({'user': struct.name}, value, memberFqn);
345
377
  }
346
378
 
@@ -363,7 +395,7 @@ function validateTypeHelper(types, type, value, memberFqn) {
363
395
  // Missing non-optional member?
364
396
  if (!(valueNew instanceof Map ? valueNew.has(memberName) : memberName in valueNew)) {
365
397
  if (!memberOptional && !isUnion) {
366
- throw new ValidationError(`Required member '${memberFqnMember}' missing`);
398
+ throw new ValidationError(`Required member "${memberFqnMember}" missing`);
367
399
  }
368
400
  } else {
369
401
  // Validate the member value
@@ -391,7 +423,7 @@ function validateTypeHelper(types, type, value, memberFqn) {
391
423
  const memberSet = new Set(getStructMembers(types, struct).map((member) => member.name));
392
424
  const [unknownKey] = valueNewKeys.filter((key) => !memberSet.has(key));
393
425
  const unknownFqn = memberFqn !== null ? `${memberFqn}.${unknownKey}` : `${unknownKey}`;
394
- throw new ValidationError(`Unknown member '${unknownFqn.slice(0, 100)}'`);
426
+ throw new ValidationError(`Unknown member "${unknownFqn.slice(0, 100)}"`);
395
427
  }
396
428
 
397
429
  // Return the validated, transformed copy
@@ -404,11 +436,11 @@ function validateTypeHelper(types, type, value, memberFqn) {
404
436
 
405
437
 
406
438
  function throwMemberError(type, value, memberFqn, attr = null) {
407
- const memberPart = memberFqn !== null ? ` for member '${memberFqn}'` : '';
439
+ const memberPart = memberFqn !== null ? ` for member "${memberFqn}"` : '';
408
440
  const typeName = 'builtin' in type ? type.builtin : ('array' in type ? 'array' : ('dict' in type ? 'dict' : type.user));
409
441
  const attrPart = attr !== null ? ` [${attr}]` : '';
410
442
  const valueStr = `${JSON.stringify(value)}`;
411
- const msg = `Invalid value ${valueStr.slice(0, 100)} (type '${typeof value}')${memberPart}, expected type '${typeName}'${attrPart}`;
443
+ const msg = `Invalid value ${valueStr.slice(0, 100)} (type "${typeof value}")${memberPart}, expected type "${typeName}"${attrPart}`;
412
444
  throw new ValidationError(msg, memberFqn);
413
445
  }
414
446
 
@@ -532,6 +564,23 @@ export function validateTypeModel(types) {
532
564
  // Do additional type model validation
533
565
  const errors = validateTypeModelErrors(validatedTypes);
534
566
  if (errors.length) {
567
+ // Sort by (typeName, memberName, message); null memberName as ''
568
+ errors.sort((errorA, errorB) => {
569
+ let result = 0;
570
+ for (let ixPart = 0; ixPart < 3; ixPart++) {
571
+ const partA = errorA[ixPart] ?? '';
572
+ const partB = errorB[ixPart] ?? '';
573
+ if (partA < partB) {
574
+ result = -1;
575
+ break;
576
+ }
577
+ if (partA > partB) {
578
+ result = 1;
579
+ break;
580
+ }
581
+ }
582
+ return result;
583
+ });
535
584
  throw new ValidationError(errors.map(([,, message]) => message).join('\n'));
536
585
  }
537
586
 
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,7 +36,7 @@ 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
  }
@@ -51,14 +51,14 @@ export function validateTypeModelErrors(types) {
51
51
  if (!(memberName in members)) {
52
52
  members[memberName] = true;
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,7 +75,7 @@ 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
  }
@@ -90,11 +90,11 @@ export function validateTypeModelErrors(types) {
90
90
  if (!(valueName in values)) {
91
91
  values[valueName] = true;
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
@@ -159,7 +159,7 @@ export function validateTypeModelErrors(types) {
159
159
  for (const [memberName, memberSectionNames] of Object.entries(memberSections)) {
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
  }
@@ -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
 
@@ -296,13 +296,13 @@ function validateTypeModelType(errors, types, type, attr, typeName, memberName)
296
296
 
297
297
  // Unknown user type?
298
298
  if (!(userTypeName in types)) {
299
- error(`Unknown type '${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
  }
@@ -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.0",
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
  }