docusaurus-plugin-generate-schema-docs 1.8.3 → 1.8.5

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 (62) hide show
  1. package/README.md +12 -0
  2. package/__tests__/__fixtures__/validateSchemas/main-schema-with-not-allof.json +11 -0
  3. package/__tests__/__fixtures__/validateSchemas/schema-with-not-anyof-multi.json +12 -0
  4. package/__tests__/__fixtures__/validateSchemas/schema-with-not-anyof.json +30 -0
  5. package/__tests__/__fixtures__/validateSchemas/schema-with-not-edge-cases.json +24 -0
  6. package/__tests__/__fixtures__/validateSchemas/schema-with-not-non-object.json +15 -0
  7. package/__tests__/__snapshots__/generateEventDocs.anchor.test.js.snap +6 -0
  8. package/__tests__/__snapshots__/generateEventDocs.nested.test.js.snap +6 -0
  9. package/__tests__/__snapshots__/generateEventDocs.test.js.snap +15 -0
  10. package/__tests__/__snapshots__/generateEventDocs.versioned.test.js.snap +6 -0
  11. package/__tests__/components/PropertiesTable.test.js +66 -0
  12. package/__tests__/components/PropertyRow.test.js +85 -4
  13. package/__tests__/components/SchemaJsonViewer.test.js +118 -0
  14. package/__tests__/generateEventDocs.anchor.test.js +1 -1
  15. package/__tests__/generateEventDocs.nested.test.js +1 -1
  16. package/__tests__/generateEventDocs.partials.test.js +1 -1
  17. package/__tests__/generateEventDocs.test.js +506 -1
  18. package/__tests__/generateEventDocs.versioned.test.js +1 -1
  19. package/__tests__/helpers/buildExampleFromSchema.test.js +240 -0
  20. package/__tests__/helpers/constraintSchemaPaths.test.js +208 -0
  21. package/__tests__/helpers/continuingLinesStyle.test.js +492 -0
  22. package/__tests__/helpers/example-helper.test.js +12 -0
  23. package/__tests__/helpers/exampleModel.test.js +209 -0
  24. package/__tests__/helpers/file-system.test.js +73 -1
  25. package/__tests__/helpers/getConstraints.test.js +43 -0
  26. package/__tests__/helpers/mergeSchema.test.js +94 -0
  27. package/__tests__/helpers/processSchema.test.js +309 -1
  28. package/__tests__/helpers/schema-doc-template.test.js +54 -0
  29. package/__tests__/helpers/schema-processing.test.js +122 -2
  30. package/__tests__/helpers/schemaToExamples.test.js +1007 -0
  31. package/__tests__/helpers/schemaToTableData.mutations.test.js +970 -0
  32. package/__tests__/helpers/schemaToTableData.test.js +157 -0
  33. package/__tests__/helpers/schemaTraversal.test.js +110 -0
  34. package/__tests__/helpers/snippetTargets.test.js +432 -0
  35. package/__tests__/helpers/trackingTargets.test.js +319 -0
  36. package/__tests__/helpers/validator.test.js +385 -1
  37. package/__tests__/index.test.js +436 -0
  38. package/__tests__/syncGtm.test.js +366 -6
  39. package/__tests__/update-schema-ids.test.js +70 -1
  40. package/__tests__/validateSchemas-integration.test.js +2 -2
  41. package/__tests__/validateSchemas.test.js +192 -1
  42. package/components/PropertiesTable.js +32 -2
  43. package/components/PropertyRow.js +29 -2
  44. package/components/SchemaJsonViewer.js +234 -131
  45. package/components/SchemaRows.css +40 -0
  46. package/components/SchemaViewer.js +11 -2
  47. package/generateEventDocs.js +21 -1
  48. package/helpers/constraintSchemaPaths.js +10 -14
  49. package/helpers/example-helper.js +2 -2
  50. package/helpers/getConstraints.js +20 -0
  51. package/helpers/processSchema.js +32 -1
  52. package/helpers/schema-doc-template.js +4 -0
  53. package/helpers/schemaToExamples.js +29 -35
  54. package/helpers/schemaToTableData.js +538 -492
  55. package/helpers/schemaTraversal.cjs +148 -0
  56. package/helpers/trackingTargets.js +26 -3
  57. package/helpers/validator.js +18 -4
  58. package/index.js +1 -2
  59. package/package.json +1 -1
  60. package/scripts/sync-gtm.js +65 -34
  61. package/test-data/payloadContracts.js +35 -0
  62. package/validateSchemas.js +1 -1
@@ -7,8 +7,7 @@ import { getExamples } from './example-helper';
7
7
  * nested group gets a unique visual position on the right side.
8
8
  */
9
9
  function computeOwnBracket(level, parentGroupBrackets) {
10
- const bracketIndex = parentGroupBrackets.length;
11
- return { level, bracketIndex };
10
+ return { level, bracketIndex: parentGroupBrackets.length };
12
11
  }
13
12
 
14
13
  function materializeConditionalBranchSchema(branchSchema, parentSchema) {
@@ -30,45 +29,146 @@ function materializeConditionalBranchSchema(branchSchema, parentSchema) {
30
29
  .map((name) => [name, parentSchema.properties[name]]),
31
30
  );
32
31
 
33
- if (Object.keys(branchProperties).length === 0) {
34
- return branchSchema;
35
- }
36
-
37
- return {
38
- ...branchSchema,
39
- type: 'object',
40
- properties: branchProperties,
41
- };
32
+ return Object.keys(branchProperties).length === 0
33
+ ? branchSchema
34
+ : { ...branchSchema, type: 'object', properties: branchProperties };
42
35
  }
43
36
 
