diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3e588d9..99737aa 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,5 +27,11 @@ jobs: - name: Build project run: npm run build + - name: Generate schema + run: SKIP_VALIDATION=true npm run generate + + - name: Validate generated schema + run: npm run validate + - name: Run tests run: npm test diff --git a/dist/schema.json b/dist/schema.json index 0a8fc46..0374522 100644 --- a/dist/schema.json +++ b/dist/schema.json @@ -7813,7 +7813,19 @@ "securitySchemes": { "OAuth2": { "type": "oauth2", - "description": "OAuth 2.0 authentication" + "description": "OAuth 2.0 authentication", + "flows": { + "authorizationCode": { + "authorizationUrl": "https://mastodon.example/oauth/authorize", + "tokenUrl": "https://mastodon.example/oauth/token", + "scopes": { + "read": "Read access", + "write": "Write access", + "follow": "Follow access", + "push": "Push access" + } + } + } }, "BearerAuth": { "type": "http", diff --git a/package-lock.json b/package-lock.json index 41d9c0b..3548d59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { + "@seriousme/openapi-schema-validator": "^2.4.1", "gray-matter": "^4.0.3" }, "devDependencies": { @@ -919,6 +920,40 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@seriousme/openapi-schema-validator": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@seriousme/openapi-schema-validator/-/openapi-schema-validator-2.4.1.tgz", + "integrity": "sha512-OX15CKLV2JFGcoXxFVD/CMtWzys+r6G9gArKY8iaUqOkIEqp80ispclk5c8j5i1iEIIlyCRJ0R0N5MddHFg2xg==", + "license": "MIT", + "dependencies": { + "ajv": "^8.17.1", + "ajv-draft-04": "^1.0.0", + "ajv-formats": "^3.0.1", + "js-yaml": "^4.1.0" + }, + "bin": { + "bundle-api": "bin/bundle-api-cli.js", + "validate-api": "bin/validate-api-cli.js" + } + }, + "node_modules/@seriousme/openapi-schema-validator/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/@seriousme/openapi-schema-validator/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1134,6 +1169,53 @@ "node": ">=0.4.0" } }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -1846,6 +1928,12 @@ "node": ">=0.10.0" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -1853,6 +1941,22 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -2962,6 +3066,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -3454,6 +3564,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", diff --git a/package.json b/package.json index d5eff3b..283fea2 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "start": "node dist/index.js", "dev": "ts-node src/index.ts", "generate": "ts-node src/generate.ts", + "validate": "ts-node src/validate.ts", "test": "jest", "test:watch": "jest --watch", "clean": "rm -rf dist", @@ -34,6 +35,7 @@ "typescript": "^5.0.0" }, "dependencies": { + "@seriousme/openapi-schema-validator": "^2.4.1", "gray-matter": "^4.0.3" } } diff --git a/src/__tests__/main.test.ts b/src/__tests__/main.test.ts index d14a123..7b27119 100644 --- a/src/__tests__/main.test.ts +++ b/src/__tests__/main.test.ts @@ -12,9 +12,12 @@ describe('main', () => { } }); - it('should generate and write schema.json to dist directory', () => { + it('should generate and write schema.json to dist directory', async () => { + // Set skip validation to avoid ES module import issues in tests + process.env.SKIP_VALIDATION = 'true'; + // Run the main function - main(); + await main(); // Check that the file was created expect(fs.existsSync(schemaPath)).toBe(true); @@ -30,5 +33,8 @@ describe('main', () => { expect(schema.paths).toBeDefined(); expect(schema.components).toBeDefined(); expect(schema.components.schemas).toBeDefined(); + + // Clean up env var + delete process.env.SKIP_VALIDATION; }); }); diff --git a/src/generators/OpenAPIGenerator.ts b/src/generators/OpenAPIGenerator.ts index d842275..b61097d 100644 --- a/src/generators/OpenAPIGenerator.ts +++ b/src/generators/OpenAPIGenerator.ts @@ -35,6 +35,18 @@ class OpenAPIGenerator { OAuth2: { type: 'oauth2', description: 'OAuth 2.0 authentication', + flows: { + authorizationCode: { + authorizationUrl: 'https://mastodon.example/oauth/authorize', + tokenUrl: 'https://mastodon.example/oauth/token', + scopes: { + read: 'Read access', + write: 'Write access', + follow: 'Follow access', + push: 'Push access', + }, + }, + }, }, BearerAuth: { type: 'http', diff --git a/src/interfaces/OpenAPISchema.ts b/src/interfaces/OpenAPISchema.ts index aa2afb5..f36d8bc 100644 --- a/src/interfaces/OpenAPISchema.ts +++ b/src/interfaces/OpenAPISchema.ts @@ -14,6 +14,25 @@ interface OpenAPISecurityScheme { scheme?: string; bearerFormat?: string; description?: string; + flows?: { + authorizationCode?: { + authorizationUrl: string; + tokenUrl: string; + scopes: Record; + }; + implicit?: { + authorizationUrl: string; + scopes: Record; + }; + password?: { + tokenUrl: string; + scopes: Record; + }; + clientCredentials?: { + tokenUrl: string; + scopes: Record; + }; + }; } interface OpenAPIProperty { diff --git a/src/main.ts b/src/main.ts index 52761a7..c97dde6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,7 +4,26 @@ import { OpenAPIGenerator } from './generators/OpenAPIGenerator'; import * as fs from 'fs'; import * as path from 'path'; -function main() { +async function validateSchemaIfEnabled(schemaPath: string): Promise { + const shouldSkipValidation = process.env.SKIP_VALIDATION === 'true'; + if (shouldSkipValidation) { + console.log('⏭️ Schema validation skipped (SKIP_VALIDATION=true)'); + return; + } + + try { + // Dynamic import to avoid Jest ES module issues + const { validateSchema } = await import('./validator'); + await validateSchema(schemaPath, true); + } catch (error) { + if (error instanceof Error) { + throw error; + } + throw new Error(`Schema validation failed: ${String(error)}`); + } +} + +async function main() { console.log('Parsing Mastodon entity files...'); const parser = new EntityParser(); @@ -44,10 +63,16 @@ function main() { // Write schema to file fs.writeFileSync(schemaPath, generator.toJSON()); console.log(`Schema written to ${schemaPath}`); + + // Validate the generated schema + await validateSchemaIfEnabled(schemaPath); } if (require.main === module) { - main(); + main().catch(error => { + console.error('Error:', error.message); + process.exit(1); + }); } export { main }; diff --git a/src/validate.ts b/src/validate.ts new file mode 100644 index 0000000..1a0379c --- /dev/null +++ b/src/validate.ts @@ -0,0 +1,14 @@ +import { validateSchema } from './validator'; +import * as path from 'path'; + +async function main() { + const schemaPath = path.join(__dirname, '..', 'dist', 'schema.json'); + await validateSchema(schemaPath, true); +} + +if (require.main === module) { + main().catch(error => { + console.error('Error:', error.message); + process.exit(1); + }); +} \ No newline at end of file diff --git a/src/validator.ts b/src/validator.ts new file mode 100644 index 0000000..260fe5b --- /dev/null +++ b/src/validator.ts @@ -0,0 +1,46 @@ +import { Validator } from '@seriousme/openapi-schema-validator'; +import * as fs from 'fs'; + +export async function validateSchema(schemaPath: string, exitOnError: boolean = true): Promise { + console.log('Validating OpenAPI schema...'); + + try { + const schemaContent = fs.readFileSync(schemaPath, 'utf-8'); + const schema = JSON.parse(schemaContent); + + const validator = new Validator(); + const result = await validator.validate(schema); + + if (result.valid) { + console.log('✅ Schema validation passed'); + return true; + } else { + console.error('❌ Schema validation failed with the following errors:'); + const errors = result.errors; + if (Array.isArray(errors)) { + errors.forEach((error: any, index: number) => { + console.error(` ${index + 1}. ${error.instancePath}: ${error.message}`); + }); + if (exitOnError) { + throw new Error(`Schema validation failed with ${errors.length} error(s)`); + } + } else if (typeof errors === 'string') { + console.error(` 1. ${errors}`); + if (exitOnError) { + throw new Error(`Schema validation failed: ${errors}`); + } + } else { + console.error('Unknown error format:', errors); + if (exitOnError) { + throw new Error('Schema validation failed with unknown errors'); + } + } + return false; + } + } catch (error) { + if (error instanceof Error) { + throw error; + } + throw new Error(`Schema validation failed: ${String(error)}`); + } +} \ No newline at end of file