docusaurus-plugin-generate-schema-docs 1.8.3 → 1.8.4
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 +2 -0
- package/__tests__/__fixtures__/validateSchemas/main-schema-with-not-allof.json +11 -0
- package/__tests__/__snapshots__/generateEventDocs.anchor.test.js.snap +6 -0
- package/__tests__/__snapshots__/generateEventDocs.nested.test.js.snap +6 -0
- package/__tests__/__snapshots__/generateEventDocs.test.js.snap +15 -0
- package/__tests__/__snapshots__/generateEventDocs.versioned.test.js.snap +6 -0
- package/__tests__/components/PropertiesTable.test.js +66 -0
- package/__tests__/components/PropertyRow.test.js +85 -4
- package/__tests__/components/SchemaJsonViewer.test.js +118 -0
- package/__tests__/helpers/example-helper.test.js +12 -0
- package/__tests__/helpers/getConstraints.test.js +16 -0
- package/__tests__/helpers/processSchema.test.js +18 -0
- package/__tests__/helpers/schemaTraversal.test.js +110 -0
- package/__tests__/syncGtm.test.js +227 -3
- package/__tests__/validateSchemas.test.js +50 -0
- package/components/PropertiesTable.js +32 -2
- package/components/PropertyRow.js +29 -2
- package/components/SchemaJsonViewer.js +234 -131
- package/components/SchemaRows.css +40 -0
- package/components/SchemaViewer.js +11 -2
- package/helpers/example-helper.js +2 -2
- package/helpers/getConstraints.js +20 -0
- package/helpers/processSchema.js +32 -1
- package/helpers/schema-doc-template.js +4 -0
- package/helpers/schemaToExamples.js +29 -35
- package/helpers/schemaTraversal.cjs +148 -0
- package/package.json +1 -1
- package/scripts/sync-gtm.js +41 -28
- package/test-data/payloadContracts.js +35 -0
- package/validateSchemas.js +1 -1
|
@@ -14,6 +14,29 @@ jest.mock(
|
|
|
14
14
|
{ virtual: true },
|
|
15
15
|
);
|
|
16
16
|
|
|
17
|
+
jest.mock('@docusaurus/theme-common', () => ({
|
|
18
|
+
usePrismTheme: () => ({
|
|
19
|
+
plain: {
|
|
20
|
+
color: 'rgb(1, 2, 3)',
|
|
21
|
+
backgroundColor: 'rgb(4, 5, 6)',
|
|
22
|
+
},
|
|
23
|
+
styles: [
|
|
24
|
+
{
|
|
25
|
+
types: ['property'],
|
|
26
|
+
style: { color: 'rgb(0, 0, 255)' },
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
types: ['string'],
|
|
30
|
+
style: { color: 'rgb(255, 0, 0)' },
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
types: ['number'],
|
|
34
|
+
style: { color: 'rgb(0, 128, 0)' },
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
}),
|
|
38
|
+
}));
|
|
39
|
+
|
|
17
40
|
describe('SchemaJsonViewer', () => {
|
|
18
41
|
it('renders the schema in a syntax-highlighted pre block', () => {
|
|
19
42
|
const schema = {
|
|
@@ -38,6 +61,22 @@ describe('SchemaJsonViewer', () => {
|
|
|
38
61
|
);
|
|
39
62
|
});
|
|
40
63
|
|
|
64
|
+
it('uses the prism theme for base json token styling', () => {
|
|
65
|
+
const schema = {
|
|
66
|
+
type: 'string',
|
|
67
|
+
examples: ['x'],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
render(<SchemaJsonViewer schema={schema} />);
|
|
71
|
+
|
|
72
|
+
fireEvent.click(screen.getByText('View Raw JSON Schema'));
|
|
73
|
+
|
|
74
|
+
expect(screen.getByText('"type"')).toHaveStyle({ color: 'rgb(0, 0, 255)' });
|
|
75
|
+
expect(screen.getByText('"string"')).toHaveStyle({
|
|
76
|
+
color: 'rgb(255, 0, 0)',
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
41
80
|
it('navigates local $ref values inside the viewer and resets to root', () => {
|
|
42
81
|
const rootSchema = {
|
|
43
82
|
type: 'object',
|
|
@@ -83,6 +122,84 @@ describe('SchemaJsonViewer', () => {
|
|
|
83
122
|
).toBeInTheDocument();
|
|
84
123
|
});
|
|
85
124
|
|
|
125
|
+
it('highlights schema meta keywords inside the raw json view', () => {
|
|
126
|
+
const schema = {
|
|
127
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
128
|
+
$id: 'https://example.com/event.json',
|
|
129
|
+
$anchor: 'eventRoot',
|
|
130
|
+
$comment: 'Internal authoring note',
|
|
131
|
+
$vocabulary: {
|
|
132
|
+
'https://json-schema.org/draft/2020-12/vocab/validation': true,
|
|
133
|
+
},
|
|
134
|
+
type: 'object',
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
render(<SchemaJsonViewer schema={schema} sourcePath="event.json" />);
|
|
138
|
+
|
|
139
|
+
fireEvent.click(screen.getByText('View Raw JSON Schema'));
|
|
140
|
+
|
|
141
|
+
expect(screen.getByText('"$schema"')).toHaveClass(
|
|
142
|
+
'schema-json-viewer__keyword',
|
|
143
|
+
'schema-json-viewer__keyword--meta',
|
|
144
|
+
);
|
|
145
|
+
expect(screen.getByText('"$anchor"')).toHaveClass(
|
|
146
|
+
'schema-json-viewer__keyword',
|
|
147
|
+
'schema-json-viewer__keyword--meta',
|
|
148
|
+
);
|
|
149
|
+
expect(screen.getByText('"$vocabulary"')).toHaveClass(
|
|
150
|
+
'schema-json-viewer__keyword',
|
|
151
|
+
'schema-json-viewer__keyword--meta',
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('highlights structural schema keywords inside the raw json view', () => {
|
|
156
|
+
const schema = {
|
|
157
|
+
type: 'object',
|
|
158
|
+
allOf: [{ $ref: './component.json' }],
|
|
159
|
+
properties: {
|
|
160
|
+
name: { type: 'string' },
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
render(<SchemaJsonViewer schema={schema} sourcePath="event.json" />);
|
|
165
|
+
|
|
166
|
+
fireEvent.click(screen.getByText('View Raw JSON Schema'));
|
|
167
|
+
|
|
168
|
+
expect(screen.getByText('"allOf"')).toHaveClass(
|
|
169
|
+
'schema-json-viewer__keyword',
|
|
170
|
+
'schema-json-viewer__keyword--structural',
|
|
171
|
+
);
|
|
172
|
+
expect(screen.getByText('"properties"')).toHaveClass(
|
|
173
|
+
'schema-json-viewer__keyword',
|
|
174
|
+
'schema-json-viewer__keyword--structural',
|
|
175
|
+
);
|
|
176
|
+
expect(screen.getByText('"$ref"')).toHaveClass(
|
|
177
|
+
'schema-json-viewer__keyword',
|
|
178
|
+
'schema-json-viewer__keyword--structural',
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('does not highlight payload property names inside properties as meta keywords', () => {
|
|
183
|
+
const schema = {
|
|
184
|
+
type: 'object',
|
|
185
|
+
properties: {
|
|
186
|
+
$schema: {
|
|
187
|
+
type: 'string',
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
render(<SchemaJsonViewer schema={schema} sourcePath="event.json" />);
|
|
193
|
+
|
|
194
|
+
fireEvent.click(screen.getByText('View Raw JSON Schema'));
|
|
195
|
+
|
|
196
|
+
expect(screen.getByText('"properties"')).toHaveClass(
|
|
197
|
+
'schema-json-viewer__keyword',
|
|
198
|
+
'schema-json-viewer__keyword--structural',
|
|
199
|
+
);
|
|
200
|
+
expect(screen.getByText('"$schema"')).toHaveClass('token property');
|
|
201
|
+
});
|
|
202
|
+
|
|
86
203
|
it('renders external $ref values as new-tab links', () => {
|
|
87
204
|
const schema = {
|
|
88
205
|
$ref: 'https://example.com/schema.json',
|
|
@@ -98,5 +215,6 @@ describe('SchemaJsonViewer', () => {
|
|
|
98
215
|
expect(refLink).toHaveAttribute('href', 'https://example.com/schema.json');
|
|
99
216
|
expect(refLink).toHaveAttribute('target', '_blank');
|
|
100
217
|
expect(refLink).toHaveAttribute('rel', 'noreferrer');
|
|
218
|
+
expect(refLink).toHaveClass('schema-json-viewer__ref-link');
|
|
101
219
|
});
|
|
102
220
|
});
|
|
@@ -34,6 +34,12 @@ describe('example-helper', () => {
|
|
|
34
34
|
};
|
|
35
35
|
expect(getExamples(schema)).toEqual(['example1']);
|
|
36
36
|
});
|
|
37
|
+
|
|
38
|
+
it('preserves falsy "example" values', () => {
|
|
39
|
+
expect(getExamples({ example: false })).toEqual([false]);
|
|
40
|
+
expect(getExamples({ example: 0 })).toEqual([0]);
|
|
41
|
+
expect(getExamples({ example: '' })).toEqual(['']);
|
|
42
|
+
});
|
|
37
43
|
});
|
|
38
44
|
|
|
39
45
|
describe('getSingleExampleValue', () => {
|
|
@@ -67,5 +73,11 @@ describe('example-helper', () => {
|
|
|
67
73
|
};
|
|
68
74
|
expect(getSingleExampleValue(schema)).toBe('default-value');
|
|
69
75
|
});
|
|
76
|
+
|
|
77
|
+
it('returns falsy "example" values when present', () => {
|
|
78
|
+
expect(getSingleExampleValue({ example: false })).toBe(false);
|
|
79
|
+
expect(getSingleExampleValue({ example: 0 })).toBe(0);
|
|
80
|
+
expect(getSingleExampleValue({ example: '' })).toBe('');
|
|
81
|
+
});
|
|
70
82
|
});
|
|
71
83
|
});
|
|
@@ -59,6 +59,7 @@ describe('getConstraints', () => {
|
|
|
59
59
|
contains: { type: 'string' },
|
|
60
60
|
enum: ['a', 'b', 'c'],
|
|
61
61
|
const: 'hello',
|
|
62
|
+
not: { const: 'US' },
|
|
62
63
|
};
|
|
63
64
|
const expected = [
|
|
64
65
|
'pattern: /^[a-z]+$/',
|
|
@@ -69,6 +70,7 @@ describe('getConstraints', () => {
|
|
|
69
70
|
'contains: {"type":"string"}',
|
|
70
71
|
'enum: [a, b, c]',
|
|
71
72
|
'const: "hello"',
|
|
73
|
+
'not: { const: "US" }',
|
|
72
74
|
];
|
|
73
75
|
expect(getConstraints(prop, false)).toEqual(
|
|
74
76
|
expect.arrayContaining(expected),
|
|
@@ -100,4 +102,18 @@ describe('getConstraints', () => {
|
|
|
100
102
|
expect.arrayContaining(['required', ...expected]),
|
|
101
103
|
);
|
|
102
104
|
});
|
|
105
|
+
|
|
106
|
+
it('should render "not" constraint in JSON schema syntax for const', () => {
|
|
107
|
+
const prop = {
|
|
108
|
+
not: { const: 'US' },
|
|
109
|
+
};
|
|
110
|
+
expect(getConstraints(prop, false)).toEqual(['not: { const: "US" }']);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should render "not" constraint in JSON schema syntax for type', () => {
|
|
114
|
+
const prop = {
|
|
115
|
+
not: { type: 'string' },
|
|
116
|
+
};
|
|
117
|
+
expect(getConstraints(prop, false)).toEqual(['not: { type: "string" }']);
|
|
118
|
+
});
|
|
103
119
|
});
|
|
@@ -82,4 +82,22 @@ describe('processSchema', () => {
|
|
|
82
82
|
}),
|
|
83
83
|
);
|
|
84
84
|
});
|
|
85
|
+
|
|
86
|
+
it('keeps simple not const constraints readable after allOf merge', async () => {
|
|
87
|
+
const filePath = path.join(
|
|
88
|
+
__dirname,
|
|
89
|
+
'..',
|
|
90
|
+
'__fixtures__',
|
|
91
|
+
'validateSchemas',
|
|
92
|
+
'main-schema-with-not-allof.json',
|
|
93
|
+
);
|
|
94
|
+
const mergedSchema = await processSchema(filePath);
|
|
95
|
+
|
|
96
|
+
expect(mergedSchema.properties.country).toEqual(
|
|
97
|
+
expect.objectContaining({
|
|
98
|
+
type: 'string',
|
|
99
|
+
not: { const: 'US' },
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
});
|
|
85
103
|
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const {
|
|
2
|
+
visitSchemaNodes,
|
|
3
|
+
visitSchemaPropertyEntries,
|
|
4
|
+
} = require('../../helpers/schemaTraversal.cjs');
|
|
5
|
+
|
|
6
|
+
describe('schemaTraversal', () => {
|
|
7
|
+
it('visits nested choice and conditional schema nodes', () => {
|
|
8
|
+
const schema = {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {
|
|
11
|
+
payment_method: {
|
|
12
|
+
oneOf: [
|
|
13
|
+
{
|
|
14
|
+
properties: {
|
|
15
|
+
card_number: { type: 'string' },
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
if: {
|
|
22
|
+
properties: {
|
|
23
|
+
platform: { const: 'ios' },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
then: {
|
|
27
|
+
properties: {
|
|
28
|
+
att_status: { type: 'string' },
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const visitedPaths = [];
|
|
34
|
+
visitSchemaNodes(schema, (_node, context) => {
|
|
35
|
+
visitedPaths.push(context.path.join('.'));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(visitedPaths).toEqual(
|
|
39
|
+
expect.arrayContaining([
|
|
40
|
+
'',
|
|
41
|
+
'properties.payment_method',
|
|
42
|
+
'properties.payment_method.oneOf.0',
|
|
43
|
+
'properties.payment_method.oneOf.0.properties.card_number',
|
|
44
|
+
'if',
|
|
45
|
+
'then',
|
|
46
|
+
'then.properties.att_status',
|
|
47
|
+
]),
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('collects property entries through object, array, choice, and conditional branches', () => {
|
|
52
|
+
const schema = {
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: {
|
|
55
|
+
event: { type: 'string' },
|
|
56
|
+
items: {
|
|
57
|
+
type: 'array',
|
|
58
|
+
items: {
|
|
59
|
+
properties: {
|
|
60
|
+
sku: { type: 'string' },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
contact_method: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
oneOf: [
|
|
67
|
+
{
|
|
68
|
+
properties: {
|
|
69
|
+
email: { type: 'string' },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
properties: {
|
|
74
|
+
phone_number: { type: 'string' },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
then: {
|
|
81
|
+
properties: {
|
|
82
|
+
att_status: { type: 'string' },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
else: {
|
|
86
|
+
properties: {
|
|
87
|
+
ad_personalization_enabled: { type: 'boolean' },
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const variableNames = [];
|
|
93
|
+
visitSchemaPropertyEntries(schema, (_property, context) => {
|
|
94
|
+
variableNames.push(context.name);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(variableNames).toEqual(
|
|
98
|
+
expect.arrayContaining([
|
|
99
|
+
'event',
|
|
100
|
+
'items',
|
|
101
|
+
'items.0.sku',
|
|
102
|
+
'contact_method',
|
|
103
|
+
'contact_method.email',
|
|
104
|
+
'contact_method.phone_number',
|
|
105
|
+
'att_status',
|
|
106
|
+
'ad_personalization_enabled',
|
|
107
|
+
]),
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -79,7 +79,7 @@ describe('getVariablesFromSchemas', () => {
|
|
|
79
79
|
|
|
80
80
|
const SCHEMA_PATH = '/fake/schemas';
|
|
81
81
|
const mockFiles = {
|
|
82
|
-
[SCHEMA_PATH]: ['complex-event.json', 'components'],
|
|
82
|
+
[SCHEMA_PATH]: ['complex-event.json', 'mobile-event.json', 'components'],
|
|
83
83
|
[path.join(SCHEMA_PATH, 'components')]: ['address.json'],
|
|
84
84
|
};
|
|
85
85
|
const addressSchema = {
|
|
@@ -92,6 +92,7 @@ describe('getVariablesFromSchemas', () => {
|
|
|
92
92
|
};
|
|
93
93
|
const complexEventSchema = {
|
|
94
94
|
title: 'Complex Event',
|
|
95
|
+
'x-tracking-targets': ['web-datalayer-js'],
|
|
95
96
|
type: 'object',
|
|
96
97
|
properties: {
|
|
97
98
|
$schema: { type: 'string', description: 'Should now be included.' },
|
|
@@ -107,12 +108,79 @@ describe('getVariablesFromSchemas', () => {
|
|
|
107
108
|
},
|
|
108
109
|
},
|
|
109
110
|
},
|
|
111
|
+
contact_method: {
|
|
112
|
+
type: 'object',
|
|
113
|
+
oneOf: [
|
|
114
|
+
{
|
|
115
|
+
title: 'Email Contact',
|
|
116
|
+
properties: {
|
|
117
|
+
email: {
|
|
118
|
+
type: 'string',
|
|
119
|
+
description: 'Email address.',
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
title: 'Phone Contact',
|
|
125
|
+
properties: {
|
|
126
|
+
phone_number: {
|
|
127
|
+
type: 'string',
|
|
128
|
+
description: 'Phone number.',
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
platform: {
|
|
135
|
+
type: 'string',
|
|
136
|
+
description: 'Target platform.',
|
|
137
|
+
},
|
|
110
138
|
timestamp: { type: 'number', description: 'Event timestamp.' },
|
|
111
139
|
},
|
|
140
|
+
if: {
|
|
141
|
+
properties: {
|
|
142
|
+
platform: { const: 'ios' },
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
then: {
|
|
146
|
+
properties: {
|
|
147
|
+
att_status: {
|
|
148
|
+
type: 'string',
|
|
149
|
+
description: 'App Tracking Transparency status.',
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
else: {
|
|
154
|
+
properties: {
|
|
155
|
+
ad_personalization_enabled: {
|
|
156
|
+
type: 'boolean',
|
|
157
|
+
description: 'Whether ad personalization is enabled.',
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
const mobileEventSchema = {
|
|
163
|
+
title: 'Mobile Event',
|
|
164
|
+
'x-tracking-targets': ['android-firebase-kotlin-sdk'],
|
|
165
|
+
type: 'object',
|
|
166
|
+
properties: {
|
|
167
|
+
event: { type: 'string', const: 'screen_view' },
|
|
168
|
+
screen_name: { type: 'string', description: 'Screen name.' },
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
const untaggedEventSchema = {
|
|
172
|
+
title: 'Untagged Event',
|
|
173
|
+
type: 'object',
|
|
174
|
+
properties: {
|
|
175
|
+
event: { type: 'string', const: 'legacy_event' },
|
|
176
|
+
legacy_field: { type: 'string', description: 'Legacy field.' },
|
|
177
|
+
},
|
|
112
178
|
};
|
|
113
179
|
const mockFileContents = {
|
|
114
180
|
[path.join(SCHEMA_PATH, 'complex-event.json')]:
|
|
115
181
|
JSON.stringify(complexEventSchema),
|
|
182
|
+
[path.join(SCHEMA_PATH, 'mobile-event.json')]:
|
|
183
|
+
JSON.stringify(mobileEventSchema),
|
|
116
184
|
[path.join(SCHEMA_PATH, 'components', 'address.json')]:
|
|
117
185
|
JSON.stringify(addressSchema),
|
|
118
186
|
};
|
|
@@ -148,7 +216,7 @@ describe('getVariablesFromSchemas', () => {
|
|
|
148
216
|
expect.objectContaining({ name: 'timestamp' }),
|
|
149
217
|
]);
|
|
150
218
|
|
|
151
|
-
expect(result.length).toBe(
|
|
219
|
+
expect(result.length).toBe(14);
|
|
152
220
|
expect(result).toEqual(expected);
|
|
153
221
|
});
|
|
154
222
|
|
|
@@ -172,7 +240,163 @@ describe('getVariablesFromSchemas', () => {
|
|
|
172
240
|
];
|
|
173
241
|
|
|
174
242
|
expect(result.map((r) => r.name)).toEqual(expect.arrayContaining(expected));
|
|
175
|
-
expect(result.length).toBe(
|
|
243
|
+
expect(result.length).toBe(12);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should include variables from oneOf choices and conditional branches', async () => {
|
|
247
|
+
const bundledSchema = JSON.parse(JSON.stringify(complexEventSchema));
|
|
248
|
+
bundledSchema.properties.user_data.properties.addresses.items =
|
|
249
|
+
addressSchema;
|
|
250
|
+
RefParser.bundle.mockResolvedValue(bundledSchema);
|
|
251
|
+
|
|
252
|
+
const result = await gtmScript.getVariablesFromSchemas(SCHEMA_PATH, {});
|
|
253
|
+
const variableNames = result.map((variable) => variable.name);
|
|
254
|
+
|
|
255
|
+
expect(variableNames).toEqual(
|
|
256
|
+
expect.arrayContaining([
|
|
257
|
+
'contact_method.email',
|
|
258
|
+
'contact_method.phone_number',
|
|
259
|
+
'att_status',
|
|
260
|
+
'ad_personalization_enabled',
|
|
261
|
+
]),
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should only include schemas explicitly targeted to web-datalayer-js', async () => {
|
|
266
|
+
const untaggedEventPath = path.join(SCHEMA_PATH, 'legacy-event.json');
|
|
267
|
+
mockFiles[SCHEMA_PATH].push('legacy-event.json');
|
|
268
|
+
mockFileContents[untaggedEventPath] = JSON.stringify(untaggedEventSchema);
|
|
269
|
+
|
|
270
|
+
const bundledWebSchema = JSON.parse(JSON.stringify(complexEventSchema));
|
|
271
|
+
bundledWebSchema.properties.user_data.properties.addresses.items =
|
|
272
|
+
addressSchema;
|
|
273
|
+
|
|
274
|
+
RefParser.bundle.mockImplementation(async (filePath) => {
|
|
275
|
+
if (filePath.endsWith('complex-event.json')) {
|
|
276
|
+
return bundledWebSchema;
|
|
277
|
+
}
|
|
278
|
+
if (filePath.endsWith('mobile-event.json')) {
|
|
279
|
+
return mobileEventSchema;
|
|
280
|
+
}
|
|
281
|
+
if (filePath.endsWith('legacy-event.json')) {
|
|
282
|
+
return untaggedEventSchema;
|
|
283
|
+
}
|
|
284
|
+
if (filePath.endsWith('address.json')) {
|
|
285
|
+
return addressSchema;
|
|
286
|
+
}
|
|
287
|
+
throw new Error(`Unexpected schema file: ${filePath}`);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const result = await gtmScript.getVariablesFromSchemas(SCHEMA_PATH, {});
|
|
291
|
+
|
|
292
|
+
expect(result.map((variable) => variable.name)).toEqual(
|
|
293
|
+
expect.arrayContaining([
|
|
294
|
+
'$schema',
|
|
295
|
+
'event',
|
|
296
|
+
'user_data',
|
|
297
|
+
'user_data.user_id',
|
|
298
|
+
'user_data.addresses',
|
|
299
|
+
'user_data.addresses.0.street',
|
|
300
|
+
'user_data.addresses.0.city',
|
|
301
|
+
'timestamp',
|
|
302
|
+
]),
|
|
303
|
+
);
|
|
304
|
+
expect(result.map((variable) => variable.name)).not.toContain(
|
|
305
|
+
'screen_name',
|
|
306
|
+
);
|
|
307
|
+
expect(result.map((variable) => variable.name)).not.toContain(
|
|
308
|
+
'legacy_field',
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('should include root tracking schemas based on content instead of path names', async () => {
|
|
313
|
+
const nestedSchemaDir = path.join(SCHEMA_PATH, 'event-components-demo');
|
|
314
|
+
const nestedSchemaPath = path.join(nestedSchemaDir, 'checkout-event.json');
|
|
315
|
+
const nestedSchema = {
|
|
316
|
+
title: 'Checkout Event',
|
|
317
|
+
'x-tracking-targets': ['web-datalayer-js'],
|
|
318
|
+
type: 'object',
|
|
319
|
+
properties: {
|
|
320
|
+
event: { type: 'string', const: 'checkout' },
|
|
321
|
+
order_id: { type: 'string', description: 'Order identifier.' },
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
mockFiles[SCHEMA_PATH].push('event-components-demo');
|
|
326
|
+
mockFiles[nestedSchemaDir] = ['checkout-event.json'];
|
|
327
|
+
mockFileContents[nestedSchemaPath] = JSON.stringify(nestedSchema);
|
|
328
|
+
|
|
329
|
+
const bundledWebSchema = JSON.parse(JSON.stringify(complexEventSchema));
|
|
330
|
+
bundledWebSchema.properties.user_data.properties.addresses.items =
|
|
331
|
+
addressSchema;
|
|
332
|
+
|
|
333
|
+
RefParser.bundle.mockImplementation(async (filePath) => {
|
|
334
|
+
if (filePath.endsWith('complex-event.json')) {
|
|
335
|
+
return bundledWebSchema;
|
|
336
|
+
}
|
|
337
|
+
if (filePath.endsWith('mobile-event.json')) {
|
|
338
|
+
return mobileEventSchema;
|
|
339
|
+
}
|
|
340
|
+
if (filePath.endsWith('address.json')) {
|
|
341
|
+
return addressSchema;
|
|
342
|
+
}
|
|
343
|
+
if (filePath.endsWith('legacy-event.json')) {
|
|
344
|
+
return untaggedEventSchema;
|
|
345
|
+
}
|
|
346
|
+
if (filePath.endsWith('checkout-event.json')) {
|
|
347
|
+
return nestedSchema;
|
|
348
|
+
}
|
|
349
|
+
throw new Error(`Unexpected schema file: ${filePath}`);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const result = await gtmScript.getVariablesFromSchemas(SCHEMA_PATH, {});
|
|
353
|
+
const variableNames = result.map((variable) => variable.name);
|
|
354
|
+
|
|
355
|
+
expect(variableNames).toContain('order_id');
|
|
356
|
+
expect(RefParser.bundle).toHaveBeenCalledWith(nestedSchemaPath);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('should ignore component schemas even when scanning all json files', async () => {
|
|
360
|
+
const bundledWebSchema = JSON.parse(JSON.stringify(complexEventSchema));
|
|
361
|
+
bundledWebSchema.properties.user_data.properties.addresses.items =
|
|
362
|
+
addressSchema;
|
|
363
|
+
const nestedSchema = {
|
|
364
|
+
title: 'Checkout Event',
|
|
365
|
+
'x-tracking-targets': ['web-datalayer-js'],
|
|
366
|
+
type: 'object',
|
|
367
|
+
properties: {
|
|
368
|
+
event: { type: 'string', const: 'checkout' },
|
|
369
|
+
order_id: { type: 'string', description: 'Order identifier.' },
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
RefParser.bundle.mockImplementation(async (filePath) => {
|
|
374
|
+
if (filePath.endsWith('complex-event.json')) {
|
|
375
|
+
return bundledWebSchema;
|
|
376
|
+
}
|
|
377
|
+
if (filePath.endsWith('mobile-event.json')) {
|
|
378
|
+
return mobileEventSchema;
|
|
379
|
+
}
|
|
380
|
+
if (filePath.endsWith('address.json')) {
|
|
381
|
+
return addressSchema;
|
|
382
|
+
}
|
|
383
|
+
if (filePath.endsWith('legacy-event.json')) {
|
|
384
|
+
return untaggedEventSchema;
|
|
385
|
+
}
|
|
386
|
+
if (filePath.endsWith('checkout-event.json')) {
|
|
387
|
+
return nestedSchema;
|
|
388
|
+
}
|
|
389
|
+
throw new Error(`Unexpected schema file: ${filePath}`);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const result = await gtmScript.getVariablesFromSchemas(SCHEMA_PATH, {});
|
|
393
|
+
const variableNames = result.map((variable) => variable.name);
|
|
394
|
+
|
|
395
|
+
expect(RefParser.bundle).toHaveBeenCalledWith(
|
|
396
|
+
path.join(SCHEMA_PATH, 'components', 'address.json'),
|
|
397
|
+
);
|
|
398
|
+
expect(variableNames).not.toContain('street');
|
|
399
|
+
expect(variableNames).not.toContain('city');
|
|
176
400
|
});
|
|
177
401
|
});
|
|
178
402
|
|
|
@@ -102,4 +102,54 @@ describe('validateSchemas', () => {
|
|
|
102
102
|
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
103
103
|
expect(consoleErrorSpy.mock.calls[0][0]).toContain('x-tracking-targets');
|
|
104
104
|
});
|
|
105
|
+
|
|
106
|
+
it('should treat falsy scalar examples as valid examples', async () => {
|
|
107
|
+
const schemaDir = path.join(tmpDir, 'schemas');
|
|
108
|
+
fs.mkdirSync(schemaDir, { recursive: true });
|
|
109
|
+
fs.writeFileSync(
|
|
110
|
+
path.join(schemaDir, 'event.json'),
|
|
111
|
+
JSON.stringify({
|
|
112
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
113
|
+
type: 'object',
|
|
114
|
+
properties: {
|
|
115
|
+
event: {
|
|
116
|
+
type: 'string',
|
|
117
|
+
const: 'test_event',
|
|
118
|
+
},
|
|
119
|
+
is_enabled: {
|
|
120
|
+
type: 'boolean',
|
|
121
|
+
example: false,
|
|
122
|
+
},
|
|
123
|
+
retry_count: {
|
|
124
|
+
type: 'integer',
|
|
125
|
+
example: 0,
|
|
126
|
+
},
|
|
127
|
+
note: {
|
|
128
|
+
type: 'string',
|
|
129
|
+
example: '',
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
required: ['event', 'is_enabled', 'retry_count', 'note'],
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const result = await validateSchemas(schemaDir);
|
|
137
|
+
expect(result).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should allow a top-level falsy example value', async () => {
|
|
141
|
+
const schemaDir = path.join(tmpDir, 'schemas');
|
|
142
|
+
fs.mkdirSync(schemaDir, { recursive: true });
|
|
143
|
+
fs.writeFileSync(
|
|
144
|
+
path.join(schemaDir, 'flag.json'),
|
|
145
|
+
JSON.stringify({
|
|
146
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
147
|
+
type: 'boolean',
|
|
148
|
+
example: false,
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const result = await validateSchemas(schemaDir);
|
|
153
|
+
expect(result).toBe(true);
|
|
154
|
+
});
|
|
105
155
|
});
|
|
@@ -5,9 +5,39 @@ import WordWrapButton from './WordWrapButton';
|
|
|
5
5
|
import { schemaToTableData } from '../helpers/schemaToTableData';
|
|
6
6
|
import styles from './PropertiesTable.module.css';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
function filterInheritedTopLevelProperties(schema, sourceSchema) {
|
|
9
|
+
if (!schema?.properties || !sourceSchema?.properties) {
|
|
10
|
+
return schema;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const sourceKeys = new Set(Object.keys(sourceSchema.properties));
|
|
14
|
+
const filteredEntries = Object.entries(schema.properties).filter(
|
|
15
|
+
([key, propSchema]) => {
|
|
16
|
+
if (sourceKeys.has(key)) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const hasDescription =
|
|
21
|
+
typeof propSchema?.description === 'string' &&
|
|
22
|
+
propSchema.description.trim().length > 0;
|
|
23
|
+
const hasExamples =
|
|
24
|
+
Array.isArray(propSchema?.examples) && propSchema.examples.length > 0;
|
|
25
|
+
const hasExample = propSchema?.example !== undefined;
|
|
26
|
+
|
|
27
|
+
return hasDescription || hasExamples || hasExample;
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
...schema,
|
|
33
|
+
properties: Object.fromEntries(filteredEntries),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default function PropertiesTable({ schema, sourceSchema }) {
|
|
9
38
|
const [isWordWrapOn, setIsWordWrapOn] = useState(true);
|
|
10
|
-
const
|
|
39
|
+
const tableSchema = filterInheritedTopLevelProperties(schema, sourceSchema);
|
|
40
|
+
const tableData = schemaToTableData(tableSchema);
|
|
11
41
|
const stripeState = { current: 0 };
|
|
12
42
|
|
|
13
43
|
return (
|