schema-markdown 1.3.1 → 1.3.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/lib/encode.js CHANGED
@@ -106,6 +106,8 @@ export function decodeQueryString(paramStr) {
106
106
  if (obj === null) {
107
107
  obj = {};
108
108
  parent[keyParent] = obj;
109
+ } else if (typeof obj !== 'object') {
110
+ throw new Error(`Invalid key '${keyFqn.slice(0, 100)}'`);
109
111
  }
110
112
 
111
113
  // Create the index for this key - check own properties only, and define "__proto__"
package/lib/parser.js CHANGED
@@ -97,6 +97,9 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
97
97
  lines.push(...text.split(rLineSplit));
98
98
  } else {
99
99
  for (const textPart of text) {
100
+ if (typeof textPart !== 'string') {
101
+ throw new TypeError(`Invalid Schema Markdown text ${JSON.stringify(textPart)}`.slice(0, 100));
102
+ }
100
103
  lines.push(...textPart.split(rLineSplit));
101
104
  }
102
105
  }
@@ -109,66 +112,66 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
109
112
 
110
113
  // Line continuation?
111
114
  const linePartNoContinuation = linePart.includes('\\') ? linePart.replace(rLineCont, '') : linePart;
112
- if (lineContinuation.length || linePartNoContinuation !== linePart) {
113
- lineContinuation.push(linePartNoContinuation);
114
- }
115
- if (linePartNoContinuation !== linePart) {
116
- continue;
117
- }
118
115
  let line;
119
- if (lineContinuation.length) {
116
+ if (linePartNoContinuation === linePart && !lineContinuation.length) {
117
+ line = linePart;
118
+ } else {
119
+ lineContinuation.push(linePartNoContinuation);
120
+ if (linePartNoContinuation !== linePart) {
121
+ continue;
122
+ }
120
123
  line = lineContinuation.join('');
121
124
  lineContinuation.length = 0;
122
- } else {
123
- line = linePart;
124
125
  }
125
126
 
126
127
  // Match syntax - each regex is guarded by a cheap string test that only skips it when it cannot match
127
- const lineIndented = rIndent.test(line);
128
128
  const lineTrimmed = line.trim();
129
129
  let matchName = 'comment';
130
130
  let match = (lineTrimmed === '' || lineTrimmed.startsWith('#')) ? line.match(rComment) : null;
131
+
132
+ // Not a comment or blank line? Try the remaining syntax
131
133
  if (match === null) {
134
+ const lineIndented = rIndent.test(line);
132
135
  matchName = 'group';
133
136
  match = line.startsWith('group') ? line.match(rGroup) : null;
134
- }
135
- if (match === null) {
136
- matchName = 'action';
137
- match = line.startsWith('action') ? line.match(rAction) : null;
138
- }
139
- if (match === null) {
140
- matchName = 'definition';
141
- match = (line.startsWith('struct') || line.startsWith('union') || line.startsWith('enum')) ? line.match(rDefinition) : null;
142
- }
143
- if (match === null && action !== null && lineIndented) {
144
- matchName = 'section';
145
- match = line.match(rSection);
146
- }
147
- if (match === null && action !== null && lineIndented) {
148
- matchName = 'sectionPlain';
149
- match = line.match(rSectionPlain);
150
- }
151
- if (match === null && userType !== null && 'enum' in userType && lineIndented) {
152
- matchName = 'value';
153
- match = line.match(rValue);
154
137
  if (match === null) {
155
- match = line.match(rValueQuoted);
138
+ matchName = 'action';
139
+ match = line.startsWith('action') ? line.match(rAction) : null;
140
+ }
141
+ if (match === null) {
142
+ matchName = 'definition';
143
+ match = (line.startsWith('struct') || line.startsWith('union') || line.startsWith('enum')) ? line.match(rDefinition) : null;
144
+ }
145
+ if (match === null && action !== null && lineIndented) {
146
+ matchName = 'section';
147
+ match = line.match(rSection);
148
+ }
149
+ if (match === null && action !== null && lineIndented) {
150
+ matchName = 'sectionPlain';
151
+ match = line.match(rSectionPlain);
152
+ }
153
+ if (match === null && userType !== null && 'enum' in userType && lineIndented) {
154
+ matchName = 'value';
155
+ match = line.match(rValue);
156
+ if (match === null) {
157
+ match = line.match(rValueQuoted);
158
+ }
159
+ }
160
+ if (match === null && userType !== null && 'struct' in userType && lineIndented) {
161
+ matchName = 'member';
162
+ match = line.match(rMember);
163
+ }
164
+ if (match === null && urls !== null && lineIndented) {
165
+ matchName = 'urls';
166
+ match = line.match(rURL);
167
+ }
168
+ if (match === null) {
169
+ matchName = 'typedef';
170
+ match = line.startsWith('typedef') ? line.match(rTypedef) : null;
171
+ }
172
+ if (match === null) {
173
+ matchName = null;
156
174
  }
157
- }
158
- if (match === null && userType !== null && 'struct' in userType && lineIndented) {
159
- matchName = 'member';
160
- match = line.match(rMember);
161
- }
162
- if (match === null && urls !== null && lineIndented) {
163
- matchName = 'urls';
164
- match = line.match(rURL);
165
- }
166
- if (match === null) {
167
- matchName = 'typedef';
168
- match = line.startsWith('typedef') ? line.match(rTypedef) : null;
169
- }
170
- if (match === null) {
171
- matchName = null;
172
175
  }
173
176
 
174
177
  // Comment?
@@ -178,39 +181,36 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
178
181
  doc.push(!docString.startsWith(' ') ? docString : docString.slice(1));
179
182
  }
