docusaurus-plugin-generate-schema-docs 1.8.4 → 1.8.5

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 (39) hide show
  1. package/README.md +10 -0
  2. package/__tests__/__fixtures__/validateSchemas/schema-with-not-anyof-multi.json +12 -0
  3. package/__tests__/__fixtures__/validateSchemas/schema-with-not-anyof.json +30 -0
  4. package/__tests__/__fixtures__/validateSchemas/schema-with-not-edge-cases.json +24 -0
  5. package/__tests__/__fixtures__/validateSchemas/schema-with-not-non-object.json +15 -0
  6. package/__tests__/generateEventDocs.anchor.test.js +1 -1
  7. package/__tests__/generateEventDocs.nested.test.js +1 -1
  8. package/__tests__/generateEventDocs.partials.test.js +1 -1
  9. package/__tests__/generateEventDocs.test.js +506 -1
  10. package/__tests__/generateEventDocs.versioned.test.js +1 -1
  11. package/__tests__/helpers/buildExampleFromSchema.test.js +240 -0
  12. package/__tests__/helpers/constraintSchemaPaths.test.js +208 -0
  13. package/__tests__/helpers/continuingLinesStyle.test.js +492 -0
  14. package/__tests__/helpers/exampleModel.test.js +209 -0
  15. package/__tests__/helpers/file-system.test.js +73 -1
  16. package/__tests__/helpers/getConstraints.test.js +27 -0
  17. package/__tests__/helpers/mergeSchema.test.js +94 -0
  18. package/__tests__/helpers/processSchema.test.js +291 -1
  19. package/__tests__/helpers/schema-doc-template.test.js +54 -0
  20. package/__tests__/helpers/schema-processing.test.js +122 -2
  21. package/__tests__/helpers/schemaToExamples.test.js +1007 -0
  22. package/__tests__/helpers/schemaToTableData.mutations.test.js +970 -0
  23. package/__tests__/helpers/schemaToTableData.test.js +157 -0
  24. package/__tests__/helpers/snippetTargets.test.js +432 -0
  25. package/__tests__/helpers/trackingTargets.test.js +319 -0
  26. package/__tests__/helpers/validator.test.js +385 -1
  27. package/__tests__/index.test.js +436 -0
  28. package/__tests__/syncGtm.test.js +139 -3
  29. package/__tests__/update-schema-ids.test.js +70 -1
  30. package/__tests__/validateSchemas-integration.test.js +2 -2
  31. package/__tests__/validateSchemas.test.js +142 -1
  32. package/generateEventDocs.js +21 -1
  33. package/helpers/constraintSchemaPaths.js +10 -14
  34. package/helpers/schemaToTableData.js +538 -492
  35. package/helpers/trackingTargets.js +26 -3
  36. package/helpers/validator.js +18 -4
  37. package/index.js +1 -2
  38. package/package.json +1 -1
  39. package/scripts/sync-gtm.js +25 -7
@@ -132,4 +132,213 @@ describe('buildExampleModel', () => {
132
132
  'android-firebase-kotlin-sdk',
133
133
  ]);
134
134
  });
