docusaurus-plugin-generate-schema-docs 1.4.0 → 1.5.0

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.
@@ -0,0 +1,115 @@
1
+ import { createValidator } from '../../helpers/validator';
2
+
3
+ describe('createValidator', () => {
4
+ it('creates a validator that returns true for valid data with no schema version (draft-07)', async () => {
5
+ const schema = {
6
+ type: 'object',
7
+ properties: {
8
+ name: {
9
+ type: 'string',
10
+ },
11
+ },
12
+ required: ['name'],
13
+ };
14
+
15
+ const validator = await createValidator(schema);
16
+ const result = validator({ name: 'test' });
17
+
18
+ expect(result.valid).toBe(true);
19
+ expect(result.errors).toEqual([]);
20
+ });
21
+
22
+ it('creates a validator that returns false for invalid data with no schema version (draft-07)', async () => {
23
+ const schema = {
24
+ type: 'object',
25
+ properties: {
26
+ name: {
27
+ type: 'string',
28
+ },
29
+ },
30
+ required: ['name'],
31
+ };
32
+
33
+ const validator = await createValidator(schema);
34
+ const result = validator({ name: 123 });
35
+
36
+ expect(result.valid).toBe(false);
37
+ expect(result.errors).not.toEqual([]);
38
+ });
39
+
40
+ it('creates a validator that can validate against a draft-04 schema', async () => {
41
+ const schema = {
42
+ $schema: 'http://json-schema.org/draft-04/schema#',
43
+ type: 'object',
44
+ properties: {
45
+ name: {
46
+ type: 'string',
47
+ },
48
+ },
49
+ required: ['name'],
50
+ };
51
+
52
+ const validator = await createValidator(schema);
53
+ const result = validator({ name: 'test' });
54
+
55
+ expect(result.valid).toBe(true);
56
+ expect(result.errors).toEqual([]);
57
+ });
58
+
59
+ it('creates a validator that can validate against a draft-07 schema', async () => {
60
+ const schema = {
61
+ $schema: 'http://json-schema.org/draft-07/schema#',
62
+ type: 'object',
63
+ properties: {
64
+ name: {
65
+ type: 'string',
66
+ },
67
+ },
68
+ required: ['name'],
69
+ };
70
+
71
+ const validator = await createValidator(schema);
72
+ const result = validator({ name: 'test' });
73
+
74
+ expect(result.valid).toBe(true);
75
+ expect(result.errors).toEqual([]);
76
+ });
77
+
78
+ it('creates a validator that can validate against a 2019-09 schema', async () => {
79
+ const schema = {
80
+ $schema: 'https://json-schema.org/draft/2019-09/schema',
81
+ type: 'object',
82
+ properties: {
83
+ name: {
84
+ type: 'string',
85
+ },
86
+ },
87
+ required: ['name'],
88
+ };
89
+
90
+ const validator = await createValidator(schema);
91
+ const result = validator({ name: 'test' });
92
+
93
+ expect(result.valid).toBe(true);
94
+ expect(result.errors).toEqual([]);
95
+ });
96
+
97
+ it('creates a validator that can validate against a 2020-12 schema', async () => {
98
+ const schema = {
99
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
100
+ type: 'object',
101
+ properties: {
102
+ name: {
103
+ type: 'string',
104
+ },
105
+ },
106
+ required: ['name'],
107
+ };
108
+
109
+ const validator = await createValidator(schema);
110
+ const result = validator({ name: 'test' });
111
+
112
+ expect(result.valid).toBe(true);
113
+ expect(result.errors).toEqual([]);
114
+ });
115
+ });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @jest-environment node
3
+ */
4
+
5
+ import path from 'path';
6
+ import validateSchemas from '../validateSchemas';
7
+ import { getPathsForVersion } from '../helpers/path-helpers';
8
+
9
+ describe('validateSchemas - Integration', () => {
10
+ let consoleErrorSpy;
11
+ let consoleLogSpy;
12
+
13
+ beforeEach(() => {
14
+ // Spy on console.error and console.log to keep test output clean
15
+ consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
16
+ consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
17
+ });
18
+
19
+ afterEach(() => {
20
+ jest.restoreAllMocks();
21
+ });
22
+
23
+ it('should return true for the "next" version schemas', async () => {
24
+ const siteDir = path.resolve(__dirname, '../../../demo');
25
+ const { schemaDir } = getPathsForVersion('next', siteDir);
26
+ const result = await validateSchemas(schemaDir);
27
+ expect(result).toBe(true);
28
+ });
29
+ });
@@ -14,124 +14,40 @@ describe('validateSchemas', () => {
14
14
 
15
15
  beforeEach(() => {
16
16
  tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'schema-test-'));
17
- consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
18
- consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
19
17
  });