44
37
  function hasRenderableAdditionalProperties(schemaNode) {
45
38
  return !!(
46
- schemaNode &&
47
- schemaNode.additionalProperties &&
39
+ schemaNode?.additionalProperties &&
48
40
  typeof schemaNode.additionalProperties === 'object' &&
49
41
  !Array.isArray(schemaNode.additionalProperties)
50
42
  );
51
43
  }
52
44
 
53
45
  function getRenderablePatternProperties(schemaNode) {
54
- if (!schemaNode?.patternProperties) {
55
- return [];
56
- }
57
-
46
+ if (!schemaNode?.patternProperties) return [];
58
47
  return Object.entries(schemaNode.patternProperties)
59
- .filter(
60
- ([, patternSchema]) =>
61
- patternSchema &&
62
- typeof patternSchema === 'object' &&
63
- !Array.isArray(patternSchema),
64
- )
65
- .map(([pattern, patternSchema]) => [
48
+ .filter(([, s]) => s && typeof s === 'object' && !Array.isArray(s))
49
+ .map(([pattern, s]) => [
66
50
  `patternProperties /${pattern}/`,
67
- {
68
- ...patternSchema,
69
- 'x-schema-keyword-row': true,
70
- },
51
+ { ...s, 'x-schema-keyword-row': true },
52
+ ]);
53
+ }
54
+
55
+ function isEffectivelyEmpty(schemaNode) {
56
+ if (
57
+ schemaNode.type !== 'object' &&
58
+ typeof schemaNode.properties === 'undefined'
59
+ ) {
60
+ return false;
61
+ }
62
+ if (schemaNode.oneOf || schemaNode.anyOf || schemaNode.if) return false;
63
+ if (!schemaNode.properties || Object.keys(schemaNode.properties).length === 0)
64
+ return true;
65
+ return Object.values(schemaNode.properties).every(isEffectivelyEmpty);
66
+ }
67
+
68
+ // --- Step 4: extracted containerType resolution ---
69
+ function resolveContainerType(
70
+ propSchema,
71
+ {
72
+ hasNestedProperties,
73
+ hasAdditionalProperties,
74
+ hasArrayItems,
75
+ isChoiceWrapper,
76
+ isConditionalWrapper,
77
+ choiceOptionsAreObjects,
78
+ },
79
+ ) {
80
+ if (hasNestedProperties || hasAdditionalProperties) return 'object';
81
+ if (
82
+ isChoiceWrapper &&
83
+ (propSchema.type === 'object' || choiceOptionsAreObjects)
84
+ )
85
+ return 'object';
86
+ if (isConditionalWrapper && propSchema.type === 'object') return 'object';
87
+ if (hasArrayItems) return 'array';
88
+ return null;
89
+ }
90
+
91
+ /**
92
+ * Returns whether a property schema has children and what container type it renders as.
93
+ * Also returns flags used for child-building dispatch.
94
+ */
95
+ function getContainerInfo(propSchema) {
96
+ const isChoiceWrapper = !!(propSchema.oneOf || propSchema.anyOf);
97
+ const isConditionalWrapper = !!(
98
+ propSchema.if &&
99
+ (propSchema.then || propSchema.else)
100
+ );
101
+ const hasNestedProperties = !!propSchema.properties;
102
+ const hasAdditionalProperties = hasRenderableAdditionalProperties(propSchema);
103
+ const hasArrayItems =
104
+ propSchema.type === 'array' &&
105
+ !!(propSchema.items?.properties || propSchema.items?.if);
106
+ const choiceOptionsAreObjects =
107
+ isChoiceWrapper &&
108
+ (propSchema.oneOf || propSchema.anyOf).some(
109
+ (opt) => opt.type === 'object' || opt.properties,
110
+ );
111
+
112
+ const hasChildren =
113
+ hasNestedProperties ||
114
+ hasAdditionalProperties ||
115
+ hasArrayItems ||
116
+ isChoiceWrapper ||
117
+ isConditionalWrapper;
118
+
119
+ const containerType = resolveContainerType(propSchema, {
120
+ hasNestedProperties,
121
+ hasAdditionalProperties,
122
+ hasArrayItems,
123
+ isChoiceWrapper,
124
+ isConditionalWrapper,
125
+ choiceOptionsAreObjects,
126
+ });
127
+
128
+ return {
129
+ hasChildren,
130
+ containerType,
131
+ isChoiceWrapper,
132
+ isConditionalWrapper,
133
+ hasArrayItems,
134
+ hasAdditionalProperties,
135
+ };
136
+ }
137
+
138
+ // --- Step 1: Row factory functions ---
139
+ function makeBaseRow(overrides) {
140
+ return {
141
+ hasChildren: false,
142
+ containerType: null,
143
+ ...overrides,
144
+ };
145
+ }
146
+
147
+ function makePropertyRow(fields) {
148
+ return makeBaseRow({ type: 'property', ...fields });
149
+ }
150
+
151
+ function makeChoiceRow(fields) {
152
+ return makeBaseRow({ type: 'choice', ...fields });
153
+ }
154
+
155
+ function makeConditionalRow(fields) {
156
+ return makeBaseRow({ type: 'conditional', ...fields });
157
+ }
158
+
159
+ // --- Step 2: buildPropEntries helper ---
160
+ function buildPropEntries(subSchema, patternPropertyEntries) {
161
+ const entries = subSchema.properties
162
+ ? Object.entries(subSchema.properties)
163
+ : [];
164
+ if (hasRenderableAdditionalProperties(subSchema)) {
165
+ entries.push([
166
+ 'additionalProperties',
167
+ { ...subSchema.additionalProperties, 'x-schema-keyword-row': true },
71
168
  ]);
169
+ }
170
+ entries.push(...patternPropertyEntries);
171
+ return entries;
72
172
  }
73
173
 
74
174
  function processOptions(
@@ -83,179 +183,111 @@ function processOptions(
83
183
  ) {
84
184
  return choices.map((optionSchema, index) => {
85
185
  const optionTitle = optionSchema.title || 'Option';
186
+ const isLast = index === choices.length - 1 && choiceIsLastInGroup;
86
187
 
87
- // Determine if this is the last option in the list.
88
- // If it is NOT the last option, its children must not close the visual tree branch.
89
- const isLastOption = index === choices.length - 1;
90
-
91
- let optionRows = [];
92
-
93
- // This is a primitive type (string, number, etc.) within a choice
188
+ let rows;
94
189
  if (optionSchema.type && !optionSchema.properties) {
190
+ // Primitive type within a choice
95
191
  const isRequired = requiredArray.includes(path[path.length - 1]);
96
192
  const constraints = getConstraints(optionSchema);
97
- if (isRequired) {
98
- constraints.unshift('required');
99
- }
100
-
101
- optionRows.push({
102
- type: 'property',
103
- // The name of the property is the name of the parent property that holds the choice
104
- name: path.length > 0 ? path[path.length - 1] : optionTitle,
105
- path: [...path, `(${optionTitle})`],
106
- // If it's a top-level choice (like user_id), the level is the same as the choice itself.
107
- // Otherwise, it's nested.
108
- level: level,
109
- required: isRequired,
110
- propertyType: optionSchema.type,
111
- description: optionSchema.description,
112
- examples: getExamples(optionSchema),
113
- constraints: constraints,
114
- // Keep connector lines open when the enclosing choice block isn't truly last.
115
- isLastInGroup: isLastOption && choiceIsLastInGroup,
116
- hasChildren: false,
117
- containerType: null,
118
- continuingLevels: [...continuingLevels],
119
- groupBrackets: [...groupBrackets],
120
- });
193
+ if (isRequired) constraints.unshift('required');
194
+ rows = [
195
+ makePropertyRow({
196
+ name: path.length > 0 ? path[path.length - 1] : optionTitle,
197
+ path: [...path, `(${optionTitle})`],
198
+ level,
199
+ required: isRequired,
200
+ propertyType: optionSchema.type,
201
+ description: optionSchema.description,
202
+ examples: getExamples(optionSchema),
203
+ constraints,
204
+ isLastInGroup: isLast,
205
+ continuingLevels: [...continuingLevels],
206
+ groupBrackets: [...groupBrackets],
207
+ }),
208
+ ];
121
209
  } else {
122
- // This is a complex object within a choice
123
- optionRows = schemaToTableData(
210
+ rows = schemaToTableData(
124
211
  optionSchema,
125
- // If nested in a property (like payment_method), the sub-properties start at the same level as the choice
126
- // Otherwise, they are one level deeper.
127
212
  level,
128
213
  isNestedInProperty ? [] : path,
129
214
  continuingLevels,
130
- isLastOption && choiceIsLastInGroup,
215
+ isLast,
131
216
  groupBrackets,
132
217
  );
133
218
  }
134
219
 
135
- return {
136
- title: optionTitle,
137
- description: optionSchema.description,
138
- rows: optionRows,
139
- };
220
+ return { title: optionTitle, description: optionSchema.description, rows };
140
221
  });
141
222
  }
142
223
 
143
- export function schemaToTableData(
144
- schema,
145
- level = 0,
146
- path = [],
147
- parentContinuingLevels = [],
148
- isLastOption = true,
149
- parentGroupBrackets = [],
150
- ) {
151
- const flatRows = [];
224
+ // --- Row building (module-level, each function returns its rows) ---
152
225
 
153
- function isEffectivelyEmpty(schemaNode) {
154
- if (
155
- schemaNode.type !== 'object' &&
156
- typeof schemaNode.properties === 'undefined'
157
- ) {
158
- return false;
159
- }
160
- if (schemaNode.oneOf || schemaNode.anyOf || schemaNode.if) {
161
- return false;
162
- }
163
- if (
164
- !schemaNode.properties ||
165
- Object.keys(schemaNode.properties).length === 0
166
- ) {
167
- return true;
168
- }
169
- return Object.values(schemaNode.properties).every(isEffectivelyEmpty);
170
- }
226
+ function buildConditionalRow(
227
+ subSchema,
228
+ currentLevel,
229
+ currentPath,
230
+ continuingLevels,
231
+ currentGroupBrackets = [],
232
+ ownContinuingLevels,
233
+ conditionalIsLastInGroup = true,
234
+ ) {
235
+ const ownBracket = computeOwnBracket(currentLevel, currentGroupBrackets);
236
+ const innerGroupBrackets = [...currentGroupBrackets, ownBracket];
171
237
 
172
- function buildConditionalRow(
173
- subSchema,
238
+ const conditionRows = schemaToTableData(
239
+ subSchema.if,
174
240
  currentLevel,
175
241
  currentPath,
176
242
  continuingLevels,
177
- currentGroupBrackets = [],
178
- ownContinuingLevels,
179
- conditionalIsLastInGroup = true,
180
- ) {
181
- // Inner rows (condition, branches) inherit the parent's continuingLevels.
182
- // The immediate parent connector (currentLevel - 1) is handled by the
183
- // ConditionalRows component via its ancestorLevels.push(level - 1).
184
- const innerContinuingLevels = [...continuingLevels];
243
+ false, // branches always follow condition rows, so they are never "last"
244
+ innerGroupBrackets,
245
+ ).map((row) => ({ ...row, isCondition: true }));
246
+
247
+ const hasElse = !!subSchema.else;
248
+ const branches = [];
249
+
250
+ if (subSchema.then) {
251
+ branches.push({
252
+ title: 'Then',
253
+ description: subSchema.then.description,
254
+ rows: schemaToTableData(
255
+ materializeConditionalBranchSchema(subSchema.then, subSchema),
256
+ currentLevel,
257
+ currentPath,
258
+ continuingLevels,
259
+ !hasElse && conditionalIsLastInGroup,
260
+ innerGroupBrackets,
261
+ ),
262
+ });
263
+ }
185
264
 
186
- // Compute the bracket for this if/then/else group
187
- const ownBracket = computeOwnBracket(currentLevel, currentGroupBrackets);
188
- const innerGroupBrackets = [...currentGroupBrackets, ownBracket];
189
-
190
- const conditionRows = schemaToTableData(
191
- subSchema.if,
192
- currentLevel,
193
- currentPath,
194
- innerContinuingLevels,
195
- false, // branches always follow condition rows, so they are never "last"
196
- innerGroupBrackets,
197
- ).map((row) => ({ ...row, isCondition: true }));
198
-
199
- const hasThen = !!subSchema.then;
200
- const hasElse = !!subSchema.else;
201
-
202
- const branches = [];
203
- if (hasThen) {
204
- // Then is NOT the last branch if Else exists — use innerContinuingLevels
205
- // to keep the parent line flowing. If Then IS the last branch, use original.
206
- const thenLevels = hasElse ? innerContinuingLevels : continuingLevels;
207
- const thenSchema = materializeConditionalBranchSchema(
208
- subSchema.then,
209
- subSchema,
210
- );
211
- branches.push({
212
- title: 'Then',
213
- description: subSchema.then.description,
214
- rows: schemaToTableData(
215
- thenSchema,
216
- currentLevel,
217
- currentPath,
218
- thenLevels,
219
- // Keep branch connectors open if this conditional block isn't truly last.
220
- !hasElse && conditionalIsLastInGroup,
221
- innerGroupBrackets,
222
- ),
223
- });
224
- }
225
- if (hasElse) {
226
- // Else is always the last branch — use original continuingLevels
227
- const elseSchema = materializeConditionalBranchSchema(
228
- subSchema.else,
229
- subSchema,
230
- );
231
- branches.push({
232
- title: 'Else',
233
- description: subSchema.else.description,
234
- rows: schemaToTableData(
235
- elseSchema,
236
- currentLevel,
237
- currentPath,
238
- continuingLevels,
239
- conditionalIsLastInGroup,
240
- innerGroupBrackets,
241
- ),
242
- });
243
- }
265
+ if (hasElse) {
266
+ branches.push({
267
+ title: 'Else',
268
+ description: subSchema.else.description,
269
+ rows: schemaToTableData(
270
+ materializeConditionalBranchSchema(subSchema.else, subSchema),
271
+ currentLevel,
272
+ currentPath,
273
+ continuingLevels,
274
+ conditionalIsLastInGroup,
275
+ innerGroupBrackets,
276
+ ),
277
+ });
278
+ }
244
279
 
245
- // ownContinuingLevels (when provided) includes currentLevel for the row's
246
- // header/toggle rendering, since sibling properties' tree lines must continue.
247
- // Merge with innerContinuingLevels to also include the parent level.
248
- const rowContinuingLevels = ownContinuingLevels
249
- ? [...new Set([...innerContinuingLevels, ...ownContinuingLevels])]
250
- : innerContinuingLevels;
280
+ // ownContinuingLevels (when provided) includes currentLevel for the row's
281
+ // header/toggle rendering, since sibling properties' tree lines must continue.
282
+ const rowContinuingLevels = ownContinuingLevels
283
+ ? [...new Set([...continuingLevels, ...ownContinuingLevels])]
284
+ : continuingLevels;
251
285
 
252
- flatRows.push({
253
- type: 'conditional',
286
+ return [
287
+ makeConditionalRow({
254
288
  path: [...currentPath, 'if/then/else'],
255
289
  level: currentLevel,
256
290
  isLastInGroup: conditionalIsLastInGroup,
257
- hasChildren: false,
258
- containerType: null,
259
291
  continuingLevels: [...rowContinuingLevels],
260
292
  groupBrackets: [...currentGroupBrackets],
261
293
  condition: {
@@ -264,335 +296,340 @@ export function schemaToTableData(
264
296
  rows: conditionRows,
265
297
  },
266
298
  branches,
267
- });
299
+ }),
300
+ ];
301
+ }
302
+
303
+ // Dispatches child row building after a property row has been pushed.
304
+ function buildPropertyChildren(
305
+ ctx,
306
+ propSchema,
307
+ currentLevel,
308
+ newPath,
309
+ childContinuingLevels,
310
+ currentGroupBrackets,
311
+ isLast,
312
+ {
313
+ isChoiceWrapper,
314
+ isConditionalWrapper,
315
+ hasArrayItems,
316
+ hasAdditionalProperties,
317
+ },
318
+ ) {
319
+ const rows = [];
320
+
321
+ if (propSchema.properties) {
322
+ rows.push(
323
+ ...buildRows(
324
+ ctx,
325
+ propSchema,
326
+ currentLevel + 1,
327
+ newPath,
328
+ propSchema.required,
329
+ childContinuingLevels,
330
+ currentGroupBrackets,
331
+ ),
332
+ );
333
+ } else if (propSchema.type === 'object' && hasAdditionalProperties) {
334
+ rows.push(
335
+ ...buildRows(
336
+ ctx,
337
+ propSchema,
338
+ currentLevel + 1,
339
+ newPath,
340
+ [],
341
+ childContinuingLevels,
342
+ currentGroupBrackets,
343
+ ),
344
+ );
345
+ } else if (hasArrayItems) {
346
+ const itemPath = [...newPath, '[n]'];
347
+ if (propSchema.items.properties) {
348
+ rows.push(
349
+ ...buildRows(
350
+ ctx,
351
+ propSchema.items,
352
+ currentLevel + 1,
353
+ itemPath,
354
+ propSchema.items.required,
355
+ childContinuingLevels,
356
+ currentGroupBrackets,
357
+ ),
358
+ );
359
+ }
360
+ if (
361
+ propSchema.items.if &&
362
+ (propSchema.items.then || propSchema.items.else)
363
+ ) {
364
+ rows.push(
365
+ ...buildConditionalRow(
366
+ propSchema.items,
367
+ currentLevel + 1,
368
+ itemPath,
369
+ childContinuingLevels,
370
+ currentGroupBrackets,
371
+ undefined,
372
+ isLast,
373
+ ),
374
+ );
375
+ }
376
+ } else if (isChoiceWrapper) {
377
+ // Complex choice property (e.g. payment_method): property row already pushed,
378
+ // now add the nested choice row.
379
+ const choiceType = propSchema.oneOf ? 'oneOf' : 'anyOf';
380
+ const ownBracket = computeOwnBracket(
381
+ currentLevel + 1,
382
+ currentGroupBrackets,
383
+ );
384
+ const innerBrackets = [...currentGroupBrackets, ownBracket];
385
+ rows.push(
386
+ makeChoiceRow({
387
+ choiceType,
388
+ path: [...newPath, choiceType],
389
+ level: currentLevel + 1,
390
+ title: propSchema.title,
391
+ description: null,
392
+ isLastInGroup: true,
393
+ continuingLevels: childContinuingLevels,
394
+ groupBrackets: [...currentGroupBrackets],
395
+ options: processOptions(
396
+ propSchema[choiceType],
397
+ currentLevel + 1,
398
+ newPath,
399
+ true,
400
+ propSchema.required,
401
+ childContinuingLevels,
402
+ innerBrackets,
403
+ ),
404
+ }),
405
+ );
268
406
  }
269
407
 
270
- function buildRows(
271
- subSchema,
272
- currentLevel,
273
- currentPath,
274
- requiredFromParent = [],
275
- continuingLevels = [],
276
- currentGroupBrackets = [],
277
- ) {
278
- if (!subSchema) return;
408
+ // Handle if/then/else nested inside a property without its own properties.
409
+ // When propSchema HAS properties, the recursive buildRows call above
410
+ // already handles if/then/else via the root-level check at the end of buildRows.
411
+ if (isConditionalWrapper && !propSchema.properties) {
412
+ rows.push(
413
+ ...buildConditionalRow(
414
+ propSchema,
415
+ currentLevel + 1,
416
+ newPath,
417
+ childContinuingLevels,
418
+ currentGroupBrackets,
419
+ undefined,
420
+ isLast,
421
+ ),
422
+ );
423
+ }
279
424
 
280
- const patternPropertyEntries = getRenderablePatternProperties(subSchema);
425
+ return rows;
426
+ }
281
427
 
282
- if (
283
- subSchema.properties ||
284
- hasRenderableAdditionalProperties(subSchema) ||
285
- patternPropertyEntries.length > 0
286
- ) {
287
- const propEntries = subSchema.properties
288
- ? Object.entries(subSchema.properties)
289
- : [];
290
- if (hasRenderableAdditionalProperties(subSchema)) {
291
- propEntries.push([
292
- 'additionalProperties',
428
+ function buildPropertyRows(
429
+ ctx,
430
+ subSchema,
431
+ currentLevel,
432
+ currentPath,
433
+ requiredFromParent,
434
+ continuingLevels,
435
+ currentGroupBrackets,
436
+ visiblePropEntries,
437
+ hasSiblingChoices,
438
+ ) {
439
+ const rows = [];
440
+
441
+ visiblePropEntries.forEach(([name, propSchema], index) => {
442
+ const newPath = [...currentPath, name];
443
+ const isLastProp =
444
+ index === visiblePropEntries.length - 1 && !hasSiblingChoices;
445
+ const isLast =
446
+ isLastProp && (currentLevel !== ctx.topLevel || ctx.isLastOption);
447
+
448
+ // If this is not the last item, add currentLevel to continuing levels for children
449
+ // so ancestor tree lines keep flowing through all descendants.
450
+ const childContinuingLevels = isLast
451
+ ? [...continuingLevels]
452
+ : [...continuingLevels, currentLevel];
453
+
454
+ const {
455
+ hasChildren,
456
+ containerType,
457
+ isChoiceWrapper,
458
+ isConditionalWrapper,
459
+ hasArrayItems,
460
+ hasAdditionalProperties,
461
+ } = getContainerInfo(propSchema);
462
+
463
+ // A "simple" choice has scalar options (no nested properties) and renders inline.
464
+ // A "complex" choice has object options and needs its own property row first.
465
+ const isSimpleChoice = isChoiceWrapper && containerType === null;
466
+
467
+ if (isSimpleChoice) {
468
+ const choiceType = propSchema.oneOf ? 'oneOf' : 'anyOf';
469
+ const ownBracket = computeOwnBracket(currentLevel, currentGroupBrackets);
470
+ rows.push(
471
+ makeChoiceRow({
472
+ choiceType,
473
+ name,
474
+ path: newPath,
475
+ level: currentLevel,
476
+ title: propSchema.title,
477
+ description: propSchema.description,
478
+ isLastInGroup: isLast,
479
+ continuingLevels: [...continuingLevels],
480
+ groupBrackets: [...currentGroupBrackets],
481
+ options: processOptions(
482
+ propSchema[choiceType],
483
+ currentLevel,
484
+ newPath,
485
+ false,
486
+ subSchema.required || requiredFromParent,
487
+ childContinuingLevels,
488
+ [...currentGroupBrackets, ownBracket],
489
+ isLast,
490
+ ),
491
+ }),
492
+ );
493
+ } else {
494
+ const isRequired =
495
+ (subSchema.required || requiredFromParent)?.includes(name) || false;
496
+ const constraints = getConstraints(propSchema);
497
+ if (isRequired) constraints.unshift('required');
498
+
499
+ rows.push(
500
+ makePropertyRow({
501
+ name,
502
+ path: newPath,
503
+ level: currentLevel,
504
+ required: isRequired,
505
+ propertyType:
506
+ propSchema.type || (propSchema.enum ? 'enum' : 'object'),
507
+ description: propSchema.description,
508
+ examples: getExamples(propSchema),
509
+ constraints,
510
+ isLastInGroup: isLast,
511
+ hasChildren,
512
+ containerType,
513
+ continuingLevels: [...continuingLevels],
514
+ groupBrackets: [...currentGroupBrackets],
515
+ isSchemaKeywordRow: propSchema['x-schema-keyword-row'] === true,
516
+ keepConnectorOpen: propSchema['x-keep-connector-open'] === true,
517
+ }),
518
+ );
519
+
520
+ rows.push(
521
+ ...buildPropertyChildren(
522
+ ctx,
523
+ propSchema,
524
+ currentLevel,
525
+ newPath,
526
+ childContinuingLevels,
527
+ currentGroupBrackets,
528
+ isLast,
293
529
  {
294
- ...subSchema.additionalProperties,
295
- 'x-schema-keyword-row': true,
530
+ isChoiceWrapper,
531
+ isConditionalWrapper,
532
+ hasArrayItems,
533
+ hasAdditionalProperties,
296
534
  },
297
- ]);
298
- }
299
- propEntries.push(...patternPropertyEntries);
300
- const hasSiblingChoices = !!(
301
- subSchema.oneOf ||
302
- subSchema.anyOf ||
303
- subSchema.if
535
+ ),
304
536
  );
305
-
306
- // Filter out properties that should be skipped to get accurate count
307
- const visiblePropEntries = propEntries.filter(([name, propSchema]) => {
308
- return !(
309
- propSchema['x-gtm-clear'] === true && isEffectivelyEmpty(propSchema)
310
- );
311
- });
312
-
313
- visiblePropEntries.forEach(([name, propSchema], index) => {
314
- const newPath = [...currentPath, name];
315
-
316
- const isLastProp =
317
- index === visiblePropEntries.length - 1 && !hasSiblingChoices;
318
-
319
- // Updated Logic:
320
- // A property is visually "last" only if it is the last property
321
- // AND (it is deeper in the hierarchy OR the parent option itself is the last one).
322
- const isLast = isLastProp && (currentLevel !== level || isLastOption);
323
-
324
- const isChoiceWrapper = !!(propSchema.oneOf || propSchema.anyOf);
325
- const isConditionalWrapper = !!(
326
- propSchema.if &&
327
- (propSchema.then || propSchema.else)
328
- );
329
-
330
- // Determine if this property has children and what type
331
- const hasNestedProperties = !!propSchema.properties;
332
- const hasAdditionalProperties =
333
- hasRenderableAdditionalProperties(propSchema);
334
- const hasArrayItems =
335
- propSchema.type === 'array' &&
336
- !!(propSchema.items?.properties || propSchema.items?.if);
337
- const hasNestedChoice = isChoiceWrapper;
338
- const hasNestedConditional = isConditionalWrapper;
339
- const hasChildren =
340
- hasNestedProperties ||
341
- hasAdditionalProperties ||
342
- hasArrayItems ||
343
- hasNestedChoice ||
344
- hasNestedConditional;
345
-
346
- // Determine container type for the symbol
347
- let containerType = null;
348
- const choiceOptions = propSchema.oneOf || propSchema.anyOf || [];
349
- const choiceOptionsAreObjects =
350
- isChoiceWrapper &&
351
- choiceOptions.some((opt) => opt.type === 'object' || opt.properties);
352
- if (
353
- hasNestedProperties ||
354
- hasAdditionalProperties ||
355
- (isChoiceWrapper && propSchema.type === 'object') ||
356
- (isConditionalWrapper && propSchema.type === 'object') ||
357
- choiceOptionsAreObjects
358
- ) {
359
- containerType = 'object';
360
- } else if (hasArrayItems) {
361
- containerType = 'array';
362
- }
363
-
364
- // Calculate continuing levels for children
365
- // If this is not the last item, add current level to continuing levels for children
366
- // If this IS the last item, don't add currentLevel (no more siblings at this level).
367
- // We keep all existing continuingLevels intact — they represent ancestor lines
368
- // that must continue through all descendants regardless of last-child status.
369
- const childContinuingLevels = isLast
370
- ? [...continuingLevels]
371
- : [...continuingLevels, currentLevel];
372
-
373
- // A "simple" choice property like user_id: { oneOf: [{ type: "string" }, { type: "integer" }] }
374
- // where the options are scalar types (no nested properties). These get unwrapped
375
- // into a choice row directly without their own property row.
376
- // In contrast, choice wrappers whose options are objects with properties
377
- // (like contact_method) need their own property row to start a nesting level.
378
- const isSimpleChoice =
379
- isChoiceWrapper &&
380
- !propSchema.properties &&
381
- propSchema.type !== 'object' &&
382
- !(propSchema.oneOf || propSchema.anyOf).some((opt) => opt.properties);
383
-
384
- if (isSimpleChoice) {
385
- const choiceType = propSchema.oneOf ? 'oneOf' : 'anyOf';
386
- const choices = propSchema[choiceType];
387
- const ownBracket = computeOwnBracket(
388
- currentLevel,
389
- currentGroupBrackets,
390
- );
391
- const innerGroupBrackets = [...currentGroupBrackets, ownBracket];
392
- flatRows.push({
393
- type: 'choice',
394
- choiceType,
395
- name,
396
- path: newPath,
397
- level: currentLevel,
398
- title: propSchema.title,
399
- description: propSchema.description,
400
- isLastInGroup: isLast,
401
- hasChildren: false,
402
- containerType: null,
403
- continuingLevels: [...continuingLevels],
404
- groupBrackets: [...currentGroupBrackets],
405
- options: processOptions(
406
- choices,
407
- currentLevel,
408
- newPath,
409
- false,
410
- subSchema.required || requiredFromParent,
411
- childContinuingLevels,
412
- innerGroupBrackets,
413
- isLast,
414
- ),
415
- });
416
- } else {
417
- // This is a "normal" property or a complex one with a nested choice.
418
- const isRequired =
419
- (subSchema.required || requiredFromParent)?.includes(name) || false;
420
- const constraints = getConstraints(propSchema);
421
- if (isRequired) {
422
- constraints.unshift('required');
423
- }
424
-
425
- flatRows.push({
426
- type: 'property',
427
- name,
428
- path: newPath,
429
- level: currentLevel,
430
- required: isRequired,
431
- propertyType:
432
- propSchema.type || (propSchema.enum ? 'enum' : 'object'),
433
- description: propSchema.description,
434
- examples: getExamples(propSchema),
435
- constraints,
436
- isLastInGroup: isLast,
437
- hasChildren,
438
- containerType,
439
- continuingLevels: [...continuingLevels],
440
- groupBrackets: [...currentGroupBrackets],
441
- isSchemaKeywordRow: propSchema['x-schema-keyword-row'] === true,
442
- keepConnectorOpen: propSchema['x-keep-connector-open'] === true,
443
- });
444
-
445
- if (propSchema.properties) {
446
- buildRows(
447
- propSchema,
448
- currentLevel + 1,
449
- newPath,
450
- propSchema.required,
451
- childContinuingLevels,
452
- currentGroupBrackets,
453
- );
454
- } else if (propSchema.type === 'object' && hasAdditionalProperties) {
455
- buildRows(
456
- propSchema,
457
- currentLevel + 1,
458
- newPath,
459
- [],
460
- childContinuingLevels,
461
- currentGroupBrackets,
462
- );
463
- } else if (
464
- propSchema.type === 'array' &&
465
- (propSchema.items?.properties || propSchema.items?.if)
466
- ) {
467
- if (propSchema.items.properties) {
468
- buildRows(
469
- propSchema.items,
470
- currentLevel + 1,
471
- [...newPath, '[n]'],
472
- propSchema.items.required,
473
- childContinuingLevels,
474
- currentGroupBrackets,
475
- );
476
- }
477
- // Handle if/then/else inside array items
478
- if (
479
- propSchema.items.if &&
480
- (propSchema.items.then || propSchema.items.else)
481
- ) {
482
- buildConditionalRow(
483
- propSchema.items,
484
- currentLevel + 1,
485
- [...newPath, '[n]'],
486
- childContinuingLevels,
487
- currentGroupBrackets,
488
- undefined,
489
- isLast,
490
- );
491
- }
492
- } else if (isChoiceWrapper) {
493
- // This handles the "complex" choice property like payment_method.
494
- // A property row has already been created above, now we add the choice row.
495
- const choiceType = propSchema.oneOf ? 'oneOf' : 'anyOf';
496
- const choices = propSchema[choiceType];
497
- const complexOwnBracket = computeOwnBracket(
498
- currentLevel + 1,
499
- currentGroupBrackets,
500
- );
501
- const complexInnerBrackets = [
502
- ...currentGroupBrackets,
503
- complexOwnBracket,
504
- ];
505
- flatRows.push({
506
- type: 'choice',
507
- choiceType,
508
- path: [...newPath, choiceType], // Make path unique
509
- level: currentLevel + 1,
510
- title: propSchema.title,
511
- description: null,
512
- isLastInGroup: true,
513
- hasChildren: false,
514
- containerType: null,
515
- continuingLevels: childContinuingLevels,
516
- groupBrackets: [...currentGroupBrackets],
517
- options: processOptions(
518
- choices,
519
- currentLevel + 1,
520
- newPath,
521
- true,
522
- propSchema.required,
523
- childContinuingLevels,
524
- complexInnerBrackets,
525
- ),
526
- });
527
- }
528
-
529
- // Handle if/then/else nested inside a property without its own properties.
530
- // When propSchema HAS properties, the recursive buildRows call above
531
- // already handles if/then/else via the root-level check at the end of buildRows.
532
- if (isConditionalWrapper && !propSchema.properties) {
533
- buildConditionalRow(
534
- propSchema,
535
- currentLevel + 1,
536
- newPath,
537
- childContinuingLevels,
538
- currentGroupBrackets,
539
- undefined,
540
- isLast,
541
- );
542
- }
543
- }
544
- });
545
537
  }
538
+ });
546
539
 
547
- // When properties coexist with root-level choices or conditionals,
548
- // the header/toggle rows need the tree line at currentLevel to continue.
549
- // Only used for the row's own continuingLevels — NOT propagated to inner rows.
550
- const hasProperties =
551
- subSchema.properties && Object.keys(subSchema.properties).length > 0;
552
- const ownContinuingLevels =
553
- hasProperties && !continuingLevels.includes(currentLevel)
554
- ? [...continuingLevels, currentLevel]
555
- : [...continuingLevels];
556
-
557
- // This handles choices at the root of a schema
558
- const choiceType = subSchema.oneOf
559
- ? 'oneOf'
560
- : subSchema.anyOf
561
- ? 'anyOf'
562
- : null;
563
- if (choiceType) {
564
- const choices = subSchema[choiceType];
565
- const ownBracket = computeOwnBracket(currentLevel, currentGroupBrackets);
566
- const innerGroupBrackets = [...currentGroupBrackets, ownBracket];
567
- const choiceIsLastInGroup =
568
- isLastOption && !(subSchema.if && (subSchema.then || subSchema.else));
569
- flatRows.push({
570
- type: 'choice',
540
+ return rows;
541
+ }
542
+
543
+ function buildRows(
544
+ ctx,
545
+ subSchema,
546
+ currentLevel,
547
+ currentPath,
548
+ requiredFromParent = [],
549
+ continuingLevels = [],
550
+ currentGroupBrackets = [],
551
+ ) {
552
+ if (!subSchema) return [];
553
+
554
+ const rows = [];
555
+ const patternPropertyEntries = getRenderablePatternProperties(subSchema);
556
+ const hasAnyProperties =
557
+ subSchema.properties ||
558
+ hasRenderableAdditionalProperties(subSchema) ||
559
+ patternPropertyEntries.length > 0;
560
+
561
+ if (hasAnyProperties) {
562
+ const propEntries = buildPropEntries(subSchema, patternPropertyEntries);
563
+ const hasSiblingChoices = !!(
564
+ subSchema.oneOf ||
565
+ subSchema.anyOf ||
566
+ subSchema.if
567
+ );
568
+ const visiblePropEntries = propEntries.filter(
569
+ ([, propSchema]) =>
570
+ !(propSchema['x-gtm-clear'] === true && isEffectivelyEmpty(propSchema)),
571
+ );
572
+
573
+ rows.push(
574
+ ...buildPropertyRows(
575
+ ctx,
576
+ subSchema,
577
+ currentLevel,
578
+ currentPath,
579
+ requiredFromParent,
580
+ continuingLevels,
581
+ currentGroupBrackets,
582
+ visiblePropEntries,
583
+ hasSiblingChoices,
584
+ ),
585
+ );
586
+ }
587
+
588
+ // When properties coexist with root-level choices or conditionals,
589
+ // the header/toggle rows need the tree line at currentLevel to continue.
590
+ // Only used for the row's own continuingLevels — NOT propagated to inner rows.
591
+ const hasProperties =
592
+ subSchema.properties && Object.keys(subSchema.properties).length > 0;
593
+ const ownContinuingLevels =
594
+ hasProperties && !continuingLevels.includes(currentLevel)
595
+ ? [...continuingLevels, currentLevel]
596
+ : [...continuingLevels];
597
+
598
+ const choiceType = subSchema.oneOf
599
+ ? 'oneOf'
600
+ : subSchema.anyOf
601
+ ? 'anyOf'
602
+ : null;
603
+ if (choiceType) {
604
+ const ownBracket = computeOwnBracket(currentLevel, currentGroupBrackets);
605
+ const choiceIsLastInGroup =
606
+ ctx.isLastOption && !(subSchema.if && (subSchema.then || subSchema.else));
607
+ rows.push(
608
+ makeChoiceRow({
571
609
  choiceType,
572
610
  path: currentPath,
573
611
  level: currentLevel,
574
612
  title: subSchema.title,
575
613
  description: subSchema.description,
576
614
  isLastInGroup: choiceIsLastInGroup,
577
- hasChildren: false,
578
- containerType: null,
579
615
  continuingLevels: [...ownContinuingLevels],
580
616
  groupBrackets: [...currentGroupBrackets],
581
617
  options: processOptions(
582
- choices,
618
+ subSchema[choiceType],
583
619
  currentLevel,
584
620
  currentPath,
585
621
  false,
586
622
  subSchema.required || requiredFromParent,
587
623
  continuingLevels,
588
- innerGroupBrackets,
624
+ [...currentGroupBrackets, ownBracket],
589
625
  choiceIsLastInGroup,
590
626
  ),
591
- });
592
- } else if (!subSchema.properties && subSchema.type) {
593
- // This handles a schema that is just a single primitive type
594
- flatRows.push({
595
- type: 'property',
627
+ }),
628
+ );
629
+ } else if (!subSchema.properties && subSchema.type) {
630
+ // Root-level primitive schema
631
+ rows.push(
632
+ makePropertyRow({
596
633
  name: subSchema.title || '<value>',
597
634
  path: currentPath,
598
635
  level: currentLevel,
@@ -602,30 +639,40 @@ export function schemaToTableData(
602
639
  examples: getExamples(subSchema),
603
640
  constraints: getConstraints(subSchema),
604
641
  isLastInGroup: true,
605
- hasChildren: false,
606
- containerType: null,
607
642
  continuingLevels: [...continuingLevels],
608
643
  groupBrackets: [...currentGroupBrackets],
609
- });
610
- }
644
+ }),
645
+ );
646
+ }
611
647
 
612
- // Handle if/then/else at the schema root (or sub-schema root)
613
- if (subSchema.if && (subSchema.then || subSchema.else)) {
614
- // ownContinuingLevels includes currentLevel for the row's header/toggle rendering.
615
- // Inner rows (condition, branches) use the original continuingLevels.
616
- buildConditionalRow(
648
+ if (subSchema.if && (subSchema.then || subSchema.else)) {
649
+ rows.push(
650
+ ...buildConditionalRow(
617
651
  subSchema,
618
652
  currentLevel,
619
653
  currentPath,
620
654
  continuingLevels,
621
655
  currentGroupBrackets,
622
656
  hasProperties ? [...ownContinuingLevels] : undefined,
623
- isLastOption,
624
- );
625
- }
657
+ ctx.isLastOption,
658
+ ),
659
+ );
626
660
  }
627
661
 
628
- buildRows(
662
+ return rows;
663
+ }
664
+
665
+ export function schemaToTableData(
666
+ schema,
667
+ level = 0,
668
+ path = [],
669
+ parentContinuingLevels = [],
670
+ isLastOption = true,
671
+ parentGroupBrackets = [],
672
+ ) {
673
+ const ctx = { topLevel: level, isLastOption };
674
+ return buildRows(
675
+ ctx,
629
676
  schema,
630
677
  level,
631
678
  path,
@@ -633,5 +680,4 @@ export function schemaToTableData(
633
680
  parentContinuingLevels,
634
681
  parentGroupBrackets,
635
682
  );
636
- return flatRows;
637
683
  }