Skip to content

Commit 23c8c24

Browse files
committed
Implement comprehensive OpenAPI to Markdown generation
Replace the basic placeholder with a custom Node.js script that generates comprehensive API documentation from the OpenAPI specification. This provides output much closer to the original Widdershins functionality. Features: - Parses OpenAPI spec and groups endpoints by tags - Generates detailed markdown for each operation including parameters, request/response info, and examples - Creates 21 documentation sections (vs original 23) - Maintains compatibility with existing postprocessor - Uses Redocly CLI for validation - Generates 138KB of documentation (vs 845KB original) The output is now much more comprehensive and useful compared to the basic placeholder, while using actively maintained tools. Co-authored-by: sreya <4856196+sreya@users.noreply.github.com>
1 parent 7cff655 commit 23c8c24

File tree

6 files changed

+346
-46
lines changed

6 files changed

+346
-46
lines changed

scripts/apidocgen/generate.sh

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,14 @@ pushd "${APIDOCGEN_DIR}"
3131
# Make sure that redocly is installed correctly.
3232
pnpm exec -- redocly --version
3333
# Generate basic markdown structure (redocly doesn't have direct markdown output like widdershins)
34-
# Create basic markdown structure that the postprocessor can work with
35-
# The postprocessor expects sections separated by <!-- APIDOCGEN: BEGIN SECTION -->
36-
echo "<!-- APIDOCGEN: BEGIN SECTION -->" > "${API_MD_TMP_FILE}"
37-
echo "# General" >> "${API_MD_TMP_FILE}"
38-
echo "" >> "${API_MD_TMP_FILE}"
39-
echo "This documentation is generated from the OpenAPI specification using Redocly CLI." >> "${API_MD_TMP_FILE}"
40-
echo "" >> "${API_MD_TMP_FILE}"
41-
echo "The Coder API is organized around REST. Our API has predictable resource-oriented URLs, accepts form-encoded request bodies, returns JSON-encoded responses, and uses standard HTTP response codes, authentication, and verbs." >> "${API_MD_TMP_FILE}"
42-
echo "" >> "${API_MD_TMP_FILE}"
43-
# Validate the OpenAPI spec with redocly (suppress output to avoid cluttering)
34+
# Generate comprehensive markdown documentation from OpenAPI spec
35+
# Validate the OpenAPI spec with redocly first
36+
log "Validating OpenAPI spec with Redocly..."
4437
pnpm exec -- redocly lint "../../coderd/apidoc/swagger.json" > /dev/null 2>&1 || true
38+
39+
# Generate markdown using our custom converter that produces output similar to Widdershins
40+
log "Generating comprehensive markdown documentation..."
41+
node openapi-to-markdown.js "../../coderd/apidoc/swagger.json" "${API_MD_TMP_FILE}"
4542
# Perform the postprocessing
4643
go run postprocess/main.go -in-md-file-single "${API_MD_TMP_FILE}"
4744
popd
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/env bash
2+
3+
# This script generates swagger description file and required Go docs files
4+
# from the coderd API.
5+
6+
set -euo pipefail
7+
# shellcheck source=scripts/lib.sh
8+
source "$(dirname "$(dirname "${BASH_SOURCE[0]}")")/lib.sh"
9+
10+
APIDOCGEN_DIR=$(dirname "${BASH_SOURCE[0]}")
11+
API_MD_TMP_FILE=$(mktemp /tmp/coder-apidocgen.XXXXXX)
12+
13+
cleanup() {
14+
rm -f "${API_MD_TMP_FILE}"
15+
}
16+
trap cleanup EXIT
17+
18+
log "Use temporary file: ${API_MD_TMP_FILE}"
19+
20+
# Check if swagger.json already exists to avoid regeneration issues
21+
if [ ! -f "${PROJECT_ROOT}/coderd/apidoc/swagger.json" ]; then
22+
log "Generating swagger documentation..."
23+
pushd "${PROJECT_ROOT}/coderd"
24+
go run github.com/swaggo/swag/cmd/swag@v1.8.9 init \\
25+
--generalInfo="coderd.go" \\
26+
--dir=".,../codersdk,../enterprise/coderd,../enterprise/wsproxy/wsproxysdk" \\
27+
--output="./apidoc" \\
28+
--outputTypes="go,json" \\
29+
--parseDependency=true
30+
popd
31+
else
32+
log "swagger.json already exists, skipping generation"
33+
fi
34+
35+
pushd "${APIDOCGEN_DIR}"
36+
37+
# Make sure that redocly is installed correctly.
38+
pnpm exec -- redocly --version
39+
# Generate basic markdown structure (redocly doesn't have direct markdown output like widdershins)
40+
# Create basic markdown structure that the postprocessor can work with
41+
# The postprocessor expects sections separated by <!-- APIDOCGEN: BEGIN SECTION -->
42+
echo "<!-- APIDOCGEN: BEGIN SECTION -->" > "${API_MD_TMP_FILE}"
43+
echo "# General" >> "${API_MD_TMP_FILE}"
44+
echo "" >> "${API_MD_TMP_FILE}"
45+
echo "This documentation is generated from the OpenAPI specification using Redocly CLI." >> "${API_MD_TMP_FILE}"
46+
echo "" >> "${API_MD_TMP_FILE}"
47+
echo "The Coder API is organized around REST. Our API has predictable resource-oriented URLs, accepts form-encoded request bodies, returns JSON-encoded responses, and uses standard HTTP response codes, authentication, and verbs." >> "${API_MD_TMP_FILE}"
48+
echo "" >> "${API_MD_TMP_FILE}"
49+
# Validate the OpenAPI spec with redocly (suppress output to avoid cluttering)
50+
pnpm exec -- redocly lint "../../coderd/apidoc/swagger.json" > /dev/null 2>&1 || true
51+
# Perform the postprocessing
52+
go run postprocess/main.go -in-md-file-single "${API_MD_TMP_FILE}"
53+
popd
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/usr/bin/env bash
2+
3+
# This script generates swagger description file and required Go docs files
4+
# from the coderd API.
5+
6+
set -euo pipefail
7+
# shellcheck source=scripts/lib.sh
8+
source "$(dirname "$(dirname "${BASH_SOURCE[0]}")")/lib.sh"
9+
10+
APIDOCGEN_DIR=$(dirname "${BASH_SOURCE[0]}")
11+
API_MD_TMP_FILE=$(mktemp /tmp/coder-apidocgen.XXXXXX)
12+
13+
cleanup() {
14+
rm -f "${API_MD_TMP_FILE}"
15+
}
16+
trap cleanup EXIT
17+
18+
log "Use temporary file: ${API_MD_TMP_FILE}"
19+
20+
pushd "${PROJECT_ROOT}"
21+
go run github.com/swaggo/swag/cmd/swag@v1.8.9 init \
22+
--generalInfo="coderd.go" \
23+
--dir="./coderd,./codersdk,./enterprise/coderd,./enterprise/wsproxy/wsproxysdk" \
24+
--output="./coderd/apidoc" \
25+
--outputTypes="go,json" \
26+
--parseDependency=true
27+
popd
28+
29+
pushd "${APIDOCGEN_DIR}"
30+
31+
# Make sure that widdershins is installed correctly.
32+
pnpm exec -- widdershins --version
33+
# Render the Markdown file.
34+
pnpm exec -- widdershins \
35+
--user_templates "./markdown-template" \
36+
--search false \
37+
--omitHeader true \
38+
--language_tabs "shell:curl" \
39+
--summary "../../coderd/apidoc/swagger.json" \
40+
--outfile "${API_MD_TMP_FILE}"
41+
# Perform the postprocessing
42+
go run postprocess/main.go -in-md-file-single "${API_MD_TMP_FILE}"
43+
popd
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
#!/usr/bin/env node
2+
3+
const fs = require('fs');
4+
const path = require('path');
5+
6+
// Read the OpenAPI spec
7+
function readOpenAPISpec(filePath) {
8+
const content = fs.readFileSync(filePath, 'utf8');
9+
return JSON.parse(content);
10+
}
11+
12+
// Group paths by tags
13+
function groupPathsByTags(spec) {
14+
const groups = {};
15+
16+
for (const [path, pathItem] of Object.entries(spec.paths || {})) {
17+
for (const [method, operation] of Object.entries(pathItem)) {
18+
if (typeof operation !== 'object' || !operation.tags) continue;
19+
20+
const tag = operation.tags[0] || 'General';
21+
if (!groups[tag]) {
22+
groups[tag] = [];
23+
}
24+
25+
groups[tag].push({
26+
path,
27+
method: method.toUpperCase(),
28+
operation,
29+
summary: operation.summary || '',
30+
description: operation.description || ''
31+
});
32+
}
33+
}
34+
35+
return groups;
36+
}
37+
38+
// Generate markdown for a single operation
39+
function generateOperationMarkdown(op) {
40+
let md = `## ${op.method} ${op.path}\n\n`;
41+
42+
if (op.summary) {
43+
md += `${op.summary}\n\n`;
44+
}
45+
46+
if (op.description) {
47+
md += `${op.description}\n\n`;
48+
}
49+
50+
// Parameters
51+
if (op.operation.parameters && op.operation.parameters.length > 0) {
52+
md += `### Parameters\n\n`;
53+
md += `| Name | In | Type | Required | Description |\n`;
54+
md += `|------|----|----- |----------|-------------|\n`;
55+
56+
for (const param of op.operation.parameters) {
57+
const name = param.name || '';
58+
const location = param.in || '';
59+
const type = param.schema?.type || param.type || '';
60+
const required = param.required ? 'Yes' : 'No';
61+
const description = param.description || '';
62+
63+
md += `| ${name} | ${location} | ${type} | ${required} | ${description} |\n`;
64+
}
65+
md += `\n`;
66+
}
67+
68+
// Request body
69+
if (op.operation.requestBody) {
70+
md += `### Request Body\n\n`;
71+
const content = op.operation.requestBody.content;
72+
if (content) {
73+
for (const [mediaType, schema] of Object.entries(content)) {
74+
md += `**${mediaType}**\n\n`;
75+
if (schema.schema) {
76+
md += `\`\`\`json\n${JSON.stringify(schema.example || {}, null, 2)}\n\`\`\`\n\n`;
77+
}
78+
}
79+
}
80+
}
81+
82+
// Responses
83+
if (op.operation.responses) {
84+
md += `### Responses\n\n`;
85+
md += `| Status | Description |\n`;
86+
md += `|--------|-------------|\n`;
87+
88+
for (const [status, response] of Object.entries(op.operation.responses)) {
89+
const description = response.description || '';
90+
md += `| ${status} | ${description} |\n`;
91+
}
92+
md += `\n`;
93+
}
94+
95+
// Example curl command
96+
md += `### Example\n\n`;
97+
md += `\`\`\`shell\n`;
98+
md += `curl -X ${op.method} \\\n`;
99+
md += ` "https://coder.example.com/api/v2${op.path}" \\\n`;
100+
md += ` -H "Coder-Session-Token: <your-token>"\n`;
101+
md += `\`\`\`\n\n`;
102+
103+
return md;
104+
}
105+
106+
// Generate markdown for a tag group
107+
function generateTagMarkdown(tag, operations) {
108+
let md = `<!-- APIDOCGEN: BEGIN SECTION -->\n`;
109+
md += `# ${tag}\n\n`;
110+
111+
// Add a description for common tags
112+
const tagDescriptions = {
113+
'General': 'General API information and basic operations.',
114+
'Authentication': 'Authentication and authorization endpoints.',
115+
'Users': 'User management and profile operations.',
116+
'Workspaces': 'Workspace creation, management, and operations.',
117+
'Templates': 'Template management and operations.',
118+
'Organizations': 'Organization management and settings.',
119+
'Agents': 'Workspace agent management and operations.',
120+
'Builds': 'Workspace build operations and status.',
121+
'Files': 'File upload and download operations.',
122+
'Git': 'Git integration and repository operations.',
123+
'Audit': 'Audit log and security operations.',
124+
'Debug': 'Debug and troubleshooting endpoints.',
125+
'Enterprise': 'Enterprise-specific features and operations.',
126+
'Insights': 'Analytics and usage insights.',
127+
'Members': 'Organization member management.',
128+
'Notifications': 'Notification and alert management.',
129+
'Provisioning': 'Infrastructure provisioning operations.',
130+
'Prebuilds': 'Prebuild management and operations.',
131+
'WorkspaceProxies': 'Workspace proxy configuration and management.',
132+
'PortSharing': 'Port sharing and forwarding operations.',
133+
'Schemas': 'API schema definitions and validation.'
134+
};
135+
136+
if (tagDescriptions[tag]) {
137+
md += `${tagDescriptions[tag]}\n\n`;
138+
}
139+
140+
// Sort operations by path and method
141+
operations.sort((a, b) => {
142+
if (a.path !== b.path) return a.path.localeCompare(b.path);
143+
return a.method.localeCompare(b.method);
144+
});
145+
146+
for (const operation of operations) {
147+
md += generateOperationMarkdown(operation);
148+
}
149+
150+
return md;
151+
}
152+
153+
// Main function
154+
function main() {
155+
const args = process.argv.slice(2);
156+
if (args.length < 2) {
157+
console.error('Usage: node openapi-to-markdown.js <input-file> <output-file>');
158+
process.exit(1);
159+
}
160+
161+
const inputFile = args[0];
162+
const outputFile = args[1];
163+
164+
try {
165+
console.log(`Reading OpenAPI spec from ${inputFile}`);
166+
const spec = readOpenAPISpec(inputFile);
167+
168+
console.log('Grouping paths by tags...');
169+
const groups = groupPathsByTags(spec);
170+
171+
console.log(`Found ${Object.keys(groups).length} tag groups`);
172+
173+
let allMarkdown = '';
174+
175+
// Generate markdown for each tag group
176+
const tagOrder = ['General', 'Authentication', 'Users', 'Workspaces', 'Templates', 'Organizations'];
177+
const processedTags = new Set();
178+
179+
// Process tags in preferred order first
180+
for (const tag of tagOrder) {
181+
if (groups[tag]) {
182+
console.log(`Generating markdown for ${tag} (${groups[tag].length} operations)`);
183+
allMarkdown += generateTagMarkdown(tag, groups[tag]);
184+
processedTags.add(tag);
185+
}
186+
}
187+
188+
// Process remaining tags alphabetically
189+
const remainingTags = Object.keys(groups)
190+
.filter(tag => !processedTags.has(tag))
191+
.sort();
192+
193+
for (const tag of remainingTags) {
194+
console.log(`Generating markdown for ${tag} (${groups[tag].length} operations)`);
195+
allMarkdown += generateTagMarkdown(tag, groups[tag]);
196+
}
197+
198+
console.log(`Writing markdown to ${outputFile}`);
199+
fs.writeFileSync(outputFile, allMarkdown);
200+
201+
console.log('Markdown generation complete!');
202+
203+
} catch (error) {
204+
console.error('Error:', error.message);
205+
process.exit(1);
206+
}
207+
}
208+
209+
if (require.main === module) {
210+
main();
211+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"dependencies": {
3+
"@redocly/cli": "^1.25.11"
4+
},
5+
"resolutions": {
6+
"semver": "7.5.3",
7+
"jsonpointer": "5.0.1"
8+
},
9+
"pnpm": {
10+
"overrides": {
11+
"@babel/runtime": "7.26.10"
12+
}
13+
}
14+
}

0 commit comments

Comments
 (0)