Skip to content

Commit acceeee

Browse files
heiskrCopilot
andauthored
Article API: deduplicate REST response schemas and simplify curl (#60073)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 17719a3 commit acceeee

3 files changed

Lines changed: 50 additions & 19 deletions

File tree

src/article-api/templates/rest-page.template.md

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
{{ manualContent }}
66

7+
> [!NOTE]
8+
> Most endpoints use `Authorization: Bearer <YOUR-TOKEN>` and `Accept: application/vnd.github+json` headers{% if apiVersion %}, plus `X-GitHub-Api-Version: {{ apiVersion }}`{% endif %}. Curl examples below omit these standard headers for brevity.
9+
710
{% for operation in restOperations %}
811

912
## {{ operation.title }}
@@ -81,13 +84,8 @@
8184
```curl
8285
curl -L \
8386
-X {{ operation.verb | upcase }} \
84-
{{ example.request.url }} \
85-
{%- if example.request.acceptHeader %}
86-
-H "Accept: {{ example.request.acceptHeader }}" \
87-
{%- endif %}
88-
-H "Authorization: Bearer <YOUR-TOKEN>"{% if apiVersion %} \
89-
-H "X-GitHub-Api-Version: {{ apiVersion }}"{% endif -%}
90-
{%- if example.request.bodyParameters %} \
87+
{{ example.request.url }}{% if example.request.acceptHeader and example.request.acceptHeader != 'application/vnd.github+json' and example.request.acceptHeader != 'application/vnd.github.v3+json' %} \
88+
-H "Accept: {{ example.request.acceptHeader }}"{% endif %}{% if example.request.bodyParameters %} \
9189
-d '{{ example.request.bodyParameters }}'{% endif %}
9290
```
9391

src/article-api/tests/rest-transformer.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -113,25 +113,33 @@ describe('REST transformer', () => {
113113
expect(res.body).toContain('```curl')
114114
expect(res.body).toContain('curl -L \\')
115115
expect(res.body).toContain('-X GET \\')
116-
expect(res.body).toContain('https://api.github.com/repos/OWNER/REPO/actions/artifacts \\')
117-
expect(res.body).toContain('-H "Accept: application/vnd.github.v3+json" \\')
118-
expect(res.body).toContain('-H "Authorization: Bearer <YOUR-TOKEN>"')
116+
expect(res.body).toContain('https://api.github.com/repos/OWNER/REPO/actions/artifacts')
119117
})
120118

121-
test('Code examples include X-GitHub-Api-Version header by default', async () => {
119+
test('Authentication note is included at top of page', async () => {
122120
const res = await get(makeURL('/en/rest/actions/artifacts'))
123121
expect(res.statusCode).toBe(200)
124122

125-
// Check for API version header in curl example
126-
expect(res.body).toContain('-H "X-GitHub-Api-Version: 2026-03-10"')
123+
// Check that auth note is at the top using [!NOTE] syntax
124+
expect(res.body).toContain('[!NOTE]')
125+
expect(res.body).toContain('Authorization: Bearer <YOUR-TOKEN>')
126+
expect(res.body).toContain('application/vnd.github+json')
127127
})
128128

129-
test('Code examples include specified API version', async () => {
129+
test('API version is mentioned in auth note', async () => {
130+
const res = await get(makeURL('/en/rest/actions/artifacts'))
131+
expect(res.statusCode).toBe(200)
132+
133+
// Check for API version in the auth note (any valid date format)
134+
expect(res.body).toMatch(/X-GitHub-Api-Version: \d{4}-\d{2}-\d{2}/)
135+
})
136+
137+
test('Code examples include specified API version in auth note', async () => {
130138
const res = await get(makeURL('/en/rest/actions/artifacts', '2022-11-28'))
131139
expect(res.statusCode).toBe(200)
132140

133-
// Check for the specified API version header
134-
expect(res.body).toContain('-H "X-GitHub-Api-Version: 2022-11-28"')
141+
// Check for the specified API version in auth note
142+
expect(res.body).toContain('X-GitHub-Api-Version: 2022-11-28')
135143
})
136144

137145
test('Liquid tags are rendered in intro', async () => {
@@ -224,16 +232,16 @@ describe('REST transformer', () => {
224232
const res = await get(makeURL('/en/rest/actions/artifacts', '2022-11-28'))
225233

226234
expect(res.statusCode).toBe(200)
227-
expect(res.body).toContain('-H "X-GitHub-Api-Version: 2022-11-28"')
235+
expect(res.body).toContain('X-GitHub-Api-Version: 2022-11-28')
228236
})
229237

230238
test('Missing apiVersion defaults to latest', async () => {
231239
// When no apiVersion is provided, it should default to the latest version
232240
const res = await get(makeURL('/en/rest/actions/artifacts'))
233241

234242
expect(res.statusCode).toBe(200)
235-
// Should include the default API version header
236-
expect(res.body).toContain('-H "X-GitHub-Api-Version: 2026-03-10"')
243+
// Should include the default API version in auth note (any valid date format)
244+
expect(res.body).toMatch(/X-GitHub-Api-Version: \d{4}-\d{2}-\d{2}/)
237245
})
238246

239247
test('Multiple operations on a page are all rendered', async () => {

src/article-api/transformers/rest-transformer.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { loadTemplate } from '@/article-api/lib/load-template'
66
import { summarizeSchema } from '@/article-api/lib/summarize-schema'
77
import matter from '@gr2m/gray-matter'
88
import { fastTextOnly } from '@/content-render/unified/text-only'
9+
import GithubSlugger from 'github-slugger'
910

1011
const DEBUG = process.env.RUNNER_DEBUG === '1' || process.env.DEBUG === '1'
1112

@@ -134,6 +135,30 @@ export class RestTransformer implements PageTransformer {
134135
operations.map(async (operation) => await this.prepareOperation(operation)),
135136
)
136137

138+
// Deduplicate identical response schemas across operations on the same page.
139+
// When multiple endpoints share the same schema, render it once and reference it.
140+
const slugger = new GithubSlugger()
141+
const titleToSlug = new Map<string, string>()
142+
for (const op of preparedOperations) {
143+
titleToSlug.set(op.title, slugger.slug(op.title))
144+
}
145+
const schemaMap = new Map<string, string>()
146+
for (const op of preparedOperations) {
147+
if (!op.codeExamples) continue
148+
for (const example of op.codeExamples as any[]) {
149+
const schema = example.response?.schema
150+
if (!schema || typeof schema !== 'string') continue
151+
152+
const existing = schemaMap.get(schema)
153+
if (existing && existing !== op.title) {
154+
const slug = titleToSlug.get(existing) || ''
155+
example.response.schema = `Same response schema as [${existing}](#${slug}).`
156+
} else if (!existing) {
157+
schemaMap.set(schema, op.title)
158+
}
159+
}
160+
}
161+
137162
return {
138163
page: {
139164
title: page.title,

0 commit comments

Comments
 (0)