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.
Files changed (29) hide show
  1. package/README.md +22 -0
  2. package/__tests__/ExampleDataLayer.test.js +149 -2
  3. package/__tests__/__fixtures__/schema-processing/components/dataLayer.json +9 -0
  4. package/__tests__/__fixtures__/schema-processing/event-reference.json +14 -0
  5. package/__tests__/__fixtures__/schema-processing/purchase-event.json +14 -0
  6. package/__tests__/components/PropertyRow.test.js +30 -0
  7. package/__tests__/components/__snapshots__/ConnectorLines.visualRegression.test.js.snap +3 -3
  8. package/__tests__/helpers/exampleModel.test.js +135 -0
  9. package/__tests__/helpers/schema-processing.test.js +56 -0
  10. package/__tests__/helpers/schemaToTableData.test.js +41 -0
  11. package/__tests__/helpers/snippetTargets.test.js +744 -0
  12. package/__tests__/helpers/trackingTargets.test.js +42 -0
  13. package/__tests__/runtimePayload.android.test.js +292 -0
  14. package/__tests__/runtimePayload.ios.test.js +282 -0
  15. package/__tests__/runtimePayload.web.test.js +32 -0
  16. package/__tests__/validateSchemas.test.js +53 -1
  17. package/components/ExampleDataLayer.js +191 -56
  18. package/components/PropertyRow.js +3 -2
  19. package/components/SchemaRows.css +10 -1
  20. package/helpers/exampleModel.js +70 -0
  21. package/helpers/schema-processing.js +41 -5
  22. package/helpers/schemaToExamples.js +52 -2
  23. package/helpers/schemaToTableData.js +40 -2
  24. package/helpers/snippetTargets.js +853 -0
  25. package/helpers/trackingTargets.js +73 -0
  26. package/helpers/validator.js +1 -0
  27. package/package.json +1 -1
  28. package/test-data/payloadContracts.js +155 -0
  29. package/validateSchemas.js +15 -0
package/README.md CHANGED
@@ -90,6 +90,28 @@ docusaurus sync-gtm
90
90
 
91
91
  By default, it resolves schemas from the project root. Use `--path=<siteDir>` to target a different site directory.
92
92
 
93
+ ### Firebase Snippet Targets
94
+
95
+ `ExampleDataLayer` supports Firebase snippet targets for:
96
+
97
+ - `android-firebase-kotlin-sdk`
98
+ - `android-firebase-java-sdk`
99
+ - `ios-firebase-swift-sdk`
100
+ - `ios-firebase-objc-sdk`
101
+
102
+ Mapping rules for generated parameters:
103
+
104
+ - `string` -> string parameter
105
+ - `integer`/`boolean` -> integer/long parameter (`true` = `1`, `false` = `0`)
106
+ - `number` -> double parameter
107
+ - `items` -> non-empty array of flat item objects
108
+ - unsupported nested values cause a generation error (no automatic flattening or JSON-string fallback)
109
+
110
+ Reference docs used for syntax and kept as source of truth:
111
+
112
+ - https://firebase.google.com/docs/analytics/android/events
113
+ - https://firebase.google.com/docs/analytics/ios/events
114
+
93
115
  ## How it Works
94
116
 
95
117
  The plugin reads your JSON schemas, dereferences any `$ref` properties, and merges `allOf` properties. It then generates an MDX file for each schema, which uses custom React components to render the schema details.
@@ -3,6 +3,10 @@ import { render, screen } from '@testing-library/react';
3
3
  import '@testing-library/jest-dom';
4
4
  import ExampleDataLayer, {
5
5
  findClearableProperties,
6
+ readHashTarget,
7
+ readSearchTarget,
8
+ resolveInitialTargetId,
9
+ TARGET_STORAGE_KEY,
6
10
  } from '../components/ExampleDataLayer';
7
11
  import choiceEventSchema from './__fixtures__/static/schemas/choice-event.json';
8
12
 
@@ -14,8 +18,20 @@ jest.mock('@theme/CodeBlock', () => {
14
18
  });
15
19
 
