Skip to content

Commit 50d9956

Browse files
authored
Merge pull request #543 from abraham/async-refresh
Add Mastodon-Async-Refresh header
2 parents 1b6c042 + 7f1f5c7 commit 50d9956

File tree

3 files changed

+201
-0
lines changed

3 files changed

+201
-0
lines changed

dist/schema.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23417,6 +23417,12 @@
2341723417
"type": "string",
2341823418
"format": "date-time"
2341923419
}
23420+
},
23421+
"Mastodon-Async-Refresh": {
23422+
"description": "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.",
23423+
"schema": {
23424+
"type": "string"
23425+
}
2342023426
}
2342123427
},
2342223428
"content": {
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { OpenAPIGenerator } from '../../generators/OpenAPIGenerator';
2+
import { ApiMethodsFile } from '../../interfaces/ApiMethodsFile';
3+
4+
describe('OpenAPIGenerator - Mastodon-Async-Refresh Header', () => {
5+
let generator: OpenAPIGenerator;
6+
7+
beforeEach(() => {
8+
generator = new OpenAPIGenerator();
9+
});
10+
11+
it('should add Mastodon-Async-Refresh header to status context endpoint', () => {
12+
const methodFiles: ApiMethodsFile[] = [
13+
{
14+
name: 'statuses',
15+
description: 'Status methods',
16+
methods: [
17+
{
18+
name: 'Get parent and child statuses in context',
19+
httpMethod: 'GET',
20+
endpoint: '/api/v1/statuses/:id/context',
21+
description:
22+
'View statuses above and below this status in the thread.',
23+
returns: 'Context',
24+
},
25+
],
26+
},
27+
];
28+
29+
const spec = generator.generateSchema([], methodFiles);
30+
31+
// Find the status context endpoint
32+
const contextPath = '/api/v1/statuses/{id}/context';
33+
expect(spec.paths[contextPath]).toBeDefined();
34+
35+
const getOperation = spec.paths[contextPath]?.get;
36+
expect(getOperation).toBeDefined();
37+
expect(getOperation?.operationId).toBe('getStatusContext');
38+
39+
// Check that 200 response has headers
40+
const response200 = getOperation?.responses['200'];
41+
expect(response200).toBeDefined();
42+
expect(response200?.headers).toBeDefined();
43+
44+
// Verify rate limit headers are present
45+
expect(response200?.headers?.['X-RateLimit-Limit']).toBeDefined();
46+
expect(response200?.headers?.['X-RateLimit-Remaining']).toBeDefined();
47+
expect(response200?.headers?.['X-RateLimit-Reset']).toBeDefined();
48+
49+
// Verify Mastodon-Async-Refresh header is present
50+
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'
62+
);
63+
});
64+
65+
it('should not add Mastodon-Async-Refresh header to other status endpoints', () => {
66+
const methodFiles: ApiMethodsFile[] = [
67+
{
68+
name: 'statuses',
69+
description: 'Status methods',
70+
methods: [
71+
{
72+
name: 'View specific status',
73+
httpMethod: 'GET',
74+
endpoint: '/api/v1/statuses/:id',
75+
description: 'View information about a status.',
76+
returns: 'Status',
77+
},
78+
{
79+
name: 'Favourite status',
80+
httpMethod: 'POST',
81+
endpoint: '/api/v1/statuses/:id/favourite',
82+
description: 'Add a status to your favourites list.',
83+
returns: 'Status',
84+
},
85+
{
86+
name: 'Reblog status',
87+
httpMethod: 'POST',
88+
endpoint: '/api/v1/statuses/:id/reblog',
89+
description: 'Reshare a status.',
90+
returns: 'Status',
91+
},
92+
{
93+
name: 'Bookmark status',
94+
httpMethod: 'POST',
95+
endpoint: '/api/v1/statuses/:id/bookmark',
96+
description: 'Bookmark a status.',
97+
returns: 'Status',
98+
},
99+
],
100+
},
101+
];
102+
103+
const spec = generator.generateSchema([], methodFiles);
104+
105+
// Check that other status endpoints don't have the header
106+
const otherEndpoints = [
107+
'/api/v1/statuses/{id}',
108+
'/api/v1/statuses/{id}/favourite',
109+
'/api/v1/statuses/{id}/reblog',
110+
'/api/v1/statuses/{id}/bookmark',
111+
];
112+
113+
for (const endpoint of otherEndpoints) {
114+
if (spec.paths[endpoint]) {
115+
const operations = spec.paths[endpoint];
116+
for (const operation of Object.values(operations)) {
117+
if (
118+
operation &&
119+
typeof operation === 'object' &&
120+
'responses' in operation
121+
) {
122+
const op = operation as {
123+
responses: Record<string, { headers?: Record<string, unknown> }>;
124+
};
125+
if (op.responses?.['200']?.headers) {
126+
expect(
127+
op.responses['200'].headers['Mastodon-Async-Refresh']
128+
).toBeUndefined();
129+
}
130+
}
131+
}
132+
}
133+
}
134+
});
135+
136+
it('should include correct format description in Mastodon-Async-Refresh header', () => {
137+
const methodFiles: ApiMethodsFile[] = [
138+
{
139+
name: 'statuses',
140+
description: 'Status methods',
141+
methods: [
142+
{
143+
name: 'Get parent and child statuses in context',
144+
httpMethod: 'GET',
145+
endpoint: '/api/v1/statuses/:id/context',
146+
description:
147+
'View statuses above and below this status in the thread.',
148+
returns: 'Context',
149+
},
150+
],
151+
},
152+
];
153+
154+
const spec = generator.generateSchema([], methodFiles);
155+
156+
const contextPath = '/api/v1/statuses/{id}/context';
157+
const getOperation = spec.paths[contextPath]?.get;
158+
const asyncRefreshHeader =
159+
getOperation?.responses['200']?.headers?.['Mastodon-Async-Refresh'];
160+
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>');
166+
});
167+
});

src/generators/MethodConverter.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,29 @@ class MethodConverter {
628628
};
629629
}
630630

631+
/**
632+
* Check if a method is the status context endpoint
633+
*/
634+
private isStatusContextMethod(method: ApiMethod): boolean {
635+
return (
636+
method.httpMethod === 'GET' &&
637+
method.endpoint === '/api/v1/statuses/:id/context'
638+
);
639+
}
640+
641+
/**
642+
* Generate Mastodon-Async-Refresh header for status context endpoint
643+
*/
644+
private generateAsyncRefreshHeader(): OpenAPIHeader {
645+
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+
},
651+
};
652+
}
653+
631654
/**
632655
* Generate combined headers for 2xx responses (rate limit + Link if applicable)
633656
*/
@@ -643,6 +666,11 @@ class MethodConverter {
643666
headers['Link'] = this.generateLinkHeader();
644667
}
645668

669+
// Add Mastodon-Async-Refresh header for status context endpoint
670+
if (this.isStatusContextMethod(method)) {
671+
headers['Mastodon-Async-Refresh'] = this.generateAsyncRefreshHeader();
672+
}
673+
646674
return headers;
647675
}
648676

0 commit comments

Comments
 (0)