schema-markdown 1.3.1 → 1.3.2

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
@@ -187,10 +187,10 @@ function validateTypeHelper(types, type, value, memberFqn) {
187
187
  } else if (builtin === 'int' || builtin === 'float') {
188
188
  // Convert string?
189
189
  if (typeof value === 'string') {
190
- if (isNaN(value)) {
190
+ valueNew = parseFloat(value);
191
+ if (isNaN(value) || !Number.isFinite(valueNew)) {
191
192
  throwMemberError(type, value, memberFqn);
192
193
  }
193
- valueNew = parseFloat(value);
194
194
 
195
195
  // Not a number?
196
196
  } else if (typeof value !== 'number') {
@@ -277,91 +277,6 @@ function validateTypeHelper(types, type, value, memberFqn) {
277
277
  }
278
278
  }
279
279
 
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
280
  // User type?
366
281
  } else if ('user' in type) {
367
282
  const userType = getUserType(types, type.user);
@@ -423,7 +338,6 @@ function validateTypeHelper(types, type, value, memberFqn) {
423
338
  const memberFqnMember = [memberFqn, memberName];
424
339
  const memberOptional = 'optional' in member && member.optional;
425
340
  const memberAttr = 'attr' in member ? member.attr : null;
426
- const memberNullable = memberAttr !== null && 'nullable' in memberAttr && memberAttr.nullable;
427
341
 
428
342
  // Missing non-optional member? Check own properties only - "in" matches inherited
429
343
  // Object.prototype properties for member names like "constructor"
@@ -434,13 +348,18 @@ function validateTypeHelper(types, type, value, memberFqn) {
434
348
  } else {
435
349
  // Validate the member value
436
350
  let memberValue = isMap ? valueNew.get(memberName) : valueNew[memberName];
437
- if (memberNullable && (memberValue === null || memberValue === 'null')) {
351
+
352
+ // No member attributes? Validate the member value directly
353
+ if (memberAttr === null) {
354
+ memberValue = validateTypeHelper(types, member.type, memberValue, memberFqnMember);
355
+
356
+ // Nullable null?
357
+ } else if ('nullable' in memberAttr && memberAttr.nullable &&
358
+ (memberValue === null || memberValue === 'null')) {
438
359
  memberValue = null;
439
360
  } else {
440
361
  memberValue = validateTypeHelper(types, member.type, memberValue, memberFqnMember);
441
- if (memberAttr !== null) {
442
- validateAttr(member.type, memberAttr, memberValue, memberFqnMember);
443
- }
362
+ validateAttr(member.type, memberAttr, memberValue, memberFqnMember);
444
363
  }
445
364
 
446
365
  // Copy the validated member
@@ -466,6 +385,91 @@ function validateTypeHelper(types, type, value, memberFqn) {
466
385
  // Return the validated, transformed copy
467
386
  valueNew = valueCopy;
468
387
  }
388
+
389
+ // array?
390
+ } else if ('array' in type) {
391
+ // Valid value type?
392
+ const {array} = type;
393
+ const arrayType = array.type;
394
+ const arrayAttr = 'attr' in array ? array.attr : null;
395
+ if (value === '') {
396
+ valueNew = [];
397
+ } else if (!Array.isArray(value)) {
398
+ throwMemberError(type, value, memberFqn);
399
+ }
400
+
401
+ // Validate the list contents
402
+ const valueCopy = [];
403
+ const arrayValueNullable = arrayAttr !== null && 'nullable' in arrayAttr && arrayAttr.nullable;
404
+ for (let ixArrayValue = 0; ixArrayValue < valueNew.length; ixArrayValue++) {
405
+ const memberFqnValue = [memberFqn, ixArrayValue];
406
+ let arrayValue = valueNew[ixArrayValue];
407
+ if (arrayValueNullable && (arrayValue === null || arrayValue === 'null')) {
408
+ arrayValue = null;
409
+ } else {
410
+ arrayValue = validateTypeHelper(types, arrayType, arrayValue, memberFqnValue);
411
+ if (arrayAttr !== null) {
412
+ validateAttr(arrayType, arrayAttr, arrayValue, memberFqnValue);
413
+ }
414
+ }
415
+ valueCopy.push(arrayValue);
416
+ }
417
+
418
+ // Return the validated, transformed copy
419
+ valueNew = valueCopy;
420
+
421
+ // dict?
422
+ } else if ('dict' in type) {
423
+ // Valid value type?
424
+ const {dict} = type;
425
+ const dictType = dict.type;
426
+ const dictAttr = 'attr' in dict ? dict.attr : null;
427
+ const dictKeyType = 'keyType' in dict ? dict.keyType : defaultDictKeyType;
428
+ const dictKeyAttr = 'keyAttr' in dict ? dict.keyAttr : null;
429
+ if (value === '') {
430
+ valueNew = {};
431
+ } else if (value === null || typeof value !== 'object' || Array.isArray(value)) {
432
+ throwMemberError(type, value, memberFqn);
433
+ }
434
+
435
+ // Validate the dict key/value pairs
436
+ const isMap = valueNew instanceof Map;
437
+ const valueCopy = isMap ? new Map() : {};
438
+ const dictKeyNullable = dictKeyAttr !== null && 'nullable' in dictKeyAttr && dictKeyAttr.nullable;
439
+ const dictValueNullable = dictAttr !== null && 'nullable' in dictAttr && dictAttr.nullable;
440
+ for (let [dictKey, dictValue] of (isMap ? valueNew.entries() : Object.entries(valueNew))) {
441
+ const memberFqnKey = [memberFqn, dictKey];
442
+
443
+ // Validate the key
444
+ if (dictKeyNullable && (dictKey === null || dictKey === 'null')) {
445
+ dictKey = null;
446
+ } else {
447
+ dictKey = validateTypeHelper(types, dictKeyType, dictKey, memberFqn);
448
+ if (dictKeyAttr !== null) {
449
+ validateAttr(dictKeyType, dictKeyAttr, dictKey, memberFqn);
450
+ }
451
+ }
452
+
453
+ // Validate the value
454
+ if (dictValueNullable && (dictValue === null || dictValue === 'null')) {
455
+ dictValue = null;
456
+ } else {
457
+ dictValue = validateTypeHelper(types, dictType, dictValue, memberFqnKey);
458
+ if (dictAttr !== null) {
459
+ validateAttr(dictType, dictAttr, dictValue, memberFqnKey);
460
+ }
461
+ }
462
+
463
+ // Copy the key/value
464
+ if (isMap) {
465
+ valueCopy.set(dictKey, dictValue);
466
+ } else {
467
+ setValue(valueCopy, dictKey, dictValue);
468
+ }
469
+ }
470
+
471
+ // Return the validated, transformed copy
472
+ valueNew = valueCopy;
469
473
  }
470
474
 
471
475
  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.2",
5
5
  "description": "A schema definition and validation library",
6
6
  "keywords": [
7
7
  "schema",