135
+
136
+ it('falls back to default target when x-tracking-targets is null (optional chain)', () => {
137
+ const schema = {
138
+ 'x-tracking-targets': null,
139
+ type: 'object',
140
+ properties: {
141
+ event: { type: 'string', examples: ['test_event'] },
142
+ },
143
+ };
144
+
145
+ const targets = resolveExampleTargets(schema);
146
+ expect(targets.map((t) => t.id)).toEqual([DEFAULT_SNIPPET_TARGET_ID]);
147
+ });
148
+
149
+ it('falls back to default target when x-tracking-targets is an empty array', () => {
150
+ const schema = {
151
+ 'x-tracking-targets': [],
152
+ type: 'object',
153
+ properties: {
154
+ event: { type: 'string', examples: ['test_event'] },
155
+ },
156
+ };
157
+
158
+ const targets = resolveExampleTargets(schema);
159
+ expect(targets.map((t) => t.id)).toEqual([DEFAULT_SNIPPET_TARGET_ID]);
160
+ expect(targets).toHaveLength(1);
161
+ });
162
+
163
+ it('filters out null targets and falls back to default when all target IDs are invalid', () => {
164
+ const schema = {
165
+ 'x-tracking-targets': ['nonexistent-target-id'],
166
+ type: 'object',
167
+ properties: {
168
+ event: { type: 'string', examples: ['test_event'] },
169
+ },
170
+ };
171
+
172
+ // All invalid IDs should be caught, filtered out, then fall back to default
173
+ const targets = resolveExampleTargets(schema);
174
+ expect(targets.map((t) => t.id)).toEqual([DEFAULT_SNIPPET_TARGET_ID]);
175
+ });
176
+
177
+ it('filters out null when one target ID is invalid but keeps valid ones', () => {
178
+ const schema = {
179
+ 'x-tracking-targets': [
180
+ 'web-datalayer-js',
181
+ 'nonexistent-target-id',
182
+ 'android-firebase-kotlin-sdk',
183
+ ],
184
+ type: 'object',
185
+ properties: {
186
+ event: { type: 'string', examples: ['test_event'] },
187
+ },
188
+ };
189
+
190
+ const targets = resolveExampleTargets(schema);
191
+ expect(targets.map((t) => t.id)).toEqual([
192
+ 'web-datalayer-js',
193
+ 'android-firebase-kotlin-sdk',
194
+ ]);
195
+ });
196
+
197
+ it('returns empty variantGroups and isSimpleDefault false when schema produces no examples', () => {
198
+ // A schema with type object but no properties and no examples yields no example groups
199
+ const schema = {
200
+ type: 'object',
201
+ };
202
+
203
+ const model = buildExampleModel(schema);
204
+ expect(model.variantGroups).toEqual([]);
205
+ expect(model.isSimpleDefault).toBe(false);
206
+ expect(model.targets).toHaveLength(1);
207
+ expect(model.targets[0].id).toBe(DEFAULT_SNIPPET_TARGET_ID);
208
+ });
209
+
210
+ it('sets isSimpleDefault to false when there are multiple variantGroups', () => {
211
+ const model = buildExampleModel(choiceEventSchema);
212
+ expect(model.isSimpleDefault).toBe(false);
213
+ expect(model.variantGroups.length).toBeGreaterThan(1);
214
+ });
215
+
216
+ it('sets isSimpleDefault to false when single variantGroup property is not "default"', () => {
217
+ // A schema with exactly one oneOf choice point produces a single variantGroup
218
+ // whose property is the field name (not 'default')
219
+ const schema = {
220
+ type: 'object',
221
+ properties: {
222
+ event: { type: 'string', examples: ['test_event'] },
223
+ category: {
224
+ oneOf: [
225
+ { title: 'Standard', const: 'standard' },
226
+ { title: 'Premium', const: 'premium' },
227
+ ],
228
+ },
229
+ },
230
+ };
231
+
232
+ const model = buildExampleModel(schema);
233
+ expect(model.variantGroups).toHaveLength(1);
234
+ expect(model.variantGroups[0].property).not.toBe('default');
235
+ expect(model.isSimpleDefault).toBe(false);
236
+ });
237
+
238
+ it('falls back to default target when schema is undefined (optional chaining guard)', () => {
239
+ const targets = resolveExampleTargets(undefined);
240
+ expect(targets).toHaveLength(1);
241
+ expect(targets[0].id).toBe(DEFAULT_SNIPPET_TARGET_ID);
242
+ });
243
+
244
+ it('falls back to default target when schema is null (optional chaining guard)', () => {
245
+ const targets = resolveExampleTargets(null);
246
+ expect(targets).toHaveLength(1);
247
+ expect(targets[0].id).toBe(DEFAULT_SNIPPET_TARGET_ID);
248
+ });
249
+
250
+ it('uses configured array content (not empty) when x-tracking-targets is absent', () => {
251
+ // Kills ArrayDeclaration mutant: [DEFAULT_SNIPPET_TARGET_ID] -> []
252
+ // If the fallback were [], targets would be empty, triggering the line-27 fallback
253
+ // but the target IDs would differ in the mapping step
254
+ const schema = { type: 'object' };
255
+ const targets = resolveExampleTargets(schema);
256
+ expect(targets).toHaveLength(1);
257
+ expect(targets[0].id).toBe(DEFAULT_SNIPPET_TARGET_ID);
258
+ });
259
+
260
+ it('treats non-array x-tracking-targets as absent and falls back to default', () => {
261
+ // Kills ConditionalExpression mutant on line 12: configured.length > 0 -> true
262
+ // When configured is a string (truthy but not an Array), Array.isArray is false
263
+ // so it should still fall back to default
264
+ const schema = {
265
+ 'x-tracking-targets': 'not-an-array',
266
+ type: 'object',
267
+ };
268
+ const targets = resolveExampleTargets(schema);
269
+ expect(targets).toHaveLength(1);
270
+ expect(targets[0].id).toBe(DEFAULT_SNIPPET_TARGET_ID);
271
+ });
272
+
273
+ it('generates option ids from property name and index', () => {
274
+ // Kills StringLiteral mutant: `${group.property}-${index}` -> ``
275
+ const schema = {
276
+ type: 'object',
277
+ properties: {
278
+ event: { type: 'string', examples: ['test_event'] },
279
+ },
280
+ };
281
+
282
+ const model = buildExampleModel(schema);
283
+ expect(model.variantGroups[0].options[0].id).toBe('default-0');
284
+ });
285
+
286
+ it('generates sequential option ids for choice schemas', () => {
287
+ // Further kills StringLiteral mutant by checking index > 0
288
+ const schema = {
289
+ type: 'object',
290
+ properties: {
291
+ event: { type: 'string', examples: ['test_event'] },
292
+ category: {
293
+ oneOf: [
294
+ { title: 'Standard', const: 'standard' },
295
+ { title: 'Premium', const: 'premium' },
296
+ ],
297
+ },
298
+ },
299
+ };
300
+
301
+ const model = buildExampleModel(schema);
302
+ const group = model.variantGroups[0];
303
+ expect(group.options[0].id).toBe(`${group.property}-0`);
304
+ expect(group.options[1].id).toBe(`${group.property}-1`);
305
+ });
306
+
307
+ it('sets isSimpleDefault to false for multiple groups even if first is "default"', () => {
308
+ // Kills ConditionalExpression mutant on line 66: variantGroups.length === 1 -> true
309
+ // choiceEventSchema produces multiple variant groups, so isSimpleDefault must be false
310
+ // even if the mutant forces length===1 to true
311
+ const model = buildExampleModel(choiceEventSchema);
312
+ expect(model.variantGroups.length).toBeGreaterThan(1);
313
+ expect(model.isSimpleDefault).toBe(false);
314
+ });
315
+
316
+ it('returns early return shape when schema has no properties', () => {
317
+ // Kills BlockStatement and condition mutants on line 34
318
+ // Verifies the exact shape of the early-return object
319
+ const schema = { type: 'object' };
320
+ const model = buildExampleModel(schema);
321
+ expect(model).toEqual({
322
+ targets: expect.any(Array),
323
+ variantGroups: [],
324
+ isSimpleDefault: false,
325
+ });
326
+ });
327
+
328
+ it('includes snippets keyed by each target id when multiple targets are configured', () => {
329
+ const schema = {
330
+ 'x-tracking-targets': ['web-datalayer-js', 'android-firebase-kotlin-sdk'],
331
+ type: 'object',
332
+ properties: {
333
+ event: { type: 'string', examples: ['test_event'] },
334
+ },
335
+ };
336
+
337
+ const model = buildExampleModel(schema);
338
+ const snippetKeys = Object.keys(model.variantGroups[0].options[0].snippets);
339
+ expect(snippetKeys).toEqual([
340
+ 'web-datalayer-js',
341
+ 'android-firebase-kotlin-sdk',
342
+ ]);
343
+ });
135
344
  });
