docusaurus-plugin-generate-schema-docs 1.8.1 → 1.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -7
- package/__tests__/__fixtures__/validateSchemas/main-schema-with-constraints-ref.json +20 -0
- package/__tests__/__snapshots__/generateEventDocs.anchor.test.js.snap +15 -3
- package/__tests__/__snapshots__/generateEventDocs.nested.test.js.snap +20 -4
- package/__tests__/__snapshots__/generateEventDocs.test.js.snap +30 -6
- package/__tests__/__snapshots__/generateEventDocs.versioned.test.js.snap +10 -2
- package/__tests__/components/ConditionalRows.test.js +28 -0
- package/__tests__/components/FoldableRows.test.js +31 -290
- package/__tests__/components/PropertyRow.test.js +216 -0
- package/__tests__/components/SchemaJsonViewer.test.js +76 -10
- package/__tests__/components/SchemaRows.test.js +62 -12
- package/__tests__/components/__snapshots__/ConnectorLines.visualRegression.test.js.snap +3 -3
- package/__tests__/generateEventDocs.partials.test.js +95 -0
- package/__tests__/generateEventDocs.test.js +3 -0
- package/__tests__/helpers/processSchema.test.js +29 -0
- package/__tests__/helpers/schemaToTableData.test.js +112 -0
- package/__tests__/helpers/validator.test.js +32 -0
- package/components/ConditionalRows.js +6 -3
- package/components/FoldableRows.js +9 -3
- package/components/PropertiesTable.js +2 -1
- package/components/PropertyRow.js +90 -5
- package/components/SchemaJsonViewer.js +221 -4
- package/components/SchemaRows.css +98 -7
- package/components/SchemaRows.js +11 -1
- package/generateEventDocs.js +184 -18
- package/helpers/buildExampleFromSchema.js +3 -3
- package/helpers/choice-index-template.js +6 -2
- package/helpers/constraintSchemaPaths.js +46 -0
- package/helpers/file-system.js +28 -0
- package/helpers/mergeSchema.js +16 -0
- package/helpers/processSchema.js +19 -6
- package/helpers/schema-doc-template.js +7 -1
- package/helpers/schema-processing.js +4 -11
- package/helpers/schemaToExamples.js +4 -4
- package/helpers/schemaToTableData.js +68 -7
- package/helpers/validator.js +7 -0
- package/package.json +1 -1
|
@@ -1,17 +1,21 @@
|
|
|
1
|
+
/* eslint-disable @docusaurus/no-html-links */
|
|
1
2
|
import '@testing-library/jest-dom';
|
|
2
3
|
import React from 'react';
|
|
3
|
-
import { render } from '@testing-library/react';
|
|
4
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
4
5
|
import SchemaJsonViewer from '../../components/SchemaJsonViewer';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return
|
|
10
|
-
|
|
11
|
-
}
|
|
7
|
+
jest.mock(
|
|
8
|
+
'@docusaurus/Link',
|
|
9
|
+
() => {
|
|
10
|
+
return function DocusaurusLink({ children, ...props }) {
|
|
11
|
+
return <a {...props}>{children}</a>;
|
|
12
|
+
};
|
|
13
|
+
},
|
|
14
|
+
{ virtual: true },
|
|
15
|
+
);
|
|
12
16
|
|
|
13
17
|
describe('SchemaJsonViewer', () => {
|
|
14
|
-
it('renders the schema in a
|
|
18
|
+
it('renders the schema in a syntax-highlighted pre block', () => {
|
|
15
19
|
const schema = {
|
|
16
20
|
type: 'object',
|
|
17
21
|
properties: {
|
|
@@ -29,8 +33,70 @@ describe('SchemaJsonViewer', () => {
|
|
|
29
33
|
const codeBlockElement = container.querySelector('pre');
|
|
30
34
|
expect(codeBlockElement).toBeInTheDocument();
|
|
31
35
|
expect(codeBlockElement).toHaveAttribute('data-language', 'json');
|
|
32
|
-
expect(codeBlockElement.textContent).toEqual(
|
|
33
|
-
JSON.stringify(schema, null, 2),
|
|
36
|
+
expect(codeBlockElement.textContent.replace(/\s+/g, '')).toEqual(
|
|
37
|
+
JSON.stringify(schema, null, 2).replace(/\s+/g, ''),
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('navigates local $ref values inside the viewer and resets to root', () => {
|
|
42
|
+
const rootSchema = {
|
|
43
|
+
type: 'object',
|
|
44
|
+
properties: {
|
|
45
|
+
component: { $ref: './components/referenced.json' },
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
const referencedSchema = {
|
|
49
|
+
title: 'Referenced Component',
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
prop: { type: 'string' },
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
render(
|
|
57
|
+
<SchemaJsonViewer
|
|
58
|
+
schema={rootSchema}
|
|
59
|
+
sourcePath="main-schema.json"
|
|
60
|
+
schemaSources={{
|
|
61
|
+
'main-schema.json': rootSchema,
|
|
62
|
+
'components/referenced.json': referencedSchema,
|
|
63
|
+
}}
|
|
64
|
+
/>,
|
|
34
65
|
);
|
|
66
|
+
|
|
67
|
+
fireEvent.click(screen.getByText('View Raw JSON Schema'));
|
|
68
|
+
fireEvent.click(
|
|
69
|
+
screen.getByRole('button', { name: '"./components/referenced.json"' }),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
expect(screen.getAllByText(/Referenced Component/).length).toBeGreaterThan(
|
|
73
|
+
0,
|
|
74
|
+
);
|
|
75
|
+
expect(
|
|
76
|
+
screen.getByRole('button', { name: 'Back to root' }),
|
|
77
|
+
).toBeInTheDocument();
|
|
78
|
+
|
|
79
|
+
fireEvent.click(screen.getByRole('button', { name: 'Back to root' }));
|
|
80
|
+
|
|
81
|
+
expect(
|
|
82
|
+
screen.getByText('"./components/referenced.json"'),
|
|
83
|
+
).toBeInTheDocument();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('renders external $ref values as new-tab links', () => {
|
|
87
|
+
const schema = {
|
|
88
|
+
$ref: 'https://example.com/schema.json',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
render(<SchemaJsonViewer schema={schema} sourcePath="root.json" />);
|
|
92
|
+
|
|
93
|
+
fireEvent.click(screen.getByText('View Raw JSON Schema'));
|
|
94
|
+
const refLink = screen.getByRole('link', {
|
|
95
|
+
name: '"https://example.com/schema.json"',
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(refLink).toHaveAttribute('href', 'https://example.com/schema.json');
|
|
99
|
+
expect(refLink).toHaveAttribute('target', '_blank');
|
|
100
|
+
expect(refLink).toHaveAttribute('rel', 'noreferrer');
|
|
35
101
|
});
|
|
36
102
|
});
|
|
@@ -7,7 +7,9 @@ import SchemaRows from '../../components/SchemaRows';
|
|
|
7
7
|
jest.mock('../../components/PropertyRow', () => {
|
|
8
8
|
const MockPropertyRow = (props) => (
|
|
9
9
|
<tr>
|
|
10
|
-
<td>
|
|
10
|
+
<td>
|
|
11
|
+
Mocked PropertyRow: {props.row.name} ({props.stripeIndex})
|
|
12
|
+
</td>
|
|
11
13
|
</tr>
|
|
12
14
|
);
|
|
13
15
|
MockPropertyRow.displayName = 'MockPropertyRow';
|
|
@@ -17,7 +19,10 @@ jest.mock('../../components/PropertyRow', () => {
|
|
|
17
19
|
jest.mock('../../components/FoldableRows', () => {
|
|
18
20
|
const MockFoldableRows = (props) => (
|
|
19
21
|
<tr>
|
|
20
|
-
<td>
|
|
22
|
+
<td>
|
|
23
|
+
Mocked FoldableRows: {props.row.choiceType} ({props.stripeIndex},{' '}
|
|
24
|
+
{String(!!props.stripeState)})
|
|
25
|
+
</td>
|
|
21
26
|
</tr>
|
|
22
27
|
);
|
|
23
28
|
MockFoldableRows.displayName = 'MockFoldableRows';
|
|
@@ -27,7 +32,10 @@ jest.mock('../../components/FoldableRows', () => {
|
|
|
27
32
|
jest.mock('../../components/ConditionalRows', () => {
|
|
28
33
|
const MockConditionalRows = (props) => (
|
|
29
34
|
<tr>
|
|
30
|
-
<td>
|
|
35
|
+
<td>
|
|
36
|
+
Mocked ConditionalRows: {props.row.condition.title} ({props.stripeIndex}
|
|
37
|
+
, {String(!!props.stripeState)})
|
|
38
|
+
</td>
|
|
31
39
|
</tr>
|
|
32
40
|
);
|
|
33
41
|
MockConditionalRows.displayName = 'MockConditionalRows';
|
|
@@ -49,8 +57,8 @@ describe('SchemaRows', () => {
|
|
|
49
57
|
</table>,
|
|
50
58
|
);
|
|
51
59
|
|
|
52
|
-
expect(getByText('Mocked PropertyRow: name')).toBeInTheDocument();
|
|
53
|
-
expect(getByText('Mocked PropertyRow: age')).toBeInTheDocument();
|
|
60
|
+
expect(getByText('Mocked PropertyRow: name (0)')).toBeInTheDocument();
|
|
61
|
+
expect(getByText('Mocked PropertyRow: age (1)')).toBeInTheDocument();
|
|
54
62
|
});
|
|
55
63
|
|
|
56
64
|
it('renders nested properties from a flat list', () => {
|
|
@@ -68,8 +76,8 @@ describe('SchemaRows', () => {
|
|
|
68
76
|
);
|
|
69
77
|
|
|
70
78
|
// It should render both the parent and child property from the flat list
|
|
71
|
-
expect(getByText('Mocked PropertyRow: user')).toBeInTheDocument();
|
|
72
|
-
expect(getByText('Mocked PropertyRow: id')).toBeInTheDocument();
|
|
79
|
+
expect(getByText('Mocked PropertyRow: user (0)')).toBeInTheDocument();
|
|
80
|
+
expect(getByText('Mocked PropertyRow: id (1)')).toBeInTheDocument();
|
|
73
81
|
});
|
|
74
82
|
|
|
75
83
|
it('renders a FoldableRows for choice type items in tableData', () => {
|
|
@@ -90,7 +98,9 @@ describe('SchemaRows', () => {
|
|
|
90
98
|
</table>,
|
|
91
99
|
);
|
|
92
100
|
|
|
93
|
-
expect(
|
|
101
|
+
expect(
|
|
102
|
+
getByText('Mocked FoldableRows: oneOf (0, true)'),
|
|
103
|
+
).toBeInTheDocument();
|
|
94
104
|
});
|
|
95
105
|
|
|
96
106
|
it('renders a mix of properties and choices', () => {
|
|
@@ -113,9 +123,11 @@ describe('SchemaRows', () => {
|
|
|
113
123
|
</table>,
|
|
114
124
|
);
|
|
115
125
|
|
|
116
|
-
expect(getByText('Mocked PropertyRow: prop1')).toBeInTheDocument();
|
|
117
|
-
expect(
|
|
118
|
-
|
|
126
|
+
expect(getByText('Mocked PropertyRow: prop1 (0)')).toBeInTheDocument();
|
|
127
|
+
expect(
|
|
128
|
+
getByText('Mocked FoldableRows: anyOf (1, true)'),
|
|
129
|
+
).toBeInTheDocument();
|
|
130
|
+
expect(getByText('Mocked PropertyRow: prop2 (2)')).toBeInTheDocument();
|
|
119
131
|
});
|
|
120
132
|
|
|
121
133
|
it('renders a ConditionalRows for conditional type items in tableData', () => {
|
|
@@ -136,6 +148,44 @@ describe('SchemaRows', () => {
|
|
|
136
148
|
</table>,
|
|
137
149
|
);
|
|
138
150
|
|
|
139
|
-
expect(
|
|
151
|
+
expect(
|
|
152
|
+
getByText('Mocked ConditionalRows: If (0, true)'),
|
|
153
|
+
).toBeInTheDocument();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('increments stripe indices across logical rows', () => {
|
|
157
|
+
const tableData = [
|
|
158
|
+
{ type: 'property', name: 'prop1', path: ['prop1'] },
|
|
159
|
+
{
|
|
160
|
+
type: 'choice',
|
|
161
|
+
choiceType: 'anyOf',
|
|
162
|
+
path: ['choice'],
|
|
163
|
+
options: [],
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
type: 'conditional',
|
|
167
|
+
path: ['if/then/else'],
|
|
168
|
+
condition: { title: 'If', rows: [] },
|
|
169
|
+
branches: [],
|
|
170
|
+
},
|
|
171
|
+
{ type: 'property', name: 'prop2', path: ['prop2'] },
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
const { getByText } = render(
|
|
175
|
+
<table>
|
|
176
|
+
<tbody>
|
|
177
|
+
<SchemaRows tableData={tableData} />
|
|
178
|
+
</tbody>
|
|
179
|
+
</table>,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
expect(getByText('Mocked PropertyRow: prop1 (0)')).toBeInTheDocument();
|
|
183
|
+
expect(
|
|
184
|
+
getByText('Mocked FoldableRows: anyOf (1, true)'),
|
|
185
|
+
).toBeInTheDocument();
|
|
186
|
+
expect(
|
|
187
|
+
getByText('Mocked ConditionalRows: If (2, true)'),
|
|
188
|
+
).toBeInTheDocument();
|
|
189
|
+
expect(getByText('Mocked PropertyRow: prop2 (3)')).toBeInTheDocument();
|
|
140
190
|
});
|
|
141
191
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
|
2
2
|
|
|
3
|
-
exports[`connector lines visual regressions keeps cvv row open when payment choice has following options 1`] = `"<td rowspan="1" style="padding-left: 1.75rem;" class="property-cell level-1"><span class="property-name"><strong>cvv</strong></span></td>"`;
|
|
3
|
+
exports[`connector lines visual regressions keeps cvv row open when payment choice has following options 1`] = `"<td rowspan="1" style="padding-left: 1.75rem;" class="property-cell property-cell--tree level-1"><span class="property-name"><strong>cvv</strong></span></td>"`;
|
|
4
4
|
|
|
5
|
-
exports[`connector lines visual regressions keeps user_id option row open when user conditional follows 1`] = `"<td rowspan="1" style="padding-left: 1.75rem;" class="property-cell level-1"><span class="property-name"><strong>user_id</strong></span></td>"`;
|
|
5
|
+
exports[`connector lines visual regressions keeps user_id option row open when user conditional follows 1`] = `"<td rowspan="1" style="padding-left: 1.75rem;" class="property-cell property-cell--tree level-1"><span class="property-name"><strong>user_id</strong></span></td>"`;
|
|
6
6
|
|
|
7
|
-
exports[`connector lines visual regressions keeps wallet_provider option row open when wallet_email follows 1`] = `"<td rowspan="2" style="padding-left: 1.75rem;" class="property-cell level-1"><span class="property-name"><strong>wallet_provider</strong></span></td>"`;
|
|
7
|
+
exports[`connector lines visual regressions keeps wallet_provider option row open when wallet_email follows 1`] = `"<td rowspan="2" style="padding-left: 1.75rem;" class="property-cell property-cell--required property-cell--tree level-1"><span class="property-name"><strong>wallet_provider</strong></span></td>"`;
|
|
@@ -45,6 +45,48 @@ describe('generateEventDocs (partials)', () => {
|
|
|
45
45
|
readDirRecursive(fixturesDir);
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
function writeDuplicateNameOneOfSchemas() {
|
|
49
|
+
const schemaA = {
|
|
50
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
51
|
+
$id: 'https://example.com/schemas/duplicate-a.json',
|
|
52
|
+
title: 'Duplicate A',
|
|
53
|
+
oneOf: [
|
|
54
|
+
{
|
|
55
|
+
title: 'Shared Option',
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: {
|
|
58
|
+
event: { type: 'string', const: 'dup_a' },
|
|
59
|
+
},
|
|
60
|
+
required: ['event'],
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
const schemaB = {
|
|
65
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
66
|
+
$id: 'https://example.com/schemas/duplicate-b.json',
|
|
67
|
+
title: 'Duplicate B',
|
|
68
|
+
oneOf: [
|
|
69
|
+
{
|
|
70
|
+
title: 'Shared Option',
|
|
71
|
+
type: 'object',
|
|
72
|
+
properties: {
|
|
73
|
+
event: { type: 'string', const: 'dup_b' },
|
|
74
|
+
},
|
|
75
|
+
required: ['event'],
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
fs.vol.writeFileSync(
|
|
81
|
+
path.join(fixturesDir, 'static', 'schemas', 'duplicate-a.json'),
|
|
82
|
+
JSON.stringify(schemaA, null, 2),
|
|
83
|
+
);
|
|
84
|
+
fs.vol.writeFileSync(
|
|
85
|
+
path.join(fixturesDir, 'static', 'schemas', 'duplicate-b.json'),
|
|
86
|
+
JSON.stringify(schemaB, null, 2),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
48
90
|
it('injects top partial when _<eventName>.mdx exists in partials dir', async () => {
|
|
49
91
|
console.log = jest.fn();
|
|
50
92
|
fs.vol.mkdirSync(partialsDir, { recursive: true });
|
|
@@ -131,4 +173,57 @@ describe('generateEventDocs (partials)', () => {
|
|
|
131
173
|
expect(output).not.toContain('TopPartial');
|
|
132
174
|
expect(output).not.toContain('BottomPartial');
|
|
133
175
|
});
|
|
176
|
+
|
|
177
|
+
it('skips basename fallback partials when event names are ambiguous', async () => {
|
|
178
|
+
console.log = jest.fn();
|
|
179
|
+
writeDuplicateNameOneOfSchemas();
|
|
180
|
+
fs.vol.mkdirSync(partialsDir, { recursive: true });
|
|
181
|
+
fs.vol.writeFileSync(
|
|
182
|
+
path.join(partialsDir, '_shared-option.mdx'),
|
|
183
|
+
'Shared',
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
await generateEventDocs(options);
|
|
187
|
+
|
|
188
|
+
const outputA = fs.readFileSync(
|
|
189
|
+
path.join(outputDir, 'duplicate-a', '01-shared-option.mdx'),
|
|
190
|
+
'utf-8',
|
|
191
|
+
);
|
|
192
|
+
const outputB = fs.readFileSync(
|
|
193
|
+
path.join(outputDir, 'duplicate-b', '01-shared-option.mdx'),
|
|
194
|
+
'utf-8',
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
expect(outputA).not.toContain('TopPartial');
|
|
198
|
+
expect(outputB).not.toContain('TopPartial');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('uses scoped partials for ambiguous event names', async () => {
|
|
202
|
+
console.log = jest.fn();
|
|
203
|
+
writeDuplicateNameOneOfSchemas();
|
|
204
|
+
fs.vol.mkdirSync(path.join(partialsDir, 'duplicate-a'), {
|
|
205
|
+
recursive: true,
|
|
206
|
+
});
|
|
207
|
+
fs.vol.writeFileSync(
|
|
208
|
+
path.join(partialsDir, 'duplicate-a', '_shared-option.mdx'),
|
|
209
|
+
'Scoped shared',
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
await generateEventDocs(options);
|
|
213
|
+
|
|
214
|
+
const outputA = fs.readFileSync(
|
|
215
|
+
path.join(outputDir, 'duplicate-a', '01-shared-option.mdx'),
|
|
216
|
+
'utf-8',
|
|
217
|
+
);
|
|
218
|
+
const outputB = fs.readFileSync(
|
|
219
|
+
path.join(outputDir, 'duplicate-b', '01-shared-option.mdx'),
|
|
220
|
+
'utf-8',
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
expect(outputA).toContain(
|
|
224
|
+
"import TopPartial from '@site/docs/partials/duplicate-a/_shared-option.mdx'",
|
|
225
|
+
);
|
|
226
|
+
expect(outputA).toContain('<TopPartial />');
|
|
227
|
+
expect(outputB).not.toContain('TopPartial');
|
|
228
|
+
});
|
|
134
229
|
});
|
|
@@ -56,6 +56,9 @@ describe('generateEventDocs (non-versioned)', () => {
|
|
|
56
56
|
'utf-8',
|
|
57
57
|
);
|
|
58
58
|
expect(addToCart).toMatchSnapshot();
|
|
59
|
+
expect(addToCart).toContain('sourcePath={"add-to-cart-event.json"}');
|
|
60
|
+
expect(addToCart).toContain(JSON.stringify('components/product.json'));
|
|
61
|
+
expect(addToCart).toContain('"$ref":"./components/product.json"');
|
|
59
62
|
|
|
60
63
|
const choiceEvent = fs.readFileSync(
|
|
61
64
|
path.join(outputDir, 'choice-event.mdx'),
|
|
@@ -53,4 +53,33 @@ describe('processSchema', () => {
|
|
|
53
53
|
expect(mergedSchema.properties.b.title).toBe('Schema B');
|
|
54
54
|
expect(mergedSchema.properties.b.properties.a.$ref).toBe('#');
|
|
55
55
|
});
|
|
56
|
+
|
|
57
|
+
it('resolves published /constraints refs when bundling and merging allOf', async () => {
|
|
58
|
+
const filePath = path.join(
|
|
59
|
+
__dirname,
|
|
60
|
+
'..',
|
|
61
|
+
'__fixtures__',
|
|
62
|
+
'validateSchemas',
|
|
63
|
+
'main-schema-with-constraints-ref.json',
|
|
64
|
+
);
|
|
65
|
+
const mergedSchema = await processSchema(filePath);
|
|
66
|
+
|
|
67
|
+
expect(mergedSchema.title).toBe('Main Schema with Constraints Ref');
|
|
68
|
+
expect(mergedSchema.additionalProperties).toEqual(
|
|
69
|
+
expect.objectContaining({
|
|
70
|
+
type: expect.arrayContaining([
|
|
71
|
+
'string',
|
|
72
|
+
'number',
|
|
73
|
+
'integer',
|
|
74
|
+
'boolean',
|
|
75
|
+
'null',
|
|
76
|
+
]),
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
expect(mergedSchema.properties.items).toEqual(
|
|
80
|
+
expect.objectContaining({
|
|
81
|
+
type: 'array',
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
});
|
|
56
85
|
});
|
|
@@ -217,6 +217,118 @@ describe('schemaToTableData', () => {
|
|
|
217
217
|
expect(tableData[0].examples).toEqual(['default-value']);
|
|
218
218
|
});
|
|
219
219
|
|
|
220
|
+
it('renders schema-valued additionalProperties as a synthetic row', () => {
|
|
221
|
+
const schema = {
|
|
222
|
+
properties: {
|
|
223
|
+
metadata: {
|
|
224
|
+
type: 'object',
|
|
225
|
+
description: 'Free-form metadata.',
|
|
226
|
+
additionalProperties: {
|
|
227
|
+
type: 'string',
|
|
228
|
+
description: 'Metadata value.',
|
|
229
|
+
pattern: '^[a-z]+$',
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const tableData = schemaToTableData(schema);
|
|
236
|
+
|
|
237
|
+
const metadataRow = tableData.find((row) => row.name === 'metadata');
|
|
238
|
+
expect(metadataRow).toBeDefined();
|
|
239
|
+
expect(metadataRow.containerType).toBe('object');
|
|
240
|
+
|
|
241
|
+
const additionalPropertiesRow = tableData.find(
|
|
242
|
+
(row) =>
|
|
243
|
+
row.type === 'property' &&
|
|
244
|
+
row.name === 'additionalProperties' &&
|
|
245
|
+
row.level === 1,
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
expect(additionalPropertiesRow).toBeDefined();
|
|
249
|
+
expect(additionalPropertiesRow.propertyType).toBe('string');
|
|
250
|
+
expect(additionalPropertiesRow.description).toBe('Metadata value.');
|
|
251
|
+
expect(additionalPropertiesRow.constraints).toContain(
|
|
252
|
+
'pattern: /^[a-z]+$/',
|
|
253
|
+
);
|
|
254
|
+
expect(additionalPropertiesRow.isSchemaKeywordRow).toBe(true);
|
|
255
|
+
expect(additionalPropertiesRow.keepConnectorOpen).toBe(false);
|
|
256
|
+
expect(additionalPropertiesRow.isLastInGroup).toBe(true);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('treats additionalProperties as the last sibling after declared properties', () => {
|
|
260
|
+
const schema = {
|
|
261
|
+
properties: {
|
|
262
|
+
user_properties: {
|
|
263
|
+
type: 'object',
|
|
264
|
+
properties: {
|
|
265
|
+
allow_ad_personalization_signals: {
|
|
266
|
+
type: ['string', 'null'],
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
additionalProperties: {
|
|
270
|
+
type: ['string', 'null'],
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const tableData = schemaToTableData(schema);
|
|
277
|
+
|
|
278
|
+
const explicitPropertyRow = tableData.find(
|
|
279
|
+
(row) =>
|
|
280
|
+
row.name === 'allow_ad_personalization_signals' && row.level === 1,
|
|
281
|
+
);
|
|
282
|
+
const additionalPropertiesRow = tableData.find(
|
|
283
|
+
(row) => row.name === 'additionalProperties' && row.level === 1,
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
expect(explicitPropertyRow).toBeDefined();
|
|
287
|
+
expect(additionalPropertiesRow).toBeDefined();
|
|
288
|
+
expect(explicitPropertyRow.isLastInGroup).toBe(false);
|
|
289
|
+
expect(additionalPropertiesRow.isLastInGroup).toBe(true);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('renders schema-valued patternProperties as synthetic keyword rows', () => {
|
|
293
|
+
const schema = {
|
|
294
|
+
properties: {
|
|
295
|
+
attributes: {
|
|
296
|
+
type: 'object',
|
|
297
|
+
properties: {
|
|
298
|
+
segment: { type: 'string' },
|
|
299
|
+
},
|
|
300
|
+
patternProperties: {
|
|
301
|
+
'^custom_': {
|
|
302
|
+
type: 'number',
|
|
303
|
+
description: 'Numeric custom attribute values.',
|
|
304
|
+
minimum: 0,
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const tableData = schemaToTableData(schema);
|
|
312
|
+
|
|
313
|
+
const explicitPropertyRow = tableData.find(
|
|
314
|
+
(row) => row.name === 'segment' && row.level === 1,
|
|
315
|
+
);
|
|
316
|
+
const patternPropertyRow = tableData.find(
|
|
317
|
+
(row) => row.name === 'patternProperties /^custom_/' && row.level === 1,
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
expect(explicitPropertyRow).toBeDefined();
|
|
321
|
+
expect(patternPropertyRow).toBeDefined();
|
|
322
|
+
expect(explicitPropertyRow.isLastInGroup).toBe(false);
|
|
323
|
+
expect(patternPropertyRow.isLastInGroup).toBe(true);
|
|
324
|
+
expect(patternPropertyRow.propertyType).toBe('number');
|
|
325
|
+
expect(patternPropertyRow.description).toBe(
|
|
326
|
+
'Numeric custom attribute values.',
|
|
327
|
+
);
|
|
328
|
+
expect(patternPropertyRow.constraints).toContain('minimum: 0');
|
|
329
|
+
expect(patternPropertyRow.isSchemaKeywordRow).toBe(true);
|
|
330
|
+
});
|
|
331
|
+
|
|
220
332
|
describe('if/then/else conditional support', () => {
|
|
221
333
|
it('creates a conditional row for schema with if/then/else at root level', () => {
|
|
222
334
|
const tableData = schemaToTableData(conditionalEventSchema);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createValidator } from '../../helpers/validator';
|
|
2
|
+
import path from 'path';
|
|
2
3
|
|
|
3
4
|
describe('createValidator', () => {
|
|
4
5
|
it('creates a validator that returns true for valid data with no schema version (draft-07)', async () => {
|
|
@@ -112,4 +113,35 @@ describe('createValidator', () => {
|
|
|
112
113
|
expect(result.valid).toBe(true);
|
|
113
114
|
expect(result.errors).toEqual([]);
|
|
114
115
|
});
|
|
116
|
+
|
|
117
|
+
it('resolves published /constraints refs to local constraint schemas', async () => {
|
|
118
|
+
const schema = {
|
|
119
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
120
|
+
type: 'object',
|
|
121
|
+
allOf: [
|
|
122
|
+
{
|
|
123
|
+
$ref: 'https://tracking-docs-demo.buchert.digital/constraints/schemas/firebase/v1/flat-event-params.json',
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const schemaPath = path.resolve(
|
|
129
|
+
__dirname,
|
|
130
|
+
'../../../../demo/static/schemas/next',
|
|
131
|
+
);
|
|
132
|
+
const validator = await createValidator([], schema, schemaPath);
|
|
133
|
+
|
|
134
|
+
const valid = validator({
|
|
135
|
+
event: 'screen_view',
|
|
136
|
+
screen_name: 'Checkout',
|
|
137
|
+
attempt: 1,
|
|
138
|
+
});
|
|
139
|
+
expect(valid.valid).toBe(true);
|
|
140
|
+
|
|
141
|
+
const invalid = validator({
|
|
142
|
+
event: 'screen_view',
|
|
143
|
+
nested: { disallowed: true },
|
|
144
|
+
});
|
|
145
|
+
expect(invalid.valid).toBe(false);
|
|
146
|
+
});
|
|
115
147
|
});
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
*/
|
|
18
18
|
export default function ConditionalRows({
|
|
19
19
|
row,
|
|
20
|
+
stripeIndex = 0,
|
|
21
|
+
stripeState,
|
|
20
22
|
bracketEnds: parentBracketEnds,
|
|
21
23
|
}) {
|
|
22
24
|
const {
|
|
@@ -91,7 +93,7 @@ export default function ConditionalRows({
|
|
|
91
93
|
return (
|
|
92
94
|
<>
|
|
93
95
|
{/* Condition (if) section - always visible */}
|
|
94
|
-
<tr className="conditional-condition-header">
|
|
96
|
+
<tr className="conditional-condition-header schema-row--control">
|
|
95
97
|
<td colSpan={5} style={headerStyle}>
|
|
96
98
|
<span className="conditional-condition-label">
|
|
97
99
|
<span className="conditional-info-icon-wrapper">
|
|
@@ -111,7 +113,7 @@ export default function ConditionalRows({
|
|
|
111
113
|
)}
|
|
112
114
|
</td>
|
|
113
115
|
</tr>
|
|
114
|
-
<SchemaRows tableData={condition.rows} />
|
|
116
|
+
<SchemaRows tableData={condition.rows} stripeState={stripeState} />
|
|
115
117
|
|
|
116
118
|
{/* Branch toggles (then / else) */}
|
|
117
119
|
{branches.map((branch, index) => {
|
|
@@ -121,7 +123,7 @@ export default function ConditionalRows({
|
|
|
121
123
|
isLastBranch && !isActive ? lastToggleStyle : middleStyle;
|
|
122
124
|
return (
|
|
123
125
|
<React.Fragment key={branch.title}>
|
|
124
|
-
<tr className="choice-row">
|
|
126
|
+
<tr className="choice-row schema-row--control">
|
|
125
127
|
<td colSpan={5} style={toggleStyle}>
|
|
126
128
|
<label className="choice-row-header">
|
|
127
129
|
<input
|
|
@@ -141,6 +143,7 @@ export default function ConditionalRows({
|
|
|
141
143
|
{isActive && (
|
|
142
144
|
<SchemaRows
|
|
143
145
|
tableData={branch.rows}
|
|
146
|
+
stripeState={stripeState}
|
|
144
147
|
bracketEnds={
|
|
145
148
|
isLastBranch
|
|
146
149
|
? [ownBracket, ...(parentBracketEnds || [])]
|
|
@@ -18,7 +18,7 @@ const ChoiceRow = ({
|
|
|
18
18
|
name,
|
|
19
19
|
continuingLinesStyle,
|
|
20
20
|
}) => (
|
|
21
|
-
<tr className="choice-row">
|
|
21
|
+
<tr className="choice-row schema-row--control">
|
|
22
22
|
<td colSpan={5} style={continuingLinesStyle}>
|
|
23
23
|
<label className="choice-row-header">
|
|
24
24
|
<input
|
|
@@ -41,7 +41,12 @@ const ChoiceRow = ({
|
|
|
41
41
|
* Renders 'oneOf' and 'anyOf' choices as a set of foldable `<tr>` elements
|
|
42
42
|
* that integrate directly into the main table body.
|
|
43
43
|
*/
|
|
44
|
-
export default function FoldableRows({
|
|
44
|
+
export default function FoldableRows({
|
|
45
|
+
row,
|
|
46
|
+
stripeIndex = 0,
|
|
47
|
+
stripeState,
|
|
48
|
+
bracketEnds: parentBracketEnds,
|
|
49
|
+
}) {
|
|
45
50
|
const {
|
|
46
51
|
choiceType,
|
|
47
52
|
options,
|
|
@@ -136,7 +141,7 @@ export default function FoldableRows({ row, bracketEnds: parentBracketEnds }) {
|
|
|
136
141
|
return (
|
|
137
142
|
<>
|
|
138
143
|
{/* A header row for the entire choice block */}
|
|
139
|
-
<tr>
|
|
144
|
+
<tr className="schema-row--control">
|
|
140
145
|
<td colSpan={5} style={headerStyle}>
|
|
141
146
|
<Heading as="h4" className="choice-row-header-headline">
|
|
142
147
|
{header}
|
|
@@ -176,6 +181,7 @@ export default function FoldableRows({ row, bracketEnds: parentBracketEnds }) {
|
|
|
176
181
|
{isActive && (
|
|
177
182
|
<SchemaRows
|
|
178
183
|
tableData={option.rows}
|
|
184
|
+
stripeState={stripeState}
|
|
179
185
|
bracketEnds={
|
|
180
186
|
isLastOption
|
|
181
187
|
? [ownBracket, ...(parentBracketEnds || [])]
|
|
@@ -8,6 +8,7 @@ import styles from './PropertiesTable.module.css';
|
|
|
8
8
|
export default function PropertiesTable({ schema }) {
|
|
9
9
|
const [isWordWrapOn, setIsWordWrapOn] = useState(true);
|
|
10
10
|
const tableData = schemaToTableData(schema);
|
|
11
|
+
const stripeState = { current: 0 };
|
|
11
12
|
|
|
12
13
|
return (
|
|
13
14
|
<div
|
|
@@ -24,7 +25,7 @@ export default function PropertiesTable({ schema }) {
|
|
|
24
25
|
<table className="schema-table">
|
|
25
26
|
<TableHeader />
|
|
26
27
|
<tbody>
|
|
27
|
-
<SchemaRows tableData={tableData} />
|
|
28
|
+
<SchemaRows tableData={tableData} stripeState={stripeState} />
|
|
28
29
|
</tbody>
|
|
29
30
|
</table>
|
|
30
31
|
</div>
|