Skip to content

Commit bc94cca

Browse files
committed
feat: a little work on a new page generator for endpoint pages
1 parent 5639af1 commit bc94cca

File tree

10 files changed

+560
-9297
lines changed

10 files changed

+560
-9297
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
22

33
# dependencies
4-
/node_modules
4+
node_modules
55
/.pnp
66
.pnp.js
77

generator/index.ts

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
type ACLMode = 'user' | 'system'
2+
3+
export type Page = {
4+
path: string
5+
title: string
6+
meta: string
7+
description: string
8+
sections: Array<{ title: string; description: string }>
9+
endpoints: Array<
10+
{
11+
name: string
12+
path: string
13+
apiLevel: ACLMode
14+
acl: string
15+
description: string
16+
response: {
17+
description?: string
18+
json: string
19+
}
20+
} & (
21+
| { method: 'GET' | 'HEAD'; body?: undefined }
22+
| {
23+
method: 'POST' | 'PATCH' | 'PUT' | 'DELETE'
24+
body:
25+
| string
26+
| {
27+
[key: string]: {
28+
type: string
29+
description: string
30+
example: string
31+
}
32+
}
33+
}
34+
)
35+
>
36+
}
37+
38+
async function createCodeGroupSection(endpoint: Page['endpoints'][number]) {
39+
if (endpoint.method == 'GET' || endpoint.method == 'HEAD' || !endpoint.body) {
40+
return `
41+
<CodeGroup title="Request" tag="${endpoint.method}" label="${endpoint.path}">
42+
43+
\`\`\`bash {{ title: 'cURL' }}
44+
curl -${endpoint.method == 'GET' ? 'G' : 'I'} http://localhost:3000${endpoint.path} \\
45+
-H "Authorization: Bearer {token}"
46+
\`\`\`
47+
48+
\`\`\`js
49+
const response = await fetch("http://localhost:3000${endpoint.path}", {
50+
headers: {
51+
Authorization: "Bearer {token}"
52+
},${endpoint.method == 'HEAD' ? '\n method: "HEAD"' : ''}
53+
});
54+
55+
const results = await response.json();
56+
\`\`\`
57+
58+
</CodeGroup>`
59+
}
60+
61+
return `
62+
<CodeGroup title="Request" tag="${endpoint.method}" label="${endpoint.path}">
63+
64+
\`\`\`bash {{ title: 'cURL' }}
65+
curl -X POST http://localhost:3000${endpoint.path} \\
66+
-H "Authorization: Bearer {token}" \\
67+
-H "Content-Type: application/json" \\
68+
-d "{ ... }"
69+
\`\`\`
70+
71+
\`\`\`js
72+
${(await prettier.format(`const response = await fetch("http://localhost:3000${endpoint.path}", {
73+
headers: {
74+
Authorization: "Bearer {token}"
75+
},
76+
method: "${endpoint.method}",
77+
${
78+
typeof endpoint.body == 'string'
79+
? 'body: /* see notes */'
80+
: `body: {
81+
${Object.entries(endpoint.body)
82+
.map(([name, { example }]) => `${name}: ${example},`)
83+
.join('\n')}
84+
}`
85+
}
86+
});
87+
88+
const body = await response.json();`, {parser: "typescript"})).split("\n").map((v) => ` ${v}`).join("\n")}
89+
\`\`\`
90+
91+
</CodeGroup>
92+
`
93+
}
94+
95+
function createRequestDescription(endpoint: Page['endpoints'][number]) {
96+
if (!endpoint.body) return ''
97+
if (typeof endpoint.body == 'string')
98+
return `## Request
99+
100+
${endpoint.body}`
101+
return `
102+
## Request
103+
104+
<Properties>
105+
${Object.entries(endpoint.body)
106+
.map(
107+
([name, { type, description }]) => `
108+
<Property name="${name}" type="${type}">
109+
${description.trim()}
110+
</Property>
111+
`,
112+
)
113+
.join('\n')}
114+
</Properties>
115+
116+
`
117+
}
118+
119+
import fs from 'node:fs'
120+
import { join } from 'node:path'
121+
import prettier from 'prettier'
122+
123+
const pages = ['./pages/import']
124+
125+
for (const page of pages) {
126+
const pageData: Page = (await import(page)).default
127+
128+
const header = `
129+
export const metadata = {
130+
title: "${pageData.title}",
131+
description: "${pageData.meta}"
132+
};
133+
134+
# ${pageData.title}
135+
136+
${pageData.description}`.trim()
137+
138+
const sections = pageData.sections.map((section) =>
139+
`
140+
## ${section.title}
141+
142+
${section.description}`.trim(),
143+
)
144+
145+
const endpoints = await Promise.all(
146+
pageData.endpoints.map(async (endpoint) =>
147+
`
148+
## ${endpoint.name} {{ tag: '${endpoint.method}', label: '${endpoint.path}', apilevel: "${endpoint.apiLevel}", acl: "${endpoint.acl}" }}
149+
150+
<Row>
151+
<Col>
152+
153+
${endpoint.description}
154+
155+
${createRequestDescription(endpoint)}
156+
157+
${
158+
endpoint.response.description
159+
? `## Response
160+
161+
${endpoint.response.description}`
162+
: ''
163+
}
164+
165+
</Col>
166+
<Col sticky>
167+
168+
${await createCodeGroupSection(endpoint)}
169+
170+
\`\`\`json {{ title: 'Response' }}
171+
${(await prettier.format(endpoint.response.json, { parser: 'json', })).split("\n").map((v) => ` ${v}`).join("\n")}
172+
\`\`\`
173+
174+
</Col>
175+
</Row>`.trim(),
176+
),
177+
)
178+
179+
const finalPage = [header, ...sections, ...endpoints].join('\n\n---\n\n')
180+
181+
fs.mkdirSync(pageData.path, { recursive: true })
182+
fs.writeFileSync(pageData.path + '/page.mdx', finalPage)
183+
}

