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.
Files changed (30) hide show
  1. package/README.md +2 -0
  2. package/__tests__/__fixtures__/validateSchemas/main-schema-with-not-allof.json +11 -0
  3. package/__tests__/__snapshots__/generateEventDocs.anchor.test.js.snap +6 -0
  4. package/__tests__/__snapshots__/generateEventDocs.nested.test.js.snap +6 -0
  5. package/__tests__/__snapshots__/generateEventDocs.test.js.snap +15 -0
  6. package/__tests__/__snapshots__/generateEventDocs.versioned.test.js.snap +6 -0
  7. package/__tests__/components/PropertiesTable.test.js +66 -0
  8. package/__tests__/components/PropertyRow.test.js +85 -4
  9. package/__tests__/components/SchemaJsonViewer.test.js +118 -0
  10. package/__tests__/helpers/example-helper.test.js +12 -0
  11. package/__tests__/helpers/getConstraints.test.js +16 -0
  12. package/__tests__/helpers/processSchema.test.js +18 -0
  13. package/__tests__/helpers/schemaTraversal.test.js +110 -0
  14. package/__tests__/syncGtm.test.js +227 -3
  15. package/__tests__/validateSchemas.test.js +50 -0
  16. package/components/PropertiesTable.js +32 -2
  17. package/components/PropertyRow.js +29 -2
  18. package/components/SchemaJsonViewer.js +234 -131
  19. package/components/SchemaRows.css +40 -0
  20. package/components/SchemaViewer.js +11 -2
  21. package/helpers/example-helper.js +2 -2
  22. package/helpers/getConstraints.js +20 -0
  23. package/helpers/processSchema.js +32 -1
  24. package/helpers/schema-doc-template.js +4 -0
  25. package/helpers/schemaToExamples.js +29 -35
  26. package/helpers/schemaTraversal.cjs +148 -0
  27. package/package.json +1 -1
  28. package/scripts/sync-gtm.js +41 -28
  29. package/test-data/payloadContracts.js +35 -0
  30. 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(8);
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(expected.length);
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
- export default function PropertiesTable({ schema }) {
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 tableData = schemaToTableData(schema);
39
+ const tableSchema = filterInheritedTopLevelProperties(schema, sourceSchema);
40
+ const tableData = schemaToTableData(tableSchema);
11
41
  const stripeState = { current: 0 };
12
42
 
13
43
  return (