Skip to content

Commit 4e64618

Browse files
authored
Merge pull request #544 from abraham/headers
Extract headers to components
2 parents 50d9956 + 735fd0e commit 4e64618

File tree

9 files changed

+1221
-2850
lines changed

9 files changed

+1221
-2850
lines changed

dist/schema.json

Lines changed: 778 additions & 2775 deletions
Large diffs are not rendered by default.

src/__tests__/generators/OpenAPIGenerator.asyncRefreshHeader.test.ts

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,8 @@ describe('OpenAPIGenerator - Mastodon-Async-Refresh Header', () => {
4848

4949
// Verify Mastodon-Async-Refresh header is present
5050
expect(response200?.headers?.['Mastodon-Async-Refresh']).toBeDefined();
51-
expect(
52-
response200?.headers?.['Mastodon-Async-Refresh']?.description
53-
).toContain('async refresh');
54-
expect(
55-
response200?.headers?.['Mastodon-Async-Refresh']?.description
56-
).toContain('retry');
57-
expect(
58-
response200?.headers?.['Mastodon-Async-Refresh']?.description
59-
).toContain('result_count');
60-
expect(response200?.headers?.['Mastodon-Async-Refresh']?.schema.type).toBe(
61-
'string'
51+
expect(response200?.headers?.['Mastodon-Async-Refresh']?.$ref).toBe(
52+
'#/components/headers/Mastodon-Async-Refresh'
6253
);
6354
});
6455

@@ -158,10 +149,18 @@ describe('OpenAPIGenerator - Mastodon-Async-Refresh Header', () => {
158149
const asyncRefreshHeader =
159150
getOperation?.responses['200']?.headers?.['Mastodon-Async-Refresh'];
160151

161-
expect(asyncRefreshHeader?.description).toContain('id=');
162-
expect(asyncRefreshHeader?.description).toContain('retry=');
163-
expect(asyncRefreshHeader?.description).toContain('result_count=');
164-
expect(asyncRefreshHeader?.description).toContain('<string>');
165-
expect(asyncRefreshHeader?.description).toContain('<int>');
152+
// Should reference the shared component
153+
expect(asyncRefreshHeader?.$ref).toBe(
154+
'#/components/headers/Mastodon-Async-Refresh'
155+
);
156+
157+
// Check the component definition has the correct format description
158+
const componentHeader =
159+
spec.components?.headers?.['Mastodon-Async-Refresh'];
160+
expect(componentHeader?.description).toContain('id=');
161+
expect(componentHeader?.description).toContain('retry=');
162+
expect(componentHeader?.description).toContain('result_count=');
163+
expect(componentHeader?.description).toContain('<string>');
164+
expect(componentHeader?.description).toContain('<int>');
166165
});
167166
});

