Skip to content

Commit d9caa86

Browse files
authored
Merge pull request #12 from abraham/copilot/fix-11
Add parsing of API methods from content/en/methods directory
2 parents 8e775e6 + 813cd37 commit d9caa86

File tree

2 files changed

+313
-2
lines changed

2 files changed

+313
-2
lines changed

src/__tests__/generate.test.ts

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { EntityParser, EntityClass, EntityAttribute } from '../generate';
1+
import { EntityParser, EntityClass, EntityAttribute, MethodParser, ApiMethodsFile, ApiMethod, ApiParameter } from '../generate';
22
import * as fs from 'fs';
33
import * as path from 'path';
44

@@ -79,4 +79,101 @@ describe('EntityParser', () => {
7979
expect(nameAttribute?.type).toBe('String');
8080
}
8181
});
82+
});
83+
84+
describe('MethodParser', () => {
85+
let methodParser: MethodParser;
86+
87+
beforeEach(() => {
88+
methodParser = new MethodParser();
89+
});
90+
91+
test('should parse all method files without throwing errors', () => {
92+
expect(() => {
93+
const methodFiles = methodParser.parseAllMethods();
94+
expect(methodFiles).toBeInstanceOf(Array);
95+
expect(methodFiles.length).toBeGreaterThan(0);
96+
}).not.toThrow();
97+
});
98+
99+
test('should parse method files and extract basic structure', () => {
100+
const methodFiles = methodParser.parseAllMethods();
101+
102+
// Verify we found method files
103+
expect(methodFiles.length).toBeGreaterThan(30); // Should be around 40 method files
104+
105+
// Find a specific method file to test
106+
const appsMethodFile = methodFiles.find(f => f.name.includes('apps'));
107+
expect(appsMethodFile).toBeDefined();
108+
109+
if (appsMethodFile) {
110+
expect(appsMethodFile.name).toContain('apps');
111+
expect(appsMethodFile.description).toContain('OAuth');
112+
expect(appsMethodFile.methods.length).toBeGreaterThan(0);
113+
114+
// Check that create app method exists
115+
const createMethod = appsMethodFile.methods.find(method =>
116+
method.name.toLowerCase().includes('create') && method.endpoint.includes('/api/v1/apps')
117+
);
118+
expect(createMethod).toBeDefined();
119+
120+
if (createMethod) {
121+
expect(createMethod.httpMethod).toBe('POST');
122+
expect(createMethod.endpoint).toBe('/api/v1/apps');
123+
expect(createMethod.description).toBeTruthy();
124+
}
125+
}
126+
});
127+
128+
test('should parse method parameters correctly', () => {
129+
const methodFiles = methodParser.parseAllMethods();
130+
131+
// Find a method with parameters
132+
let foundMethodWithParams = false;
133+
let foundRequiredParam = false;
134+
135+
for (const methodFile of methodFiles) {
136+
for (const method of methodFile.methods) {
137+
if (method.parameters && method.parameters.length > 0) {
138+
foundMethodWithParams = true;
139+
140+
for (const param of method.parameters) {
141+
expect(param.name).toBeTruthy();
142+
expect(param.description).toBeTruthy();
143+
144+
if (param.required) {
145+
foundRequiredParam = true;
146+
}
147+
}
148+
}
149+
}
150+
}
151+
152+
expect(foundMethodWithParams).toBe(true);
153+
expect(foundRequiredParam).toBe(true);
154+
});
155+
156+
test('should extract HTTP methods and endpoints correctly', () => {
157+
const methodFiles = methodParser.parseAllMethods();
158+
159+
// Find some specific methods to verify
160+
let foundGetMethod = false;
161+
let foundPostMethod = false;
162+
let foundDeleteMethod = false;
163+
164+
for (const methodFile of methodFiles) {
165+
for (const method of methodFile.methods) {
166+
if (method.httpMethod === 'GET') foundGetMethod = true;
167+
if (method.httpMethod === 'POST') foundPostMethod = true;
168+
if (method.httpMethod === 'DELETE') foundDeleteMethod = true;
169+
170+
// Verify endpoint format (all endpoints should start with /)
171+
expect(method.endpoint).toMatch(/^\//);
172+
}
173+
}
174+
175+
expect(foundGetMethod).toBe(true);
176+
expect(foundPostMethod).toBe(true);
177+
expect(foundDeleteMethod).toBe(true);
178+
});
82179
});

src/generate.ts

Lines changed: 215 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,30 @@ interface EntityClass {
1616
attributes: EntityAttribute[];
1717
}
1818

19+
interface ApiParameter {
20+
name: string;
21+
description: string;
22+
required?: boolean;
23+
type?: string;
24+
}
25+
26+
interface ApiMethod {
27+
name: string;
28+
httpMethod: string;
29+
endpoint: string;
30+
description: string;
31+
parameters?: ApiParameter[];
32+
returns?: string;
33+
oauth?: string;
34+
version?: string;
35+
}
36+
37+
interface ApiMethodsFile {
38+
name: string;
39+
description: string;
40+
methods: ApiMethod[];
41+
}
42+
1943
class EntityParser {
2044
private entitiesPath: string;
2145

@@ -129,6 +153,163 @@ class EntityParser {
129153
}
130154
}
131155

156+
class MethodParser {
157+
private methodsPath: string;
158+
159+
constructor() {
160+
this.methodsPath = path.join(__dirname, '../mastodon-documentation/content/en/methods');
161+
}
162+
163+
public parseAllMethods(): ApiMethodsFile[] {
164+
const methodFiles: ApiMethodsFile[] = [];
165+
166+
if (!fs.existsSync(this.methodsPath)) {
167+
console.error(`Methods path does not exist: ${this.methodsPath}`);
168+
return methodFiles;
169+
}
170+
171+
const files = fs.readdirSync(this.methodsPath).filter(file =>
172+
file.endsWith('.md') && fs.statSync(path.join(this.methodsPath, file)).isFile()
173+
);
174+
175+
for (const file of files) {
176+
try {
177+
const methodFile = this.parseMethodFile(path.join(this.methodsPath, file));
178+
if (methodFile) {
179+
methodFiles.push(methodFile);
180+
}
181+
} catch (error) {
182+
console.error(`Error parsing method file ${file}:`, error);
183+
}
184+
}
185+
186+
return methodFiles;
187+
}
188+
189+
private parseMethodFile(filePath: string): ApiMethodsFile | null {
190+
const content = fs.readFileSync(filePath, 'utf-8');
191+
const parsed = matter(content);
192+
193+
// Extract file name from frontmatter title
194+
const fileName = parsed.data.title || path.basename(filePath, '.md');
195+
if (!fileName) {
196+
console.warn(`No title found in ${filePath}`);
197+
return null;
198+
}
199+
200+
// Extract description from frontmatter
201+
const description = parsed.data.description || '';
202+
203+
// Parse methods from markdown content
204+
const methods = this.parseMethods(parsed.content);
205+
206+
return {
207+
name: fileName,
208+
description,
209+
methods
210+
};
211+
}
212+
213+
private parseMethods(content: string): ApiMethod[] {
214+
const methods: ApiMethod[] = [];
215+
216+
// Match method sections: ## Method Name {#anchor}
217+
const methodSections = content.split(/(?=^## [^{]*\{#[^}]+\})/m);
218+
219+
for (const section of methodSections) {
220+
if (section.trim() === '') continue;
221+
222+
const method = this.parseMethodSection(section);
223+
if (method) {
224+
methods.push(method);
225+
}
226+
}
227+
228+
return methods;
229+
}
230+
231+
private parseMethodSection(section: string): ApiMethod | null {
232+
// Extract method name from header: ## Method Name {#anchor}
233+
const nameMatch = section.match(/^## ([^{]+)\{#[^}]+\}/m);
234+
if (!nameMatch) return null;
235+
236+
const name = nameMatch[1].trim();
237+
238+
// Extract HTTP method and endpoint: ```http\nMETHOD /path\n```
239+
const httpMatch = section.match(/```http\s*\n([A-Z]+)\s+([^\s\n]+)[^\n]*\n```/);
240+
if (!httpMatch) return null;
241+
242+
const httpMethod = httpMatch[1].trim();
243+
const endpoint = httpMatch[2].trim();
244+
245+
// Extract description (first paragraph after the endpoint)
246+
const descriptionMatch = section.match(/```http[^`]*```\s*\n\n([^*\n][^\n]*)/);
247+
const description = descriptionMatch ? descriptionMatch[1].trim() : '';
248+
249+
// Extract returns, oauth, version info
250+
const returnsMatch = section.match(/\*\*Returns:\*\*\s*([^\\\n]+)/);
251+
const returns = returnsMatch ? this.cleanMarkdown(returnsMatch[1].trim()) : undefined;
252+
253+
const oauthMatch = section.match(/\*\*OAuth:\*\*\s*([^\\\n]+)/);
254+
const oauth = oauthMatch ? this.cleanMarkdown(oauthMatch[1].trim()) : undefined;
255+
256+
const versionMatch = section.match(/\*\*Version history:\*\*\s*([^\n]*)/);
257+
const version = versionMatch ? this.cleanMarkdown(versionMatch[1].trim()) : undefined;
258+
259+
// Parse parameters from Form data parameters section
260+
const parameters = this.parseParameters(section);
261+
262+
return {
263+
name,
264+
httpMethod,
265+
endpoint,
266+
description,
267+
parameters: parameters.length > 0 ? parameters : undefined,
268+
returns,
269+
oauth,
270+
version
271+
};
272+
}
273+
274+
private parseParameters(section: string): ApiParameter[] {
275+
const parameters: ApiParameter[] = [];
276+
277+
// Find parameters section
278+
const paramMatch = section.match(/##### Form data parameters\s*([\s\S]*?)(?=\n#|$)/);
279+
if (!paramMatch) return parameters;
280+
281+
const paramSection = paramMatch[1];
282+
283+
// Match parameter definitions: parameter_name\n: description
284+
const paramRegex = /^([a-zA-Z_][a-zA-Z0-9_]*)\s*\n:\s*([^]*?)(?=\n[a-zA-Z_]|\n\n|$)/gm;
285+
286+
let match;
287+
while ((match = paramRegex.exec(paramSection)) !== null) {
288+
const [, name, desc] = match;
289+
290+
const cleanDesc = this.cleanMarkdown(desc.trim());
291+
const required = cleanDesc.includes('{{<required>}}') || cleanDesc.includes('required');
292+
293+
parameters.push({
294+
name: name.trim(),
295+
description: cleanDesc.replace(/\{\{<required>\}\}\s*/g, ''),
296+
required: required ? true : undefined
297+
});
298+
}
299+
300+
return parameters;
301+
}
302+
303+
private cleanMarkdown(text: string): string {
304+
return text
305+
.replace(/\*\*/g, '') // Remove bold markdown
306+
.replace(/\{\{<[^>]+>\}\}/g, '') // Remove Hugo shortcodes
307+
.replace(/\[[^\]]*\]\([^)]*\)/g, '') // Remove markdown links
308+
.replace(/\\\s*$/, '') // Remove trailing backslashes
309+
.trim();
310+
}
311+
}
312+
132313
function main() {
133314
console.log('Parsing Mastodon entity files...');
134315

@@ -154,10 +335,43 @@ function main() {
154335
}
155336

156337
console.log(`Total entities parsed: ${entities.length}`);
338+
339+
console.log('\nParsing Mastodon API method files...');
340+
341+
const methodParser = new MethodParser();
342+
const methodFiles = methodParser.parseAllMethods();
343+
344+
console.log(`\nFound ${methodFiles.length} method files:\n`);
345+
346+
for (const methodFile of methodFiles) {
347+
console.log(`File: ${methodFile.name}`);
348+
console.log(`Description: ${methodFile.description}`);
349+
console.log(`Methods (${methodFile.methods.length}):`);
350+
351+
for (const method of methodFile.methods) {
352+
console.log(` - ${method.httpMethod} ${method.endpoint}`);
353+
console.log(` Name: ${method.name}`);
354+
console.log(` Description: ${method.description}`);
355+
if (method.returns) console.log(` Returns: ${method.returns}`);
356+
if (method.oauth) console.log(` OAuth: ${method.oauth}`);
357+
if (method.parameters && method.parameters.length > 0) {
358+
console.log(` Parameters (${method.parameters.length}):`);
359+
for (const param of method.parameters) {
360+
const reqText = param.required ? ' [required]' : '';
361+
console.log(` - ${param.name}: ${param.description}${reqText}`);
362+
}
363+
}
364+
}
365+
console.log('');
366+
}
367+
368+
console.log(`Total method files parsed: ${methodFiles.length}`);
369+
const totalMethods = methodFiles.reduce((sum, file) => sum + file.methods.length, 0);
370+
console.log(`Total API methods parsed: ${totalMethods}`);
157371
}
158372

159373
if (require.main === module) {
160374
main();
161375
}
162376

163-
export { EntityParser, EntityClass, EntityAttribute };
377+
export { EntityParser, EntityClass, EntityAttribute, MethodParser, ApiMethodsFile, ApiMethod, ApiParameter };

0 commit comments

Comments
 (0)