docusaurus-plugin-generate-schema-docs 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/__tests__/__fixtures__/static/schemas/battle-test-event.json +771 -0
  2. package/__tests__/__fixtures__/static/schemas/conditional-event.json +52 -0
  3. package/__tests__/__fixtures__/static/schemas/nested-conditional-event.json +50 -0
  4. package/__tests__/components/ConditionalRows.test.js +150 -0
  5. package/__tests__/components/ConnectorLines.visualRegression.test.js +93 -0
  6. package/__tests__/components/FoldableRows.test.js +7 -4
  7. package/__tests__/components/SchemaRows.test.js +31 -0
  8. package/__tests__/components/__snapshots__/ConnectorLines.visualRegression.test.js.snap +7 -0
  9. package/__tests__/generateEventDocs.partials.test.js +134 -0
  10. package/__tests__/helpers/buildExampleFromSchema.test.js +49 -0
  11. package/__tests__/helpers/schemaToExamples.test.js +75 -0
  12. package/__tests__/helpers/schemaToTableData.battleTest.test.js +704 -0
  13. package/__tests__/helpers/schemaToTableData.hierarchicalLines.test.js +190 -7
  14. package/__tests__/helpers/schemaToTableData.test.js +263 -2
  15. package/__tests__/helpers/validator.test.js +6 -6
  16. package/components/ConditionalRows.js +156 -0
  17. package/components/FoldableRows.js +88 -61
  18. package/components/PropertiesTable.js +1 -1
  19. package/components/PropertyRow.js +24 -8
  20. package/components/SchemaRows.css +115 -0
  21. package/components/SchemaRows.js +31 -4
  22. package/generateEventDocs.js +41 -34
  23. package/helpers/buildExampleFromSchema.js +11 -0
  24. package/helpers/continuingLinesStyle.js +169 -0
  25. package/helpers/schema-doc-template.js +2 -5
  26. package/helpers/schemaToExamples.js +75 -2
  27. package/helpers/schemaToTableData.js +252 -26
  28. package/helpers/update-schema-ids.js +3 -3
  29. package/helpers/validator.js +7 -19
  30. package/package.json +1 -1
@@ -1,6 +1,16 @@
1
1
  import { getConstraints } from './getConstraints';
2
2
  import { getExamples } from './example-helper';
3
3
 
