schema-markdown 1.1.4 → 1.2.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
@@ -24,14 +24,14 @@ its features at a glance:
24
24
  ## Usage
25
25
 
26
26
  To schema-validate an object, first parse its *Schema Markdown* using the
27
- [SchemaMarkdownParser](https://craigahobbs.github.io/schema-markdown-js/module-lib_parser.SchemaMarkdownParser.html)
28
- class:
27
+ [parseSchemaMarkdown](https://craigahobbs.github.io/schema-markdown-js/module-lib_parser.parseSchemaMarkdown.html)
28
+ function:
29
29
 
30
30
  ``` javascript
31
- import {SchemaMarkdownParser} from 'schema-markdown/parser.js';
31
+ import {parseSchemaMarkdown} from 'schema-markdown/parser.js';
32
32
  import {validateType} from 'schema-markdown/schema.js';
33
33
 
34
- const parser = new SchemaMarkdownParser(`\
34
+ const types = parseSchemaMarkdown(`\
35
35
  # An aggregation function
36
36
  enum Aggregation
37
37
  Average
@@ -52,7 +52,7 @@ Then, validate an object using the
52
52
  function:
53
53
 
54
54
  ``` javascript
55
- const obj = validateType(parser.types, 'Operation', {
55
+ const obj = validateType(types, 'Operation', {
56
56
  'numbers': [1, 2, '3', 4]
57
57
  });
58
58
  console.assert(obj.numbers[2] === 3);
@@ -63,7 +63,7 @@ Validation fails if the object does not match the schema:
63
63
 
64
64
  ``` javascript
65
65
  try {
66
- validateType(parser.types, 'Operation', {
66
+ validateType(types, 'Operation', {
67
67
  'numbers': [1, 2, 'asdf', 4]
68
68
  });
69
69
  } catch ({message}) {
@@ -75,7 +75,7 @@ Validation also fails if a member contraint is violated:
75
75
 
76
76
  ``` javascript
77
77
  try {
78
- validateType(parser.types, 'Operation', {
78
+ validateType(types, 'Operation', {
79
79
  'numbers': []
80
80
  });
81
81
  } catch ({message}) {
package/lib/parser.js CHANGED
@@ -3,11 +3,16 @@
3
3
 
4
4
  /** @module lib/parser */
5
5
 
6
- import {validateTypeModelTypesErrors} from './schemaUtil.js';
6
+ import {validateTypeModelErrors} from './schemaUtil.js';
7
+
7
8
 
8
9
  /* eslint-disable prefer-template */
9
10
 
10
11
 
12
+ // Built-in types
13
+ const BUILTIN_TYPES = new Set(['bool', 'date', 'datetime', 'float', 'int', 'object', 'string', 'uuid']);
14
+
15
+
11
16
  // Schema Markdown regex
12
17
  const RE_PART_ID = '(?:[A-Za-z]\\w*)';
13
18
  const RE_PART_ATTR_GROUP =
@@ -45,66 +50,416 @@ const RE_MEMBER = new RegExp('^\\s+(?<optional>optional\\s+)?' + RE_PART_TYPEDEF
45
50
  const RE_VALUE = new RegExp('^\\s+(?<id>' + RE_PART_ID + ')\\s*$');
46
51
  const RE_VALUE_QUOTED = /^\s+"(?<id>.*?)"\s*$/;
47
52
  const RE_URL = /^\s+(?<method>[A-Za-z]+|\*)(?:\s+(?<path>\/[^\s]*))?/;
53
+ const RE_LINE_SPLIT = /\r?\n/;
48
54
 
49
55
 
50
- // Built-in types
51
- const BUILTIN_TYPES = new Set(['bool', 'date', 'datetime', 'float', 'int', 'object', 'string', 'uuid']);
56
+ /**
57
+ * Parse Schema Markdown from an iterator of line strings (e.g an input stream). This method can
58
+ * be called repeatedly.
59
+ *
60
+ * @param {string|string[]} text - The Schema Markdown text
61
+ * @param {Object} [options.types=''] - The [type model]{@link https://craigahobbs.github.io/schema-markdown-doc/doc/#var.vName='Types'}
62
+ * @param {string} [options.filename=''] - The name of file being parsed (for error messages)
63
+ * @param {boolean} [options.validate=true] - If true, validate after parsing
64
+ * @returns {Object} The [type model]{@link https://craigahobbs.github.io/schema-markdown-doc/doc/#var.vName='Types'}
65
+ * @throws [SchemaMarkdownParserError]{@link module:lib/parser.SchemaMarkdownParserError}
66
+ */
67
+ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate = true} = {}) {
68
+ // Current parser state
69
+ const errorMap = {};
70
+ const filepos = {};
71
+ let action = null;
72
+ let urls = null;
73
+ let userType = null;
74
+ let doc = [];
75
+ let docGroup = null;
76
+ let linenum = 0;
77
+
78
+ // Helper function to add an error message
79
+ const addError = (msg, errorFilename, errorLinenum) => {
80
+ const errorMsg = `${errorFilename}:${errorLinenum}: error: ${msg}`;
81
+ errorMap[errorMsg] = [errorFilename, errorLinenum, errorMsg];
82
+ };
83
+
84
+ // Helper function to get documentation strings
85
+ const getDoc = () => {
86
+ let result = null;
87
+ if (doc.length) {
88
+ result = doc;
89
+ doc = [];
90
+ }
91
+ return result;
92
+ };
93
+
94
+ // Line-split all script text
95
+ const lines = [];
96
+ if (typeof text === 'string') {
97
+ lines.push(...text.split(RE_LINE_SPLIT));
98
+ } else {
99
+ for (const textPart of text) {
100
+ lines.push(...textPart.split(RE_LINE_SPLIT));
101
+ }
102
+ }
52
103
 
104
+ // Process each line
105
+ const lineContinuation = [];
106
+ const lineGroups = [lines, ['']];
107
+ for (const lineGroup of lineGroups) {
108
+ for (const linePart of lineGroup) {
109
+ linenum += 1;
110
+
111
+ // Line continuation?
112
+ const linePartNoContinuation = linePart.replace(RE_LINE_CONT, '');
113
+ if (lineContinuation.length || linePartNoContinuation !== linePart) {
114
+ lineContinuation.push(linePartNoContinuation);
115
+ }
116
+ if (linePartNoContinuation !== linePart) {
117
+ continue;
118
+ }
119
+ let line;
120
+ if (lineContinuation.length) {
121
+ line = lineContinuation.join('');
122
+ lineContinuation.length = 0;
123
+ } else {
124
+ line = linePart;
125
+ }
53
126
 
54
- // Helper function to parse an attributes string - returns an attributes model
55
- function parseAttr(attrsString) {
56
- let attrs = null;
57
- if (typeof attrsString !== 'undefined') {
58
- for (const [attrString] of attrsString.matchAll(RE_FIND_ATTRS)) {
59
- if (attrs === null) {
60
- attrs = {};
127
+ // Match syntax
128
+ let matchName = 'comment';
129
+ let match = line.match(RE_COMMENT);
130
+ if (match === null) {
131
+ matchName = 'group';
132
+ match = line.match(RE_GROUP);
133
+ }
134
+ if (match === null) {
135
+ matchName = 'action';
136
+ match = line.match(RE_ACTION);
137
+ }
138
+ if (match === null) {
139
+ matchName = 'definition';
140
+ match = line.match(RE_DEFINITION);
141
+ }
142
+ if (match === null && action !== null) {
143
+ matchName = 'section';
144
+ match = line.match(RE_SECTION);
145
+ }
146
+ if (match === null && action !== null) {
147
+ matchName = 'section_plain';
148
+ match = line.match(RE_SECTION_PLAIN);
149
+ }
150
+ if (match === null && userType !== null && 'enum' in userType) {
151
+ matchName = 'value';
152
+ match = line.match(RE_VALUE);
153
+ if (match === null) {
154
+ match = line.match(RE_VALUE_QUOTED);
155
+ }
156
+ }
157
+ if (match === null && userType !== null && 'struct' in userType) {
158
+ matchName = 'member';
159
+ match = line.match(RE_MEMBER);
160
+ }
161
+ if (match === null && urls !== null) {
162
+ matchName = 'urls';
163
+ match = line.match(RE_URL);
164
+ }
165
+ if (match === null) {
166
+ matchName = 'typedef';
167
+ match = line.match(RE_TYPEDEF);
168
+ }
169
+ if (match === null) {
170
+ matchName = null;
61
171
  }
62
- const matchAttr = attrString.match(RE_ATTR_GROUP);
63
- const attrOp = matchAttr.groups.op;
64
- const attrLengthOp = matchAttr.groups.lop;
65
172
 
66
- if (typeof matchAttr.groups.nullable !== 'undefined') {
67
- attrs.nullable = true;
68
- } else if (typeof attrOp !== 'undefined') {
69
- const attrValue = parseFloat(matchAttr.groups.opnum);
70
- if (attrOp === '<') {
71
- attrs.lt = attrValue;
72
- } else if (attrOp === '<=') {
73
- attrs.lte = attrValue;
74
- } else if (attrOp === '>') {
75
- attrs.gt = attrValue;
76
- } else if (attrOp === '>=') {
77
- attrs.gte = attrValue;
173
+ // Comment?
174
+ if (matchName === 'comment') {
175
+ const docString = match.groups.doc;
176
+ if (typeof docString !== 'undefined') {
177
+ doc.push(!docString.startsWith(' ') ? docString : docString.slice(1));
178
+ }
179
+
180
+ // Documentation group?
181
+ } else if (matchName === 'group') {
182
+ docGroup = match.groups.group;
183
+ if (typeof docGroup !== 'undefined') {
184
+ docGroup = docGroup.trim();
78
185
  } else {
79
- attrs.eq = attrValue;
186
+ docGroup = null;
80
187
  }
81
- } else {
82
- // typeof attrLengthOp !== 'undefined'
83
- const attrValue = parseInt(matchAttr.groups.lopnum, 10);
84
- if (attrLengthOp === '<') {
85
- attrs.lenLT = attrValue;
86
- } else if (attrLengthOp === '<=') {
87
- attrs.lenLTE = attrValue;
88
- } else if (attrLengthOp === '>') {
89
- attrs.lenGT = attrValue;
90
- } else if (attrLengthOp === '>=') {
91
- attrs.lenGTE = attrValue;
188
+
189
+ // Action?
190
+ } else if (matchName === 'action') {
191
+ const actionId = match.groups.id;
192
+
193
+ // Action already defined?
194
+ if (actionId in types) {
195
+ addError(`Redefinition of action '${actionId}'`, filename, linenum);
196
+ }
197
+
198
+ // Clear parser state
199
+ urls = null;
200
+ userType = null;
201
+ const actionDoc = getDoc();
202
+
203
+ // Create the new action
204
+ action = {'name': actionId};
205
+ types[actionId] = {'action': action};
206
+ if (actionDoc !== null) {
207
+ action.doc = actionDoc;
208
+ }
209
+ if (docGroup !== null) {
210
+ action.docGroup = docGroup;
211
+ }
212
+
213
+ // Definition?
214
+ } else if (matchName === 'definition') {
215
+ const definitionString = match.groups.type;
216
+ const definitionId = match.groups.id;
217
+ const definitionBaseIds = match.groups.base_ids;
218
+
219
+ // Type already defined?
220
+ if (BUILTIN_TYPES.has(definitionId) || definitionId in types) {
221
+ addError(`Redefinition of type '${definitionId}'`, filename, linenum);
222
+ }
223
+
224
+ // Clear parser state
225
+ action = null;
226
+ urls = null;
227
+ const definitionDoc = getDoc();
228
+
229
+ // Struct definition
230
+ if (definitionString === 'struct' || definitionString === 'union') {
231
+ // Create the new struct type
232
+ const struct = {'name': definitionId};
233
+ userType = {'struct': struct};
234
+ types[definitionId] = userType;
235
+ if (definitionDoc !== null) {
236
+ struct.doc = definitionDoc;
237
+ }
238
+ if (docGroup !== null) {
239
+ struct.docGroup = docGroup;
240
+ }
241
+ if (definitionString === 'union') {
242
+ struct.union = true;
243
+ }
244
+ if (typeof definitionBaseIds !== 'undefined') {
245
+ struct.bases = definitionBaseIds.split(RE_BASE_IDS_SPLIT);
246
+ }
247
+
248
+ // Enum definition
92
249
  } else {
93
- attrs.lenEq = attrValue;
250
+ // definition_string == 'enum':
251
+ // Create the new enum type
252
+ const enum_ = {'name': definitionId};
253
+ userType = {'enum': enum_};
254
+ types[definitionId] = userType;
255
+ if (definitionDoc !== null) {
256
+ enum_.doc = definitionDoc;
257
+ }
258
+ if (docGroup !== null) {
259
+ enum_.docGroup = docGroup;
260
+ }
261
+ if (typeof definitionBaseIds !== 'undefined') {
262
+ enum_.bases = definitionBaseIds.split(RE_BASE_IDS_SPLIT);
263
+ }
264
+ }
265
+
266
+ // Record the definition's line number
267
+ filepos[definitionId] = linenum;
268
+
269
+ // Action section?
270
+ } else if (matchName === 'section') {
271
+ const sectionString = match.groups.type;
272
+ const sectionBaseIds = match.groups.base_ids;
273
+
274
+ // Action section redefinition?
275
+ if (sectionString in action) {
276
+ addError(`Redefinition of action ${sectionString}`, filename, linenum);
277
+ }
278
+
279
+ // Clear parser state
280
+ urls = null;
281
+
282
+ // Set the action section type
283
+ const sectionTypeName = `${action.name}_${sectionString}`;
284
+ action[sectionString] = sectionTypeName;
285
+ if (sectionString === 'errors') {
286
+ const enum_ = {'name': sectionTypeName};
287
+ userType = {'enum': enum_};
288
+ types[sectionTypeName] = userType;
289
+ if (typeof sectionBaseIds !== 'undefined') {
290
+ enum_.bases = sectionBaseIds.split(RE_BASE_IDS_SPLIT);
291
+ }
292
+ } else {
293
+ const struct = {'name': sectionTypeName};
294
+ userType = {'struct': struct};
295
+ types[sectionTypeName] = userType;
296
+ if (typeof sectionBaseIds !== 'undefined') {
297
+ struct.bases = sectionBaseIds.split(RE_BASE_IDS_SPLIT);
298
+ }
299
+ }
300
+
301
+ // Record the definition's line number
302
+ filepos[sectionTypeName] = linenum;
303
+
304
+ // Plain action section?
305
+ } else if (matchName === 'section_plain') {
306
+ const sectionString = match.groups.type;
307
+
308
+ // Action section redefinition?
309
+ if (sectionString in action) {
310
+ addError(`Redefinition of action ${sectionString}`, filename, linenum);
311
+ }
312
+
313
+ // Clear parser state
314
+ userType = null;
315
+
316
+ // Update the parser state
317
+ urls = [];
318
+
319
+ // Enum value?
320
+ } else if (matchName === 'value') {
321
+ const valueString = match.groups.id;
322
+
323
+ // Add the enum value
324
+ const enum_ = userType.enum;
325
+ if (!('values' in enum_)) {
326
+ enum_.values = [];
327
+ }
328
+ const enumValue = {'name': valueString};
329
+ enum_.values.push(enumValue);
330
+ const enumValueDoc = getDoc();
331
+ if (enumValueDoc !== null) {
332
+ enumValue.doc = enumValueDoc;
333
+ }
334
+
335
+ // Record the definition's line number
336
+ filepos[`${enum_.name}.${valueString}`] = linenum;
337
+
338
+ // Struct member?
339
+ } else if (matchName === 'member') {
340
+ const optional = typeof match.groups.optional !== 'undefined';
341
+ const memberName = match.groups.id;
342
+
343
+ // Add the member
344
+ const {struct} = userType;
345
+ if (!('members' in struct)) {
346
+ struct.members = [];
347
+ }
348
+ const [memberType, memberAttr] = parseTypedef(match);
349
+ const memberDoc = getDoc();
350
+ const member = {
351
+ 'name': memberName,
352
+ 'type': memberType
353
+ };
354
+ struct.members.push(member);
355
+ if (memberAttr !== null) {
356
+ member.attr = memberAttr;
357
+ }
358
+ if (memberDoc !== null) {
359
+ member.doc = memberDoc;
360
+ }
361
+ if (optional) {
362
+ member.optional = true;
363
+ }
364
+
365
+ // Record the definition's line number
366
+ filepos[`${struct.name}.${memberName}`] = linenum;
367
+
368
+ // URL?
369
+ } else if (matchName === 'urls') {
370
+ const {method, path} = match.groups;
371
+
372
+ // Create the action URL object
373
+ const actionUrl = {};
374
+ if (method !== '*') {
375
+ actionUrl.method = method;
376
+ }
377
+ if (typeof path !== 'undefined') {
378
+ actionUrl.path = path;
379
+ }
380
+
381
+ // Duplicate URL?
382
+ if (urls.some((url) => url.method === actionUrl.method && url.path === actionUrl.path)) {
383
+ addError(`Duplicate URL: ${method} ${'path' in actionUrl ? actionUrl.path : ''}`, filename, linenum);
384
+ }
385
+
386
+ // Add the URL
387
+ if (!('urls' in action)) {
388
+ action.urls = urls;
389
+ }
390
+ urls.push(actionUrl);
391
+
392
+ // Typedef?
393
+ } else if (matchName === 'typedef') {
394
+ const definitionId = match.groups.id;
395
+
396
+ // Type already defined?
397
+ if (BUILTIN_TYPES.has(definitionId) || 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;
94
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
+ // Unrecognized line syntax
428
+ } else {
429
+ addError('Syntax error', filename, linenum);
95
430
  }
96
431
  }
97
432
  }
98
- return attrs;
99
- }
100
433
 
434
+ // Validate the type model, if requested
435
+ if (validate) {
436
+ for (const [typeName, memberName, errorMsg] of validateTypeModelErrors(types)) {
437
+ let errorFilename = filename;
438
+ let errorLinenum = null;
439
+ if (memberName !== null) {
440
+ errorLinenum = filepos[`${typeName}.${memberName}`] ?? null;
441
+ }
442
+ errorLinenum ??= filepos[typeName] ?? null;
443
+ if (errorLinenum === null) {
444
+ errorFilename = '';
445
+ errorLinenum = 1;
446
+ }
447
+ addError(errorMsg, errorFilename, errorLinenum);
448
+ }
449
+ }
101
450
 
102
- // Helper function to create a type model
103
- function createType(typeName) {
104
- if (BUILTIN_TYPES.has(typeName)) {
105
- return {'builtin': typeName};
451
+ // Raise a parser exception if there are any errors
452
+ const errors = Array.from(Object.values(errorMap)).sort(compareTuple).map(([,, msg]) => msg);
453
+ if (errors.length) {
454
+ throw new SchemaMarkdownParserError(errors);
106
455
  }
107
- return {'user': typeName};
456
+
457
+ return types;
458
+ }
459
+
460
+
461
+ function compareTuple(v1, v2) {
462
+ return v1.reduce((tot, val, idx) => (tot !== 0 ? tot : (val < v2[idx] ? -1 : (val > v2[idx] ? 1 : 0))), 0);
108
463
  }
109
464
 
110
465
 
@@ -159,9 +514,61 @@ function parseTypedef(matchTypedef) {
159
514
  }
160
515
 
161
516
 
162
- // Helper function to compare tuples/lists
163
- const compareTuple =
164
- (v1, v2) => (v1.reduce((tot, val, idx) => (tot !== 0 ? tot : (val < v2[idx] ? -1 : (val > v2[idx] ? 1 : 0))), 0));
517
+ // Helper function to create a type model
518
+ function createType(typeName) {
519
+ if (BUILTIN_TYPES.has(typeName)) {
520
+ return {'builtin': typeName};
521
+ }
522
+ return {'user': typeName};
523
+ }
524
+
525
+
526
+ // Helper function to parse an attributes string - returns an attributes model
527
+ function parseAttr(attrsString) {
528
+ let attrs = null;
529
+ if (typeof attrsString !== 'undefined') {
530
+ for (const [attrString] of attrsString.matchAll(RE_FIND_ATTRS)) {
531
+ if (attrs === null) {
532
+ attrs = {};
533
+ }
534
+ const matchAttr = attrString.match(RE_ATTR_GROUP);
535
+ const attrOp = matchAttr.groups.op;
536
+ const attrLengthOp = matchAttr.groups.lop;
537
+
538
+ if (typeof matchAttr.groups.nullable !== 'undefined') {
539
+ attrs.nullable = true;
540
+ } else if (typeof attrOp !== 'undefined') {
541
+ const attrValue = parseFloat(matchAttr.groups.opnum);
542
+ if (attrOp === '<') {
543
+ attrs.lt = attrValue;
544
+ } else if (attrOp === '<=') {
545
+ attrs.lte = attrValue;
546
+ } else if (attrOp === '>') {
547
+ attrs.gt = attrValue;
548
+ } else if (attrOp === '>=') {
549
+ attrs.gte = attrValue;
550
+ } else {
551
+ attrs.eq = attrValue;
552
+ }
553
+ } else {
554
+ // typeof attrLengthOp !== 'undefined'
555
+ const attrValue = parseInt(matchAttr.groups.lopnum, 10);
556
+ if (attrLengthOp === '<') {
557
+ attrs.lenLT = attrValue;
558
+ } else if (attrLengthOp === '<=') {
559
+ attrs.lenLTE = attrValue;
560
+ } else if (attrLengthOp === '>') {
561
+ attrs.lenGT = attrValue;
562
+ } else if (attrLengthOp === '>=') {
563
+ attrs.lenGTE = attrValue;
564
+ } else {
565
+ attrs.lenEq = attrValue;
566
+ }
567
+ }
568
+ }
569
+ }
570
+ return attrs;
571
+ }
165
572
 
166
573
 
167
574
  /**
@@ -180,448 +587,3 @@ export class SchemaMarkdownParserError extends Error {
180
587
  this.errors = errors;
181
588
  }
182
589
  }
183
-
184
-
185
- /**
186
- * The parser class for Schema Markdown. Parsing can occur at initialization time or by calling the
187
- * "parseString" method, which can be called repeatedly.
188
- *
189
- * @property {Object} types - The map of user type name to user type model
190
- * @property {Object} errorMap - Set of filename, linenum, and error-message tuples
191
- * @property {Object} filepos - Map of definition-name to filename/linenum-tuples
192
- */
193
- export class SchemaMarkdownParser {
194
- /**
195
- * Create a Schema Markdown parser instance
196
- *
197
- * @param {string} [text=null]: An optional Schema Markdown string to parse
198
- * @param {Object} [types=null]: An optional map of user type name to user type model
199
- * @throws {SchemaMarkdownParserError}
200
- */
201
- constructor(text = null, types = null) {
202
- this.types = types !== null ? types : {};
203
- this.errorMap = {};
204
- this.filepos = {};
205
-
206
- // Parse the Schema Markdown string, if any
207
- if (text !== null) {
208
- this.parseString(text);
209
- }
210
- }
211
-
212
- addError(msg, filename, linenum) {
213
- const errorMsg = `${filename}:${linenum}: error: ${msg}`;
214
- this.errorMap[errorMsg] = [filename, linenum, errorMsg];
215
- }
216
-
217
- getFilepos(typeName, typeKey) {
218
- let filepos = null;
219
- if (typeKey !== null) {
220
- const key = `${typeName}.${typeKey}`;
221
- filepos = key in this.filepos ? this.filepos[key] : null;
222
- }
223
- if (filepos === null) {
224
- filepos = typeName in this.filepos ? this.filepos[typeName] : null;
225
- }
226
- if (filepos === null) {
227
- filepos = ['', 1];
228
- }
229
- return filepos;
230
- }
231
-
232
- /**
233
- * The list of parser error strings
234
- */
235
- get errors() {
236
- return Array.from(Object.values(this.errorMap)).sort(compareTuple).map(([,, msg]) => msg);
237
- }
238
-
239
- /**
240
- * Finalize a parsing operation. You only need to call this method if you set the "parse"
241
- * method's "finalize" argument to false.
242
- *
243
- * @throws {SchemaMarkdownParserError}
244
- */
245
- finalize() {
246
- // Do the finalization
247
- for (const [typeName, memberName, errorMsg] of validateTypeModelTypesErrors(this.types)) {
248
- this.addError(errorMsg, ...this.getFilepos(typeName, memberName));
249
- }
250
-
251
- // Raise a parser exception if there are any errors
252
- const {errors} = this;
253
- if (errors.length) {
254
- throw new SchemaMarkdownParserError(errors);
255
- }
256
- }
257
-
258
- /**
259
- * Parse a Schema Markdown string. This method can be called repeatedly.
260
- *
261
- * @param {string} text - The Schema Markdown string
262
- * @param {string} [filename=''] - The name of file being parsed (for error messages)
263
- * @param {boolean} [finalize=true] - If true, resolve names after parsing. Be sure to call the
264
- * "finalize" method when finished.
265
- * @throws {SchemaMarkdownParserError}
266
- */
267
- parseString(text, filename = '', finalize = true) {
268
- this.parse(text.split(/\r?\n/), filename, finalize);
269
- }
270
-
271
- /**
272
- * Parse Schema Markdown from an iterator of line strings (e.g an input stream). This method can
273
- * be called repeatedly.
274
- *
275
- * @param {string[]} lines - An array of Schema Markdown line strings
276
- * @param {string} [filename=''] - The name of file being parsed (for error messages)
277
- * @param {boolean} [finalize=true] - If true, resolve names after parsing. Be sure to call the
278
- * "finalize" method when finished.
279
- * @throws {SchemaMarkdownParserError}
280
- */
281
- parse(lines, filename = '', finalize = true) {
282
- // Current parser state
283
- let action = null;
284
- let urls = null;
285
- let userType = null;
286
- let doc = [];
287
- let docGroup = null;
288
- let linenum = 0;
289
-
290
- // Helper function to get documentation strings
291
- const getDoc = () => {
292
- let result = null;
293
- if (doc.length) {
294
- result = doc;
295
- doc = [];
296
- }
297
- return result;
298
- };
299
-
300
- // Process each line
301
- const lineContinuation = [];
302
- const lineGroups = [lines, ['']];
303
- for (const lineGroup of lineGroups) {
304
- for (const linePart of lineGroup) {
305
- linenum += 1;
306
-
307
- // Line continuation?
308
- const linePartNoContinuation = linePart.replace(RE_LINE_CONT, '');
309
- if (lineContinuation.length || linePartNoContinuation !== linePart) {
310
- lineContinuation.push(linePartNoContinuation);
311
- }
312
- if (linePartNoContinuation !== linePart) {
313
- continue;
314
- }
315
- let line;
316
- if (lineContinuation.length) {
317
- line = lineContinuation.join('');
318
- lineContinuation.length = 0;
319
- } else {
320
- line = linePart;
321
- }
322
-
323
- // Match syntax
324
- let [matchName, match] = ['comment', line.match(RE_COMMENT)];
325
- if (match === null) {
326
- [matchName, match] = ['group', line.match(RE_GROUP)];
327
- }
328
- if (match === null) {
329
- [matchName, match] = ['action', line.match(RE_ACTION)];
330
- }
331
- if (match === null) {
332
- [matchName, match] = ['definition', line.match(RE_DEFINITION)];
333
- }
334
- if (match === null && action !== null) {
335
- [matchName, match] = ['section', line.match(RE_SECTION)];
336
- }
337
- if (match === null && action !== null) {
338
- [matchName, match] = ['section_plain', line.match(RE_SECTION_PLAIN)];
339
- }
340
- if (match === null && userType !== null && 'enum' in userType) {
341
- const matchValue = line.match(RE_VALUE);
342
- if (matchValue !== null) {
343
- [matchName, match] = ['value', matchValue];
344
- } else {
345
- [matchName, match] = ['value', line.match(RE_VALUE_QUOTED)];
346
- }
347
- }
348
- if (match === null && userType !== null && 'struct' in userType) {
349
- [matchName, match] = ['member', line.match(RE_MEMBER)];
350
- }
351
- if (match === null && urls !== null) {
352
- [matchName, match] = ['urls', line.match(RE_URL)];
353
- }
354
- if (match === null) {
355
- [matchName, match] = ['typedef', line.match(RE_TYPEDEF)];
356
- }
357
- if (match === null) {
358
- matchName = null;
359
- }
360
-
361
- // Comment?
362
- if (matchName === 'comment') {
363
- const docString = match.groups.doc;
364
- if (typeof docString !== 'undefined') {
365
- doc.push(!docString.startsWith(' ') ? docString : docString.slice(1));
366
- }
367
-
368
- // Documentation group?
369
- } else if (matchName === 'group') {
370
- docGroup = match.groups.group;
371
- if (typeof docGroup !== 'undefined') {
372
- docGroup = docGroup.trim();
373
- } else {
374
- docGroup = null;
375
- }
376
-
377
- // Action?
378
- } else if (matchName === 'action') {
379
- const actionId = match.groups.id;
380
-
381
- // Action already defined?
382
- if (actionId in this.types) {
383
- this.addError(`Redefinition of action '${actionId}'`, filename, linenum);
384
- }
385
-
386
- // Clear parser state
387
- urls = null;
388
- userType = null;
389
- const actionDoc = getDoc();
390
-
391
- // Create the new action
392
- action = {'name': actionId};
393
- this.types[actionId] = {'action': action};
394
- if (actionDoc !== null) {
395
- action.doc = actionDoc;
396
- }
397
- if (docGroup !== null) {
398
- action.docGroup = docGroup;
399
- }
400
-
401
- // Definition?
402
- } else if (matchName === 'definition') {
403
- const definitionString = match.groups.type;
404
- const definitionId = match.groups.id;
405
- const definitionBaseIds = match.groups.base_ids;
406
-
407
- // Type already defined?
408
- if (BUILTIN_TYPES.has(definitionId) || definitionId in this.types) {
409
- this.addError(`Redefinition of type '${definitionId}'`, filename, linenum);
410
- }
411
-
412
- // Clear parser state
413
- action = null;
414
- urls = null;
415
- const definitionDoc = getDoc();
416
-
417
- // Struct definition
418
- if (definitionString === 'struct' || definitionString === 'union') {
419
- // Create the new struct type
420
- const struct = {'name': definitionId};
421
- userType = {'struct': struct};
422
- this.types[definitionId] = userType;
423
- if (definitionDoc !== null) {
424
- struct.doc = definitionDoc;
425
- }
426
- if (docGroup !== null) {
427
- struct.docGroup = docGroup;
428
- }
429
- if (definitionString === 'union') {
430
- struct.union = true;
431
- }
432
- if (typeof definitionBaseIds !== 'undefined') {
433
- struct.bases = definitionBaseIds.split(RE_BASE_IDS_SPLIT);
434
- }
435
-
436
- // Enum definition
437
- } else {
438
- // definition_string == 'enum':
439
- // Create the new enum type
440
- const enum_ = {'name': definitionId};
441
- userType = {'enum': enum_};
442
- this.types[definitionId] = userType;
443
- if (definitionDoc !== null) {
444
- enum_.doc = definitionDoc;
445
- }
446
- if (docGroup !== null) {
447
- enum_.docGroup = docGroup;
448
- }
449
- if (typeof definitionBaseIds !== 'undefined') {
450
- enum_.bases = definitionBaseIds.split(RE_BASE_IDS_SPLIT);
451
- }
452
- }
453
-
454
- // Record finalization information
455
- this.filepos[definitionId] = [filename, linenum];
456
-
457
- // Action section?
458
- } else if (matchName === 'section') {
459
- const sectionString = match.groups.type;
460
- const sectionBaseIds = match.groups.base_ids;
461
-
462
- // Action section redefinition?
463
- if (sectionString in action) {
464
- this.addError(`Redefinition of action ${sectionString}`, filename, linenum);
465
- }
466
-
467
- // Clear parser state
468
- urls = null;
469
-
470
- // Set the action section type
471
- const sectionTypeName = `${action.name}_${sectionString}`;
472
- action[sectionString] = sectionTypeName;
473
- if (sectionString === 'errors') {
474
- const enum_ = {'name': sectionTypeName};
475
- userType = {'enum': enum_};
476
- this.types[sectionTypeName] = userType;
477
- if (typeof sectionBaseIds !== 'undefined') {
478
- enum_.bases = sectionBaseIds.split(RE_BASE_IDS_SPLIT);
479
- }
480
- } else {
481
- const struct = {'name': sectionTypeName};
482
- userType = {'struct': struct};
483
- this.types[sectionTypeName] = userType;
484
- if (typeof sectionBaseIds !== 'undefined') {
485
- struct.bases = sectionBaseIds.split(RE_BASE_IDS_SPLIT);
486
- }
487
- }
488
-
489
- // Record finalization information
490
- this.filepos[sectionTypeName] = [filename, linenum];
491
-
492
- // Plain action section?
493
- } else if (matchName === 'section_plain') {
494
- const sectionString = match.groups.type;
495
-
496
- // Action section redefinition?
497
- if (sectionString in action) {
498
- this.addError(`Redefinition of action ${sectionString}`, filename, linenum);
499
- }
500
-
501
- // Clear parser state
502
- userType = null;
503
-
504
- // Update the parser state
505
- urls = [];
506
-
507
- // Enum value?
508
- } else if (matchName === 'value') {
509
- const valueString = match.groups.id;
510
-
511
- // Add the enum value
512
- const enum_ = userType.enum;
513
- if (!('values' in enum_)) {
514
- enum_.values = [];
515
- }
516
- const enumValue = {'name': valueString};
517
- enum_.values.push(enumValue);
518
- const enumValueDoc = getDoc();
519
- if (enumValueDoc !== null) {
520
- enumValue.doc = enumValueDoc;
521
- }
522
-
523
- // Record finalization information
524
- this.filepos[`${enum_.name}.${valueString}`] = [filename, linenum];
525
-
526
- // Struct member?
527
- } else if (matchName === 'member') {
528
- const optional = typeof match.groups.optional !== 'undefined';
529
- const memberName = match.groups.id;
530
-
531
- // Add the member
532
- const {struct} = userType;
533
- if (!('members' in struct)) {
534
- struct.members = [];
535
- }
536
- const [memberType, memberAttr] = parseTypedef(match);
537
- const memberDoc = getDoc();
538
- const member = {
539
- 'name': memberName,
540
- 'type': memberType
541
- };
542
- struct.members.push(member);
543
- if (memberAttr !== null) {
544
- member.attr = memberAttr;
545
- }
546
- if (memberDoc !== null) {
547
- member.doc = memberDoc;
548
- }
549
- if (optional) {
550
- member.optional = true;
551
- }
552
-
553
- // Record finalization information
554
- this.filepos[`${struct.name}.${memberName}`] = [filename, linenum];
555
-
556
- // URL?
557
- } else if (matchName === 'urls') {
558
- const {method, path} = match.groups;
559
-
560
- // Create the action URL object
561
- const actionUrl = {};
562
- if (method !== '*') {
563
- actionUrl.method = method;
564
- }
565
- if (typeof path !== 'undefined') {
566
- actionUrl.path = path;
567
- }
568
-
569
- // Duplicate URL?
570
- if (urls.some((url) => url.method === actionUrl.method && url.path === actionUrl.path)) {
571
- this.addError(`Duplicate URL: ${method} ${'path' in actionUrl ? actionUrl.path : ''}`, filename, linenum);
572
- }
573
-
574
- // Add the URL
575
- if (!('urls' in action)) {
576
- action.urls = urls;
577
- }
578
- urls.push(actionUrl);
579
-
580
- // Typedef?
581
- } else if (matchName === 'typedef') {
582
- const definitionId = match.groups.id;
583
-
584
- // Type already defined?
585
- if (BUILTIN_TYPES.has(definitionId) || definitionId in this.types) {
586
- this.addError(`Redefinition of type '${definitionId}'`, filename, linenum);
587
- }
588
-
589
- // Clear parser state
590
- action = null;
591
- urls = null;
592
- userType = null;
593
- const typedefDoc = getDoc();
594
-
595
- // Create the typedef
596
- const [typedefType, typedefAttr] = parseTypedef(match);
597
- const typedef = {
598
- 'name': definitionId,
599
- 'type': typedefType
600
- };
601
- this.types[definitionId] = {'typedef': typedef};
602
- if (typedefAttr !== null) {
603
- typedef.attr = typedefAttr;
604
- }
605
- if (typedefDoc !== null) {
606
- typedef.doc = typedefDoc;
607
- }
608
- if (docGroup !== null) {
609
- typedef.docGroup = docGroup;
610
- }
611
-
612
- // Record finalization information
613
- this.filepos[definitionId] = [filename, linenum];
614
-
615
- // Unrecognized line syntax
616
- } else {
617
- this.addError('Syntax error', filename, linenum);
618
- }
619
- }
620
- }
621
-
622
- // Finalize, if requested
623
- if (finalize) {
624
- this.finalize();
625
- }
626
- }
627
- }
package/lib/schema.js CHANGED
@@ -4,7 +4,7 @@
4
4
  /** @module lib/schema */
5
5
 
6
6
  import {typeModel} from './typeModel.js';
7
- import {validateTypeModelTypesErrors} from './schemaUtil.js';
7
+ import {validateTypeModelErrors} from './schemaUtil.js';
8
8
 
9
9
 
10
10
  /**
@@ -20,7 +20,6 @@ export function getReferencedTypes(types, typeName, referencedTypes = {}) {
20
20
  }
21
21
 
22
22
 
23
- // eslint-disable-next-line no-unused-vars
24
23
  function getReferencedTypesHelper(types, type, referencedTypes) {
25
24
  // Array?
26
25
  if ('array' in type) {
@@ -123,7 +122,7 @@ export class ValidationError extends Error {
123
122
  * @param {Object} value - The value object to validate
124
123
  * @param {string} [memberFqn=null] - Optional fully-qualified member name
125
124
  * @returns {Object} The validated, transformed value object
126
- * @throws {ValidationError} A validation error occurred
125
+ * @throws [ValidationError]{@link module:lib/schema.ValidationError}
127
126
  */
128
127
  export function validateType(types, typeName, value, memberFqn = null) {
129
128
  if (!(typeName in types)) {
@@ -515,38 +514,17 @@ export function getEnumValues(types, enum_) {
515
514
  *
516
515
  * @param {Object} types - The map of user type name to user type model
517
516
  * @returns {Object} The validated, transformed types object
518
- * @throws {ValidationError} A validation error occurred
517
+ * @throws [ValidationError]{@link module:lib/schema.ValidationError}
519
518
  */
520
- export function validateTypeModelTypes(types) {
519
+ export function validateTypeModel(types) {
521
520
  // Validate with the type model
522
- const validatedTypes = validateType(typeModel.types, 'Types', types);
521
+ const validatedTypes = validateType(typeModel, 'Types', types);
523
522
 
524
523
  // Do additional type model validation
525
- const errors = validateTypeModelTypesErrors(validatedTypes);
524
+ const errors = validateTypeModelErrors(validatedTypes);
526
525
  if (errors.length) {
527
526
  throw new ValidationError(errors.map(([,, message]) => message).join('\n'));
528
527
  }
529
528
 
530
529
  return validatedTypes;
531
530
  }
532
-
533
-
534
- /**
535
- * Validate a user type model
536
- *
537
- * @param {Object} userTypeModel - The user type model
538
- * @returns {Object} The validated, transformed type model
539
- * @throws {ValidationError} A validation error occurred
540
- */
541
- export function validateTypeModel(userTypeModel) {
542
- // Validate with the type model
543
- const validatedUserTypeModel = validateType(typeModel.types, 'TypeModel', userTypeModel);
544
-
545
- // Do additional type model validation
546
- const errors = validateTypeModelTypesErrors(validatedUserTypeModel.types);
547
- if (errors.length) {
548
- throw new ValidationError(errors.map(([,, message]) => message).join('\n'));
549
- }
550
-
551
- return validatedUserTypeModel;
552
- }
package/lib/schemaUtil.js CHANGED
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * @ignore
12
12
  */
13
- export function validateTypeModelTypesErrors(types) {
13
+ export function validateTypeModelErrors(types) {
14
14
  const errors = [];
15
15
 
16
16
  // Check each user type
package/lib/typeModel.js CHANGED
@@ -3,21 +3,13 @@
3
3
 
4
4
  /** @module lib/typeModel */
5
5
 
6
- import {SchemaMarkdownParser} from './parser.js';
7
-
8
-
9
- // The Schema Markdown type model defined as Schema Markdown
10
- const typeModelSmd = `\
11
- # A type model with a title
12
- struct TypeModel
13
-
14
- # The type model's title
15
- string title
16
-
17
- # The type model
18
- Types types
6
+ import {parseSchemaMarkdown} from './parser.js';
19
7
 
20
8
 
9
+ /**
10
+ * The Schema Markdown type model
11
+ */
12
+ export const typeModel = parseSchemaMarkdown(`\
21
13
  # Map of user type name to user type model
22
14
  typedef UserType{len > 0} Types
23
15
 
@@ -250,13 +242,4 @@ struct ActionURL
250
242
 
251
243
  # The URL path. If not provided, uses the default URL path of "/<actionName>".
252
244
  optional string path
253
- `;
254
-
255
-
256
- /**
257
- * The Schema Markdown type model
258
- */
259
- export const typeModel = {
260
- 'title': 'The Schema Markdown Type Model',
261
- 'types': (new SchemaMarkdownParser(typeModelSmd)).types
262
- };
245
+ `);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "schema-markdown",
4
- "version": "1.1.4",
4
+ "version": "1.2.0",
5
5
  "description": "Human-friendly schema definition language and schema validator",
6
6
  "keywords": [
7
7
  "schema",
@@ -24,10 +24,10 @@
24
24
  "test": "ava -v test/"
25
25
  },
26
26
  "devDependencies": {
27
- "ava": "^4.0.0-rc.1",
28
- "c8": "~7.10",
29
- "eslint": "~8.4",
27
+ "ava": "~4.3",
28
+ "c8": "~7.12",
29
+ "eslint": "~8.21",
30
30
  "jsdoc": "~3.6",
31
- "jsdom": "~19.0"
31
+ "jsdom": "~20.0"
32
32
  }
33
33
  }