generator/package.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "generator",
3+
"version": "1.0.0",
4+
"description": "",
5+
"type": "module",
6+
"main": "index.ts",
7+
"scripts": {
8+
"generate": "deno run --allow-sys --allow-env --allow-read --sloppy-imports --allow-write index.ts"
9+
},
10+
"keywords": [],
11+
"author": "",
12+
"license": "ISC",
13+
"packageManager": "[email protected]",
14+
"dependencies": {
15+
"deno": "^2.5.4",
16+
"prettier": "^3.6.2"
17+
}
18+
}

generator/pages/import.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { Page } from '../index.js'
2+
3+
export default {
4+
path: 'web/import',
5+
title: 'Import',
6+
meta: "On this page, we'll dive into how to import games and versions on Drop, and the options for both.",
7+
description: `While games and versions should be covered in separate sections, importing is a complicated enough of a process to warrant a separate page. Importing is the process of pulling and providing metadata for various complex objects in Drop, namely games and versions.
8+
9+
Both games and versions in Drop are required to imported manually, due to them having additional metadata that must be user-provided.`,
10+
sections: [
11+
{
12+
title: 'Game metadata',
13+
description:
14+
"Game metadata is provided by a series of backend 'metadata providers'. Drop unifies them all into a single API to import the metadata, and handle authentication seamlessly.",
15+
},
16+
],
17+
endpoints: [
18+
{
19+
name: 'Fetch unimported games',
20+
path: '/api/v1/admin/import/game',
21+
apiLevel: 'system',
22+
acl: 'import:game:read',
23+
description:
24+
'This endpoint fetches all unimported games on the instance.',
25+
method: 'GET',
26+
response: {
27+
json: `{
28+
"unimportedGames": [
29+
{
30+
"game": "Abiotic Factor",
31+
"library": {
32+
"id": "8dc4b769-090f-4aec-b73a-d8fafc84f418",
33+
"name": "Example Library",
34+
"backend": "Filesystem",
35+
"options": {
36+
"baseDir": "./.data/library"
37+
},
38+
"working": true
39+
}
40+
},
41+
{
42+
"game": "Balatro",
43+
"library": {
44+
"id": "8dc4b769-090f-4aec-b73a-d8fafc84f418",
45+
"name": "Example Library",
46+
"backend": "Filesystem",
47+
"options": {
48+
"baseDir": "./.data/library"
49+
},
50+
"working": true
51+
}
52+
},
53+
{
54+
"game": "SuperTuxKart",
55+
"library": {
56+
"id": "8dc4b769-090f-4aec-b73a-d8fafc84f418",
57+
"name": "Example Library",
58+
"backend": "Filesystem",
59+
"options": {
60+
"baseDir": "./.data/library"
61+
},
62+
"working": true
63+
}
64+
}
65+
]
66+
}`,
67+
},
68+
},
69+
// Search metadata,
70+
{
71+
name: 'Import game',
72+
path: '/api/v1/admin/import/game',
73+
method: 'POST',
74+
apiLevel: 'system',
75+
acl: 'import:game:new',
76+
description: 'This endpoint imports a game, optionally with metadata.',
77+
body: {
78+
library: {
79+
type: 'string',
80+
description:
81+
"The ID of the library you're importing from. Fetched from `library.id` on the GET endpoint.",
82+
example: `"8dc4b769-090f-4aec-b73a-d8fafc84f418"`,
83+
},
84+
path: {
85+
type: 'string',
86+
description:
87+
"Path of the game you're importing. Fetched from the `game` on the GET endpoint.",
88+
example: `"SuperTuxKart"`,
89+
},
90+
metadata: {
91+
type: 'object',
92+
description: `
93+
Optional, metadata to import from. It requires three fields if set:
94+
\`\`\`json
95+
{
96+
"id": "game ID",
97+
"sourceId": "source ID",
98+
"name": "Name of game"
99+
}
100+
\`\`\`
101+
102+
All these properties are returned from the search endpoint. While you can guess these values, as they are generally the internal IDs of the respective platforms, they *are* internal values and are not recommended to be guessed.
103+
104+
For example, if you had the game already from IGDB, you may be able to use:
105+
\`\`\`json
106+
{
107+
"id": "<IGDB ID>",
108+
"sourceId": "IGDB",
109+
"name": "<Name of game on IGDB>"
110+
}
111+
\`\`\`
112+
113+
Without searching for the game first. *This is officially not recommended, but we are unlikely to break this behaviour.*
114+
`,
115+
example: `{
116+
id: "289018",
117+
sourceId: "IGDB",
118+
name: "Example Block Game"
119+
}`,
120+
},
121+
},
122+
response: {
123+
json: `{
124+
"taskId": "..."
125+
}`,
126+
},
127+
},
128+
],
129+
} satisfies Page

0 commit comments

Comments
 (0)