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 +7 -7
- package/lib/parser.js +455 -493
- package/lib/schema.js +6 -28
- package/lib/schemaUtil.js +1 -1
- package/lib/typeModel.js +6 -23
- package/package.json +5 -5
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
|
-
[
|
|
28
|
-
|
|
27
|
+
[parseSchemaMarkdown](https://craigahobbs.github.io/schema-markdown-js/module-lib_parser.parseSchemaMarkdown.html)
|
|
28
|
+
function:
|
|
29
29
|
|
|
30
30
|
``` javascript
|
|
31
|
-
import {
|
|
31
|
+
import {parseSchemaMarkdown} from 'schema-markdown/parser.js';
|
|
32
32
|
import {validateType} from 'schema-markdown/schema.js';
|
|
33
33
|
|
|
34
|
-
const
|
|
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(
|
|
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(
|
|
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(
|
|
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 {
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
186
|
+
docGroup = null;
|
|
80
187
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
103
|
-
|
|
104
|
-
if (
|
|
105
|
-
|
|
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
|
-
|
|
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
|
|
163
|
-
|
|
164
|
-
|
|
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 {
|
|
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}
|
|
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}
|
|
517
|
+
* @throws [ValidationError]{@link module:lib/schema.ValidationError}
|
|
519
518
|
*/
|
|
520
|
-
export function
|
|
519
|
+
export function validateTypeModel(types) {
|
|
521
520
|
// Validate with the type model
|
|
522
|
-
const validatedTypes = validateType(typeModel
|
|
521
|
+
const validatedTypes = validateType(typeModel, 'Types', types);
|
|
523
522
|
|
|
524
523
|
// Do additional type model validation
|
|
525
|
-
const errors =
|
|
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
package/lib/typeModel.js
CHANGED
|
@@ -3,21 +3,13 @@
|
|
|
3
3
|
|
|
4
4
|
/** @module lib/typeModel */
|
|
5
5
|
|
|
6
|
-
import {
|
|
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.
|
|
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": "
|
|
28
|
-
"c8": "~7.
|
|
29
|
-
"eslint": "~8.
|
|
27
|
+
"ava": "~4.3",
|
|
28
|
+
"c8": "~7.12",
|
|
29
|
+
"eslint": "~8.21",
|
|
30
30
|
"jsdoc": "~3.6",
|
|
31
|
-
"jsdom": "~
|
|
31
|
+
"jsdom": "~20.0"
|
|
32
32
|
}
|
|
33
33
|
}
|