@@ -1,6 +1,11 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { readSchemas, writeDoc, createDir } from '../../helpers/file-system';
3
+ import {
4
+ readSchemas,
5
+ writeDoc,
6
+ createDir,
7
+ readSchemaSources,
8
+ } from '../../helpers/file-system';
4
9
 
5
10
  jest.mock('fs', () => {
6
11
  const memfs = require('memfs');
@@ -24,6 +29,16 @@ describe('file-system helpers', () => {
24
29
  expect(schemas[0].fileName).toBe('schema1.json');
25
30
  expect(schemas[1].fileName).toBe('schema2.json');
26
31
  });
32
+
33
+ it('should parse schema content as JSON', () => {
34
+ // L17: StringLiteral → "" — encoding must be 'utf-8' to get string
35
+ fs.vol.reset();
36
+ fs.vol.fromJSON({
37
+ '/s/test.json': '{"id":"test-schema"}',
38
+ });
39
+ const schemas = readSchemas('/s');
40
+ expect(schemas[0].schema).toEqual({ id: 'test-schema' });
41
+ });
27
42
  });
28
43
 
29
44
  describe('writeDoc', () => {
@@ -33,6 +48,15 @@ describe('file-system helpers', () => {
33
48
  const content = fs.readFileSync('/output/doc1.mdx', 'utf-8');
34
49
  expect(content).toBe('content');
35
50
  });
51
+
52
+ it('should log the generated file path', () => {
53
+ // L57: StringLiteral → `` — empty template literal
54
+ fs.mkdirSync('/out', { recursive: true });
55
+ const spy = jest.spyOn(console, 'log').mockImplementation(() => {});
56
+ writeDoc('/out', 'doc.mdx', 'hello');
57
+ expect(spy).toHaveBeenCalledWith(expect.stringContaining('Generated'));
58
+ spy.mockRestore();
59
+ });
36
60
  });
