docusaurus-plugin-generate-schema-docs 1.7.1 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/__tests__/ExampleDataLayer.test.js +149 -2
- package/__tests__/__fixtures__/schema-processing/components/dataLayer.json +9 -0
- package/__tests__/__fixtures__/schema-processing/event-reference.json +14 -0
- package/__tests__/__fixtures__/schema-processing/purchase-event.json +14 -0
- package/__tests__/components/PropertyRow.test.js +30 -0
- package/__tests__/components/__snapshots__/ConnectorLines.visualRegression.test.js.snap +3 -3
- package/__tests__/helpers/exampleModel.test.js +135 -0
- package/__tests__/helpers/schema-processing.test.js +56 -0
- package/__tests__/helpers/schemaToTableData.test.js +41 -0
- package/__tests__/helpers/snippetTargets.test.js +744 -0
- package/__tests__/helpers/trackingTargets.test.js +42 -0
- package/__tests__/runtimePayload.android.test.js +292 -0
- package/__tests__/runtimePayload.ios.test.js +282 -0
- package/__tests__/runtimePayload.web.test.js +32 -0
- package/__tests__/validateSchemas.test.js +53 -1
- package/components/ExampleDataLayer.js +191 -56
- package/components/PropertyRow.js +3 -2
- package/components/SchemaRows.css +10 -1
- package/helpers/exampleModel.js +70 -0
- package/helpers/schema-processing.js +41 -5
- package/helpers/schemaToExamples.js +52 -2
- package/helpers/schemaToTableData.js +40 -2
- package/helpers/snippetTargets.js +853 -0
- package/helpers/trackingTargets.js +73 -0
- package/helpers/validator.js +1 -0
- package/package.json +1 -1
- package/test-data/payloadContracts.js +155 -0
- package/validateSchemas.js +15 -0
|
@@ -10,13 +10,16 @@ import validateSchemas from '../validateSchemas';
|
|
|
10
10
|
describe('validateSchemas', () => {
|
|
11
11
|
let tmpDir;
|
|
12
12
|
let consoleErrorSpy;
|
|
13
|
-
let
|
|
13
|
+
let consoleWarnSpy;
|
|
14
14
|
|
|
15
15
|
beforeEach(() => {
|
|
16
16
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'schema-test-'));
|
|
17
|
+
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
18
|
+
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
17
19
|
});
|
|
18
20
|
|
|
19
21
|
afterEach(() => {
|
|
22
|
+
jest.restoreAllMocks();
|
|
20
23
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
21
24
|
});
|
|
22
25
|
|
|
@@ -50,4 +53,53 @@ describe('validateSchemas', () => {
|
|
|
50
53
|
const result = await validateSchemas(schemaDir);
|
|
51
54
|
expect(result).toBe(false);
|
|
52
55
|
});
|
|
56
|
+
|
|
57
|
+
it('should warn and fallback to default target when x-tracking-targets is missing', async () => {
|
|
58
|
+
const schemaDir = path.join(tmpDir, 'schemas');
|
|
59
|
+
fs.mkdirSync(schemaDir, { recursive: true });
|
|
60
|
+
fs.writeFileSync(
|
|
61
|
+
path.join(schemaDir, 'event.json'),
|
|
62
|
+
JSON.stringify({
|
|
63
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {
|
|
66
|
+
event: {
|
|
67
|
+
type: 'string',
|
|
68
|
+
const: 'test_event',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
required: ['event'],
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const result = await validateSchemas(schemaDir);
|
|
76
|
+
expect(result).toBe(true);
|
|
77
|
+
expect(consoleWarnSpy).toHaveBeenCalled();
|
|
78
|
+
expect(consoleWarnSpy.mock.calls[0][0]).toContain('web-datalayer-js');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should fail when x-tracking-targets has an unsupported target', async () => {
|
|
82
|
+
const schemaDir = path.join(tmpDir, 'schemas');
|
|
83
|
+
fs.mkdirSync(schemaDir, { recursive: true });
|
|
84
|
+
fs.writeFileSync(
|
|
85
|
+
path.join(schemaDir, 'event.json'),
|
|
86
|
+
JSON.stringify({
|
|
87
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
88
|
+
type: 'object',
|
|
89
|
+
'x-tracking-targets': ['web-not-supported-js'],
|
|
90
|
+
properties: {
|
|
91
|
+
event: {
|
|
92
|
+
type: 'string',
|
|
93
|
+
const: 'test_event',
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
required: ['event'],
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const result = await validateSchemas(schemaDir);
|
|
101
|
+
expect(result).toBe(false);
|
|
102
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
103
|
+
expect(consoleErrorSpy.mock.calls[0][0]).toContain('x-tracking-targets');
|
|
104
|
+
});
|
|
53
105
|
});
|
|
@@ -1,80 +1,215 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useEffect, useMemo } from 'react';
|
|
2
2
|
import CodeBlock from '@theme/CodeBlock';
|
|
3
3
|
import Tabs from '@theme/Tabs';
|
|
4
4
|
import TabItem from '@theme/TabItem';
|
|
5
5
|
import Heading from '@theme/Heading';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
buildExampleModel,
|
|
8
|
+
findClearableProperties,
|
|
9
|
+
} from '../helpers/exampleModel';
|
|
7
10
|
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const propertiesToClear = clearableProperties.filter(
|
|
12
|
-
(prop) => prop in example,
|
|
13
|
-
);
|
|
11
|
+
const TARGET_HASH_KEY = 'target';
|
|
12
|
+
const TARGET_HASH_PREFIX = `${TARGET_HASH_KEY}-`;
|
|
13
|
+
const TARGET_STORAGE_KEY = 'tracking-docs-selected-target';
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
codeSnippet += `window.${dataLayerName}.push(${JSON.stringify(
|
|
21
|
-
resetObject,
|
|
22
|
-
null,
|
|
23
|
-
2,
|
|
24
|
-
)});\n`;
|
|
15
|
+
function parseHashTarget(rawHash = '') {
|
|
16
|
+
const raw = rawHash.startsWith('#') ? rawHash.substring(1) : rawHash;
|
|
17
|
+
if (!raw) return null;
|
|
18
|
+
if (raw.startsWith(TARGET_HASH_PREFIX)) {
|
|
19
|
+
return raw.substring(TARGET_HASH_PREFIX.length) || null;
|
|
25
20
|
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readHashTarget() {
|
|
25
|
+
if (typeof window === 'undefined') return null;
|
|
26
|
+
return parseHashTarget(window.location.hash || '');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readSearchTarget(search = '') {
|
|
30
|
+
const query = search.startsWith('?') ? search.substring(1) : search;
|
|
31
|
+
if (!query) return null;
|
|
32
|
+
const params = new URLSearchParams(query);
|
|
33
|
+
return params.get(TARGET_HASH_KEY);
|
|
34
|
+
}
|
|
26
35
|
|
|
27
|
-
|
|
28
|
-
|
|
36
|
+
function persistTarget(targetId) {
|
|
37
|
+
if (typeof window === 'undefined') return;
|
|
38
|
+
window.localStorage.setItem(TARGET_STORAGE_KEY, targetId);
|
|
39
|
+
window.history.replaceState(
|
|
29
40
|
null,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
41
|
+
'',
|
|
42
|
+
`${window.location.pathname}${window.location.search}#${TARGET_HASH_PREFIX}${targetId}`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resolveInitialTargetId(targets) {
|
|
47
|
+
const safeTargets = Array.isArray(targets)
|
|
48
|
+
? targets.filter((t) => t && typeof t.id === 'string' && t.id.length > 0)
|
|
49
|
+
: [];
|
|
50
|
+
if (safeTargets.length === 0) return null;
|
|
51
|
+
const validTargetIds = new Set(safeTargets.map((t) => t.id));
|
|
52
|
+
|
|
53
|
+
const fromHash = readHashTarget();
|
|
54
|
+
if (fromHash && validTargetIds.has(fromHash)) {
|
|
55
|
+
return fromHash;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (typeof window !== 'undefined') {
|
|
59
|
+
const fromStorage = window.localStorage.getItem(TARGET_STORAGE_KEY);
|
|
60
|
+
if (fromStorage && validTargetIds.has(fromStorage)) {
|
|
61
|
+
return fromStorage;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return safeTargets[0].id;
|
|
66
|
+
}
|
|
34
67
|
|
|
35
68
|
export default function ExampleDataLayer({ schema, dataLayerName }) {
|
|
36
|
-
const
|
|
69
|
+
const model = useMemo(
|
|
70
|
+
() => buildExampleModel(schema, { dataLayerName }),
|
|
71
|
+
[schema, dataLayerName],
|
|
72
|
+
);
|
|
73
|
+
const safeTargets = useMemo(
|
|
74
|
+
() =>
|
|
75
|
+
(Array.isArray(model.targets) ? model.targets : []).filter(
|
|
76
|
+
(target) =>
|
|
77
|
+
target && typeof target.id === 'string' && target.id.length > 0,
|
|
78
|
+
),
|
|
79
|
+
[model.targets],
|
|
80
|
+
);
|
|
81
|
+
const exampleGroups = model.variantGroups;
|
|
82
|
+
const targetId = resolveInitialTargetId(safeTargets);
|
|
83
|
+
const showTargetTabs = safeTargets.length > 1;
|
|
84
|
+
const validTargetIds = useMemo(
|
|
85
|
+
() => new Set(safeTargets.map((target) => target.id)),
|
|
86
|
+
[safeTargets],
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (typeof window === 'undefined') return undefined;
|
|
91
|
+
let isSyncing = false;
|
|
92
|
+
const originalReplaceState = window.history.replaceState;
|
|
93
|
+
|
|
94
|
+
const syncFromSearch = () => {
|
|
95
|
+
if (isSyncing) return;
|
|
96
|
+
const targetFromSearch = readSearchTarget(window.location.search);
|
|
97
|
+
if (!targetFromSearch || !validTargetIds.has(targetFromSearch)) return;
|
|
98
|
+
|
|
99
|
+
isSyncing = true;
|
|
100
|
+
try {
|
|
101
|
+
window.localStorage.setItem(TARGET_STORAGE_KEY, targetFromSearch);
|
|
102
|
+
|
|
103
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
104
|
+
searchParams.delete(TARGET_HASH_KEY);
|
|
105
|
+
const remainingSearch = searchParams.toString();
|
|
106
|
+
const searchPart = remainingSearch ? `?${remainingSearch}` : '';
|
|
107
|
+
|
|
108
|
+
originalReplaceState.call(
|
|
109
|
+
window.history,
|
|
110
|
+
null,
|
|
111
|
+
'',
|
|
112
|
+
`${window.location.pathname}${searchPart}#${TARGET_HASH_PREFIX}${targetFromSearch}`,
|
|
113
|
+
);
|
|
114
|
+
} finally {
|
|
115
|
+
isSyncing = false;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
syncFromSearch();
|
|
120
|
+
window.history.replaceState = function patchedReplaceState(...args) {
|
|
121
|
+
const result = originalReplaceState.apply(window.history, args);
|
|
122
|
+
syncFromSearch();
|
|
123
|
+
return result;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return () => {
|
|
127
|
+
window.history.replaceState = originalReplaceState;
|
|
128
|
+
};
|
|
129
|
+
}, [validTargetIds]);
|
|
37
130
|
|
|
38
131
|
if (!exampleGroups || exampleGroups.length === 0) {
|
|
39
132
|
return null;
|
|
40
133
|
}
|
|
41
134
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
135
|
+
const getLanguageForTarget = (targetIdForSnippet) =>
|
|
136
|
+
safeTargets.find((target) => target.id === targetIdForSnippet)?.language ||
|
|
137
|
+
'javascript';
|
|
138
|
+
|
|
139
|
+
const renderVariantGroups = (currentTargetId) => (
|
|
140
|
+
<>
|
|
141
|
+
{exampleGroups.map((group) => {
|
|
142
|
+
const showVariantTabs = group.options.length > 1;
|
|
143
|
+
|
|
144
|
+
if (!showVariantTabs) {
|
|
145
|
+
return (
|
|
146
|
+
<div key={group.property} style={{ marginTop: '20px' }}>
|
|
147
|
+
{!model.isSimpleDefault && (
|
|
148
|
+
<Heading as="h4">
|
|
149
|
+
<code>{group.property}</code> options:
|
|
150
|
+
</Heading>
|
|
151
|
+
)}
|
|
152
|
+
<CodeBlock language={getLanguageForTarget(currentTargetId)}>
|
|
153
|
+
{group.options[0].snippets[currentTargetId]}
|
|
154
|
+
</CodeBlock>
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div key={group.property} style={{ marginTop: '20px' }}>
|
|
161
|
+
<Heading as="h4">
|
|
162
|
+
<code>{group.property}</code> options:
|
|
163
|
+
</Heading>
|
|
164
|
+
<Tabs>
|
|
165
|
+
{group.options.map(({ id, title, snippets }) => (
|
|
166
|
+
<TabItem value={id} label={title} key={id}>
|
|
167
|
+
<CodeBlock language={getLanguageForTarget(currentTargetId)}>
|
|
168
|
+
{snippets[currentTargetId]}
|
|
169
|
+
</CodeBlock>
|
|
170
|
+
</TabItem>
|
|
171
|
+
))}
|
|
172
|
+
</Tabs>
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
})}
|
|
176
|
+
</>
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// Single target + single default variant => keep old layout
|
|
180
|
+
if (!showTargetTabs && model.isSimpleDefault) {
|
|
181
|
+
const snippets = exampleGroups[0].options[0].snippets || {};
|
|
182
|
+
const codeSnippet =
|
|
183
|
+
(targetId && snippets[targetId]) || Object.values(snippets)[0] || '';
|
|
184
|
+
return (
|
|
185
|
+
<CodeBlock language={getLanguageForTarget(targetId)}>
|
|
186
|
+
{codeSnippet}
|
|
187
|
+
</CodeBlock>
|
|
48
188
|
);
|
|
49
|
-
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!showTargetTabs) {
|
|
192
|
+
return renderVariantGroups(targetId);
|
|
50
193
|
}
|
|
51
194
|
|
|
52
195
|
return (
|
|
53
|
-
|
|
54
|
-
{
|
|
55
|
-
|
|
56
|
-
<
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
{generateCodeSnippet(example, schema, dataLayerName)}
|
|
64
|
-
</CodeBlock>
|
|
65
|
-
</TabItem>
|
|
66
|
-
))}
|
|
67
|
-
</Tabs>
|
|
68
|
-
</div>
|
|
69
|
-
))}
|
|
70
|
-
</>
|
|
196
|
+
<div data-testid="target-tabs">
|
|
197
|
+
<Tabs defaultValue={targetId} queryString={TARGET_HASH_KEY}>
|
|
198
|
+
{safeTargets.map((target) => (
|
|
199
|
+
<TabItem value={target.id} label={target.label} key={target.id}>
|
|
200
|
+
<span id={`${TARGET_HASH_PREFIX}${target.id}`} />
|
|
201
|
+
{renderVariantGroups(target.id)}
|
|
202
|
+
</TabItem>
|
|
203
|
+
))}
|
|
204
|
+
</Tabs>
|
|
205
|
+
</div>
|
|
71
206
|
);
|
|
72
207
|
}
|
|
73
208
|
|
|
74
|
-
export
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
209
|
+
export {
|
|
210
|
+
findClearableProperties,
|
|
211
|
+
resolveInitialTargetId,
|
|
212
|
+
readHashTarget,
|
|
213
|
+
readSearchTarget,
|
|
214
|
+
TARGET_STORAGE_KEY,
|
|
80
215
|
};
|
|
@@ -125,6 +125,7 @@ export default function PropertyRow({ row, isLastInGroup, bracketEnds }) {
|
|
|
125
125
|
rowSpan={rowSpan}
|
|
126
126
|
style={{ ...indentStyle, ...continuingLinesStyle }}
|
|
127
127
|
className={clsx(
|
|
128
|
+
'property-cell',
|
|
128
129
|
level > 0 && `level-${level}`,
|
|
129
130
|
isLastInGroup && 'is-last',
|
|
130
131
|
hasChildren && 'has-children',
|
|
@@ -143,7 +144,7 @@ export default function PropertyRow({ row, isLastInGroup, bracketEnds }) {
|
|
|
143
144
|
</td>
|
|
144
145
|
|
|
145
146
|
{/* The first constraint cell */}
|
|
146
|
-
<td>
|
|
147
|
+
<td className="constraint-cell">
|
|
147
148
|
{firstConstraint && (
|
|
148
149
|
<code
|
|
149
150
|
className={clsx(
|
|
@@ -181,7 +182,7 @@ export default function PropertyRow({ row, isLastInGroup, bracketEnds }) {
|
|
|
181
182
|
{/* Render subsequent constraints in their own rows */}
|
|
182
183
|
{remainingConstraints.map((constraint) => (
|
|
183
184
|
<tr className={clsx(required && 'required-row')} key={constraint}>
|
|
184
|
-
<td>
|
|
185
|
+
<td className="constraint-cell">
|
|
185
186
|
<code
|
|
186
187
|
className={clsx(
|
|
187
188
|
'constraint-code',
|
|
@@ -63,10 +63,19 @@
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
.schema-table th:first-child,
|
|
66
|
-
.schema-table td
|
|
66
|
+
.schema-table td.property-cell {
|
|
67
67
|
border-left: none;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/*
|
|
71
|
+
* Constraint-only continuation rows render a single cell; in those rows that
|
|
72
|
+
* cell is also :first-child, but it still needs the separator before the
|
|
73
|
+
* "Constraints" column.
|
|
74
|
+
*/
|
|
75
|
+
.schema-table td.constraint-cell {
|
|
76
|
+
border-left: 1px solid var(--ifm-table-border-color);
|
|
77
|
+
}
|
|
78
|
+
|
|
70
79
|
.schema-table thead th {
|
|
71
80
|
border-bottom: 2px solid var(--ifm-table-border-color);
|
|
72
81
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { schemaToExamples } from './schemaToExamples';
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_SNIPPET_TARGET_ID,
|
|
4
|
+
findClearableProperties,
|
|
5
|
+
generateSnippetForTarget,
|
|
6
|
+
getSnippetTarget,
|
|
7
|
+
} from './snippetTargets';
|
|
8
|
+
|
|
9
|
+
export function resolveExampleTargets(schema) {
|
|
10
|
+
const configured = schema?.['x-tracking-targets'];
|
|
11
|
+
const targetIds =
|
|
12
|
+
Array.isArray(configured) && configured.length > 0
|
|
13
|
+
? configured
|
|
14
|
+
: [DEFAULT_SNIPPET_TARGET_ID];
|
|
15
|
+
|
|
16
|
+
const targets = targetIds
|
|
17
|
+
.map((id) => {
|
|
18
|
+
try {
|
|
19
|
+
return getSnippetTarget(id);
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
.filter(Boolean);
|
|
25
|
+
|
|
26
|
+
if (targets.length > 0) return targets;
|
|
27
|
+
return [getSnippetTarget(DEFAULT_SNIPPET_TARGET_ID)];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildExampleModel(schema, { dataLayerName } = {}) {
|
|
31
|
+
const exampleGroups = schemaToExamples(schema);
|
|
32
|
+
const targets = resolveExampleTargets(schema);
|
|
33
|
+
|
|
34
|
+
if (!exampleGroups || exampleGroups.length === 0) {
|
|
35
|
+
return {
|
|
36
|
+
targets,
|
|
37
|
+
variantGroups: [],
|
|
38
|
+
isSimpleDefault: false,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const variantGroups = exampleGroups.map((group) => ({
|
|
43
|
+
property: group.property,
|
|
44
|
+
options: group.options.map((option, index) => ({
|
|
45
|
+
id: `${group.property}-${index}`,
|
|
46
|
+
title: option.title,
|
|
47
|
+
example: option.example,
|
|
48
|
+
snippets: Object.fromEntries(
|
|
49
|
+
targets.map((target) => [
|
|
50
|
+
target.id,
|
|
51
|
+
generateSnippetForTarget({
|
|
52
|
+
targetId: target.id,
|
|
53
|
+
example: option.example,
|
|
54
|
+
schema,
|
|
55
|
+
dataLayerName,
|
|
56
|
+
}),
|
|
57
|
+
]),
|
|
58
|
+
),
|
|
59
|
+
})),
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
targets,
|
|
64
|
+
variantGroups,
|
|
65
|
+
isSimpleDefault:
|
|
66
|
+
variantGroups.length === 1 && variantGroups[0].property === 'default',
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export { findClearableProperties };
|
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
2
3
|
import processSchema from './processSchema.js';
|
|
4
|
+
import mergeJsonSchema from 'json-schema-merge-allof';
|
|
5
|
+
|
|
6
|
+
function mergePropertySchemas(baseProperties = {}, optionProperties = {}) {
|
|
7
|
+
const mergedProperties = {
|
|
8
|
+
...baseProperties,
|
|
9
|
+
...optionProperties,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
for (const key of Object.keys(baseProperties)) {
|
|
13
|
+
if (!Object.prototype.hasOwnProperty.call(optionProperties, key)) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
mergedProperties[key] = mergeJsonSchema(
|
|
18
|
+
{
|
|
19
|
+
allOf: [baseProperties[key], optionProperties[key]],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
resolvers: {
|
|
23
|
+
defaultResolver: mergeJsonSchema.options.resolvers.title,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return mergedProperties;
|
|
30
|
+
}
|
|
3
31
|
|
|
4
32
|
export function slugify(text) {
|
|
5
33
|
if (!text) {
|
|
@@ -20,7 +48,15 @@ export async function processOneOfSchema(schema, filePath) {
|
|
|
20
48
|
const choiceType = schema.oneOf ? 'oneOf' : null;
|
|
21
49
|
|
|
22
50
|
if (choiceType) {
|
|
23
|
-
|
|
51
|
+
let parentSchema = schema;
|
|
52
|
+
|
|
53
|
+
// When the root schema is loaded from disk, use its processed form so
|
|
54
|
+
// parent-level allOf refs (e.g. shared dataLayer metadata) are preserved.
|
|
55
|
+
if (filePath && fs.existsSync(filePath)) {
|
|
56
|
+
parentSchema = await processSchema(filePath);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const parentWithoutChoice = { ...parentSchema };
|
|
24
60
|
delete parentWithoutChoice[choiceType];
|
|
25
61
|
|
|
26
62
|
for (const option of schema[choiceType]) {
|
|
@@ -34,10 +70,10 @@ export async function processOneOfSchema(schema, filePath) {
|
|
|
34
70
|
|
|
35
71
|
// Merge the parent schema with the resolved option schema
|
|
36
72
|
const newSchema = { ...parentWithoutChoice, ...resolvedOption };
|
|
37
|
-
newSchema.properties =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
73
|
+
newSchema.properties = mergePropertySchemas(
|
|
74
|
+
parentWithoutChoice.properties,
|
|
75
|
+
resolvedOption.properties,
|
|
76
|
+
);
|
|
41
77
|
|
|
42
78
|
let slug;
|
|
43
79
|
const hadId = resolvedOption.$id && resolvedOption.$id.endsWith('.json');
|
|
@@ -63,18 +63,61 @@ const generateExampleForChoice = (rootSchema, path, option) => {
|
|
|
63
63
|
}
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
+
const pruneSiblingConditionalProperties = (
|
|
67
|
+
mergedSchema,
|
|
68
|
+
activeBranchSchema,
|
|
69
|
+
inactiveBranchSchema,
|
|
70
|
+
baseRequired = [],
|
|
71
|
+
) => {
|
|
72
|
+
const branchesOnlyAdjustRequired =
|
|
73
|
+
!activeBranchSchema?.properties && !inactiveBranchSchema?.properties;
|
|
74
|
+
|
|
75
|
+
if (
|
|
76
|
+
!mergedSchema?.properties ||
|
|
77
|
+
!branchesOnlyAdjustRequired ||
|
|
78
|
+
!Array.isArray(inactiveBranchSchema?.required)
|
|
79
|
+
) {
|
|
80
|
+
return mergedSchema;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const activeRequired = new Set(activeBranchSchema?.required || []);
|
|
84
|
+
const protectedRequired = new Set(baseRequired);
|
|
85
|
+
|
|
86
|
+
inactiveBranchSchema.required.forEach((name) => {
|
|
87
|
+
if (activeRequired.has(name) || protectedRequired.has(name)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
delete mergedSchema.properties[name];
|
|
92
|
+
if (Array.isArray(mergedSchema.required)) {
|
|
93
|
+
mergedSchema.required = mergedSchema.required.filter(
|
|
94
|
+
(requiredName) => requiredName !== name,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return mergedSchema;
|
|
100
|
+
};
|
|
101
|
+
|
|
66
102
|
const generateConditionalExample = (rootSchema, path, branch) => {
|
|
67
103
|
const schemaVariant = JSON.parse(JSON.stringify(rootSchema));
|
|
104
|
+
const siblingBranch = branch === 'then' ? 'else' : 'then';
|
|
68
105
|
|
|
69
106
|
if (path.length === 0) {
|
|
70
107
|
const branchSchema = schemaVariant[branch];
|
|
108
|
+
const siblingBranchSchema = schemaVariant[siblingBranch];
|
|
109
|
+
const baseRequired = schemaVariant.required || [];
|
|
71
110
|
delete schemaVariant.if;
|
|
72
111
|
delete schemaVariant.then;
|
|
73
112
|
delete schemaVariant.else;
|
|
74
113
|
if (branchSchema) {
|
|
75
|
-
|
|
114
|
+
const merged = pruneSiblingConditionalProperties(
|
|
76
115
|
mergeJsonSchema({ allOf: [schemaVariant, branchSchema] }),
|
|
116
|
+
branchSchema,
|
|
117
|
+
siblingBranchSchema,
|
|
118
|
+
baseRequired,
|
|
77
119
|
);
|
|
120
|
+
return buildExampleFromSchema(merged);
|
|
78
121
|
}
|
|
79
122
|
return buildExampleFromSchema(schemaVariant);
|
|
80
123
|
}
|
|
@@ -84,11 +127,18 @@ const generateConditionalExample = (rootSchema, path, branch) => {
|
|
|
84
127
|
target = target[segment];
|
|
85
128
|
}
|
|
86
129
|
const branchSchema = target[branch];
|
|
130
|
+
const siblingBranchSchema = target[siblingBranch];
|
|
131
|
+
const baseRequired = target.required || [];
|
|
87
132
|
delete target.if;
|
|
88
133
|
delete target.then;
|
|
89
134
|
delete target.else;
|
|
90
135
|
if (branchSchema) {
|
|
91
|
-
const merged =
|
|
136
|
+
const merged = pruneSiblingConditionalProperties(
|
|
137
|
+
mergeJsonSchema({ allOf: [target, branchSchema] }),
|
|
138
|
+
branchSchema,
|
|
139
|
+
siblingBranchSchema,
|
|
140
|
+
baseRequired,
|
|
141
|
+
);
|
|
92
142
|
Object.keys(target).forEach((k) => delete target[k]);
|
|
93
143
|
Object.assign(target, merged);
|
|
94
144
|
}
|
|
@@ -11,6 +11,36 @@ function computeOwnBracket(level, parentGroupBrackets) {
|
|
|
11
11
|
return { level, bracketIndex };
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
function materializeConditionalBranchSchema(branchSchema, parentSchema) {
|
|
15
|
+
if (
|
|
16
|
+
!branchSchema ||
|
|
17
|
+
branchSchema.properties ||
|
|
18
|
+
branchSchema.oneOf ||
|
|
19
|
+
branchSchema.anyOf ||
|
|
20
|
+
branchSchema.if ||
|
|
21
|
+
!Array.isArray(branchSchema.required) ||
|
|
22
|
+
!parentSchema?.properties
|
|
23
|
+
) {
|
|
24
|
+
return branchSchema;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const branchProperties = Object.fromEntries(
|
|
28
|
+
branchSchema.required
|
|
29
|
+
.filter((name) => parentSchema.properties[name])
|
|
30
|
+
.map((name) => [name, parentSchema.properties[name]]),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
if (Object.keys(branchProperties).length === 0) {
|
|
34
|
+
return branchSchema;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
...branchSchema,
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: branchProperties,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
14
44
|
function processOptions(
|
|
15
45
|
choices,
|
|
16
46
|
level,
|
|
@@ -144,11 +174,15 @@ export function schemaToTableData(
|
|
|
144
174
|
// Then is NOT the last branch if Else exists — use innerContinuingLevels
|
|
145
175
|
// to keep the parent line flowing. If Then IS the last branch, use original.
|
|
146
176
|
const thenLevels = hasElse ? innerContinuingLevels : continuingLevels;
|
|
177
|
+
const thenSchema = materializeConditionalBranchSchema(
|
|
178
|
+
subSchema.then,
|
|
179
|
+
subSchema,
|
|
180
|
+
);
|
|
147
181
|
branches.push({
|
|
148
182
|
title: 'Then',
|
|
149
183
|
description: subSchema.then.description,
|
|
150
184
|
rows: schemaToTableData(
|
|
151
|
-
|
|
185
|
+
thenSchema,
|
|
152
186
|
currentLevel,
|
|
153
187
|
currentPath,
|
|
154
188
|
thenLevels,
|
|
@@ -160,11 +194,15 @@ export function schemaToTableData(
|
|
|
160
194
|
}
|
|
161
195
|
if (hasElse) {
|
|
162
196
|
// Else is always the last branch — use original continuingLevels
|
|
197
|
+
const elseSchema = materializeConditionalBranchSchema(
|
|
198
|
+
subSchema.else,
|
|
199
|
+
subSchema,
|
|
200
|
+
);
|
|
163
201
|
branches.push({
|
|
164
202
|
title: 'Else',
|
|
165
203
|
description: subSchema.else.description,
|
|
166
204
|
rows: schemaToTableData(
|
|
167
|
-
|
|
205
|
+
elseSchema,
|
|
168
206
|
currentLevel,
|
|
169
207
|
currentPath,
|
|
170
208
|
continuingLevels,
|