4
+ /**
5
+ * Computes the bracket descriptor for a new group being opened at `level`.
6
+ * `bracketIndex` is the total number of existing parent brackets, so each
7
+ * nested group gets a unique visual position on the right side.
8
+ */
9
+ function computeOwnBracket(level, parentGroupBrackets) {
10
+ const bracketIndex = parentGroupBrackets.length;
11
+ return { level, bracketIndex };
12
+ }
13
+
4
14
  function processOptions(
5
15
  choices,
6
16
  level,
@@ -8,6 +18,8 @@ function processOptions(
8
18
  isNestedInProperty,
9
19
  requiredArray = [],
10
20
  continuingLevels = [],
21
+ groupBrackets = [],
22
+ choiceIsLastInGroup = true,
11
23
  ) {
12
24
  return choices.map((optionSchema, index) => {
13
25
  const optionTitle = optionSchema.title || 'Option';
@@ -39,10 +51,12 @@ function processOptions(
39
51
  description: optionSchema.description,
40
52
  examples: getExamples(optionSchema),
41
53
  constraints: constraints,
42
- isLastInGroup: isLastOption, // Updated: Uses the calculated flag instead of always true
54
+ // Keep connector lines open when the enclosing choice block isn't truly last.
55
+ isLastInGroup: isLastOption && choiceIsLastInGroup,
43
56
  hasChildren: false,
44
57
  containerType: null,
45
58
  continuingLevels: [...continuingLevels],
59
+ groupBrackets: [...groupBrackets],
46
60
  });
47
61
  } else {
48
62
  // This is a complex object within a choice
@@ -53,7 +67,8 @@ function processOptions(
53
67
  level,
54
68
  isNestedInProperty ? [] : path,
55
69
  continuingLevels,
56
- isLastOption,
70
+ isLastOption && choiceIsLastInGroup,
71
+ groupBrackets,
57
72
  );
58
73
  }
59
74
 
@@ -71,6 +86,7 @@ export function schemaToTableData(
71
86
  path = [],
72
87
  parentContinuingLevels = [],
73
88
  isLastOption = true,
89
+ parentGroupBrackets = [],
74
90
  ) {
75
91
  const flatRows = [];
76
92
 
@@ -81,7 +97,7 @@ export function schemaToTableData(
81
97
  ) {
82
98
  return false;
83
99
  }
84
- if (schemaNode.oneOf || schemaNode.anyOf) {
100
+ if (schemaNode.oneOf || schemaNode.anyOf || schemaNode.if) {
85
101
  return false;
86
102
  }
87
103
  if (
@@ -93,18 +109,113 @@ export function schemaToTableData(
93
109
  return Object.values(schemaNode.properties).every(isEffectivelyEmpty);
94
110
  }
95
111
 
112
+ function buildConditionalRow(
113
+ subSchema,
114
+ currentLevel,
115
+ currentPath,
116
+ continuingLevels,
117
+ currentGroupBrackets = [],
118
+ ownContinuingLevels,
119
+ conditionalIsLastInGroup = true,
120
+ ) {
121
+ // Inner rows (condition, branches) inherit the parent's continuingLevels.
122
+ // The immediate parent connector (currentLevel - 1) is handled by the
123
+ // ConditionalRows component via its ancestorLevels.push(level - 1).
124
+ const innerContinuingLevels = [...continuingLevels];
125
+
126
+ // Compute the bracket for this if/then/else group
127
+ const ownBracket = computeOwnBracket(currentLevel, currentGroupBrackets);
128
+ const innerGroupBrackets = [...currentGroupBrackets, ownBracket];
129
+
130
+ const conditionRows = schemaToTableData(
131
+ subSchema.if,
132
+ currentLevel,
133
+ currentPath,
134
+ innerContinuingLevels,
135
+ false, // branches always follow condition rows, so they are never "last"
136
+ innerGroupBrackets,
137
+ ).map((row) => ({ ...row, isCondition: true }));
138
+
139
+ const hasThen = !!subSchema.then;
140
+ const hasElse = !!subSchema.else;
141
+
142
+ const branches = [];
143
+ if (hasThen) {
144
+ // Then is NOT the last branch if Else exists — use innerContinuingLevels
145
+ // to keep the parent line flowing. If Then IS the last branch, use original.
146
+ const thenLevels = hasElse ? innerContinuingLevels : continuingLevels;
147
+ branches.push({
148
+ title: 'Then',
149
+ description: subSchema.then.description,
150
+ rows: schemaToTableData(
151
+ subSchema.then,
152
+ currentLevel,
153
+ currentPath,
154
+ thenLevels,
155
+ // Keep branch connectors open if this conditional block isn't truly last.
156
+ !hasElse && conditionalIsLastInGroup,
157
+ innerGroupBrackets,
158
+ ),
159
+ });
160
+ }
161
+ if (hasElse) {
162
+ // Else is always the last branch — use original continuingLevels
163
+ branches.push({
164
+ title: 'Else',
165
+ description: subSchema.else.description,
166
+ rows: schemaToTableData(
167
+ subSchema.else,
168
+ currentLevel,
169
+ currentPath,
170
+ continuingLevels,
171
+ conditionalIsLastInGroup,
172
+ innerGroupBrackets,
173
+ ),
174
+ });
175
+ }
176
+
177
+ // ownContinuingLevels (when provided) includes currentLevel for the row's
178
+ // header/toggle rendering, since sibling properties' tree lines must continue.
179
+ // Merge with innerContinuingLevels to also include the parent level.
180
+ const rowContinuingLevels = ownContinuingLevels
181
+ ? [...new Set([...innerContinuingLevels, ...ownContinuingLevels])]
182
+ : innerContinuingLevels;
183
+
184
+ flatRows.push({
185
+ type: 'conditional',
186
+ path: [...currentPath, 'if/then/else'],
187
+ level: currentLevel,
188
+ isLastInGroup: conditionalIsLastInGroup,
189
+ hasChildren: false,
190
+ containerType: null,
191
+ continuingLevels: [...rowContinuingLevels],
192
+ groupBrackets: [...currentGroupBrackets],
193
+ condition: {
194
+ title: 'If',
195
+ description: subSchema.if.description,
196
+ rows: conditionRows,
197
+ },
198
+ branches,
199
+ });
200
+ }
201
+
96
202
  function buildRows(
97
203
  subSchema,
98
204
  currentLevel,
99
205
  currentPath,
100
206
  requiredFromParent = [],
101
207
  continuingLevels = [],
208
+ currentGroupBrackets = [],
102
209
  ) {
103
210
  if (!subSchema) return;
104
211
 
105
212
  if (subSchema.properties) {
106
213
  const propKeys = Object.keys(subSchema.properties);
107
- const hasSiblingChoices = !!(subSchema.oneOf || subSchema.anyOf);
214
+ const hasSiblingChoices = !!(
215
+ subSchema.oneOf ||
216
+ subSchema.anyOf ||
217
+ subSchema.if
218
+ );
108
219
 
109
220
  // Filter out properties that should be skipped to get accurate count
110
221
  const visiblePropKeys = propKeys.filter((name) => {
@@ -127,20 +238,35 @@ export function schemaToTableData(
127
238
  const isLast = isLastProp && (currentLevel !== level || isLastOption);
128
239
 
129
240
  const isChoiceWrapper = !!(propSchema.oneOf || propSchema.anyOf);
241
+ const isConditionalWrapper = !!(
242
+ propSchema.if &&
243
+ (propSchema.then || propSchema.else)
244
+ );
130
245
 
131
246
  // Determine if this property has children and what type
132
247
  const hasNestedProperties = !!propSchema.properties;
133
248
  const hasArrayItems =
134
- propSchema.type === 'array' && !!propSchema.items?.properties;
249
+ propSchema.type === 'array' &&
250
+ !!(propSchema.items?.properties || propSchema.items?.if);
135
251
  const hasNestedChoice = isChoiceWrapper;
252
+ const hasNestedConditional = isConditionalWrapper;
136
253
  const hasChildren =
137
- hasNestedProperties || hasArrayItems || hasNestedChoice;
254
+ hasNestedProperties ||
255
+ hasArrayItems ||
256
+ hasNestedChoice ||
257
+ hasNestedConditional;
138
258
 
139
259
  // Determine container type for the symbol
140
260
  let containerType = null;
261
+ const choiceOptions = propSchema.oneOf || propSchema.anyOf || [];
262
+ const choiceOptionsAreObjects =
263
+ isChoiceWrapper &&
264
+ choiceOptions.some((opt) => opt.type === 'object' || opt.properties);
141
265
  if (
142
266
  hasNestedProperties ||
143
- (isChoiceWrapper && propSchema.type === 'object')
267
+ (isChoiceWrapper && propSchema.type === 'object') ||
268
+ (isConditionalWrapper && propSchema.type === 'object') ||
269
+ choiceOptionsAreObjects
144
270
  ) {
145
271
  containerType = 'object';
146
272
  } else if (hasArrayItems) {
@@ -149,24 +275,36 @@ export function schemaToTableData(
149
275
 
150
276
  // Calculate continuing levels for children
151
277
  // If this is not the last item, add current level to continuing levels for children
152
- // If this IS the last item, remove the immediate parent level (currentLevel - 1) because
153
- // that line stops at this item and should not continue through its children
278
+ // If this IS the last item, don't add currentLevel (no more siblings at this level).
279
+ // We keep all existing continuingLevels intact they represent ancestor lines
280
+ // that must continue through all descendants regardless of last-child status.
154
281
  const childContinuingLevels = isLast
155
- ? continuingLevels.filter((lvl) => lvl !== currentLevel - 1)
282
+ ? [...continuingLevels]
156
283
  : [...continuingLevels, currentLevel];
157
284
 
158
- // This is a "simple" choice property like user_id.
159
- // It gets unwrapped into a choice row directly.
160
- if (
285
+ // A "simple" choice property like user_id: { oneOf: [{ type: "string" }, { type: "integer" }] }
286
+ // where the options are scalar types (no nested properties). These get unwrapped
287
+ // into a choice row directly without their own property row.
288
+ // In contrast, choice wrappers whose options are objects with properties
289
+ // (like contact_method) need their own property row to start a nesting level.
290
+ const isSimpleChoice =
161
291
  isChoiceWrapper &&
162
292
  !propSchema.properties &&
163
- propSchema.type !== 'object'
164
- ) {
293
+ propSchema.type !== 'object' &&
294
+ !(propSchema.oneOf || propSchema.anyOf).some((opt) => opt.properties);
295
+
296
+ if (isSimpleChoice) {
165
297
  const choiceType = propSchema.oneOf ? 'oneOf' : 'anyOf';
166
298
  const choices = propSchema[choiceType];
299
+ const ownBracket = computeOwnBracket(
300
+ currentLevel,
301
+ currentGroupBrackets,
302
+ );
303
+ const innerGroupBrackets = [...currentGroupBrackets, ownBracket];
167
304
  flatRows.push({
168
305
  type: 'choice',
169
306
  choiceType,
307
+ name,
170
308
  path: newPath,
171
309
  level: currentLevel,
172
310
  title: propSchema.title,
@@ -175,6 +313,7 @@ export function schemaToTableData(
175
313
  hasChildren: false,
176
314
  containerType: null,
177
315
  continuingLevels: [...continuingLevels],
316
+ groupBrackets: [...currentGroupBrackets],
178
317
  options: processOptions(
179
318
  choices,
180
319
  currentLevel,
@@ -182,6 +321,8 @@ export function schemaToTableData(
182
321
  false,
183
322
  subSchema.required || requiredFromParent,
184
323
  childContinuingLevels,
324
+ innerGroupBrackets,
325
+ isLast,
185
326
  ),
186
327
  });
187
328
  } else {
@@ -208,6 +349,7 @@ export function schemaToTableData(
208
349
  hasChildren,
209
350
  containerType,
210
351
  continuingLevels: [...continuingLevels],
352
+ groupBrackets: [...currentGroupBrackets],
211
353
  });
212
354
 
213
355
  if (propSchema.properties) {
@@ -217,23 +359,50 @@ export function schemaToTableData(
217
359
  newPath,
218
360
  propSchema.required,
219
361
  childContinuingLevels,
362
+ currentGroupBrackets,
220
363
  );
221
364
  } else if (
222
365
  propSchema.type === 'array' &&
223
- propSchema.items?.properties
366
+ (propSchema.items?.properties || propSchema.items?.if)
224
367
  ) {
225
- buildRows(
226
- propSchema.items,
227
- currentLevel + 1,
228
- [...newPath, '[n]'],
229
- propSchema.items.required,
230
- childContinuingLevels,
231
- );
368
+ if (propSchema.items.properties) {
369
+ buildRows(
370
+ propSchema.items,
371
+ currentLevel + 1,
372
+ [...newPath, '[n]'],
373
+ propSchema.items.required,
374
+ childContinuingLevels,
375
+ currentGroupBrackets,
376
+ );
377
+ }
378
+ // Handle if/then/else inside array items
379
+ if (
380
+ propSchema.items.if &&
381
+ (propSchema.items.then || propSchema.items.else)
382
+ ) {
383
+ buildConditionalRow(
384
+ propSchema.items,
385
+ currentLevel + 1,
386
+ [...newPath, '[n]'],
387
+ childContinuingLevels,
388
+ currentGroupBrackets,
389
+ undefined,
390
+ isLast,
391
+ );
392
+ }
232
393
  } else if (isChoiceWrapper) {
233
394
  // This handles the "complex" choice property like payment_method.
234
395
  // A property row has already been created above, now we add the choice row.
235
396
  const choiceType = propSchema.oneOf ? 'oneOf' : 'anyOf';
236
397
  const choices = propSchema[choiceType];
398
+ const complexOwnBracket = computeOwnBracket(
399
+ currentLevel + 1,
400
+ currentGroupBrackets,
401
+ );
402
+ const complexInnerBrackets = [
403
+ ...currentGroupBrackets,
404
+ complexOwnBracket,
405
+ ];
237
406
  flatRows.push({
238
407
  type: 'choice',
239
408
  choiceType,
@@ -245,6 +414,7 @@ export function schemaToTableData(
245
414
  hasChildren: false,
246
415
  containerType: null,
247
416
  continuingLevels: childContinuingLevels,
417
+ groupBrackets: [...currentGroupBrackets],
248
418
  options: processOptions(
249
419
  choices,
250
420
  currentLevel + 1,
@@ -252,13 +422,39 @@ export function schemaToTableData(
252
422
  true,
253
423
  propSchema.required,
254
424
  childContinuingLevels,
425
+ complexInnerBrackets,
255
426
  ),
256
427
  });
257
428
  }
429
+
430
+ // Handle if/then/else nested inside a property without its own properties.
431
+ // When propSchema HAS properties, the recursive buildRows call above
432
+ // already handles if/then/else via the root-level check at the end of buildRows.
433
+ if (isConditionalWrapper && !propSchema.properties) {
434
+ buildConditionalRow(
435
+ propSchema,
436
+ currentLevel + 1,
437
+ newPath,
438
+ childContinuingLevels,
439
+ currentGroupBrackets,
440
+ undefined,
441
+ isLast,
442
+ );
443
+ }
258
444
  }
259
445
  });
260
446
  }
261
447
 
448
+ // When properties coexist with root-level choices or conditionals,
449
+ // the header/toggle rows need the tree line at currentLevel to continue.
450
+ // Only used for the row's own continuingLevels — NOT propagated to inner rows.
451
+ const hasProperties =
452
+ subSchema.properties && Object.keys(subSchema.properties).length > 0;
453
+ const ownContinuingLevels =
454
+ hasProperties && !continuingLevels.includes(currentLevel)
455
+ ? [...continuingLevels, currentLevel]
456
+ : [...continuingLevels];
457
+
262
458
  // This handles choices at the root of a schema
263
459
  const choiceType = subSchema.oneOf
264
460
  ? 'oneOf'
@@ -267,6 +463,10 @@ export function schemaToTableData(
267
463
  : null;
268
464
  if (choiceType) {
269
465
  const choices = subSchema[choiceType];
466
+ const ownBracket = computeOwnBracket(currentLevel, currentGroupBrackets);
467
+ const innerGroupBrackets = [...currentGroupBrackets, ownBracket];
468
+ const choiceIsLastInGroup =
469
+ isLastOption && !(subSchema.if && (subSchema.then || subSchema.else));
270
470
  flatRows.push({
271
471
  type: 'choice',
272
472
  choiceType,
@@ -274,10 +474,11 @@ export function schemaToTableData(
274
474
  level: currentLevel,
275
475
  title: subSchema.title,
276
476
  description: subSchema.description,
277
- isLastInGroup: true,
477
+ isLastInGroup: choiceIsLastInGroup,
278
478
  hasChildren: false,
279
479
  containerType: null,
280
- continuingLevels: [...continuingLevels],
480
+ continuingLevels: [...ownContinuingLevels],
481
+ groupBrackets: [...currentGroupBrackets],
281
482
  options: processOptions(
282
483
  choices,
283
484
  currentLevel,
@@ -285,6 +486,8 @@ export function schemaToTableData(
285
486
  false,
286
487
  subSchema.required || requiredFromParent,
287
488
  continuingLevels,
489
+ innerGroupBrackets,
490
+ choiceIsLastInGroup,
288
491
  ),
289
492
  });
290
493
  } else if (!subSchema.properties && subSchema.type) {
@@ -303,10 +506,33 @@ export function schemaToTableData(
303
506
  hasChildren: false,
304
507
  containerType: null,
305
508
  continuingLevels: [...continuingLevels],
509
+ groupBrackets: [...currentGroupBrackets],
306
510
  });
307
511
  }
512
+
513
+ // Handle if/then/else at the schema root (or sub-schema root)
514
+ if (subSchema.if && (subSchema.then || subSchema.else)) {
515
+ // ownContinuingLevels includes currentLevel for the row's header/toggle rendering.
516
+ // Inner rows (condition, branches) use the original continuingLevels.
517
+ buildConditionalRow(
518
+ subSchema,
519
+ currentLevel,
520
+ currentPath,
521
+ continuingLevels,
522
+ currentGroupBrackets,
523
+ hasProperties ? [...ownContinuingLevels] : undefined,
524
+ isLastOption,
525
+ );
526
+ }
308
527
  }
309
528
 
310
- buildRows(schema, level, path, schema.required, parentContinuingLevels);
529
+ buildRows(
530
+ schema,
531
+ level,
532
+ path,
533
+ schema.required,
534
+ parentContinuingLevels,
535
+ parentGroupBrackets,
536
+ );
311
537
  return flatRows;
312
538
  }
@@ -28,8 +28,8 @@ export default function updateSchemaIds(siteDir, url, version = null) {
28
28
  ? [version]
29
29
  : JSON.parse(fs.readFileSync(versionsJsonPath, 'utf8'));
30
30
 
31
- for (const version of versions) {
32
- const schemaDir = path.join(siteDir, 'static/schemas', version);
31
+ for (const v of versions) {
32
+ const schemaDir = path.join(siteDir, 'static/schemas', v);
33
33
  if (!fs.existsSync(schemaDir)) {
34
34
  continue;
35
35
  }
@@ -41,7 +41,7 @@ export default function updateSchemaIds(siteDir, url, version = null) {
41
41
  const relativePath = path.relative(path.join(siteDir, 'static'), file);
42
42
  schema.$id = `${baseUrl}/${relativePath.replace(/\\/g, '/')}`;
43
43
  fs.writeFileSync(file, JSON.stringify(schema, null, 2));
44
- console.log(`Updated $id for ${file} in version ${version}`);
44
+ console.log(`Updated $id for ${file} in version ${v}`);
45
45
  }
46
46
  }
47
47
  }
@@ -26,31 +26,23 @@ function createAjvInstance(schemas, mainSchema, schemaPath) {
26
26
  return JSON.parse(schemaContent);
27
27
  };
28
28
 
29
- const options = { allErrors: true, schemas: schemas };
29
+ const options = {
30
+ allErrors: true,
31
+ schemas: schemas,
32
+ strict: false,
33
+ loadSchema,
34
+ };
30
35
 
31
36
  let ajv;
32
37
  if (schemaVersion?.includes('2020-12')) {
33
- options.strict = false;
34
- options.loadSchema = loadSchema;
35
38
  ajv = new Ajv2020(options);
36
39
  } else if (schemaVersion?.includes('2019-09')) {
37
- options.strict = false;
38
- options.loadSchema = loadSchema;
39
40
  ajv = new Ajv2019(options);
40
- } else if (schemaVersion?.includes('draft-07')) {
41
- options.strict = false;
42
- options.loadSchema = loadSchema;
43
- ajv = new Ajv(options);
44
- } else if (schemaVersion?.includes('draft-06')) {
45
- options.strict = false;
46
- options.loadSchema = loadSchema;
47
- ajv = new Ajv(options);
48
41
  } else if (schemaVersion?.includes('draft-04')) {
49
42
  ajv = new AjvDraft4();
50
43
  schemas.forEach((s) => ajv.addSchema(s));
51
44
  } else {
52
- options.strict = false;
53
- options.loadSchema = loadSchema;
45
+ // covers draft-07, draft-06, and unknown versions
54
46
  ajv = new Ajv(options);
55
47
  }
56
48
 
@@ -71,10 +63,6 @@ function createAjvInstance(schemas, mainSchema, schemaPath) {
71
63
  * @returns {function(object): {valid: boolean, errors: object[]}} A function that takes data and returns a validation result.
72
64
  */
73
65
  export async function createValidator(schemas, mainSchema, schemaPath) {
74
- if (!mainSchema) {
75
- mainSchema = schemas;
76
- schemas = [mainSchema];
77
- }
78
66
  const ajv = createAjvInstance(schemas, mainSchema, schemaPath);
79
67
 
80
68
  let validate;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docusaurus-plugin-generate-schema-docs",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Docusaurus plugin to generate documentation from JSON schemas.",
5
5
  "main": "index.js",
6
6
  "license": "MIT",