src/__tests__/generators/OpenAPIGenerator.linkHeaders.test.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,9 @@ describe('OpenAPIGenerator Link Headers', () => {
5555

5656
// Should include Link header
5757
expect(response200.headers?.['Link']).toBeDefined();
58-
expect(response200.headers?.['Link'].description).toContain(
59-
'Pagination links'
58+
expect(response200.headers?.['Link'].$ref).toBe(
59+
'#/components/headers/Link'
6060
);
61-
expect(response200.headers?.['Link'].schema.type).toBe('string');
6261

6362
// Should also include rate limit headers
6463
expect(response200.headers?.['X-RateLimit-Limit']).toBeDefined();
@@ -104,8 +103,8 @@ describe('OpenAPIGenerator Link Headers', () => {
104103
if (response200) {
105104
expect(response200.headers).toBeDefined();
106105
expect(response200.headers?.['Link']).toBeDefined();
107-
expect(response200.headers?.['Link'].description).toContain(
108-
'Pagination links'
106+
expect(response200.headers?.['Link'].$ref).toBe(
107+
'#/components/headers/Link'
109108
);
110109
}
111110
});
@@ -147,8 +146,8 @@ describe('OpenAPIGenerator Link Headers', () => {
147146
if (response200) {
148147
expect(response200.headers).toBeDefined();
149148
expect(response200.headers?.['Link']).toBeDefined();
150-
expect(response200.headers?.['Link'].description).toContain(
151-
'Pagination links'
149+
expect(response200.headers?.['Link'].$ref).toBe(
150+
'#/components/headers/Link'
152151
);
153152
}
154153
});

src/__tests__/generators/OpenAPIGenerator.rateLimitHeaders.test.ts

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -40,30 +40,18 @@ describe('OpenAPIGenerator Rate Limit Headers', () => {
4040

4141
// Should include rate limit headers
4242
expect(response200.headers?.['X-RateLimit-Limit']).toBeDefined();
43-
expect(
44-
response200.headers?.['X-RateLimit-Limit'].description
45-
).toContain('Number of requests permitted');
46-
expect(response200.headers?.['X-RateLimit-Limit'].schema.type).toBe(
47-
'integer'
43+
expect(response200.headers?.['X-RateLimit-Limit'].$ref).toBe(
44+
'#/components/headers/X-RateLimit-Limit'
4845
);
4946

5047
expect(response200.headers?.['X-RateLimit-Remaining']).toBeDefined();
51-
expect(
52-
response200.headers?.['X-RateLimit-Remaining'].description
53-
).toContain('Number of requests you can still make');
54-
expect(response200.headers?.['X-RateLimit-Remaining'].schema.type).toBe(
55-
'integer'
48+
expect(response200.headers?.['X-RateLimit-Remaining'].$ref).toBe(
49+
'#/components/headers/X-RateLimit-Remaining'
5650
);
5751

5852
expect(response200.headers?.['X-RateLimit-Reset']).toBeDefined();
59-
expect(
60-
response200.headers?.['X-RateLimit-Reset'].description
61-
).toContain('Timestamp when your rate limit will reset');
62-
expect(response200.headers?.['X-RateLimit-Reset'].schema.type).toBe(
63-
'string'
64-
);
65-
expect(response200.headers?.['X-RateLimit-Reset'].schema.format).toBe(
66-
'date-time'
53+
expect(response200.headers?.['X-RateLimit-Reset'].$ref).toBe(
54+
'#/components/headers/X-RateLimit-Reset'
6755
);
6856
}
6957
});
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { HeaderParser } from '../../parsers/HeaderParser';
2+
3+
describe('HeaderParser', () => {
4+
describe('parseHeaders', () => {
5+
it('should parse all headers from documentation', () => {
6+
const headers = HeaderParser.parseHeaders();
7+
8+
// Should return 5 headers
9+
expect(headers).toHaveLength(5);
10+
11+
// Check X-RateLimit-Limit
12+
const rateLimitLimit = headers.find(
13+
(h) => h.name === 'X-RateLimit-Limit'
14+
);
15+
expect(rateLimitLimit).toBeDefined();
16+
expect(rateLimitLimit?.description).toBe(
17+
'Number of requests permitted per time period'
18+
);
19+
expect(rateLimitLimit?.schema.type).toBe('integer');
20+
21+
// Check X-RateLimit-Remaining
22+
const rateLimitRemaining = headers.find(
23+
(h) => h.name === 'X-RateLimit-Remaining'
24+
);
25+
expect(rateLimitRemaining).toBeDefined();
26+
expect(rateLimitRemaining?.description).toBe(
27+
'Number of requests you can still make'
28+
);
29+
expect(rateLimitRemaining?.schema.type).toBe('integer');
30+
31+
// Check X-RateLimit-Reset
32+
const rateLimitReset = headers.find(
33+
(h) => h.name === 'X-RateLimit-Reset'
34+
);
35+
expect(rateLimitReset).toBeDefined();
36+
expect(rateLimitReset?.description).toContain('Timestamp');
37+
expect(rateLimitReset?.schema.type).toBe('string');
38+
expect(rateLimitReset?.schema.format).toBe('date-time');
39+
40+
// Check Link header
41+
const link = headers.find((h) => h.name === 'Link');
42+
expect(link).toBeDefined();
43+
expect(link?.description).toContain('Pagination links');
44+
expect(link?.description).toContain('Format:');
45+
expect(link?.description).toContain('RFC 8288');
46+
expect(link?.schema.type).toBe('string');
47+
48+
// Check Mastodon-Async-Refresh header
49+
const asyncRefresh = headers.find(
50+
(h) => h.name === 'Mastodon-Async-Refresh'
51+
);
52+
expect(asyncRefresh).toBeDefined();
53+
expect(asyncRefresh?.description).toContain('async refresh');
54+
expect(asyncRefresh?.description).toContain('Format:');
55+
expect(asyncRefresh?.description).toContain('retry');
56+
expect(asyncRefresh?.description).toContain('result_count');
57+
expect(asyncRefresh?.schema.type).toBe('string');
58+
});
59+
60+
it('should parse rate limit headers correctly', () => {
61+
const headers = HeaderParser.parseHeaders();
62+
const rateLimitHeaders = headers.filter((h) =>
63+
h.name.startsWith('X-RateLimit-')
64+
);
65+
66+
expect(rateLimitHeaders).toHaveLength(3);
67+
68+
// All rate limit headers should have descriptions from the docs
69+
rateLimitHeaders.forEach((header) => {
70+
expect(header.description).toBeTruthy();
71+
expect(header.description.length).toBeGreaterThan(0);
72+
expect(header.schema).toBeDefined();
73+
expect(header.schema.type).toBeTruthy();
74+
});
75+
});
76+
77+
it('should include example format in Link header description', () => {
78+
const headers = HeaderParser.parseHeaders();
79+
const linkHeader = headers.find((h) => h.name === 'Link');
80+
81+
expect(linkHeader).toBeDefined();
82+
expect(linkHeader?.description).toContain('mastodon.example');
83+
expect(linkHeader?.description).toContain('max_id');
84+
expect(linkHeader?.description).toContain('min_id');
85+
expect(linkHeader?.description).toContain('rel="next"');
86+
expect(linkHeader?.description).toContain('rel="prev"');
87+
});
88+
89+
it('should include format details in Mastodon-Async-Refresh description', () => {
90+
const headers = HeaderParser.parseHeaders();
91+
const asyncRefreshHeader = headers.find(
92+
(h) => h.name === 'Mastodon-Async-Refresh'
93+
);
94+
95+
expect(asyncRefreshHeader).toBeDefined();
96+
expect(asyncRefreshHeader?.description).toContain('id=');
97+
expect(asyncRefreshHeader?.description).toContain('retry=');
98+
expect(asyncRefreshHeader?.description).toContain('result_count=');
99+
expect(asyncRefreshHeader?.description).toContain('<string>');
100+
expect(asyncRefreshHeader?.description).toContain('<int>');
101+
});
102+
});
103+
});

src/generators/MethodConverter.ts

Lines changed: 10 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -576,25 +576,13 @@ class MethodConverter {
576576
/**
577577
* Generate rate limit headers for 2xx responses
578578
*/
579-
private generateRateLimitHeaders(): Record<string, OpenAPIHeader> {
580-
const headers: Record<string, OpenAPIHeader> = {};
579+
private generateRateLimitHeaders(): Record<string, any> {
580+
const headers: Record<string, any> = {};
581581

582582
for (const header of this.rateLimitHeaders) {
583-
let schema: { type: string; format?: string } = { type: 'string' };
584-
585-
// Set appropriate schema types based on header name
586-
if (
587-
header.name === 'X-RateLimit-Limit' ||
588-
header.name === 'X-RateLimit-Remaining'
589-
) {
590-
schema = { type: 'integer' };
591-
} else if (header.name === 'X-RateLimit-Reset') {
592-
schema = { type: 'string', format: 'date-time' };
593-
}
594-
583+
// Reference the shared component
595584
headers[header.name] = {
596-
description: header.description,
597-
schema,
585+
$ref: `#/components/headers/${header.name}`,
598586
};
599587
}
600588

@@ -618,13 +606,9 @@ class MethodConverter {
618606
/**
619607
* Generate Link header for pagination
620608
*/
621-
private generateLinkHeader(): OpenAPIHeader {
609+
private generateLinkHeader(): any {
622610
return {
623-
description:
624-
'Pagination links for browsing older or newer results. Format: <https://mastodon.example/api/v1/endpoint?max_id=123456>; rel="next", <https://mastodon.example/api/v1/endpoint?min_id=789012>; rel="prev"',
625-
schema: {
626-
type: 'string',
627-
},
611+
$ref: '#/components/headers/Link',
628612
};
629613
}
630614

@@ -641,23 +625,17 @@ class MethodConverter {
641625
/**
642626
* Generate Mastodon-Async-Refresh header for status context endpoint
643627
*/
644-
private generateAsyncRefreshHeader(): OpenAPIHeader {
628+
private generateAsyncRefreshHeader(): any {
645629
return {
646-
description:
647-
'Indicates an async refresh is in progress. Format: id="<string>", retry=<int>, result_count=<int>. The retry value indicates seconds to wait before retrying. The result_count is optional and indicates results already fetched.',
648-
schema: {
649-
type: 'string',
650-
},
630+
$ref: '#/components/headers/Mastodon-Async-Refresh',
651631
};
652632
}
653633

654634
/**
655635
* Generate combined headers for 2xx responses (rate limit + Link if applicable)
656636
*/
657-
private generateResponseHeaders(
658-
method: ApiMethod
659-
): Record<string, OpenAPIHeader> {
660-
const headers: Record<string, OpenAPIHeader> = {
637+
private generateResponseHeaders(method: ApiMethod): Record<string, any> {
638+
const headers: Record<string, any> = {
661639
...this.generateRateLimitHeaders(),
662640
};
663641

src/generators/SpecBuilder.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,30 @@
11
import { readFileSync } from 'fs';
22
import { OpenAPISpec } from '../interfaces/OpenAPISchema';
33
import { OAuthScopeParser } from '../parsers/OAuthScopeParser';
4+
import { HeaderParser } from '../parsers/HeaderParser';
45
import { SUPPORTED_VERSION } from '../parsers/VersionParser';
56

67
/**
78
* Builder for OpenAPI specification with authentication setup
89
*/
910
class SpecBuilder {
11+
/**
12+
* Build header components from documentation
13+
*/
14+
private buildHeaderComponents(): Record<string, any> {
15+
const headers: Record<string, any> = {};
16+
const parsedHeaders = HeaderParser.parseHeaders();
17+
18+
for (const header of parsedHeaders) {
19+
headers[header.name] = {
20+
description: header.description,
21+
schema: header.schema,
22+
};
23+
}
24+
25+
return headers;
26+
}
27+
1028
/**
1129
* Build initial OpenAPI specification with OAuth configuration
1230
*/
@@ -106,6 +124,7 @@ class SpecBuilder {
106124
},
107125
},
108126
},
127+
headers: this.buildHeaderComponents(),
109128
},
110129
};
111130
}

src/interfaces/OpenAPISchema.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,12 @@ interface OpenAPIResponse {
119119
}
120120

121121
interface OpenAPIHeader {
122-
description: string;
123-
schema: {
122+
description?: string;
123+
schema?: {
124124
type: string;
125125
format?: string;
126126
};
127+
$ref?: string;
127128
}
128129

129130
interface OpenAPIOperation {
@@ -173,6 +174,7 @@ interface OpenAPISpec {
173174
schemas?: Record<string, OpenAPISchema>;
174175
examples?: Record<string, OpenAPIExample>;
175176
securitySchemes?: Record<string, OpenAPISecurityScheme>;
177+
headers?: Record<string, OpenAPIHeader>;
176178
links?: Record<string, OpenAPILink>;
177179
};
178180
}

0 commit comments

Comments
 (0)