20
18
 
21
19
  afterEach(() => {
22
20
  fs.rmSync(tmpDir, { recursive: true, force: true });
23
- jest.restoreAllMocks();
24
21
  });
25
22
 
26
- const writeSchema = (dir, fileName, content) => {
27
- const filePath = path.join(dir, fileName);
28
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
29
- fs.writeFileSync(filePath, JSON.stringify(content, null, 2));
30
- };
31
-
32
- const loadFixture = (fixtureName) => {
33
- const fixturePath = path.resolve(
23
+ it('should return true for a complex set of valid schemas', async () => {
24
+ const fixtureDir = path.resolve(
34
25
  __dirname,
35
26
  '__fixtures__',
36
27
  'validateSchemas',
37
- fixtureName,
28
+ 'complex-validation',
38
29
  );
39
- const schemaContent = fs.readFileSync(fixturePath, 'utf8');
40
- return JSON.parse(schemaContent);
41
- };
42
-
43
- it('should return true when all schemas are valid', async () => {
44
- const validSchema = loadFixture('valid-schema.json');
45
- writeSchema(tmpDir, 'valid-schema.json', validSchema);
46
-
47
- const result = await validateSchemas(tmpDir);
30
+ const schemaDir = path.join(tmpDir, 'schemas');
31
+ fs.mkdirSync(schemaDir, { recursive: true });
32
+ fs.cpSync(fixtureDir, schemaDir, { recursive: true });
33
+ const result = await validateSchemas(schemaDir);
48
34
  expect(result).toBe(true);
49
- expect(consoleLogSpy).toHaveBeenCalledWith(
50
- 'OK Schema valid-schema.json produced a valid example.',
51
- );
52
35
  });
53
36
 
54
37
  it('should return false if an example fails validation', async () => {
55
- const invalidExampleSchema = loadFixture('invalid-example-schema.json');
56
- writeSchema(tmpDir, 'invalid-example-schema.json', invalidExampleSchema);
57
-
58
- const result = await validateSchemas(tmpDir);
59
- expect(result).toBe(false);
60
- expect(consoleErrorSpy).toHaveBeenCalledWith(
61
- 'x Schema invalid-example-schema.json example data failed validation:',
38
+ const fixturePath = path.resolve(
39
+ __dirname,
40
+ '__fixtures__',
41
+ 'validateSchemas',
42
+ 'invalid-example-schema.json',
62
43
  );
63
- expect(consoleErrorSpy).toHaveBeenCalledWith(
64
- expect.arrayContaining([
65
- expect.objectContaining({
66
- instancePath: '/age',
67
- keyword: 'type',
68
- message: 'must be number',
69
- }),
70
- ]),
44
+ const schemaDir = path.join(tmpDir, 'schemas');
45
+ fs.mkdirSync(schemaDir, { recursive: true });
46
+ fs.copyFileSync(
47
+ fixturePath,
48
+ path.join(schemaDir, 'invalid-example-schema.json'),
71
49
  );
72
- });
73
-
74
- it('should fail validation for missing required property', async () => {
75
- const noValidExampleSchema = loadFixture('no-example-schema.json');
76
- writeSchema(tmpDir, 'no-valid-example-schema.json', noValidExampleSchema);
77
-
78
- const result = await validateSchemas(tmpDir);
50
+ const result = await validateSchemas(schemaDir);
79
51
  expect(result).toBe(false);
80
- expect(consoleErrorSpy).toHaveBeenCalledWith(
81
- 'x Schema no-valid-example-schema.json does not produce a valid example.',
82
- );
83
- });
84
-
85
- it('should handle schemas with $refs correctly', async () => {
86
- const componentSchema = loadFixture(
87
- path.join('components', 'referenced.json'),
88
- );
89
- const mainSchema = loadFixture('main-schema-with-ref.json');
90
-
91
- writeSchema(
92
- path.join(tmpDir, 'components'),
93
- 'referenced.json',
94
- componentSchema,
95
- );
96
- writeSchema(tmpDir, 'main-schema-with-ref.json', mainSchema);
97
-
98
- const result = await validateSchemas(tmpDir);
99
- expect(result).toBe(true);
100
- expect(consoleLogSpy).toHaveBeenCalledWith(
101
- 'OK Schema main-schema-with-ref.json produced a valid example.',
102
- );
103
- });
104
-
105
- it('should reject if a referenced schema is missing', async () => {
106
- const mainSchema = loadFixture('main-schema-with-missing-ref.json');
107
- writeSchema(tmpDir, 'main-schema-with-missing-ref.json', mainSchema);
108
-
109
- const expectedErrorPath = path.join(tmpDir, 'non-existent-component.json');
110
- await expect(validateSchemas(tmpDir)).rejects.toThrow(
111
- expect.objectContaining({
112
- message: expect.stringContaining(
113
- `Error opening file ${expectedErrorPath}`,
114
- ),
115
- }),
116
- );
117
- });
118
-
119
- it('should throw an error if duplicate schema IDs are found', async () => {
120
- const schemaA = {
121
- $id: 'duplicate-id',
122
- type: 'object',
123
- properties: { a: { type: 'string' } },
124
- };
125
- const schemaB = {
126
- $id: 'duplicate-id',
127
- type: 'object',
128
- properties: { b: { type: 'string' } },
129
- };
130
- writeSchema(path.join(tmpDir, 'A'), 'schema-A.json', schemaA);
131
- writeSchema(path.join(tmpDir, 'B'), 'schema-B.json', schemaB);
132
-
133
- await expect(validateSchemas(tmpDir)).rejects.toThrow(
134
- 'schema with key or id "duplicate-id" already exists',
135
- );
136
52
  });
137
53
  });
@@ -1,71 +1,66 @@
1
1
  import { getSingleExampleValue } from './example-helper';
2
+ import mergeJsonSchema from 'json-schema-merge-allof';
2
3
 
3
- /**
4
- * Recursively builds a single, default example object from a JSON schema.
5
- * It prefers explicit examples, consts, or defaults. For choices (`oneOf`/`anyOf`),
6
- * it defaults to the first option.
7
- *
8
- * @param {object} schema The schema to build an example from.
9
- * @returns {object|undefined} The generated example.
10
- */
11
- const buildExampleFromSchema = (schema) => {
12
- if (!schema) return undefined;
4
+ const getPrimitivePlaceholder = (type) => {
5
+ switch (type) {
6
+ case 'string':
7
+ return '';
8
+ case 'integer':
9
+ case 'number':
10
+ return 0;
11
+ case 'boolean':
12
+ return false;
13
+ default:
14
+ return undefined;
15
+ }
16
+ };
13
17
 
14
- // For choices, default to the first option.
15
- if (schema.oneOf?.length > 0) {
16
- return buildExampleFromSchema(schema.oneOf[0]);
18
+ const buildExampleFromSchema = (schema) => {
19
+ if (!schema) {
20
+ return undefined;
17
21
  }
18
- if (schema.anyOf?.length > 0) {
19
- return buildExampleFromSchema(schema.anyOf[0]);
22
+
23
+ // For choices, default to the first option and recurse.
24
+ const choiceType = schema.oneOf ? 'oneOf' : schema.anyOf ? 'anyOf' : null;
25
+ if (choiceType && schema[choiceType]?.length > 0) {
26
+ const newSchema = { ...schema };
27
+ const choice = newSchema[choiceType][0];
28
+ delete newSchema[choiceType];
29
+ const merged = mergeJsonSchema({ allOf: [newSchema, choice] });
30
+ return buildExampleFromSchema(merged);
20
31
  }
21
32
 
33
+ // If there's an explicit example, use it.
22
34
  const exampleValue = getSingleExampleValue(schema);
23
35
  if (typeof exampleValue !== 'undefined') {
24
36
  return exampleValue;
25
37
  }
26
38
 
27
- let type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
28
- if (!type && schema.properties) {
29
- type = 'object';
30
- }
39
+ // Determine the type, defaulting to 'object' if properties are present.
40
+ const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
41
+ const inferredType = !type && schema.properties ? 'object' : type;
31
42
 
32
- if (type === 'object') {
33
- if (schema.properties) {
34
- const obj = {};
35
- Object.entries(schema.properties).forEach(([key, propSchema]) => {
43
+ // Build examples based on type.
44
+ if (inferredType === 'object' && schema.properties) {
45
+ const obj = Object.entries(schema.properties).reduce(
46
+ (acc, [key, propSchema]) => {
36
47
  const value = buildExampleFromSchema(propSchema);
37
48
  if (typeof value !== 'undefined') {
38
- obj[key] = value;
49
+ acc[key] = value;
39
50
  }
40
- });
41
- // Return undefined for objects that end up empty.
42
- return Object.keys(obj).length > 0 ? obj : undefined;
43
- }
44
- // No properties defined, treat as an empty object (which we filter out).
45
- return undefined;
51
+ return acc;
52
+ },
53
+ {},
54
+ );
55
+ return Object.keys(obj).length > 0 ? obj : undefined;
46
56
  }
47
57
 
48
- if (type === 'array') {
49
- if (schema.items) {
50
- const itemValue = buildExampleFromSchema(schema.items);
51
- // Only return an array if the item schema generates a value.
52
- return typeof itemValue !== 'undefined' ? [itemValue] : undefined;
53
- }
54
- return undefined; // No items defined.
58
+ if (inferredType === 'array' && schema.items) {
59
+ const itemValue = buildExampleFromSchema(schema.items);
60
+ return typeof itemValue !== 'undefined' ? [itemValue] : undefined;
55
61
  }
56
62
 
57
- // Return placeholder values for primitive types if no other value is found.
58
- switch (type) {
59
- case 'string':
60
- return '';
61
- case 'integer':
62
- case 'number':
63
- return 0;
64
- case 'boolean':
65
- return false;
66
- default:
67
- return undefined;
68
- }
63
+ return getPrimitivePlaceholder(inferredType);
69
64
  };
70
65
 
71
66
  export default buildExampleFromSchema;
@@ -1,6 +1,5 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import loadSchema from './loadSchema';
4
3
 
5
4
  export function createDir(directory) {
6
5
  if (!fs.existsSync(directory)) {
@@ -15,7 +14,7 @@ export function readSchemas(directory) {
15
14
 
16
15
  return files.map((file) => {
17
16
  const filePath = path.join(directory, file);
18
- const schema = loadSchema(filePath);
17
+ const schema = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
19
18
  return {
20
19
  fileName: file,
21
20
  filePath,
@@ -15,21 +15,6 @@ export function slugify(text) {
15
15
  .replace(/-+$/, ''); // Trim - from end of text
16
16
  }
17
17
 
18
- export function fixRefs(obj, basePath) {
19
- for (const key in obj) {
20
- if (
21
- key === '$ref' &&
22
- typeof obj[key] === 'string' &&
23
- !obj[key].startsWith('#') &&
24
- !obj[key].startsWith('http')
25
- ) {
26
- obj[key] = path.resolve(basePath, obj[key]);
27
- } else if (typeof obj[key] === 'object' && obj[key] !== null) {
28
- fixRefs(obj[key], basePath);
29
- }
30
- }
31
- }
32
-
33
18
  export async function processOneOfSchema(schema, filePath) {
34
19
  const processedSchemas = [];
35
20
  const choiceType = schema.oneOf ? 'oneOf' : null;
@@ -1,99 +1,71 @@
1
1
  import buildExampleFromSchema from './buildExampleFromSchema';
2
+ import mergeJsonSchema from 'json-schema-merge-allof';
2
3
 
3
- /**
4
- * This is the main helper that generates a list of complete example objects,
5
- * grouped by the property that contains the choice.
6
- *
7
- * @param {object} rootSchema The complete schema for the event.
8
- * @returns {object[]} An array of group objects.
9
- */
10
- export function schemaToExamples(rootSchema) {
11
- const choicePoints = [];
4
+ const findChoicePoints = (subSchema, path = []) => {
5
+ if (!subSchema) {
6
+ return [];
7
+ }
12
8
 
13
- // 1. Find all choice points in the schema recursively
14
- function findChoices(subSchema, path = []) {
15
- if (!subSchema) return;
9
+ const choiceType = subSchema.oneOf
10
+ ? 'oneOf'
11
+ : subSchema.anyOf
12
+ ? 'anyOf'
13
+ : null;
14
+ const currentChoice = choiceType ? [{ path, schema: subSchema }] : [];
16
15
 
17
- // First, check for choices at the current schema level
18
- const choiceType = subSchema.oneOf
19
- ? 'oneOf'
20
- : subSchema.anyOf
21
- ? 'anyOf'
22
- : null;
23
- if (choiceType) {
24
- choicePoints.push({ path, schema: subSchema });
25
- }
16
+ const nestedChoices = subSchema.properties
17
+ ? Object.entries(subSchema.properties).flatMap(([key, propSchema]) =>
18
+ findChoicePoints(propSchema, [...path, 'properties', key]),
19
+ )
20
+ : [];
21
+
22
+ return [...currentChoice, ...nestedChoices];
23
+ };
24
+
25
+ const generateExampleForChoice = (rootSchema, path, option) => {
26
+ const schemaVariant = JSON.parse(JSON.stringify(rootSchema));
26
27
 
27
- // Then, recurse into any nested properties
28
- if (subSchema.properties) {
29
- for (const [key, propSchema] of Object.entries(subSchema.properties)) {
30
- findChoices(propSchema, [...path, 'properties', key]);
31
- }
28
+ if (path.length === 0) {
29
+ delete schemaVariant.oneOf;
30
+ delete schemaVariant.anyOf;
31
+ const newSchemaVariant = mergeJsonSchema({
32
+ allOf: [schemaVariant, option],
33
+ });
34
+ return buildExampleFromSchema(newSchemaVariant);
35
+ } else {
36
+ let parentOfChoice = schemaVariant;
37
+ for (let i = 0; i < path.length; i++) {
38
+ parentOfChoice = parentOfChoice[path[i]];
32
39
  }
40
+ delete parentOfChoice.oneOf;
41
+ delete parentOfChoice.anyOf;
42
+ Object.assign(parentOfChoice, option);
43
+ return buildExampleFromSchema(schemaVariant);
33
44
  }
45
+ };
34
46
 
35
- findChoices(rootSchema);
47
+ export function schemaToExamples(rootSchema) {
48
+ const choicePoints = findChoicePoints(rootSchema);
36
49
 
37
- // 2. If no choices are found, generate a single default example
38
50
  if (choicePoints.length === 0) {
39
51
  const example = buildExampleFromSchema(rootSchema);
40
52
  if (example && Object.keys(example).length > 0) {
41
- // Return in the same group structure for consistency
42
53
  return [
43
- {
44
- property: 'default',
45
- options: [{ title: 'Example', example }],
46
- },
54
+ { property: 'default', options: [{ title: 'Example', example }] },
47
55
  ];
48
56
  }
49
57
  return [];
50
58
  }
51
59
 
52
- // 3. Map each found choice point to a "group" of examples
53
60
  return choicePoints.map(({ path, schema }) => {
54
61
  const choiceType = schema.oneOf ? 'oneOf' : 'anyOf';
55
- const choices = schema[choiceType];
56
62
  const propertyName = path.length > 0 ? path[path.length - 1] : 'root';
57
63
 
58
- // For each option within the choice, generate a complete example
59
- const options = choices.map((option) => {
60
- // Create a deep copy of the root schema to modify for this specific example
61
- const schemaVariant = JSON.parse(JSON.stringify(rootSchema));
62
-
63
- // If path is empty, the choice is at the root of the schema.
64
- if (path.length === 0) {
65
- // Merge the chosen option's properties into the root properties
66
- schemaVariant.properties = {
67
- ...schemaVariant.properties,
68
- ...option.properties,
69
- };
70
- // Remove the choice block from the root
71
- delete schemaVariant.oneOf;
72
- delete schemaVariant.anyOf;
73
- } else {
74
- // The choice is nested. Find the parent of the choice block in the copied schema.
75
- let parentOfChoice = schemaVariant;
76
- for (let i = 0; i < path.length; i++) {
77
- parentOfChoice = parentOfChoice[path[i]];
78
- }
79
-
80
- // Delete the choice keywords and merge the selected option.
81
- // This preserves other properties on the parent (like `type` and `description`).
82
- delete parentOfChoice.oneOf;
83
- delete parentOfChoice.anyOf;
84
- Object.assign(parentOfChoice, option);
85
- }
86
-
87
- const example = buildExampleFromSchema(schemaVariant);
88
- return {
89
- title: option.title || 'Option',
90
- example,
91
- };
92
- });
64
+ const options = schema[choiceType].map((option) => ({
65
+ title: option.title || 'Option',
66
+ example: generateExampleForChoice(rootSchema, path, option),
67
+ }));
93
68
 
94
- return {
95
- property: propertyName,
96
- options,
97
- };
69
+ return { property: propertyName, options };
98
70
  });
99
71
  }