37
61
 
38
62
  describe('createDir', () => {
@@ -40,5 +64,53 @@ describe('file-system helpers', () => {
40
64
  createDir('/new-dir');
41
65
  expect(fs.existsSync('/new-dir')).toBe(true);
42
66
  });
67
+
68
+ it('should not call mkdirSync when directory already exists', () => {
69
+ // L5: ConditionalExpression → true — mutant always calls mkdirSync
70
+ fs.mkdirSync('/existing-dir', { recursive: true });
71
+ const spy = jest.spyOn(fs, 'mkdirSync');
72
+ createDir('/existing-dir');
73
+ expect(spy).not.toHaveBeenCalled();
74
+ spy.mockRestore();
75
+ });
76
+
77
+ it('should create nested directories recursively', () => {
78
+ // L6: ObjectLiteral → {} and BooleanLiteral → false
79
+ // Without recursive: true, creating nested dirs would fail
80
+ createDir('/a/b/c/d');
81
+ expect(fs.existsSync('/a/b/c/d')).toBe(true);
82
+ });
83
+ });
84
+
85
+ describe('readSchemaSources', () => {
86
+ it('should read json files recursively and key by relative path', () => {
87
+ // L39: ConditionalExpression → false and StringLiteral → ""
88
+ // L45: StringLiteral → "" (encoding)
89
+ fs.vol.reset();
90
+ fs.vol.fromJSON({
91
+ '/root/a.json': '{"name":"a"}',
92
+ '/root/sub/b.json': '{"name":"b"}',
93
+ '/root/skip.txt': 'not json',
94
+ });
95
+ const result = readSchemaSources('/root');
96
+ expect(Object.keys(result)).toEqual(
97
+ expect.arrayContaining(['a.json', 'sub/b.json']),
98
+ );
99
+ expect(Object.keys(result)).not.toContain('skip.txt');
100
+ expect(result['a.json']).toEqual({ name: 'a' });
101
+ expect(result['sub/b.json']).toEqual({ name: 'b' });
102
+ });
103
+
104
+ it('should only include .json files, not other extensions', () => {
105
+ // L39: StringLiteral → "" would match all files including .txt
106
+ fs.vol.reset();
107
+ fs.vol.fromJSON({
108
+ '/root/valid.json': '{}',
109
+ '/root/readme.txt': 'text',
110
+ '/root/data.csv': 'csv',
111
+ });
112
+ const result = readSchemaSources('/root');
113
+ expect(Object.keys(result)).toEqual(['valid.json']);
114
+ });
43
115
  });
44
116
  });
@@ -116,4 +116,31 @@ describe('getConstraints', () => {
116
116
  };
117
117
  expect(getConstraints(prop, false)).toEqual(['not: { type: "string" }']);
118
118
  });
