schema-markdown 1.2.0 → 1.2.3
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 +54 -40
- package/lib/parser.js +318 -322
- package/lib/schema.js +17 -15
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,91 +3,105 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/schema-markdown)
|
|
4
4
|
[](https://github.com/craigahobbs/schema-markdown-js/blob/main/LICENSE)
|
|
5
5
|
|
|
6
|
-
**
|
|
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
|
|
19
|
-
- [Documentation
|
|
20
|
-
- [
|
|
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
|
-
##
|
|
16
|
+
## Define a Schema
|
|
25
17
|
|
|
26
|
-
|
|
27
|
-
[
|
|
28
|
-
|
|
18
|
+
Schemas are defined using the
|
|
19
|
+
[Schema Markdown language](https://craigahobbs.github.io/schema-markdown-js/language/),
|
|
20
|
+
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
|
|
29
|
+
struct Aggregation
|
|
30
|
+
|
|
42
31
|
# The aggregation function - default is "Sum"
|
|
43
|
-
optional
|
|
32
|
+
optional AggregationFunction aggregation
|
|
44
33
|
|
|
45
|
-
# The numbers to
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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(
|
|
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
|
|
70
|
+
Validation also fails if a member constraint is violated:
|
|
75
71
|
|
|
76
72
|
``` javascript
|
|
77
73
|
try {
|
|
78
|
-
validateType(
|
|
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
|
-
|
|
90
|
-
|
|
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
|
|
10
|
+
const builtinTypes = new Set(['bool', 'date', 'datetime', 'float', 'int', 'object', 'string', 'uuid']);
|
|
14
11
|
|
|
15
12
|
|
|
16
13
|
// Schema Markdown regex
|
|
17
|
-
const
|
|
18
|
-
const
|
|
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
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
36
|
+
`(?:\\s*\\[\\s*(?<array>${rPartAttrs}?)\\s*\\])?` +
|
|
40
37
|
'|' +
|
|
41
38
|
'(?:' +
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
`\\s*:\\s*(?<dictValueType>${rPartId})` +
|
|
40
|
+
`(?:\\s*\\(\\s*(?<dictValueAttrs>${rPartAttrs})\\s*\\))?` +
|
|
44
41
|
')?' +
|
|
45
|
-
|
|
42
|
+
`(?:\\s*\\{\\s*(?<dict>${rPartAttrs}?)\\s*\\})?` +
|
|
46
43
|
')' +
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
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
|
|
58
|
-
* be called repeatedly.
|
|
54
|
+
* Parse Schema Markdown from a string or an 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(
|
|
93
|
+
lines.push(...text.split(rLineSplit));
|
|
98
94
|
} else {
|
|
99
95
|
for (const textPart of text) {
|
|
100
|
-
lines.push(...textPart.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
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
132
|
-
match = line.match(RE_GROUP);
|
|
149
|
+
match = line.match(rValueQuoted);
|
|
133
150
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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 (
|
|
166
|
-
|
|
167
|
-
match = line.match(RE_TYPEDEF);
|
|
204
|
+
if (docGroup !== null) {
|
|
205
|
+
action.docGroup = docGroup;
|
|
168
206
|
}
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
254
|
+
enum_.docGroup = docGroup;
|
|
211
255
|
}
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
267
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
269
|
+
// Action section redefinition?
|
|
270
|
+
if (sectionString in action) {
|
|
271
|
+
addError(`Redefinition of action ${sectionString}`, filename, linenum);
|
|
272
|
+
}
|
|
273
273
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
302
|
-
|
|
296
|
+
// Record the definition's line number
|
|
297
|
+
filepos[sectionTypeName] = linenum;
|
|
303
298
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
299
|
+
// Plain action section?
|
|
300
|
+
} else if (matchName === 'sectionPlain') {
|
|
301
|
+
const sectionString = match.groups.type;
|
|
307
302
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
303
|
+
// Action section redefinition?
|
|
304
|
+
if (sectionString in action) {
|
|
305
|
+
addError(`Redefinition of action ${sectionString}`, filename, linenum);
|
|
306
|
+
}
|
|
312
307
|
|
|
313
|
-
|
|
314
|
-
|
|
308
|
+
// Clear parser state
|
|
309
|
+
userType = null;
|
|
315
310
|
|
|
316
|
-
|
|
317
|
-
|
|
311
|
+
// Update the parser state
|
|
312
|
+
urls = [];
|
|
318
313
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
314
|
+
// Enum value?
|
|
315
|
+
} else if (matchName === 'value') {
|
|
316
|
+
const valueString = match.groups.id;
|
|
322
317
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
336
|
-
|
|
330
|
+
// Record the definition's line number
|
|
331
|
+
filepos[`${enum_.name}.${valueString}`] = linenum;
|
|
337
332
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
366
|
-
|
|
360
|
+
// Record the definition's line number
|
|
361
|
+
filepos[`${struct.name}.${memberName}`] = linenum;
|
|
367
362
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
363
|
+
// URL?
|
|
364
|
+
} else if (matchName === 'urls') {
|
|
365
|
+
const {method, path} = match.groups;
|
|
371
366
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
381
|
+
// Add the URL
|
|
382
|
+
if (!('urls' in action)) {
|
|
383
|
+
action.urls = urls;
|
|
384
|
+
}
|
|
385
|
+
urls.push(actionUrl);
|
|
391
386
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
387
|
+
// Typedef?
|
|
388
|
+
} else if (matchName === 'typedef') {
|
|
389
|
+
const definitionId = match.groups.id;
|
|
395
390
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
425
|
-
|
|
419
|
+
// Record the definition's line number
|
|
420
|
+
filepos[definitionId] = linenum;
|
|
426
421
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
|
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 (
|
|
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(
|
|
526
|
+
for (const [attrString] of attrsString.matchAll(rFindAttrs)) {
|
|
531
527
|
if (attrs === null) {
|
|
532
528
|
attrs = {};
|
|
533
529
|
}
|
|
534
|
-
const matchAttr = attrString.match(
|
|
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
|
|
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] -
|
|
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
|
|
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] -
|
|
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
|
*/
|
|
@@ -256,7 +256,7 @@ function validateTypeHelper(types, type, value, memberFqn) {
|
|
|
256
256
|
const dictKeyAttr = 'keyAttr' in dict ? dict.keyAttr : null;
|
|
257
257
|
if (value === '') {
|
|
258
258
|
valueNew = {};
|
|
259
|
-
} else if (typeof value !== 'object') {
|
|
259
|
+
} else if (value === null || typeof value !== 'object') {
|
|
260
260
|
throwMemberError(type, value, memberFqn);
|
|
261
261
|
}
|
|
262
262
|
|
|
@@ -333,7 +333,7 @@ function validateTypeHelper(types, type, value, memberFqn) {
|
|
|
333
333
|
// Valid value type?
|
|
334
334
|
if (value === '') {
|
|
335
335
|
valueNew = {};
|
|
336
|
-
} else if (typeof value !== 'object') {
|
|
336
|
+
} else if (value === null || typeof value !== 'object') {
|
|
337
337
|
throwMemberError({'user': struct.name}, value, memberFqn);
|
|
338
338
|
}
|
|
339
339
|
|
|
@@ -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
|
|
452
|
-
* @param {Object} struct - The struct model
|
|
453
|
-
* @returns {Array<Object>} The array of
|
|
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
|
|
484
|
-
* @param {Object} enum_ - The enum model
|
|
485
|
-
* @returns {Array<Object>} The array of
|
|
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
|
|
516
|
-
* @returns {Object} The validated
|
|
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.
|
|
5
|
-
"description": "
|
|
4
|
+
"version": "1.2.3",
|
|
5
|
+
"description": "A schema definition and validation library",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"schema",
|
|
8
8
|
"definition",
|