180
183
 
181
- // Documentation group?
182
- } else if (matchName === 'group') {
183
- docGroup = match.groups.group;
184
- if (typeof docGroup !== 'undefined') {
185
- docGroup = docGroup.trim();
186
- } else {
187
- docGroup = null;
188
- }
189
-
190
- // Action?
191
- } else if (matchName === 'action') {
192
- const actionId = match.groups.id;
184
+ // Struct member?
185
+ } else if (matchName === 'member') {
186
+ const optional = typeof match.groups.optional !== 'undefined';
187
+ const memberName = match.groups.id;
193
188
 
194
- // Action already defined?
195
- if (Object.hasOwn(types, actionId)) {
196
- addError(`Redefinition of action '${actionId}'`, filename, linenum);
189
+ // Add the member
190
+ const {struct} = userType;
191
+ if (!('members' in struct)) {
192
+ struct.members = [];
197
193
  }
198
-
199
- // Clear parser state
200
- urls = null;
201
- userType = null;
202
- const actionDoc = getDoc();
203
-
204
- // Create the new action
205
- action = {'name': actionId};
206
- types[actionId] = {'action': action};
207
- if (actionDoc !== null) {
208
- action.doc = actionDoc;
194
+ const [memberType, memberAttr] = parseTypedef(match);
195
+ const memberDoc = getDoc();
196
+ const member = {
197
+ 'name': memberName,
198
+ 'type': memberType
199
+ };
200
+ struct.members.push(member);
201
+ if (memberAttr !== null) {
202
+ member.attr = memberAttr;
209
203
  }
210
- if (docGroup !== null) {
211
- action.docGroup = docGroup;
204
+ if (memberDoc !== null) {
205
+ member.doc = memberDoc;
206
+ }
207
+ if (optional) {
208
+ member.optional = true;
212
209
  }
213
210
 
211
+ // Record the definition's line number
212
+ filepos[`${struct.name}.${memberName}`] = linenum;
213
+
214
214
  // Definition?
215
215
  } else if (matchName === 'definition') {
216
216
  const definitionString = match.groups.type;
@@ -267,6 +267,93 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
267
267
  // Record the definition's line number
268
268
  filepos[definitionId] = linenum;
269
269
 
270
+ // Enum value?
271
+ } else if (matchName === 'value') {
272
+ const valueString = match.groups.id;
273
+
274
+ // Add the enum value
275
+ const enum_ = userType.enum;
276
+ if (!('values' in enum_)) {
277
+ enum_.values = [];
278
+ }
279
+ const enumValue = {'name': valueString};
280
+ enum_.values.push(enumValue);
281
+ const enumValueDoc = getDoc();
282
+ if (enumValueDoc !== null) {
283
+ enumValue.doc = enumValueDoc;
284
+ }
285
+
286
+ // Record the definition's line number
287
+ filepos[`${enum_.name}.${valueString}`] = linenum;
288
+
289
+ // Typedef?
290
+ } else if (matchName === 'typedef') {
291
+ const definitionId = match.groups.id;
292
+
293
+ // Type already defined?
294
+ if (builtinTypes.has(definitionId) || Object.hasOwn(builtinDeprecated, definitionId) || Object.hasOwn(types, definitionId)) {
295
+ addError(`Redefinition of type '${definitionId}'`, filename, linenum);
296
+ }
297
+
298
+ // Clear parser state
299
+ action = null;
300
+ urls = null;
301
+ userType = null;
302
+ const typedefDoc = getDoc();
303
+
304
+ // Create the typedef
305
+ const [typedefType, typedefAttr] = parseTypedef(match);
306
+ const typedef = {
307
+ 'name': definitionId,
308
+ 'type': typedefType
309
+ };
310
+ types[definitionId] = {'typedef': typedef};
311
+ if (typedefAttr !== null) {
312
+ typedef.attr = typedefAttr;
313
+ }
314
+ if (typedefDoc !== null) {
315
+ typedef.doc = typedefDoc;
316
+ }
317
+ if (docGroup !== null) {
318
+ typedef.docGroup = docGroup;
319
+ }
320
+
321
+ // Record the definition's line number
322
+ filepos[definitionId] = linenum;
323
+
324
+ // Documentation group?
325
+ } else if (matchName === 'group') {
326
+ docGroup = match.groups.group;
327
+ if (typeof docGroup !== 'undefined') {
328
+ docGroup = docGroup.trim();
329
+ } else {
330
+ docGroup = null;
331
+ }
332
+
333
+ // Action?
334
+ } else if (matchName === 'action') {
335
+ const actionId = match.groups.id;
336
+
337
+ // Action already defined?
338
+ if (Object.hasOwn(types, actionId)) {
339
+ addError(`Redefinition of action '${actionId}'`, filename, linenum);
340
+ }
341
+
342
+ // Clear parser state
343
+ urls = null;
344
+ userType = null;
345
+ const actionDoc = getDoc();
346
+
347
+ // Create the new action
348
+ action = {'name': actionId};
349
+ types[actionId] = {'action': action};
350
+ if (actionDoc !== null) {
351
+ action.doc = actionDoc;
352
+ }
353
+ if (docGroup !== null) {
354
+ action.docGroup = docGroup;
355
+ }
356
+
270
357
  // Action section?
271
358
  } else if (matchName === 'section') {
272
359
  const sectionString = match.groups.type;
@@ -321,55 +408,6 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
321
408
  action[sectionString] = urls;
322
409
  }
323
410
 
324
- // Enum value?
325
- } else if (matchName === 'value') {
326
- const valueString = match.groups.id;
327
-
328
- // Add the enum value
329
- const enum_ = userType.enum;
330
- if (!('values' in enum_)) {
331
- enum_.values = [];
332
- }
333
- const enumValue = {'name': valueString};
334
- enum_.values.push(enumValue);
335
- const enumValueDoc = getDoc();
336
- if (enumValueDoc !== null) {
337
- enumValue.doc = enumValueDoc;
338
- }
339
-
340
- // Record the definition's line number
341
- filepos[`${enum_.name}.${valueString}`] = linenum;
342
-
343
- // Struct member?
344
- } else if (matchName === 'member') {
345
- const optional = typeof match.groups.optional !== 'undefined';
346
- const memberName = match.groups.id;
347
-
348
- // Add the member
349
- const {struct} = userType;
350
- if (!('members' in struct)) {
351
- struct.members = [];
352
- }
353
- const [memberType, memberAttr] = parseTypedef(match);
354
- const memberDoc = getDoc();
355
- const member = {
356
- 'name': memberName,
357
- 'type': memberType
358
- };
359
- struct.members.push(member);
360
- if (memberAttr !== null) {
361
- member.attr = memberAttr;
362
- }
363
- if (memberDoc !== null) {
364
- member.doc = memberDoc;
365
- }
366
- if (optional) {
367
- member.optional = true;
368
- }
369
-
370
- // Record the definition's line number
371
- filepos[`${struct.name}.${memberName}`] = linenum;
372
-
373
411
  // URL?
374
412
  } else if (matchName === 'urls') {
375
413
  const {method, path} = match.groups;
@@ -392,41 +430,6 @@ export function parseSchemaMarkdown(text, {types = {}, filename = '', validate =
392
430
  // Add the URL
393
431
  urls.push(actionUrl);
394
432
 
395
- // Typedef?
396
- } else if (matchName === 'typedef') {
397
- const definitionId = match.groups.id;
398
-
399
- // Type already defined?
400
- if (builtinTypes.has(definitionId) || Object.hasOwn(builtinDeprecated, definitionId) || Object.hasOwn(types, definitionId)) {
401
- addError(`Redefinition of type '${definitionId}'`, filename, linenum);
402
- }
403
-
404
- // Clear parser state
405
- action = null;
406
- urls = null;
407
- userType = null;
408
- const typedefDoc = getDoc();
409
-
410
- // Create the typedef
411
- const [typedefType, typedefAttr] = parseTypedef(match);
412
- const typedef = {
413
- 'name': definitionId,
414
- 'type': typedefType
415
- };
416
- types[definitionId] = {'typedef': typedef};
417
- if (typedefAttr !== null) {
418
- typedef.attr = typedefAttr;
419
- }
420
- if (typedefDoc !== null) {
421
- typedef.doc = typedefDoc;
422
- }
423
- if (docGroup !== null) {
424
- typedef.docGroup = docGroup;
425
- }
426
-
427
- // Record the definition's line number
428
- filepos[definitionId] = linenum;
429
-
430
433
  // Unrecognized line syntax
431
434
  } else {
432
435
  addError('Syntax error', filename, linenum);
package/lib/schema.js CHANGED
@@ -159,8 +159,17 @@ export function validateType(types, typeName, value, memberFqn = null) {
159
159
  }
160
160
 
161
161
 
162
+ // Helper to create a local date (midnight) - unlike new Date, years 0 through 99 aren't 1900 through 1999
163
+ function newLocalDate(year, month, day) {
164
+ const date = new Date(0);
165
+ date.setFullYear(year, month - 1, day);
166
+ date.setHours(0, 0, 0, 0);
167
+ return date;
168
+ }
169
+
170
+
162
171
  // Regular expressions used by validateTypeHelper
163
- const rDate = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/;
172
+ const rDate = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})(?:T00:00(?::00(?:\.0+)?)?(?:Z|(?<tzhour>[+-]\d{2}):(?<tzminute>\d{2})))?$/;
164
173
  const rDatetime = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/;
165
174
  const rUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
166
175
 
@@ -187,10 +196,10 @@ function validateTypeHelper(types, type, value, memberFqn) {
187
196
  } else if (builtin === 'int' || builtin === 'float') {
188
197
  // Convert string?
189
198
  if (typeof value === 'string') {
190
- if (isNaN(value)) {
199
+ valueNew = parseFloat(value);
200
+ if (isNaN(value) || !Number.isFinite(valueNew)) {
191
201
  throwMemberError(type, value, memberFqn);
192
202
  }
193
- valueNew = parseFloat(value);
194
203
 
195
204
  // Not a number?
196
205
  } else if (typeof value !== 'number') {
@@ -221,26 +230,27 @@ function validateTypeHelper(types, type, value, memberFqn) {
221
230
 
222
231
  // date?
223
232
  } else if (builtin === 'date') {
224
- // Convert string?
233
+ // Convert string? A date, or a datetime at midnight in its own offset - the date is the string's date.
225
234
  if (typeof value === 'string') {
226
- // Date-only?
227
235
  const mDate = value.match(rDate);
228
236
  if (mDate !== null) {
229
237
  const year = Number.parseInt(mDate.groups.year, 10);
230
238
  const month = Number.parseInt(mDate.groups.month, 10);
231
239
  const day = Number.parseInt(mDate.groups.day, 10);
232
- valueNew = new Date(year, month - 1, day);
233
- // new Date rolls an out-of-range month/day over (e.g. Feb 30 -> Mar 1); reject that
234
- if (valueNew.getMonth() !== month - 1) {
240
+ const {tzhour = null, tzminute = null} = mDate.groups;
241
+ valueNew = newLocalDate(year, month, day);
242
+ // A date rolls an out-of-range month/day over (e.g. Feb 30 -> Mar 1); reject that, and an invalid offset
243
+ if (valueNew.getMonth() !== month - 1 ||
244
+ (tzhour !== null && (Math.abs(Number.parseInt(tzhour, 10)) > 23 || Number.parseInt(tzminute, 10) > 59))) {
235
245
  throwMemberError(type, value, memberFqn);
236
246
  }
237
247
  } else {
238
248
  throwMemberError(type, value, memberFqn);
239
249
  }
240
250
 
241
- // Not a date, or a date with a time-of-day?
242
- } else if (!(value instanceof Date) ||
243
- value.getHours() + value.getMinutes() + value.getSeconds() + value.getMilliseconds() !== 0) {
251
+ // Not a date - a datetime at the start of its day? A day's start is its local midnight, or its first time when a
252
+ // timezone change skips midnight.
253
+ } else if (!(value instanceof Date) || new Date(value).setHours(0, 0, 0, 0) !== value.getTime()) {
244
254
  throwMemberError(type, value, memberFqn);
245
255
  }
246
256
 
@@ -254,8 +264,8 @@ function validateTypeHelper(types, type, value, memberFqn) {
254
264
  const year = Number.parseInt(mDatetime.groups.year, 10);
255
265
  const month = Number.parseInt(mDatetime.groups.month, 10);
256
266
  const day = Number.parseInt(mDatetime.groups.day, 10);
257
- // new Date rolls an out-of-range day over (e.g. Feb 30 -> Mar 1); reject that
258
- if (Number.isNaN(valueNew.getTime()) || new Date(year, month - 1, day).getMonth() !== month - 1) {
267
+ // A date rolls an out-of-range day over (e.g. Feb 30 -> Mar 1); reject that
268
+ if (Number.isNaN(valueNew.getTime()) || newLocalDate(year, month, day).getMonth() !== month - 1) {
259
269
  throwMemberError(type, value, memberFqn);
260
270
  }
261
271
  } else {
@@ -277,91 +287,6 @@ function validateTypeHelper(types, type, value, memberFqn) {
277
287
  }
278
288
  }
279
289
 
280
- // array?
281
- } else if ('array' in type) {
282
- // Valid value type?
283
- const {array} = type;
284
- const arrayType = array.type;
285
- const arrayAttr = 'attr' in array ? array.attr : null;
286
- if (value === '') {
287
- valueNew = [];
288
- } else if (!Array.isArray(value)) {
289
- throwMemberError(type, value, memberFqn);
290
- }
291
-
292
- // Validate the list contents
293
- const valueCopy = [];
294
- const arrayValueNullable = arrayAttr !== null && 'nullable' in arrayAttr && arrayAttr.nullable;
295
- for (let ixArrayValue = 0; ixArrayValue < valueNew.length; ixArrayValue++) {
296
- const memberFqnValue = [memberFqn, ixArrayValue];
297
- let arrayValue = valueNew[ixArrayValue];
298
- if (arrayValueNullable && (arrayValue === null || arrayValue === 'null')) {
299
- arrayValue = null;
300
- } else {
301
- arrayValue = validateTypeHelper(types, arrayType, arrayValue, memberFqnValue);
302
- if (arrayAttr !== null) {
303
- validateAttr(arrayType, arrayAttr, arrayValue, memberFqnValue);
304
- }
305
- }
306
- valueCopy.push(arrayValue);
307
- }
308
-
309
- // Return the validated, transformed copy
310
- valueNew = valueCopy;
311
-
312
- // dict?
313
- } else if ('dict' in type) {
314
- // Valid value type?
315
- const {dict} = type;
316
- const dictType = dict.type;
317
- const dictAttr = 'attr' in dict ? dict.attr : null;
318
- const dictKeyType = 'keyType' in dict ? dict.keyType : defaultDictKeyType;
319
- const dictKeyAttr = 'keyAttr' in dict ? dict.keyAttr : null;
320
- if (value === '') {
321
- valueNew = {};
322
- } else if (value === null || typeof value !== 'object' || Array.isArray(value)) {
323
- throwMemberError(type, value, memberFqn);
324
- }
325
-
326
- // Validate the dict key/value pairs
327
- const isMap = valueNew instanceof Map;
328
- const valueCopy = isMap ? new Map() : {};
329
- const dictKeyNullable = dictKeyAttr !== null && 'nullable' in dictKeyAttr && dictKeyAttr.nullable;
330
- const dictValueNullable = dictAttr !== null && 'nullable' in dictAttr && dictAttr.nullable;
331
- for (let [dictKey, dictValue] of (isMap ? valueNew.entries() : Object.entries(valueNew))) {
332
- const memberFqnKey = [memberFqn, dictKey];
333
-
334
- // Validate the key
335
- if (dictKeyNullable && (dictKey === null || dictKey === 'null')) {
336
- dictKey = null;
337
- } else {
338
- dictKey = validateTypeHelper(types, dictKeyType, dictKey, memberFqn);
339
- if (dictKeyAttr !== null) {
340
- validateAttr(dictKeyType, dictKeyAttr, dictKey, memberFqn);
341
- }
342
- }
343
-
344
- // Validate the value
345
- if (dictValueNullable && (dictValue === null || dictValue === 'null')) {
346
- dictValue = null;
347
- } else {
348
- dictValue = validateTypeHelper(types, dictType, dictValue, memberFqnKey);
349
- if (dictAttr !== null) {
350
- validateAttr(dictType, dictAttr, dictValue, memberFqnKey);
351
- }
352
- }
353
-
354
- // Copy the key/value
355
- if (isMap) {
356
- valueCopy.set(dictKey, dictValue);
357
- } else {
358
- setValue(valueCopy, dictKey, dictValue);
359
- }
360
- }
361
-
362
- // Return the validated, transformed copy
363
- valueNew = valueCopy;
364
-
365
290
  // User type?
366
291
  } else if ('user' in type) {
367
292
  const userType = getUserType(types, type.user);
@@ -423,7 +348,6 @@ function validateTypeHelper(types, type, value, memberFqn) {
423
348
  const memberFqnMember = [memberFqn, memberName];
424
349
  const memberOptional = 'optional' in member && member.optional;
425
350
  const memberAttr = 'attr' in member ? member.attr : null;
426
- const memberNullable = memberAttr !== null && 'nullable' in memberAttr && memberAttr.nullable;
427
351
 
428
352
  // Missing non-optional member? Check own properties only - "in" matches inherited
429
353
  // Object.prototype properties for member names like "constructor"
@@ -434,13 +358,18 @@ function validateTypeHelper(types, type, value, memberFqn) {
434
358
  } else {
435
359
  // Validate the member value
436
360
  let memberValue = isMap ? valueNew.get(memberName) : valueNew[memberName];
437
- if (memberNullable && (memberValue === null || memberValue === 'null')) {
361
+
362
+ // No member attributes? Validate the member value directly
363
+ if (memberAttr === null) {
364
+ memberValue = validateTypeHelper(types, member.type, memberValue, memberFqnMember);
365
+
366
+ // Nullable null?
367
+ } else if ('nullable' in memberAttr && memberAttr.nullable &&
368
+ (memberValue === null || memberValue === 'null')) {
438
369
  memberValue = null;
439
370
  } else {
440
371
  memberValue = validateTypeHelper(types, member.type, memberValue, memberFqnMember);
441
- if (memberAttr !== null) {
442
- validateAttr(member.type, memberAttr, memberValue, memberFqnMember);
443
- }
372
+ validateAttr(member.type, memberAttr, memberValue, memberFqnMember);
444
373
  }
445
374
 
446
375
  // Copy the validated member
@@ -466,6 +395,91 @@ function validateTypeHelper(types, type, value, memberFqn) {
466
395
  // Return the validated, transformed copy
467
396
  valueNew = valueCopy;
468
397
  }
398
+
399
+ // array?
400
+ } else if ('array' in type) {
401
+ // Valid value type?
402
+ const {array} = type;
403
+ const arrayType = array.type;
404
+ const arrayAttr = 'attr' in array ? array.attr : null;
405
+ if (value === '') {
406
+ valueNew = [];
407
+ } else if (!Array.isArray(value)) {
408
+ throwMemberError(type, value, memberFqn);
409
+ }
410
+
411
+ // Validate the list contents
412
+ const valueCopy = [];
413
+ const arrayValueNullable = arrayAttr !== null && 'nullable' in arrayAttr && arrayAttr.nullable;
414
+ for (let ixArrayValue = 0; ixArrayValue < valueNew.length; ixArrayValue++) {
415
+ const memberFqnValue = [memberFqn, ixArrayValue];
416
+ let arrayValue = valueNew[ixArrayValue];
417
+ if (arrayValueNullable && (arrayValue === null || arrayValue === 'null')) {
418
+ arrayValue = null;
419
+ } else {
420
+ arrayValue = validateTypeHelper(types, arrayType, arrayValue, memberFqnValue);
421
+ if (arrayAttr !== null) {
422
+ validateAttr(arrayType, arrayAttr, arrayValue, memberFqnValue);
423
+ }
424
+ }
425
+ valueCopy.push(arrayValue);
426
+ }
427
+
428
+ // Return the validated, transformed copy
429
+ valueNew = valueCopy;
430
+
431
+ // dict?
432
+ } else if ('dict' in type) {
433
+ // Valid value type?
434
+ const {dict} = type;
435
+ const dictType = dict.type;
436
+ const dictAttr = 'attr' in dict ? dict.attr : null;
437
+ const dictKeyType = 'keyType' in dict ? dict.keyType : defaultDictKeyType;
438
+ const dictKeyAttr = 'keyAttr' in dict ? dict.keyAttr : null;
439
+ if (value === '') {
440
+ valueNew = {};
441
+ } else if (value === null || typeof value !== 'object' || Array.isArray(value)) {
442
+ throwMemberError(type, value, memberFqn);
443
+ }
444
+
445
+ // Validate the dict key/value pairs
446
+ const isMap = valueNew instanceof Map;
447
+ const valueCopy = isMap ? new Map() : {};
448
+ const dictKeyNullable = dictKeyAttr !== null && 'nullable' in dictKeyAttr && dictKeyAttr.nullable;
449
+ const dictValueNullable = dictAttr !== null && 'nullable' in dictAttr && dictAttr.nullable;
450
+ for (let [dictKey, dictValue] of (isMap ? valueNew.entries() : Object.entries(valueNew))) {
451
+ const memberFqnKey = [memberFqn, dictKey];
452
+
453
+ // Validate the key
454
+ if (dictKeyNullable && (dictKey === null || dictKey === 'null')) {
455
+ dictKey = null;
456
+ } else {
457
+ dictKey = validateTypeHelper(types, dictKeyType, dictKey, memberFqn);
458
+ if (dictKeyAttr !== null) {
459
+ validateAttr(dictKeyType, dictKeyAttr, dictKey, memberFqn);
460
+ }
461
+ }
462
+
463
+ // Validate the value
464
+ if (dictValueNullable && (dictValue === null || dictValue === 'null')) {
465
+ dictValue = null;
466
+ } else {
467
+ dictValue = validateTypeHelper(types, dictType, dictValue, memberFqnKey);
468
+ if (dictAttr !== null) {
469
+ validateAttr(dictType, dictAttr, dictValue, memberFqnKey);
470
+ }
471
+ }
472
+
473
+ // Copy the key/value
474
+ if (isMap) {
475
+ valueCopy.set(dictKey, dictValue);
476
+ } else {
477
+ setValue(valueCopy, dictKey, dictValue);
478
+ }
479
+ }
480
+
481
+ // Return the validated, transformed copy
482
+ valueNew = valueCopy;
469
483
  }
470
484
 
471
485
  return valueNew;
package/lib/schemaUtil.js CHANGED
@@ -106,6 +106,23 @@ export function validateTypeModelErrors(types) {
106
106
  errors.push([typeName, null, `Inconsistent type name "${typedef.name}" for "${typeName}"`]);
107
107
  }
108
108
 
109
+ // Circular typedef?
110
+ const visited = new Set([typeName]);
111
+ let typedefType = typedef.type;
112
+ while ('user' in typedefType) {
113
+ const typedefUserName = typedefType.user;
114
+ const typedefUserType = Object.hasOwn(types, typedefUserName) ? types[typedefUserName] : null;
115
+ if (typedefUserType === null || !('typedef' in typedefUserType)) {
116
+ break;
117
+ }
118
+ if (visited.has(typedefUserName)) {
119
+ errors.push([typeName, null, `Circular typedef detected for type "${typeName}"`]);
120
+ break;
121
+ }
122
+ visited.add(typedefUserName);
123
+ typedefType = typedefUserType.typedef.type;
124
+ }
125
+
109
126
  // Check the type and its attributes
110
127
  validateTypeModelType(errors, types, typedef.type, 'attr' in typedef ? typedef.attr : null, typeName, null);
111
128
 
@@ -170,11 +187,12 @@ export function validateTypeModelErrors(types) {
170
187
  }
171
188
 
172
189
 
173
- function getEffectiveType(types, type) {
174
- if ('user' in type && Object.hasOwn(types, type.user)) {
190
+ function getEffectiveType(types, type, visited = new Set()) {
191
+ if ('user' in type && Object.hasOwn(types, type.user) && !visited.has(type.user)) {
175
192
  const userType = types[type.user];
176
193
  if ('typedef' in userType) {
177
- return getEffectiveType(types, userType.typedef.type);
194
+ visited.add(type.user);
195
+ return getEffectiveType(types, userType.typedef.type, visited);
178
196
  }
179
197
  }
180
198
  return type;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "schema-markdown",
4
- "version": "1.3.1",
4
+ "version": "1.3.3",
5
5
  "description": "A schema definition and validation library",
6
6
  "keywords": [
7
7
  "schema",
@@ -25,9 +25,9 @@
25
25
  },
26
26
  "devDependencies": {
27
27
  "@eslint/js": "~10.0",
28
- "c8": "~11.0",
29
- "eslint": "~10.6",
30
- "globals": "~17.7",
28
+ "c8": "~12.0",
29
+ "eslint": "~10.12",
30
+ "globals": "~17.13",
31
31
  "jsdoc": "~4.0"
32
32
  }
33
33
  }