schema-markdown 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,91 +3,105 @@
3
3
  [![npm](https://img.shields.io/npm/v/schema-markdown)](https://www.npmjs.com/package/schema-markdown)
4
4
  [![GitHub](https://img.shields.io/github/license/craigahobbs/schema-markdown-js)](https://github.com/craigahobbs/schema-markdown-js/blob/main/LICENSE)
5
5
 
6
- **Schema Markdown** is a human-friendly schema definition language and schema validator. Here are
7
- its features at a glance:
8
-
9
- - Schema-validate JSON objects
10
- - Human-friendly schema definition
11
- - Validates member value and length contraints
12
- - Validation *type-massages* string member values
13
- - Pure JavaScript
6
+ **schema-markdown** is a schema definition and validation library.
14
7
 
15
8
 
16
9
  ## Links
17
10
 
18
- - [Schema Markdown Language Reference](https://craigahobbs.github.io/schema-markdown/schema-markdown.html)
19
- - [Documentation on GitHub Pages](https://craigahobbs.github.io/schema-markdown-js/)
20
- - [Package on npm](https://www.npmjs.com/package/schema-markdown)
21
- - [Source code on GitHub](https://github.com/craigahobbs/schema-markdown-js)
11
+ - [The Schema Markdown Language](https://craigahobbs.github.io/schema-markdown-js/language/)
12
+ - [API Documentation](https://craigahobbs.github.io/schema-markdown-js/)
13
+ - [Source code](https://github.com/craigahobbs/schema-markdown-js)
22
14
 
23
15
 
24
- ## Usage
16
+ ## Define a Schema
25
17
 
26
- To schema-validate an object, first parse its *Schema Markdown* using the
27
- [parseSchemaMarkdown](https://craigahobbs.github.io/schema-markdown-js/module-lib_parser.parseSchemaMarkdown.html)
28
- function:
18
+ Schemas are defined using the
19
+ [Schema Markdown](https://craigahobbs.github.io/schema-markdown-js/language/)
20
+ language which is parsed by the
21
+ [parseSchemaMarkdown](https://craigahobbs.github.io/schema-markdown-js/module-lib_parser.html#.parseSchemaMarkdown)
22
+ function. For example:
29
23
 
30
24
  ``` javascript
31
25
  import {parseSchemaMarkdown} from 'schema-markdown/parser.js';
32
- import {validateType} from 'schema-markdown/schema.js';
33
-
34
- const types = parseSchemaMarkdown(`\
35
- # An aggregation function
36
- enum Aggregation
37
- Average
38
- Sum
39
26
 
27
+ export const modelTypes = parseSchemaMarkdown(`\
40
28
  # An aggregate numerical operation
41
- struct Operation
29
+ struct Aggregation
30
+
42
31
  # The aggregation function - default is "Sum"
43
- optional Aggregation aggregation
32
+ optional AggregationFunction aggregation
44
33
 
45
- # The numbers to operate on
34
+ # The numbers to aggregate on
46
35
  int[len > 0] numbers
36
+
37
+ # An aggregation function
38
+ enum AggregationFunction
39
+ Average
40
+ Sum
47
41
  `);
48
42
  ```
49
43
 
50
- Then, validate an object using the
44
+
45
+ ## Validate using a Schema
46
+
47
+ To validate an object using the schema, use the
51
48
  [validateType](https://craigahobbs.github.io/schema-markdown-js/module-lib_schema.html#.validateType)
52
- function:
49
+ function. For example:
53
50
 
54
51
  ``` javascript
55
- const obj = validateType(types, 'Operation', {
56
- 'numbers': [1, 2, '3', 4]
57
- });
52
+ import {validateType} from 'schema-markdown/schema.js';
53
+
54
+ const obj = validateType(modelTypes, 'Aggregation', {'numbers': [1, 2, '3', 4]});
58
55
  console.assert(obj.numbers[2] === 3);
59
56
  ```
60
57
 
61
58
  Notice that the numerical input '3' above is *type-massaged* to the integer 3 by validation.
59
+
62
60
  Validation fails if the object does not match the schema:
63
61
 
64
62
  ``` javascript
65
63
  try {
66
- validateType(types, 'Operation', {
67
- 'numbers': [1, 2, 'asdf', 4]
68
- });
64
+ validateType(modelTypes, 'Aggregation', {'numbers': [1, 2, 'asdf', 4]});
69
65
  } catch ({message}) {
70
66
  console.assert(message === "Invalid value \"asdf\" (type 'string') for member 'numbers.2', expected type 'int'", message);
71
67
  }
72
68
  ```
73
69
 
74
- Validation also fails if a member contraint is violated:
70
+ Validation also fails if a member constraint is violated:
75
71
 
76
72
  ``` javascript
77
73
  try {
78
- validateType(types, 'Operation', {
79
- 'numbers': []
80
- });
74
+ validateType(modelTypes, 'Aggregation', {'numbers': []});
81
75
  } catch ({message}) {
82
76
  console.assert(message === "Invalid value [] (type 'object') for member 'numbers', expected type 'array' [len > 0]", message);
83
77
  }
84
78
  ```
85
79
 
86
80
 
81
+ ## Document a Schema
82
+
83
+ To document the schema, download the
84
+ [documentation application](https://github.com/craigahobbs/schema-markdown-doc#the-schema-markdown-documentation-viewer)
85
+ stub and save the type model as JSON:
86
+
87
+ ~~~
88
+ curl -O https://craigahobbs.github.io/schema-markdown-doc/extra/index.html
89
+ node --input-type=module \
90
+ -e 'import {modelTypes} from "model.js"; console.log(JSON.stringify(modelTypes))' \
91
+ > model.json
92
+ ~~~
93
+
94
+ To host locally, start a local static web server:
95
+
96
+ ```
97
+ python3 -m http.server
98
+ ```
99
+
100
+
87
101
  ## Development
88
102
 
89
- schema-markdown is developed using [javascript-build](https://github.com/craigahobbs/javascript-build#readme)
90
- and it was started using [javascript-template](https://github.com/craigahobbs/javascript-template#readme):
103
+ This package is developed using [javascript-build](https://github.com/craigahobbs/javascript-build#readme).
104
+ It was started using [javascript-template](https://github.com/craigahobbs/javascript-template#readme) as follows:
91
105
 
92
106
  ```
93
107
  template-specialize javascript-template/template/ schema-markdown-js/ -k package schema-markdown -k name 'Craig A. Hobbs' -k email 'craigahobbs@gmail.com' -k github 'craigahobbs' -k noapp 1
package/lib/parser.js CHANGED
@@ -6,56 +6,52 @@
6
6
  import {validateTypeModelErrors} from './schemaUtil.js';
7
7
 
8
8
 
9
- /* eslint-disable prefer-template */
10
-
11
-
12
9
  // Built-in types
13
- const BUILTIN_TYPES = new Set(['bool', 'date', 'datetime', 'float', 'int', 'object', 'string', 'uuid']);
10
+ const builtinTypes = new Set(['bool', 'date', 'datetime', 'float', 'int', 'object', 'string', 'uuid']);
14
11
 
15
12
 
16
13
  // Schema Markdown regex
17
- const RE_PART_ID = '(?:[A-Za-z]\\w*)';
18
- const RE_PART_ATTR_GROUP =
14
+ const rPartId = '(?:[A-Za-z]\\w*)';
15
+ const rPartAttrGroup =
19
16
  '(?:(?<nullable>nullable)' +
20
17
  '|(?<op><=|<|>|>=|==)\\s*(?<opnum>-?\\d+(?:\\.\\d+)?)' +
21
18
  '|(?<ltype>len)\\s*(?<lop><=|<|>|>=|==)\\s*(?<lopnum>\\d+))';
22
- const RE_PART_ATTR = RE_PART_ATTR_GROUP.replace(/\(\?<[^>]+>/g, '(?:');
23
- const RE_PART_ATTRS = '(?:' + RE_PART_ATTR + '(?:\\s*,\\s*' + RE_PART_ATTR + ')*)';
24
- const RE_ATTR_GROUP = new RegExp(RE_PART_ATTR_GROUP);
25
- const RE_FIND_ATTRS = new RegExp(RE_PART_ATTR + '(?:\\s*,\\s*|\\s*$)', 'g');
26
- const RE_LINE_CONT = /\\\s*$/;
27
- const RE_COMMENT = /^\s*(?:#-.*|#(?<doc>.*))?$/;
28
- const RE_GROUP = /^group(?:\s+"(?<group>.+?)")?\s*$/;
29
- const RE_ACTION = new RegExp('^action\\s+(?<id>' + RE_PART_ID + ')');
30
- const RE_PART_BASE_IDS = '(?:\\s*\\(\\s*(?<base_ids>' + RE_PART_ID + '(?:\\s*,\\s*' + RE_PART_ID + ')*)\\s*\\)\\s*)';
31
- const RE_BASE_IDS_SPLIT = /\s*,\s*/;
32
- const RE_DEFINITION = new RegExp('^(?<type>struct|union|enum)\\s+(?<id>' + RE_PART_ID + ')' + RE_PART_BASE_IDS + '?\\s*$');
33
- const RE_SECTION = new RegExp('^\\s+(?<type>path|query|input|output|errors)' + RE_PART_BASE_IDS + '?\\s*$');
34
- const RE_SECTION_PLAIN = /^\s+(?<type>urls)\s*$/;
35
- const RE_PART_TYPEDEF =
36
- '(?<type>' + RE_PART_ID + ')' +
37
- '(?:\\s*\\(\\s*(?<attrs>' + RE_PART_ATTRS + ')\\s*\\))?' +
19
+ const rPartAttr = rPartAttrGroup.replace(/\(\?<[^>]+>/g, '(?:');
20
+ const rPartAttrs = `(?:${rPartAttr}(?:\\s*,\\s*${rPartAttr})*)`;
21
+ const rAttrGroup = new RegExp(rPartAttrGroup);
22
+ const rFindAttrs = new RegExp(`${rPartAttr}(?:\\s*,\\s*|\\s*$)`, 'g');
23
+ const rLineCont = /\\\s*$/;
24
+ const rComment = /^\s*(?:#-.*|#(?<doc>.*))?$/;
25
+ const rGroup = /^group(?:\s+"(?<group>.+?)")?\s*$/;
26
+ const rAction = new RegExp(`^action\\s+(?<id>${rPartId})`);
27
+ const rPartBaseIds = `(?:\\s*\\(\\s*(?<baseIds>${rPartId}(?:\\s*,\\s*${rPartId})*)\\s*\\)\\s*)`;
28
+ const rBaseIdsSplit = /\s*,\s*/;
29
+ const rDefinition = new RegExp(`^(?<type>struct|union|enum)\\s+(?<id>${rPartId})${rPartBaseIds}?\\s*$`);
30
+ const rSection = new RegExp(`^\\s+(?<type>path|query|input|output|errors)${rPartBaseIds}?\\s*$`);
31
+ const rSectionPlain = /^\s+(?<type>urls)\s*$/;
32
+ const rPartTypedef =
33
+ `(?<type>${rPartId})` +
34
+ `(?:\\s*\\(\\s*(?<attrs>${rPartAttrs})\\s*\\))?` +
38
35
  '(?:' +
39
- '(?:\\s*\\[\\s*(?<array>' + RE_PART_ATTRS + '?)\\s*\\])?' +
36
+ `(?:\\s*\\[\\s*(?<array>${rPartAttrs}?)\\s*\\])?` +
40
37
  '|' +
41
38
  '(?:' +
42
- '\\s*:\\s*(?<dictValueType>' + RE_PART_ID + ')' +
43
- '(?:\\s*\\(\\s*(?<dictValueAttrs>' + RE_PART_ATTRS + ')\\s*\\))?' +
39
+ `\\s*:\\s*(?<dictValueType>${rPartId})` +
40
+ `(?:\\s*\\(\\s*(?<dictValueAttrs>${rPartAttrs})\\s*\\))?` +
44
41
  ')?' +
45
- '(?:\\s*\\{\\s*(?<dict>' + RE_PART_ATTRS + '?)\\s*\\})?' +
42
+ `(?:\\s*\\{\\s*(?<dict>${rPartAttrs}?)\\s*\\})?` +
46
43
  ')' +
47
- '\\s+(?<id>' + RE_PART_ID + ')';
48
- const RE_TYPEDEF = new RegExp('^typedef\\s+' + RE_PART_TYPEDEF + '\\s*$');
49
- const RE_MEMBER = new RegExp('^\\s+(?<optional>optional\\s+)?' + RE_PART_TYPEDEF + '\\s*$');
50
- const RE_VALUE = new RegExp('^\\s+(?<id>' + RE_PART_ID + ')\\s*$');
51
- const RE_VALUE_QUOTED = /^\s+"(?<id>.*?)"\s*$/;
52
- const RE_URL = /^\s+(?<method>[A-Za-z]+|\*)(?:\s+(?<path>\/[^\s]*))?/;
53
- const RE_LINE_SPLIT = /\r?\n/;
44
+ `\\s+(?<id>${rPartId})`;
45
+ const rTypedef = new RegExp(`^typedef\\s+${rPartTypedef}\\s*$`);
46
+ const rMember = new RegExp(`^\\s+(?<optional>optional\\s+)?${rPartTypedef}\\s*$`);
47
+ const rValue = new RegExp(`^\\s+(?<id>${rPartId})\\s*$`);
48
+ const rValueQuoted = /^\s+"(?<id>.*?)"\s*$/;
49
+ const rURL = /^\s+(?<method>[A-Za-z]+|\*)(?:\s+(?<path>\/[^\s]*))?/;
50
+ const rLineSplit = /\r?\n/;
54
51
 
55
52
 
56
53
  /**
57
- * Parse Schema Markdown from an iterator of line strings (e.g an input stream). This method can
58
- * be called repeatedly.
54
+ * Parse Schema Markdown from a string or and iterator of strings
59
55
  *
60
56
  * @param {string|string[]} text - The Schema Markdown text
61
57
  * @param {Object} [options.types=''] - The [type model]{@link https://craigahobbs.github.io/schema-markdown-doc/doc/#var.vName='Types'}
@@ -94,340 +90,338 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
94
90
  // Line-split all script text
95
91
  const lines = [];
96
92
  if (typeof text === 'string') {
97
- lines.push(...text.split(RE_LINE_SPLIT));
93
+ lines.push(...text.split(rLineSplit));
98
94
  } else {
99
95
  for (const textPart of text) {
100
- lines.push(...textPart.split(RE_LINE_SPLIT));
96
+ lines.push(...textPart.split(rLineSplit));
101
97
  }
102
98
  }
99
+ lines.push('');
103
100
 
104
101
  // Process each line
105
102
  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
- }
103
+ for (const linePart of lines) {
104
+ linenum += 1;
105
+
106
+ // Line continuation?
107
+ const linePartNoContinuation = linePart.replace(rLineCont, '');
108
+ if (lineContinuation.length || linePartNoContinuation !== linePart) {
109
+ lineContinuation.push(linePartNoContinuation);
110
+ }
111
+ if (linePartNoContinuation !== linePart) {
112
+ continue;
113
+ }
114
+ let line;
115
+ if (lineContinuation.length) {
116
+ line = lineContinuation.join('');
117
+ lineContinuation.length = 0;
118
+ } else {
119
+ line = linePart;
120
+ }
126
121
 
127
- // Match syntax
128
- let matchName = 'comment';
129
- let match = line.match(RE_COMMENT);
122
+ // Match syntax
123
+ let matchName = 'comment';
124
+ let match = line.match(rComment);
125
+ if (match === null) {
126
+ matchName = 'group';
127
+ match = line.match(rGroup);
128
+ }
129
+ if (match === null) {
130
+ matchName = 'action';
131
+ match = line.match(rAction);
132
+ }
133
+ if (match === null) {
134
+ matchName = 'definition';
135
+ match = line.match(rDefinition);
136
+ }
137
+ if (match === null && action !== null) {
138
+ matchName = 'section';
139
+ match = line.match(rSection);
140
+ }
141
+ if (match === null && action !== null) {
142
+ matchName = 'sectionPlain';
143
+ match = line.match(rSectionPlain);
144
+ }
145
+ if (match === null && userType !== null && 'enum' in userType) {
146
+ matchName = 'value';
147
+ match = line.match(rValue);
130
148
  if (match === null) {
131
- matchName = 'group';
132
- match = line.match(RE_GROUP);
149
+ match = line.match(rValueQuoted);
133
150
  }
134
- if (match === null) {
135
- matchName = 'action';
136
- match = line.match(RE_ACTION);
151
+ }
152
+ if (match === null && userType !== null && 'struct' in userType) {
153
+ matchName = 'member';
154
+ match = line.match(rMember);
155
+ }
156
+ if (match === null && urls !== null) {
157
+ matchName = 'urls';
158
+ match = line.match(rURL);
159
+ }
160
+ if (match === null) {
161
+ matchName = 'typedef';
162
+ match = line.match(rTypedef);
163
+ }
164
+ if (match === null) {
165
+ matchName = null;
166
+ }
167
+
168
+ // Comment?
169
+ if (matchName === 'comment') {
170
+ const docString = match.groups.doc;
171
+ if (typeof docString !== 'undefined') {
172
+ doc.push(!docString.startsWith(' ') ? docString : docString.slice(1));
137
173
  }
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
- }
174
+
175
+ // Documentation group?
176
+ } else if (matchName === 'group') {
177
+ docGroup = match.groups.group;
178
+ if (typeof docGroup !== 'undefined') {
179
+ docGroup = docGroup.trim();
180
+ } else {
181
+ docGroup = null;
156
182
  }
157
- if (match === null && userType !== null && 'struct' in userType) {
158
- matchName = 'member';
159
- match = line.match(RE_MEMBER);
183
+
184
+ // Action?
185
+ } else if (matchName === 'action') {
186
+ const actionId = match.groups.id;
187
+
188
+ // Action already defined?
189
+ if (actionId in types) {
190
+ addError(`Redefinition of action '${actionId}'`, filename, linenum);
160
191
  }
161
- if (match === null && urls !== null) {
162
- matchName = 'urls';
163
- match = line.match(RE_URL);
192
+
193
+ // Clear parser state
194
+ urls = null;
195
+ userType = null;
196
+ const actionDoc = getDoc();
197
+
198
+ // Create the new action
199
+ action = {'name': actionId};
200
+ types[actionId] = {'action': action};
201
+ if (actionDoc !== null) {
202
+ action.doc = actionDoc;
164
203
  }
165
- if (match === null) {
166
- matchName = 'typedef';
167
- match = line.match(RE_TYPEDEF);
204
+ if (docGroup !== null) {
205
+ action.docGroup = docGroup;
168
206
  }
169
- if (match === null) {
170
- matchName = null;
207
+
208
+ // Definition?
209
+ } else if (matchName === 'definition') {
210
+ const definitionString = match.groups.type;
211
+ const definitionId = match.groups.id;
212
+ const definitionBaseIds = match.groups.baseIds;
213
+
214
+ // Type already defined?
215
+ if (builtinTypes.has(definitionId) || definitionId in types) {
216
+ addError(`Redefinition of type '${definitionId}'`, filename, linenum);
171
217
  }
172
218
 
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));
219
+ // Clear parser state
220
+ action = null;
221
+ urls = null;
222
+ const definitionDoc = getDoc();
223
+
224
+ // Struct definition
225
+ if (definitionString === 'struct' || definitionString === 'union') {
226
+ // Create the new struct type
227
+ const struct = {'name': definitionId};
228
+ userType = {'struct': struct};
229
+ types[definitionId] = userType;
230
+ if (definitionDoc !== null) {
231
+ struct.doc = definitionDoc;
178
232
  }
179
-
180
- // Documentation group?
181
- } else if (matchName === 'group') {
182
- docGroup = match.groups.group;
183
- if (typeof docGroup !== 'undefined') {
184
- docGroup = docGroup.trim();
185
- } else {
186
- docGroup = null;
233
+ if (docGroup !== null) {
234
+ struct.docGroup = docGroup;
187
235
  }
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);
236
+ if (definitionString === 'union') {
237
+ struct.union = true;
238
+ }
239
+ if (typeof definitionBaseIds !== 'undefined') {
240
+ struct.bases = definitionBaseIds.split(rBaseIdsSplit);
196
241
  }
197
242
 
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;
243
+ // Enum definition
244
+ } else {
245
+ // definitionString == 'enum':
246
+ // Create the new enum type
247
+ const enum_ = {'name': definitionId};
248
+ userType = {'enum': enum_};
249
+ types[definitionId] = userType;
250
+ if (definitionDoc !== null) {
251
+ enum_.doc = definitionDoc;
208
252
  }
209
253
  if (docGroup !== null) {
210
- action.docGroup = docGroup;
254
+ enum_.docGroup = docGroup;
211
255
  }
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);
256
+ if (typeof definitionBaseIds !== 'undefined') {
257
+ enum_.bases = definitionBaseIds.split(rBaseIdsSplit);
222
258
  }
259
+ }
223
260
 
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
249
- } else {
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
- }
261
+ // Record the definition's line number
262
+ filepos[definitionId] = linenum;
265
263
 
266
- // Record the definition's line number
267
- filepos[definitionId] = linenum;
264
+ // Action section?
265
+ } else if (matchName === 'section') {
266
+ const sectionString = match.groups.type;
267
+ const sectionBaseIds = match.groups.baseIds;
268
268
 
269
- // Action section?
270
- } else if (matchName === 'section') {
271
- const sectionString = match.groups.type;
272
- const sectionBaseIds = match.groups.base_ids;
269
+ // Action section redefinition?
270
+ if (sectionString in action) {
271
+ addError(`Redefinition of action ${sectionString}`, filename, linenum);
272
+ }
273
273
 
274
- // Action section redefinition?
275
- if (sectionString in action) {
276
- addError(`Redefinition of action ${sectionString}`, filename, linenum);
274
+ // Clear parser state
275
+ urls = null;
276
+
277
+ // Set the action section type
278
+ const sectionTypeName = `${action.name}_${sectionString}`;
279
+ action[sectionString] = sectionTypeName;
280
+ if (sectionString === 'errors') {
281
+ const enum_ = {'name': sectionTypeName};
282
+ userType = {'enum': enum_};
283
+ types[sectionTypeName] = userType;
284
+ if (typeof sectionBaseIds !== 'undefined') {
285
+ enum_.bases = sectionBaseIds.split(rBaseIdsSplit);
277
286
  }
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
- }
287
+ } else {
288
+ const struct = {'name': sectionTypeName};
289
+ userType = {'struct': struct};
290
+ types[sectionTypeName] = userType;
291
+ if (typeof sectionBaseIds !== 'undefined') {
292
+ struct.bases = sectionBaseIds.split(rBaseIdsSplit);
299
293
  }
294
+ }
300
295
 
301
- // Record the definition's line number
302
- filepos[sectionTypeName] = linenum;
296
+ // Record the definition's line number
297
+ filepos[sectionTypeName] = linenum;
303
298
 
304
- // Plain action section?
305
- } else if (matchName === 'section_plain') {
306
- const sectionString = match.groups.type;
299
+ // Plain action section?
300
+ } else if (matchName === 'sectionPlain') {
301
+ const sectionString = match.groups.type;
307
302
 
308
- // Action section redefinition?
309
- if (sectionString in action) {
310
- addError(`Redefinition of action ${sectionString}`, filename, linenum);
311
- }
303
+ // Action section redefinition?
304
+ if (sectionString in action) {
305
+ addError(`Redefinition of action ${sectionString}`, filename, linenum);
306
+ }
312
307
 
313
- // Clear parser state
314
- userType = null;
308
+ // Clear parser state
309
+ userType = null;
315
310
 
316
- // Update the parser state
317
- urls = [];
311
+ // Update the parser state
312
+ urls = [];
318
313
 
319
- // Enum value?
320
- } else if (matchName === 'value') {
321
- const valueString = match.groups.id;
314
+ // Enum value?
315
+ } else if (matchName === 'value') {
316
+ const valueString = match.groups.id;
322
317
 
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
- }
318
+ // Add the enum value
319
+ const enum_ = userType.enum;
320
+ if (!('values' in enum_)) {
321
+ enum_.values = [];
322
+ }
323
+ const enumValue = {'name': valueString};
324
+ enum_.values.push(enumValue);
325
+ const enumValueDoc = getDoc();
326
+ if (enumValueDoc !== null) {
327
+ enumValue.doc = enumValueDoc;
328
+ }
334
329
 
335
- // Record the definition's line number
336
- filepos[`${enum_.name}.${valueString}`] = linenum;
330
+ // Record the definition's line number
331
+ filepos[`${enum_.name}.${valueString}`] = linenum;
337
332
 
338
- // Struct member?
339
- } else if (matchName === 'member') {
340
- const optional = typeof match.groups.optional !== 'undefined';
341
- const memberName = match.groups.id;
333
+ // Struct member?
334
+ } else if (matchName === 'member') {
335
+ const optional = typeof match.groups.optional !== 'undefined';
336
+ const memberName = match.groups.id;
342
337
 
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
- }
338
+ // Add the member
339
+ const {struct} = userType;
340
+ if (!('members' in struct)) {
341
+ struct.members = [];
342
+ }
343
+ const [memberType, memberAttr] = parseTypedef(match);
344
+ const memberDoc = getDoc();
345
+ const member = {
346
+ 'name': memberName,
347
+ 'type': memberType
348
+ };
349
+ struct.members.push(member);
350
+ if (memberAttr !== null) {
351
+ member.attr = memberAttr;
352
+ }
353
+ if (memberDoc !== null) {
354
+ member.doc = memberDoc;
355
+ }
356
+ if (optional) {
357
+ member.optional = true;
358
+ }
364
359
 
365
- // Record the definition's line number
366
- filepos[`${struct.name}.${memberName}`] = linenum;
360
+ // Record the definition's line number
361
+ filepos[`${struct.name}.${memberName}`] = linenum;
367
362
 
368
- // URL?
369
- } else if (matchName === 'urls') {
370
- const {method, path} = match.groups;
363
+ // URL?
364
+ } else if (matchName === 'urls') {
365
+ const {method, path} = match.groups;
371
366
 
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
- }
367
+ // Create the action URL object
368
+ const actionUrl = {};
369
+ if (method !== '*') {
370
+ actionUrl.method = method;
371
+ }
372
+ if (typeof path !== 'undefined') {
373
+ actionUrl.path = path;
374
+ }
380
375
 
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
- }
376
+ // Duplicate URL?
377
+ if (urls.some((url) => url.method === actionUrl.method && url.path === actionUrl.path)) {
378
+ addError(`Duplicate URL: ${method} ${'path' in actionUrl ? actionUrl.path : ''}`, filename, linenum);
379
+ }
385
380
 
386
- // Add the URL
387
- if (!('urls' in action)) {
388
- action.urls = urls;
389
- }
390
- urls.push(actionUrl);
381
+ // Add the URL
382
+ if (!('urls' in action)) {
383
+ action.urls = urls;
384
+ }
385
+ urls.push(actionUrl);
391
386
 
392
- // Typedef?
393
- } else if (matchName === 'typedef') {
394
- const definitionId = match.groups.id;
387
+ // Typedef?
388
+ } else if (matchName === 'typedef') {
389
+ const definitionId = match.groups.id;
395
390
 
396
- // Type already defined?
397
- if (BUILTIN_TYPES.has(definitionId) || definitionId in types) {
398
- addError(`Redefinition of type '${definitionId}'`, filename, linenum);
399
- }
391
+ // Type already defined?
392
+ if (builtinTypes.has(definitionId) || definitionId in types) {
393
+ addError(`Redefinition of type '${definitionId}'`, filename, linenum);
394
+ }
400
395
 
401
- // Clear parser state
402
- action = null;
403
- urls = null;
404
- userType = null;
405
- const typedefDoc = getDoc();
406
-
407
- // Create the typedef
408
- const [typedefType, typedefAttr] = parseTypedef(match);
409
- const typedef = {
410
- 'name': definitionId,
411
- 'type': typedefType
412
- };
413
- types[definitionId] = {'typedef': typedef};
414
- if (typedefAttr !== null) {
415
- typedef.attr = typedefAttr;
416
- }
417
- if (typedefDoc !== null) {
418
- typedef.doc = typedefDoc;
419
- }
420
- if (docGroup !== null) {
421
- typedef.docGroup = docGroup;
422
- }
396
+ // Clear parser state
397
+ action = null;
398
+ urls = null;
399
+ userType = null;
400
+ const typedefDoc = getDoc();
401
+
402
+ // Create the typedef
403
+ const [typedefType, typedefAttr] = parseTypedef(match);
404
+ const typedef = {
405
+ 'name': definitionId,
406
+ 'type': typedefType
407
+ };
408
+ types[definitionId] = {'typedef': typedef};
409
+ if (typedefAttr !== null) {
410
+ typedef.attr = typedefAttr;
411
+ }
412
+ if (typedefDoc !== null) {
413
+ typedef.doc = typedefDoc;
414
+ }
415
+ if (docGroup !== null) {
416
+ typedef.docGroup = docGroup;
417
+ }
423
418
 
424
- // Record the definition's line number
425
- filepos[definitionId] = linenum;
419
+ // Record the definition's line number
420
+ filepos[definitionId] = linenum;
426
421
 
427
- // Unrecognized line syntax
428
- } else {
429
- addError('Syntax error', filename, linenum);
430
- }
422
+ // Unrecognized line syntax
423
+ } else {
424
+ addError('Syntax error', filename, linenum);
431
425
  }
432
426
  }
433
427
 
@@ -439,7 +433,9 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
439
433
  if (memberName !== null) {
440
434
  errorLinenum = filepos[`${typeName}.${memberName}`] ?? null;
441
435
  }
442
- errorLinenum ??= filepos[typeName] ?? null;
436
+ if (errorLinenum === null) {
437
+ errorLinenum = filepos[typeName] ?? null;
438
+ }
443
439
  if (errorLinenum === null) {
444
440
  errorFilename = '';
445
441
  errorLinenum = 1;
@@ -516,7 +512,7 @@ function parseTypedef(matchTypedef) {
516
512
 
517
513
  // Helper function to create a type model
518
514
  function createType(typeName) {
519
- if (BUILTIN_TYPES.has(typeName)) {
515
+ if (builtinTypes.has(typeName)) {
520
516
  return {'builtin': typeName};
521
517
  }
522
518
  return {'user': typeName};
@@ -527,11 +523,11 @@ function createType(typeName) {
527
523
  function parseAttr(attrsString) {
528
524
  let attrs = null;
529
525
  if (typeof attrsString !== 'undefined') {
530
- for (const [attrString] of attrsString.matchAll(RE_FIND_ATTRS)) {
526
+ for (const [attrString] of attrsString.matchAll(rFindAttrs)) {
531
527
  if (attrs === null) {
532
528
  attrs = {};
533
529
  }
534
- const matchAttr = attrString.match(RE_ATTR_GROUP);
530
+ const matchAttr = attrString.match(rAttrGroup);
535
531
  const attrOp = matchAttr.groups.op;
536
532
  const attrLengthOp = matchAttr.groups.lop;
537
533
 
package/lib/schema.js CHANGED
@@ -10,10 +10,10 @@ import {validateTypeModelErrors} from './schemaUtil.js';
10
10
  /**
11
11
  * Get a user type's referenced type model
12
12
  *
13
- * @param {Object} types - The map of user type name to user type model
13
+ * @param {Object} types - The [type model]{@link https://craigahobbs.github.io/schema-markdown-doc/doc/#var.vName='Types'}
14
14
  * @param {string} typeName - The type name
15
15
  * @param {Object} [referencedTypes=null] - Optional map of referenced user type name to user type model
16
- * @returns {Object}
16
+ * @returns {Object} The referenced [type model]{@link https://craigahobbs.github.io/schema-markdown-doc/doc/#var.vName='Types'}
17
17
  */
18
18
  export function getReferencedTypes(types, typeName, referencedTypes = {}) {
19
19
  return getReferencedTypesHelper(types, {'user': typeName}, referencedTypes);
@@ -104,7 +104,7 @@ export class ValidationError extends Error {
104
104
  /**
105
105
  * Schema Markdown type model validation error constructor
106
106
  *
107
- * @param {string} [memberFqn=null] - Optional fully-qualified member name
107
+ * @param {string} [memberFqn=null] - The fully-qualified member name
108
108
  */
109
109
  constructor(message, memberFqn = null) {
110
110
  super(message);
@@ -117,10 +117,10 @@ export class ValidationError extends Error {
117
117
  * Type-validate a value using a user type model. Container values are duplicated since some member types are
118
118
  * transformed during validation.
119
119
  *
120
- * @param {Object} types - The map of user type name to user type model
120
+ * @param {Object} types - The [type model]{@link https://craigahobbs.github.io/schema-markdown-doc/doc/#var.vName='Types'}
121
121
  * @param {string} typeName - The type name
122
122
  * @param {Object} value - The value object to validate
123
- * @param {string} [memberFqn=null] - Optional fully-qualified member name
123
+ * @param {?string} [memberFqn=null] - The fully-qualified member name
124
124
  * @returns {Object} The validated, transformed value object
125
125
  * @throws [ValidationError]{@link module:lib/schema.ValidationError}
126
126
  */
@@ -448,9 +448,10 @@ function validateAttr(type, attr, value, memberFqn) {
448
448
  /**
449
449
  * Get the struct's members (inherited members first)
450
450
  *
451
- * @param {Object} types - The map of user type name to user type model
452
- * @param {Object} struct - The struct model
453
- * @returns {Array<Object>} The array of struct member models
451
+ * @param {Object} types - The [type model]{@link https://craigahobbs.github.io/schema-markdown-doc/doc/#var.vName='Types'}
452
+ * @param {Object} struct - The [struct model]{@link https://craigahobbs.github.io/schema-markdown-doc/doc/#var.vName='Struct'}
453
+ * @returns {Array<Object>} The array of
454
+ * [struct member models]{@link https://craigahobbs.github.io/schema-markdown-doc/doc/#var.vName='StructMember'}
454
455
  */
455
456
  export function getStructMembers(types, struct) {
456
457
  // No base structs?
@@ -480,9 +481,10 @@ export function getStructMembers(types, struct) {
480
481
  /**
481
482
  * Get the enum's values (inherited values first)
482
483
  *
483
- * @param {Object} types - The map of user type name to user type model
484
- * @param {Object} enum_ - The enum model
485
- * @returns {Array<Object>} The array of enum value models
484
+ * @param {Object} types - The [type model]{@link https://craigahobbs.github.io/schema-markdown-doc/doc/#var.vName='Types'}
485
+ * @param {Object} enum_ - The [enum model]{@link https://craigahobbs.github.io/schema-markdown-doc/doc/#var.vName='Enum'}
486
+ * @returns {Array<Object>} The array of
487
+ * [enum value models]{@link https://craigahobbs.github.io/schema-markdown-doc/doc/#var.vName='EnumValue'}
486
488
  */
487
489
  export function getEnumValues(types, enum_) {
488
490
  // No base enums?
@@ -512,8 +514,8 @@ export function getEnumValues(types, enum_) {
512
514
  /**
513
515
  * Validate a type model's types object
514
516
  *
515
- * @param {Object} types - The map of user type name to user type model
516
- * @returns {Object} The validated, transformed types object
517
+ * @param {Object} types - The [type model]{@link https://craigahobbs.github.io/schema-markdown-doc/doc/#var.vName='Types'}
518
+ * @returns {Object} The validated [type model]{@link https://craigahobbs.github.io/schema-markdown-doc/doc/#var.vName='Types'}
517
519
  * @throws [ValidationError]{@link module:lib/schema.ValidationError}
518
520
  */
519
521
  export function validateTypeModel(types) {
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "schema-markdown",
4
- "version": "1.2.0",
5
- "description": "Human-friendly schema definition language and schema validator",
4
+ "version": "1.2.1",
5
+ "description": "A schema definition and validation library",
6
6
  "keywords": [
7
7
  "schema",
8
8
  "definition",