119
+
120
+ it('should not include uniqueItems constraint when value is false', () => {
121
+ const prop = { uniqueItems: false };
122
+ expect(getConstraints(prop, false)).toEqual([]);
123
+ });
124
+
125
+ it('should not include additionalProperties constraint when value is true', () => {
126
+ const prop = { additionalProperties: true };
127
+ expect(getConstraints(prop, false)).toEqual([]);
128
+ });
129
+
130
+ it('should render "not" constraint with nested array values', () => {
131
+ const prop = {
132
+ not: { enum: ['a', 'b'] },
133
+ };
134
+ expect(getConstraints(prop, false)).toEqual(['not: { enum: ["a", "b"] }']);
135
+ });
136
+
137
+ it('should render "not" constraint with deeply nested array of objects', () => {
138
+ const prop = {
139
+ not: { items: [{ type: 'string' }, { type: 'number' }] },
140
+ };
141
+ const result = getConstraints(prop, false);
142
+ expect(result).toEqual([
143
+ 'not: { items: [{ type: "string" }, { type: "number" }] }',
144
+ ]);
145
+ });
119
146
  });
@@ -0,0 +1,94 @@
1
+ import { mergeSchema } from '../../helpers/mergeSchema';
2
+
3
+ describe('mergeSchema', () => {
4
+ it('merges allOf into a single schema', () => {
5
+ const result = mergeSchema({
6
+ allOf: [
7
+ { type: 'object', properties: { a: { type: 'string' } } },
8
+ { type: 'object', properties: { b: { type: 'number' } } },
9
+ ],
10
+ });
11
+
12
+ expect(result.properties).toEqual({
13
+ a: { type: 'string' },
14
+ b: { type: 'number' },
15
+ });
16
+ });
17
+
18
+ it('preserves $defs via the baseResolvers', () => {
19
+ const result = mergeSchema({
20
+ allOf: [
21
+ {
22
+ type: 'object',
23
+ $defs: { shared: { type: 'string' } },
24
+ properties: { a: { type: 'string' } },
25
+ },
26
+ {
27
+ type: 'object',
28
+ $defs: { other: { type: 'number' } },
29
+ properties: { b: { type: 'number' } },
30
+ },
31
+ ],
32
+ });
33
+
34
+ expect(result.$defs).toEqual({
35
+ shared: { type: 'string' },
36
+ other: { type: 'number' },
37
+ });
38
+ });
39
+
40
+ it('passes custom resolvers through to json-schema-merge-allof', () => {
41
+ const customTitleResolver = (values) => values.join(' + ');
42
+
43
+ const result = mergeSchema(
44
+ {
45
+ allOf: [
46
+ {
47
+ type: 'object',
48
+ title: 'Foo',
49
+ properties: { a: { type: 'string' } },
50
+ },
51
+ {
52
+ type: 'object',
53
+ title: 'Bar',
54
+ properties: { b: { type: 'number' } },
55
+ },
56
+ ],
57
+ },
58
+ { resolvers: { title: customTitleResolver } },
59
+ );
60
+
61
+ expect(result.title).toBe('Foo + Bar');
62
+ });
63
+
64
+ it('works without custom options', () => {
65
+ const result = mergeSchema({
66
+ allOf: [{ type: 'object', properties: { x: { type: 'boolean' } } }],
67
+ });
68
+
69
+ expect(result.properties.x).toEqual({ type: 'boolean' });
70
+ });
71
+
72
+ it('merges additional options beyond resolvers', () => {
73
+ // ignoreAdditionalProperties is a real json-schema-merge-allof option
74
+ const result = mergeSchema(
75
+ {
76
+ allOf: [
77
+ {
78
+ type: 'object',
79
+ properties: { a: { type: 'string' } },
80
+ additionalProperties: false,
81
+ },
82
+ {
83
+ type: 'object',
84
+ properties: { b: { type: 'number' } },
85
+ },
86
+ ],
87
+ },
88
+ { ignoreAdditionalProperties: true },
89
+ );
90
+
91
+ expect(result.properties.a).toEqual({ type: 'string' });
92
+ expect(result.properties.b).toEqual({ type: 'number' });
93
+ });
94
+ });