16
20
  jest.mock('@theme/Tabs', () => {
17
- return function Tabs({ children }) {
18
- return <div data-testid="tabs">{children}</div>;
21
+ return function Tabs({ children, queryString, defaultValue }) {
22
+ const attrs = {};
23
+ if (queryString !== undefined) {
24
+ attrs['data-query-string'] = queryString;
25
+ }
26
+ if (defaultValue !== undefined) {
27
+ attrs['data-default-value'] = defaultValue;
28
+ }
29
+
30
+ return (
31
+ <div data-testid="tabs" {...attrs}>
32
+ {children}
33
+ </div>
34
+ );
19
35
  };
20
36
  });
21
37
 
@@ -30,6 +46,11 @@ jest.mock('@theme/TabItem', () => {
30
46
  });
31
47
 
32
48
  describe('ExampleDataLayer', () => {
49
+ afterEach(() => {
50
+ window.location.hash = '';
51
+ window.localStorage.clear();
52
+ });
53
+
33
54
  it('should render a single example for a simple schema', () => {
34
55
  const schema = {
35
56
  type: 'object',
@@ -81,6 +102,59 @@ describe('ExampleDataLayer', () => {
81
102
  );
82
103
  expect(getByText(/window.customDataLayer.push/)).toBeInTheDocument();
83
104
  });
105
+
106
+ it('should render target tabs when multiple targets are configured', () => {
107
+ const schema = {
108
+ type: 'object',
109
+ 'x-tracking-targets': ['web-datalayer-js', 'android-firebase-kotlin-sdk'],
110
+ properties: {
111
+ event: { type: 'string', examples: ['test_event'] },
112
+ },
113
+ };
114
+
115
+ const { getByTestId, getAllByTestId } = render(
116
+ <ExampleDataLayer schema={schema} />,
117
+ );
118
+ expect(getByTestId('target-tabs')).toBeInTheDocument();
119
+ const tabLabels = getAllByTestId('tab-item').map((item) =>
120
+ item.getAttribute('data-label'),
121
+ );
122
+ expect(tabLabels).toEqual(
123
+ expect.arrayContaining([
124
+ 'Web Data Layer (JS)',
125
+ 'Android Firebase (Kotlin)',
126
+ ]),
127
+ );
128
+ expect(getByTestId('tabs')).toHaveAttribute('data-query-string', 'target');
129
+ });
130
+
131
+ it('uses per-target syntax highlighting language', () => {
132
+ const schema = {
133
+ type: 'object',
134
+ 'x-tracking-targets': ['android-firebase-kotlin-sdk'],
135
+ properties: {
136
+ event: { type: 'string', examples: ['purchase'] },
137
+ value: { type: 'number', examples: [14.98] },
138
+ },
139
+ };
140
+
141
+ const { container } = render(<ExampleDataLayer schema={schema} />);
142
+ const codeBlocks = container.querySelectorAll('pre[data-language]');
143
+ expect(codeBlocks.length).toBeGreaterThan(0);
144
+ expect(codeBlocks[0]).toHaveAttribute('data-language', 'kotlin');
145
+ });
146
+
147
+ it('should not render target tabs for single-target schemas', () => {
148
+ const schema = {
149
+ type: 'object',
150
+ properties: {
151
+ event: { type: 'string', examples: ['test_event'] },
152
+ },
153
+ };
154
+
155
+ const { queryByTestId } = render(<ExampleDataLayer schema={schema} />);
156
+ expect(queryByTestId('target-tabs')).toBeNull();
157
+ });
84
158
  });
85
159
 
86
160
  describe('findClearableProperties', () => {
@@ -103,3 +177,76 @@ describe('findClearableProperties', () => {
103
177
  expect(findClearableProperties(schema)).toEqual(['prop2', 'prop4']);
104
178
  });
105
179
  });
180
+
181
+ describe('target selection helpers', () => {
182
+ afterEach(() => {
183
+ window.location.hash = '';
184
+ window.localStorage.clear();
185
+ });
186
+
187
+ it('reads target from hash', () => {
188
+ window.location.hash = '#target-android-firebase-java-sdk';
189
+ expect(readHashTarget()).toBe('android-firebase-java-sdk');
190
+ });
191
+
192
+ it('reads target from search', () => {
193
+ expect(readSearchTarget('?target=android-firebase-java-sdk')).toBe(
194
+ 'android-firebase-java-sdk',
195
+ );
196
+ });
197
+
198
+ it('persistTarget writes hash without duplicate query target', () => {
199
+ const originalReplaceState = window.history.replaceState;
200
+ const replaceSpy = jest.fn();
201
+ window.history.replaceState = replaceSpy;
202
+ window.history.pushState(
203
+ {},
204
+ '',
205
+ '/next/event-reference/purchase-event?target=android-firebase-kotlin-sdk',
206
+ );
207
+
208
+ const schema = {
209
+ type: 'object',
210
+ 'x-tracking-targets': ['web-datalayer-js', 'android-firebase-kotlin-sdk'],
211
+ properties: {
212
+ event: { type: 'string', examples: ['purchase'] },
213
+ },
214
+ };
215
+
216
+ render(<ExampleDataLayer schema={schema} />);
217
+ expect(replaceSpy).toHaveBeenCalled();
218
+ const lastCallUrl =
219
+ replaceSpy.mock.calls[replaceSpy.mock.calls.length - 1][2];
220
+ expect(lastCallUrl).toContain('#target-android-firebase-kotlin-sdk');
221
+ expect(lastCallUrl).not.toContain('?target=android-firebase-kotlin-sdk');
222
+
223
+ window.history.replaceState = originalReplaceState;
224
+ });
225
+
226
+ it('prefers hash over localStorage for initial target resolution', () => {
227
+ const targets = [
228
+ { id: 'web-datalayer-js' },
229
+ { id: 'android-firebase-java-sdk' },
230
+ ];
231
+ window.localStorage.setItem(TARGET_STORAGE_KEY, 'web-datalayer-js');
232
+ window.location.hash = '#target-android-firebase-java-sdk';
233
+
234
+ expect(resolveInitialTargetId(targets)).toBe('android-firebase-java-sdk');
235
+ });
236
+
237
+ it('falls back to first target when hash is unknown', () => {
238
+ const targets = [
239
+ { id: 'web-datalayer-js' },
240
+ { id: 'android-firebase-java-sdk' },
241
+ ];
242
+ window.location.hash = '#target-unknown';
243
+
244
+ expect(resolveInitialTargetId(targets)).toBe('web-datalayer-js');
245
+ });
246
+
247
+ it('ignores malformed targets and still resolves a valid initial target', () => {
248
+ const targets = [undefined, {}, { id: 'android-firebase-kotlin-sdk' }];
249
+ window.location.hash = '#target-android-firebase-kotlin-sdk';
250
+ expect(resolveInitialTargetId(targets)).toBe('android-firebase-kotlin-sdk');
251
+ });
252
+ });
@@ -0,0 +1,9 @@
1
+ {
2
+ "type": "object",
3
+ "properties": {
4
+ "ecommerce": {
5
+ "type": "object",
6
+ "x-gtm-clear": true
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "title": "Event Reference",
3
+ "type": "object",
4
+ "allOf": [
5
+ {
6
+ "$ref": "./components/dataLayer.json"
7
+ }
8
+ ],
9
+ "oneOf": [
10
+ {
11
+ "$ref": "./purchase-event.json"
12
+ }
13
+ ]
14
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "title": "Purchase",
3
+ "type": "object",
4
+ "properties": {
5
+ "ecommerce": {
6
+ "type": "object",
7
+ "properties": {
8
+ "transaction_id": {
9
+ "type": "string"
10
+ }
11
+ }
12
+ }
13
+ }
14
+ }
@@ -79,6 +79,36 @@ describe('PropertyRow', () => {
79
79
  expect(getByText('maxLength: 10')).toBeInTheDocument();
80
80
  });
81
81
 
82
+ it('marks only the property column cell with property-cell class', () => {
83
+ const row = {
84
+ name: 'name',
85
+ level: 0,
86
+ required: true,
87
+ propertyType: 'string',
88
+ description: '',
89
+ example: '',
90
+ constraints: ['required', 'minLength: 1'],
91
+ path: ['name'],
92
+ };
93
+
94
+ const { container } = render(
95
+ <table>
96
+ <tbody>
97
+ <PropertyRow row={row} />
98
+ </tbody>
99
+ </table>,
100
+ );
101
+
102
+ const propertyCell = container.querySelector('td.property-cell');
103
+ expect(propertyCell).toBeInTheDocument();
104
+
105
+ const subsequentConstraintRows = container.querySelectorAll('tbody tr');
106
+ expect(subsequentConstraintRows.length).toBeGreaterThan(1);
107
+ expect(
108
+ subsequentConstraintRows[1].querySelector('td.property-cell'),
109
+ ).not.toBeInTheDocument();
110
+ });
111
+
82
112
  it('renders an example', () => {
83
113
  const row = {
84
114
  name: 'name',
@@ -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="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 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="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 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="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 level-1"><span class="property-name"><strong>wallet_provider</strong></span></td>"`;
@@ -0,0 +1,135 @@
1
+ import {
2
+ buildExampleModel,
3
+ resolveExampleTargets,
4
+ } from '../../helpers/exampleModel';
5
+ import { DEFAULT_SNIPPET_TARGET_ID } from '../../helpers/snippetTargets';
6
+ import choiceEventSchema from '../__fixtures__/static/schemas/choice-event.json';
7
+ import conditionalEventSchema from '../__fixtures__/static/schemas/conditional-event.json';
8
+
9
+ describe('buildExampleModel', () => {
10
+ it('builds a default model for simple schema', () => {
11
+ const schema = {
12
+ type: 'object',
13
+ properties: {
14
+ event: { type: 'string', examples: ['test_event'] },
15
+ },
16
+ };
17
+
18
+ const model = buildExampleModel(schema);
19
+ expect(model.targets.map((t) => t.id)).toEqual([DEFAULT_SNIPPET_TARGET_ID]);
20
+ expect(model.isSimpleDefault).toBe(true);
21
+ expect(model.variantGroups).toHaveLength(1);
22
+ expect(model.variantGroups[0].property).toBe('default');
23
+ expect(
24
+ model.variantGroups[0].options[0].snippets[DEFAULT_SNIPPET_TARGET_ID],
25
+ ).toContain('window.dataLayer.push');
26
+ });
27
+
28
+ it('builds grouped variants for choice schemas', () => {
29
+ const model = buildExampleModel(choiceEventSchema);
30
+ const groupProperties = model.variantGroups.map((group) => group.property);
31
+
32
+ expect(model.isSimpleDefault).toBe(false);
33
+ expect(groupProperties).toEqual(
34
+ expect.arrayContaining(['user_id', 'payment_method']),
35
+ );
36
+ expect(
37
+ model.variantGroups[0].options[0].snippets[DEFAULT_SNIPPET_TARGET_ID],
38
+ ).toContain('window.dataLayer.push');
39
+ });
40
+
41
+ it('builds conditional variants for if-then-else schemas', () => {
42
+ const model = buildExampleModel(conditionalEventSchema);
43
+ const conditionalGroup = model.variantGroups.find(
44
+ (group) => group.property === 'conditional',
45
+ );
46
+
47
+ expect(conditionalGroup).toBeDefined();
48
+ expect(conditionalGroup.options).toHaveLength(2);
49
+ expect(conditionalGroup.options.map((o) => o.title)).toEqual(
50
+ expect.arrayContaining([
51
+ 'When condition is met',
52
+ 'When condition is not met',
53
+ ]),
54
+ );
55
+ });
56
+
57
+ it('builds distinct conditional examples for required-only branches', () => {
58
+ const model = buildExampleModel({
59
+ type: 'object',
60
+ properties: {
61
+ platform: {
62
+ type: 'string',
63
+ examples: ['ios'],
64
+ },
65
+ att_status: {
66
+ type: 'string',
67
+ examples: ['authorized'],
68
+ },
69
+ ad_personalization_enabled: {
70
+ type: 'boolean',
71
+ examples: [true],
72
+ },
73
+ },
74
+ if: {
75
+ properties: {
76
+ platform: {
77
+ const: 'ios',
78
+ },
79
+ },
80
+ required: ['platform'],
81
+ },
82
+ then: {
83
+ required: ['att_status'],
84
+ },
85
+ else: {
86
+ required: ['ad_personalization_enabled'],
87
+ },
88
+ });
89
+ const conditionalGroup = model.variantGroups.find(
90
+ (group) => group.property === 'conditional',
91
+ );
92
+
93
+ expect(conditionalGroup).toBeDefined();
94
+ expect(conditionalGroup.options[0].example).toEqual({
95
+ platform: 'ios',
96
+ att_status: 'authorized',
97
+ });
98
+ expect(conditionalGroup.options[1].example).toEqual({
99
+ platform: 'ios',
100
+ ad_personalization_enabled: true,
101
+ });
102
+ });
103
+
104
+ it('uses configured dataLayerName in snippets', () => {
105
+ const schema = {
106
+ type: 'object',
107
+ properties: {
108
+ event: { type: 'string', examples: ['test_event'] },
109
+ },
110
+ };
111
+
112
+ const model = buildExampleModel(schema, {
113
+ dataLayerName: 'customDataLayer',
114
+ });
115
+ expect(
116
+ model.variantGroups[0].options[0].snippets[DEFAULT_SNIPPET_TARGET_ID],
117
+ ).toContain('window.customDataLayer.push');
118
+ });
119
+
120
+ it('resolves multiple targets when x-tracking-targets is provided', () => {
121
+ const schema = {
122
+ 'x-tracking-targets': ['web-datalayer-js', 'android-firebase-kotlin-sdk'],
123
+ type: 'object',
124
+ properties: {
125
+ event: { type: 'string', examples: ['test_event'] },
126
+ },
127
+ };
128
+
129
+ const targets = resolveExampleTargets(schema);
130
+ expect(targets.map((target) => target.id)).toEqual([
131
+ 'web-datalayer-js',
132
+ 'android-firebase-kotlin-sdk',
133
+ ]);
134
+ });
135
+ });
@@ -1,5 +1,10 @@
1
+ /**
2
+ * @jest-environment node
3
+ */
4
+
1
5
  import { processOneOfSchema } from '../../helpers/schema-processing';
2
6
  import path from 'path';
7
+ import fs from 'fs';
3
8
 
4
9
  describe('schema-processing', () => {
5
10
  describe('processOneOfSchema', () => {
@@ -78,5 +83,56 @@ describe('schema-processing', () => {
78
83
  const result = await processOneOfSchema(rootSchema, filePath);
79
84
  expect(result[0].schema.$id).toBe('root.json#option-1');
80
85
  });
86
+
87
+ it('preserves parent property metadata when an option refines the same property', async () => {
88
+ const rootSchema = {
89
+ $id: 'root.json',
90
+ title: 'Root',
91
+ oneOf: [
92
+ {
93
+ title: 'Purchase',
94
+ properties: {
95
+ ecommerce: {
96
+ type: 'object',
97
+ properties: {
98
+ transaction_id: { type: 'string' },
99
+ },
100
+ },
101
+ },
102
+ },
103
+ ],
104
+ properties: {
105
+ ecommerce: {
106
+ type: 'object',
107
+ 'x-gtm-clear': true,
108
+ },
109
+ },
110
+ };
111
+
112
+ const result = await processOneOfSchema(
113
+ rootSchema,
114
+ '/path/to/schema.json',
115
+ );
116
+ const mergedEcommerce = result[0].schema.properties.ecommerce;
117
+
118
+ expect(mergedEcommerce['x-gtm-clear']).toBe(true);
119
+ expect(mergedEcommerce.properties.transaction_id.type).toBe('string');
120
+ });
121
+
122
+ it('preserves parent allOf metadata when root oneOf options are file refs', async () => {
123
+ const rootFile = path.join(
124
+ __dirname,
125
+ '..',
126
+ '__fixtures__',
127
+ 'schema-processing',
128
+ 'event-reference.json',
129
+ );
130
+ const rootSchema = JSON.parse(fs.readFileSync(rootFile, 'utf8'));
131
+ const result = await processOneOfSchema(rootSchema, rootFile);
132
+ const mergedEcommerce = result[0].schema.properties.ecommerce;
133
+
134
+ expect(mergedEcommerce['x-gtm-clear']).toBe(true);
135
+ expect(mergedEcommerce.properties.transaction_id.type).toBe('string');
136
+ });
81
137
  });
82
138
  });
@@ -278,6 +278,47 @@ describe('schemaToTableData', () => {
278
278
  expect(conditionalRow.branches[0].title).toBe('Then');
279
279
  });
280
280
 
281
+ it('materializes required-only branch rows from parent properties', () => {
282
+ const schema = {
283
+ type: 'object',
284
+ properties: {
285
+ platform: { type: 'string' },
286
+ att_status: { type: 'string' },
287
+ ad_personalization_enabled: { type: 'boolean' },
288
+ },
289
+ required: ['platform'],
290
+ if: {
291
+ properties: {
292
+ platform: { const: 'ios' },
293
+ },
294
+ required: ['platform'],
295
+ },
296
+ then: {
297
+ required: ['att_status'],
298
+ },
299
+ else: {
300
+ required: ['ad_personalization_enabled'],
301
+ },
302
+ };
303
+
304
+ const tableData = schemaToTableData(schema);
305
+ const conditionalRow = tableData.find((r) => r.type === 'conditional');
306
+ const thenBranch = conditionalRow.branches.find(
307
+ (b) => b.title === 'Then',
308
+ );
309
+ const elseBranch = conditionalRow.branches.find(
310
+ (b) => b.title === 'Else',
311
+ );
312
+
313
+ expect(thenBranch.rows).toHaveLength(1);
314
+ expect(thenBranch.rows[0].name).toBe('att_status');
315
+ expect(thenBranch.rows[0].required).toBe(true);
316
+
317
+ expect(elseBranch.rows).toHaveLength(1);
318
+ expect(elseBranch.rows[0].name).toBe('ad_personalization_enabled');
319
+ expect(elseBranch.rows[0].required).toBe(true);
320
+ });
321
+
281
322
  it('renders regular properties alongside conditional rows', () => {
282
323
  const tableData = schemaToTableData(conditionalEventSchema);
283
324
  const propRows = tableData.filter((r) => r.type === 'property');