diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 42adb44..301e978 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,7 +5,7 @@ version: 2 updates: - - package-ecosystem: "npm" - directory: "/" + - package-ecosystem: 'npm' + directory: '/' schedule: - interval: "weekly" + interval: 'weekly' diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3de3105..3e588d9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,8 +21,11 @@ jobs: - name: Install dependencies run: npm ci + - name: Check code formatting + run: npm run format:check + - name: Build project run: npm run build - name: Run tests - run: npm test \ No newline at end of file + run: npm test diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..fb6a5ae --- /dev/null +++ b/.prettierignore @@ -0,0 +1,17 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# Coverage reports +coverage/ + +# Logs +*.log + +# Documentation clone +mastodon-documentation/ + +# Lock files +package-lock.json \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..7225824 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 80, + "tabWidth": 2, + "useTabs": false +} diff --git a/README.md b/README.md index 96468bc..0068d5c 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# mastodon-openapi \ No newline at end of file +# mastodon-openapi diff --git a/jest.config.js b/jest.config.js index 783218c..86fae53 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,8 +3,5 @@ module.exports = { testEnvironment: 'node', roots: ['/src'], testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'], - collectCoverageFrom: [ - 'src/**/*.ts', - '!src/**/*.d.ts', - ], -}; \ No newline at end of file + collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts'], +}; diff --git a/package-lock.json b/package-lock.json index 3832060..41d9c0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@types/js-yaml": "^4.0.9", "@types/node": "^22.15.30", "jest": "^29.5.0", + "prettier": "^3.5.3", "ts-jest": "^29.1.0", "ts-node": "^10.9.0", "typescript": "^5.0.0" @@ -3361,6 +3362,22 @@ "node": ">=8" } }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", diff --git a/package.json b/package.json index 9b427db..d5eff3b 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "test": "jest", "test:watch": "jest --watch", "clean": "rm -rf dist", + "format": "prettier --write .", + "format:check": "prettier --check .", "postinstall": "git clone https://github.com/mastodon/documentation mastodon-documentation || true" }, "keywords": [ @@ -26,6 +28,7 @@ "@types/js-yaml": "^4.0.9", "@types/node": "^22.15.30", "jest": "^29.5.0", + "prettier": "^3.5.3", "ts-jest": "^29.1.0", "ts-node": "^10.9.0", "typescript": "^5.0.0" diff --git a/src/__tests__/generators/OpenAPIGenerator.test.ts b/src/__tests__/generators/OpenAPIGenerator.test.ts index 8fec9b6..04eb07c 100644 --- a/src/__tests__/generators/OpenAPIGenerator.test.ts +++ b/src/__tests__/generators/OpenAPIGenerator.test.ts @@ -19,22 +19,22 @@ describe('OpenAPIGenerator', () => { { name: 'id', type: 'Integer', - description: 'The ID of the entity' + description: 'The ID of the entity', }, { name: 'name', type: 'String', description: 'The name of the entity', - optional: true + optional: true, }, { name: 'active', type: 'Boolean', description: 'Whether the entity is active', - deprecated: true - } - ] - } + deprecated: true, + }, + ], + }, ]; const methodFiles: ApiMethodsFile[] = [ @@ -48,7 +48,7 @@ describe('OpenAPIGenerator', () => { endpoint: '/api/v1/test/:id', description: 'Retrieve a test entity', returns: 'TestEntity', - oauth: 'User token + read' + oauth: 'User token + read', }, { name: 'Create test entity', @@ -59,17 +59,17 @@ describe('OpenAPIGenerator', () => { { name: 'name', description: 'Name of the entity', - required: true + required: true, }, { name: 'active', - description: 'Whether entity is active' - } + description: 'Whether entity is active', + }, ], - oauth: 'User token + write' - } - ] - } + oauth: 'User token + write', + }, + ], + }, ]; const spec = generator.generateSchema(entities, methodFiles); @@ -118,12 +118,14 @@ describe('OpenAPIGenerator', () => { const postOp = spec.paths['/api/v1/test'].post!; expect(postOp.summary).toBe('Create test entity'); expect(postOp.requestBody).toBeDefined(); - expect(postOp.requestBody?.content['application/x-www-form-urlencoded']).toBeDefined(); + expect( + postOp.requestBody?.content['application/x-www-form-urlencoded'] + ).toBeDefined(); }); it('should handle empty inputs', () => { const spec = generator.generateSchema([], []); - + expect(spec.openapi).toBe('3.0.3'); expect(spec.paths).toEqual({}); expect(spec.components?.schemas).toEqual({}); @@ -134,13 +136,13 @@ describe('OpenAPIGenerator', () => { it('should return valid JSON string', () => { const entities: EntityClass[] = []; const methodFiles: ApiMethodsFile[] = []; - + generator.generateSchema(entities, methodFiles); const json = generator.toJSON(); - + expect(() => JSON.parse(json)).not.toThrow(); const parsed = JSON.parse(json); expect(parsed.openapi).toBe('3.0.3'); }); }); -}); \ No newline at end of file +}); diff --git a/src/__tests__/parsers/EntityParser.test.ts b/src/__tests__/parsers/EntityParser.test.ts index 672229d..1d7757b 100644 --- a/src/__tests__/parsers/EntityParser.test.ts +++ b/src/__tests__/parsers/EntityParser.test.ts @@ -19,25 +19,29 @@ describe('EntityParser', () => { test('should parse entities and extract basic structure', () => { const entities = parser.parseAllEntities(); - + // Verify we found entities expect(entities.length).toBeGreaterThan(50); // Should be around 64 entities - + // Find a specific entity to test - const accountEntity = entities.find(e => e.name === 'Account'); + const accountEntity = entities.find((e) => e.name === 'Account'); expect(accountEntity).toBeDefined(); - + if (accountEntity) { expect(accountEntity.name).toBe('Account'); expect(accountEntity.description).toContain('user of Mastodon'); expect(accountEntity.attributes.length).toBeGreaterThan(20); // Account has many attributes - + // Check some specific attributes exist - const idAttribute = accountEntity.attributes.find(attr => attr.name === 'id'); + const idAttribute = accountEntity.attributes.find( + (attr) => attr.name === 'id' + ); expect(idAttribute).toBeDefined(); expect(idAttribute?.type).toContain('String'); - - const usernameAttribute = accountEntity.attributes.find(attr => attr.name === 'username'); + + const usernameAttribute = accountEntity.attributes.find( + (attr) => attr.name === 'username' + ); expect(usernameAttribute).toBeDefined(); expect(usernameAttribute?.type).toBe('String'); } @@ -45,38 +49,42 @@ describe('EntityParser', () => { test('should correctly identify optional and deprecated attributes', () => { const entities = parser.parseAllEntities(); - + // Find entities with optional/deprecated attributes let foundOptional = false; let foundDeprecated = false; - + for (const entity of entities) { for (const attr of entity.attributes) { if (attr.optional) foundOptional = true; if (attr.deprecated) foundDeprecated = true; } } - + expect(foundOptional).toBe(true); expect(foundDeprecated).toBe(true); }); test('should parse entity with simple structure', () => { const entities = parser.parseAllEntities(); - + // Find Application entity which has a simpler structure - const applicationEntity = entities.find(e => e.name === 'Application'); + const applicationEntity = entities.find((e) => e.name === 'Application'); expect(applicationEntity).toBeDefined(); - + if (applicationEntity) { expect(applicationEntity.name).toBe('Application'); - expect(applicationEntity.description).toContain('interfaces with the REST API'); + expect(applicationEntity.description).toContain( + 'interfaces with the REST API' + ); expect(applicationEntity.attributes.length).toBeGreaterThan(0); - + // Check that name attribute exists - const nameAttribute = applicationEntity.attributes.find(attr => attr.name === 'name'); + const nameAttribute = applicationEntity.attributes.find( + (attr) => attr.name === 'name' + ); expect(nameAttribute).toBeDefined(); expect(nameAttribute?.type).toBe('String'); } }); -}); \ No newline at end of file +}); diff --git a/src/__tests__/parsers/MethodParser.test.ts b/src/__tests__/parsers/MethodParser.test.ts index 6b9ec38..a3a0949 100644 --- a/src/__tests__/parsers/MethodParser.test.ts +++ b/src/__tests__/parsers/MethodParser.test.ts @@ -19,25 +19,27 @@ describe('MethodParser', () => { test('should parse method files and extract basic structure', () => { const methodFiles = methodParser.parseAllMethods(); - + // Verify we found method files expect(methodFiles.length).toBeGreaterThan(30); // Should be around 40 method files - + // Find a specific method file to test - const appsMethodFile = methodFiles.find(f => f.name.includes('apps')); + const appsMethodFile = methodFiles.find((f) => f.name.includes('apps')); expect(appsMethodFile).toBeDefined(); - + if (appsMethodFile) { expect(appsMethodFile.name).toContain('apps'); expect(appsMethodFile.description).toContain('OAuth'); expect(appsMethodFile.methods.length).toBeGreaterThan(0); - + // Check that create app method exists - const createMethod = appsMethodFile.methods.find(method => - method.name.toLowerCase().includes('create') && method.endpoint.includes('/api/v1/apps') + const createMethod = appsMethodFile.methods.find( + (method) => + method.name.toLowerCase().includes('create') && + method.endpoint.includes('/api/v1/apps') ); expect(createMethod).toBeDefined(); - + if (createMethod) { expect(createMethod.httpMethod).toBe('POST'); expect(createMethod.endpoint).toBe('/api/v1/apps'); @@ -48,20 +50,20 @@ describe('MethodParser', () => { test('should parse method parameters correctly', () => { const methodFiles = methodParser.parseAllMethods(); - + // Find a method with parameters let foundMethodWithParams = false; let foundRequiredParam = false; - + for (const methodFile of methodFiles) { for (const method of methodFile.methods) { if (method.parameters && method.parameters.length > 0) { foundMethodWithParams = true; - + for (const param of method.parameters) { expect(param.name).toBeTruthy(); expect(param.description).toBeTruthy(); - + if (param.required) { foundRequiredParam = true; } @@ -69,32 +71,32 @@ describe('MethodParser', () => { } } } - + expect(foundMethodWithParams).toBe(true); expect(foundRequiredParam).toBe(true); }); test('should extract HTTP methods and endpoints correctly', () => { const methodFiles = methodParser.parseAllMethods(); - + // Find some specific methods to verify let foundGetMethod = false; let foundPostMethod = false; let foundDeleteMethod = false; - + for (const methodFile of methodFiles) { for (const method of methodFile.methods) { if (method.httpMethod === 'GET') foundGetMethod = true; if (method.httpMethod === 'POST') foundPostMethod = true; if (method.httpMethod === 'DELETE') foundDeleteMethod = true; - + // Verify endpoint format (all endpoints should start with /) expect(method.endpoint).toMatch(/^\//); } } - + expect(foundGetMethod).toBe(true); expect(foundPostMethod).toBe(true); expect(foundDeleteMethod).toBe(true); }); -}); \ No newline at end of file +}); diff --git a/src/generate.ts b/src/generate.ts index 7dfadb5..193736c 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -14,4 +14,4 @@ export { main } from './main'; if (require.main === module) { const { main } = require('./main'); main(); -} \ No newline at end of file +} diff --git a/src/generators/OpenAPIGenerator.ts b/src/generators/OpenAPIGenerator.ts index dc4d118..d842275 100644 --- a/src/generators/OpenAPIGenerator.ts +++ b/src/generators/OpenAPIGenerator.ts @@ -8,7 +8,7 @@ import { OpenAPISchema, OpenAPIProperty, OpenAPIOperation, - OpenAPIPath + OpenAPIPath, } from '../interfaces/OpenAPISchema'; class OpenAPIGenerator { @@ -20,13 +20,13 @@ class OpenAPIGenerator { info: { title: 'Mastodon API', version: '4.2.0', - description: 'Documentation for the Mastodon API' + description: 'Documentation for the Mastodon API', }, servers: [ { url: 'https://mastodon.example', - description: 'Production server' - } + description: 'Production server', + }, ], paths: {}, components: { @@ -34,26 +34,29 @@ class OpenAPIGenerator { securitySchemes: { OAuth2: { type: 'oauth2', - description: 'OAuth 2.0 authentication' + description: 'OAuth 2.0 authentication', }, BearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT', - description: 'Bearer token authentication' - } - } - } + description: 'Bearer token authentication', + }, + }, + }, }; } - public generateSchema(entities: EntityClass[], methodFiles: ApiMethodsFile[]): OpenAPISpec { + public generateSchema( + entities: EntityClass[], + methodFiles: ApiMethodsFile[] + ): OpenAPISpec { // Convert entities to OpenAPI schemas this.convertEntities(entities); - + // Convert methods to OpenAPI paths this.convertMethods(methodFiles); - + return this.spec; } @@ -67,7 +70,7 @@ class OpenAPIGenerator { type: 'object', description: entity.description, properties: {}, - required: [] + required: [], }; for (const attribute of entity.attributes) { @@ -75,7 +78,7 @@ class OpenAPIGenerator { if (schema.properties) { schema.properties[attribute.name] = property; } - + // Add to required array if not optional if (!attribute.optional && schema.required) { schema.required.push(attribute.name); @@ -95,7 +98,7 @@ class OpenAPIGenerator { private convertAttribute(attribute: EntityAttribute): OpenAPIProperty { const property: OpenAPIProperty = { - description: attribute.description + description: attribute.description, }; if (attribute.deprecated) { @@ -104,7 +107,7 @@ class OpenAPIGenerator { // Parse type information to determine OpenAPI type const type = this.parseType(attribute.type); - + if (type.type) { property.type = type.type; } @@ -126,7 +129,7 @@ class OpenAPIGenerator { private parseType(typeString: string): OpenAPIProperty { const cleanType = typeString.toLowerCase().trim(); - + // Handle arrays if (cleanType.includes('array of')) { const itemTypeMatch = typeString.match(/array of\s+(.+?)(?:\s|$)/i); @@ -134,7 +137,7 @@ class OpenAPIGenerator { const itemType = this.parseType(itemTypeMatch[1]); return { type: 'array', - items: itemType + items: itemType, }; } return { type: 'array' }; @@ -148,7 +151,7 @@ class OpenAPIGenerator { // Clean up reference name const cleanRefName = refName.replace(/[^\w:]/g, ''); return { - $ref: `#/components/schemas/${cleanRefName}` + $ref: `#/components/schemas/${cleanRefName}`, }; } } @@ -156,7 +159,7 @@ class OpenAPIGenerator { // Handle basic types if (cleanType.includes('string')) { const property: OpenAPIProperty = { type: 'string' }; - + if (cleanType.includes('url')) { property.format = 'uri'; } else if (cleanType.includes('datetime')) { @@ -168,11 +171,14 @@ class OpenAPIGenerator { } else if (cleanType.includes('html')) { property.description = (property.description || '') + ' (HTML content)'; } - + return property; } - if (cleanType.includes('integer') || cleanType.includes('cast from an integer')) { + if ( + cleanType.includes('integer') || + cleanType.includes('cast from an integer') + ) { return { type: 'integer' }; } @@ -190,16 +196,18 @@ class OpenAPIGenerator { // Handle enums if (cleanType.includes('enumerable') || cleanType.includes('oneof')) { - return { + return { type: 'string', - description: (typeString.includes('Enumerable') ? 'Enumerable value' : '') + description: typeString.includes('Enumerable') + ? 'Enumerable value' + : '', }; } // Default to string for unknown types - return { + return { type: 'string', - description: `Original type: ${typeString}` + description: `Original type: ${typeString}`, }; } @@ -225,9 +233,9 @@ class OpenAPIGenerator { tags: [category], responses: { '200': { - description: method.returns || 'Success' - } - } + description: method.returns || 'Success', + }, + }, }; // Add security if OAuth is required @@ -249,7 +257,7 @@ class OpenAPIGenerator { in: 'query', required: param.required, description: param.description, - schema: { type: 'string' } + schema: { type: 'string' }, }); } else { bodyParams.push(param); @@ -264,7 +272,7 @@ class OpenAPIGenerator { for (const param of bodyParams) { properties[param.name] = { type: 'string', - description: param.description + description: param.description, }; if (param.required) { required.push(param.name); @@ -279,10 +287,10 @@ class OpenAPIGenerator { schema: { type: 'object', properties, - required: required.length > 0 ? required : undefined - } as OpenAPIProperty - } - } + required: required.length > 0 ? required : undefined, + } as OpenAPIProperty, + }, + }, }; } } @@ -299,7 +307,7 @@ class OpenAPIGenerator { in: 'path', required: true, description: `${pathParam} parameter`, - schema: { type: 'string' } + schema: { type: 'string' }, }); } } @@ -314,7 +322,7 @@ class OpenAPIGenerator { private extractPathParameters(path: string): string[] { const matches = path.match(/\{([^}]+)\}/g); - return matches ? matches.map(match => match.slice(1, -1)) : []; + return matches ? matches.map((match) => match.slice(1, -1)) : []; } public toJSON(): string { @@ -322,4 +330,4 @@ class OpenAPIGenerator { } } -export { OpenAPIGenerator }; \ No newline at end of file +export { OpenAPIGenerator }; diff --git a/src/index.ts b/src/index.ts index 5893f9d..6be0237 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1 @@ -console.log('hello world'); \ No newline at end of file +console.log('hello world'); diff --git a/src/interfaces/ApiMethod.ts b/src/interfaces/ApiMethod.ts index f9044c6..7607d77 100644 --- a/src/interfaces/ApiMethod.ts +++ b/src/interfaces/ApiMethod.ts @@ -11,4 +11,4 @@ interface ApiMethod { version?: string; } -export { ApiMethod }; \ No newline at end of file +export { ApiMethod }; diff --git a/src/interfaces/ApiMethodsFile.ts b/src/interfaces/ApiMethodsFile.ts index 5fc244d..ce5d052 100644 --- a/src/interfaces/ApiMethodsFile.ts +++ b/src/interfaces/ApiMethodsFile.ts @@ -6,4 +6,4 @@ interface ApiMethodsFile { methods: ApiMethod[]; } -export { ApiMethodsFile }; \ No newline at end of file +export { ApiMethodsFile }; diff --git a/src/interfaces/ApiParameter.ts b/src/interfaces/ApiParameter.ts index b5b81fa..c291510 100644 --- a/src/interfaces/ApiParameter.ts +++ b/src/interfaces/ApiParameter.ts @@ -5,4 +5,4 @@ interface ApiParameter { type?: string; } -export { ApiParameter }; \ No newline at end of file +export { ApiParameter }; diff --git a/src/interfaces/EntityAttribute.ts b/src/interfaces/EntityAttribute.ts index 4799be0..6102c90 100644 --- a/src/interfaces/EntityAttribute.ts +++ b/src/interfaces/EntityAttribute.ts @@ -6,4 +6,4 @@ interface EntityAttribute { deprecated?: boolean; } -export { EntityAttribute }; \ No newline at end of file +export { EntityAttribute }; diff --git a/src/interfaces/EntityClass.ts b/src/interfaces/EntityClass.ts index ffc0601..7b753f9 100644 --- a/src/interfaces/EntityClass.ts +++ b/src/interfaces/EntityClass.ts @@ -6,4 +6,4 @@ interface EntityClass { attributes: EntityAttribute[]; } -export { EntityClass }; \ No newline at end of file +export { EntityClass }; diff --git a/src/interfaces/OpenAPISchema.ts b/src/interfaces/OpenAPISchema.ts index 5d8d22b..aa2afb5 100644 --- a/src/interfaces/OpenAPISchema.ts +++ b/src/interfaces/OpenAPISchema.ts @@ -44,16 +44,22 @@ interface OpenAPIParameter { interface OpenAPIRequestBody { description?: string; required?: boolean; - content: Record; + content: Record< + string, + { + schema: OpenAPIProperty | { $ref: string }; + } + >; } interface OpenAPIResponse { description: string; - content?: Record; + content?: Record< + string, + { + schema: OpenAPIProperty | { $ref: string }; + } + >; } interface OpenAPIOperation { @@ -96,5 +102,5 @@ export { OpenAPIResponse, OpenAPIOperation, OpenAPIPath, - OpenAPISpec -}; \ No newline at end of file + OpenAPISpec, +}; diff --git a/src/main.ts b/src/main.ts index 1d3dc96..4a0af41 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,27 +4,30 @@ import { OpenAPIGenerator } from './generators/OpenAPIGenerator'; function main() { console.log('Parsing Mastodon entity files...'); - + const parser = new EntityParser(); const entities = parser.parseAllEntities(); - + console.log(`Found ${entities.length} entities`); console.log('Parsing Mastodon API method files...'); - + const methodParser = new MethodParser(); const methodFiles = methodParser.parseAllMethods(); - + console.log(`Found ${methodFiles.length} method files`); - - const totalMethods = methodFiles.reduce((sum, file) => sum + file.methods.length, 0); + + const totalMethods = methodFiles.reduce( + (sum, file) => sum + file.methods.length, + 0 + ); console.log(`Total API methods parsed: ${totalMethods}`); console.log('Generating OpenAPI schema...'); - + const generator = new OpenAPIGenerator(); const schema = generator.generateSchema(entities, methodFiles); - + console.log('OpenAPI schema generated successfully'); console.log(generator.toJSON()); } @@ -33,4 +36,4 @@ if (require.main === module) { main(); } -export { main }; \ No newline at end of file +export { main }; diff --git a/src/parsers/EntityParser.ts b/src/parsers/EntityParser.ts index 26c7266..d9f1808 100644 --- a/src/parsers/EntityParser.ts +++ b/src/parsers/EntityParser.ts @@ -8,19 +8,24 @@ class EntityParser { private entitiesPath: string; constructor() { - this.entitiesPath = path.join(__dirname, '../../mastodon-documentation/content/en/entities'); + this.entitiesPath = path.join( + __dirname, + '../../mastodon-documentation/content/en/entities' + ); } public parseAllEntities(): EntityClass[] { const entities: EntityClass[] = []; - + if (!fs.existsSync(this.entitiesPath)) { console.error(`Entities path does not exist: ${this.entitiesPath}`); return entities; } - const files = fs.readdirSync(this.entitiesPath).filter(file => file.endsWith('.md')); - + const files = fs + .readdirSync(this.entitiesPath) + .filter((file) => file.endsWith('.md')); + for (const file of files) { try { const entity = this.parseEntityFile(path.join(this.entitiesPath, file)); @@ -38,7 +43,7 @@ class EntityParser { private parseEntityFile(filePath: string): EntityClass | null { const content = fs.readFileSync(filePath, 'utf-8'); const parsed = matter(content); - + // Extract class name from frontmatter title const className = parsed.data.title; if (!className) { @@ -55,32 +60,35 @@ class EntityParser { return { name: className, description, - attributes + attributes, }; } private parseAttributes(content: string): EntityAttribute[] { const attributes: EntityAttribute[] = []; - + // Find the "## Attributes" section - const attributesMatch = content.match(/## Attributes\s*([\s\S]*?)(?=\n## |$)/); + const attributesMatch = content.match( + /## Attributes\s*([\s\S]*?)(?=\n## |$)/ + ); if (!attributesMatch) { return attributes; } const attributesSection = attributesMatch[1]; - + // Match each attribute definition - const attributeRegex = /### `([^`]+)`[^{]*(?:\{\{%([^%]+)%\}\})?\s*\{#[^}]+\}\s*\n\n\*\*Description:\*\*\s*([^\n]+).*?\n\*\*Type:\*\*\s*([^\n]+)/g; - + const attributeRegex = + /### `([^`]+)`[^{]*(?:\{\{%([^%]+)%\}\})?\s*\{#[^}]+\}\s*\n\n\*\*Description:\*\*\s*([^\n]+).*?\n\*\*Type:\*\*\s*([^\n]+)/g; + let match; while ((match = attributeRegex.exec(attributesSection)) !== null) { const [, name, modifiers, description, type] = match; - + const attribute: EntityAttribute = { name: name.trim(), type: this.cleanType(type.trim()), - description: this.cleanDescription(description.trim()) + description: this.cleanDescription(description.trim()), }; // Check for optional/deprecated modifiers @@ -117,4 +125,4 @@ class EntityParser { } } -export { EntityParser }; \ No newline at end of file +export { EntityParser }; diff --git a/src/parsers/MethodParser.ts b/src/parsers/MethodParser.ts index 0c576c4..7c69af0 100644 --- a/src/parsers/MethodParser.ts +++ b/src/parsers/MethodParser.ts @@ -9,24 +9,33 @@ class MethodParser { private methodsPath: string; constructor() { - this.methodsPath = path.join(__dirname, '../../mastodon-documentation/content/en/methods'); + this.methodsPath = path.join( + __dirname, + '../../mastodon-documentation/content/en/methods' + ); } public parseAllMethods(): ApiMethodsFile[] { const methodFiles: ApiMethodsFile[] = []; - + if (!fs.existsSync(this.methodsPath)) { console.error(`Methods path does not exist: ${this.methodsPath}`); return methodFiles; } - const files = fs.readdirSync(this.methodsPath).filter(file => - file.endsWith('.md') && fs.statSync(path.join(this.methodsPath, file)).isFile() - ); - + const files = fs + .readdirSync(this.methodsPath) + .filter( + (file) => + file.endsWith('.md') && + fs.statSync(path.join(this.methodsPath, file)).isFile() + ); + for (const file of files) { try { - const methodFile = this.parseMethodFile(path.join(this.methodsPath, file)); + const methodFile = this.parseMethodFile( + path.join(this.methodsPath, file) + ); if (methodFile) { methodFiles.push(methodFile); } @@ -41,7 +50,7 @@ class MethodParser { private parseMethodFile(filePath: string): ApiMethodsFile | null { const content = fs.readFileSync(filePath, 'utf-8'); const parsed = matter(content); - + // Extract file name from frontmatter title const fileName = parsed.data.title || path.basename(filePath, '.md'); if (!fileName) { @@ -58,19 +67,19 @@ class MethodParser { return { name: fileName, description, - methods + methods, }; } private parseMethods(content: string): ApiMethod[] { const methods: ApiMethod[] = []; - + // Match method sections: ## Method Name {#anchor} const methodSections = content.split(/(?=^## [^{]*\{#[^}]+\})/m); - + for (const section of methodSections) { if (section.trim() === '') continue; - + const method = this.parseMethodSection(section); if (method) { methods.push(method); @@ -84,29 +93,39 @@ class MethodParser { // Extract method name from header: ## Method Name {#anchor} const nameMatch = section.match(/^## ([^{]+)\{#[^}]+\}/m); if (!nameMatch) return null; - + const name = nameMatch[1].trim(); // Extract HTTP method and endpoint: ```http\nMETHOD /path\n``` - const httpMatch = section.match(/```http\s*\n([A-Z]+)\s+([^\s\n]+)[^\n]*\n```/); + const httpMatch = section.match( + /```http\s*\n([A-Z]+)\s+([^\s\n]+)[^\n]*\n```/ + ); if (!httpMatch) return null; - + const httpMethod = httpMatch[1].trim(); const endpoint = httpMatch[2].trim(); // Extract description (first paragraph after the endpoint) - const descriptionMatch = section.match(/```http[^`]*```\s*\n\n([^*\n][^\n]*)/); + const descriptionMatch = section.match( + /```http[^`]*```\s*\n\n([^*\n][^\n]*)/ + ); const description = descriptionMatch ? descriptionMatch[1].trim() : ''; // Extract returns, oauth, version info const returnsMatch = section.match(/\*\*Returns:\*\*\s*([^\\\n]+)/); - const returns = returnsMatch ? this.cleanMarkdown(returnsMatch[1].trim()) : undefined; - + const returns = returnsMatch + ? this.cleanMarkdown(returnsMatch[1].trim()) + : undefined; + const oauthMatch = section.match(/\*\*OAuth:\*\*\s*([^\\\n]+)/); - const oauth = oauthMatch ? this.cleanMarkdown(oauthMatch[1].trim()) : undefined; - + const oauth = oauthMatch + ? this.cleanMarkdown(oauthMatch[1].trim()) + : undefined; + const versionMatch = section.match(/\*\*Version history:\*\*\s*([^\n]*)/); - const version = versionMatch ? this.cleanMarkdown(versionMatch[1].trim()) : undefined; + const version = versionMatch + ? this.cleanMarkdown(versionMatch[1].trim()) + : undefined; // Parse parameters from Form data parameters section const parameters = this.parseParameters(section); @@ -119,33 +138,37 @@ class MethodParser { parameters: parameters.length > 0 ? parameters : undefined, returns, oauth, - version + version, }; } private parseParameters(section: string): ApiParameter[] { const parameters: ApiParameter[] = []; - + // Find parameters section - const paramMatch = section.match(/##### Form data parameters\s*([\s\S]*?)(?=\n#|$)/); + const paramMatch = section.match( + /##### Form data parameters\s*([\s\S]*?)(?=\n#|$)/ + ); if (!paramMatch) return parameters; const paramSection = paramMatch[1]; - + // Match parameter definitions: parameter_name\n: description - const paramRegex = /^([a-zA-Z_][a-zA-Z0-9_]*)\s*\n:\s*([^]*?)(?=\n[a-zA-Z_]|\n\n|$)/gm; - + const paramRegex = + /^([a-zA-Z_][a-zA-Z0-9_]*)\s*\n:\s*([^]*?)(?=\n[a-zA-Z_]|\n\n|$)/gm; + let match; while ((match = paramRegex.exec(paramSection)) !== null) { const [, name, desc] = match; - + const cleanDesc = this.cleanMarkdown(desc.trim()); - const required = cleanDesc.includes('{{}}') || cleanDesc.includes('required'); - + const required = + cleanDesc.includes('{{}}') || cleanDesc.includes('required'); + parameters.push({ name: name.trim(), description: cleanDesc.replace(/\{\{\}\}\s*/g, ''), - required: required ? true : undefined + required: required ? true : undefined, }); } @@ -162,4 +185,4 @@ class MethodParser { } } -export { MethodParser }; \ No newline at end of file +export { MethodParser }; diff --git a/tsconfig.json b/tsconfig.json index af50e2b..2f8cd7c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,12 +14,6 @@ "sourceMap": true, "resolveJsonModule": true }, - "include": [ - "src/**/*" - ], - "exclude": [ - "node_modules", - "dist", - "**/*.test.ts" - ] -} \ No newline at end of file + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +}