mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: Build framework for generating API docs (#5383)
* WIP * Gen * WIP * chi swagger * WIP * WIP * WIP * GetWorkspaces * GetWorkspaces * Markdown * Use widdershins * WIP * WIP * WIP * Markdown template * Fix: makefile * fmt * Fix: comment * Enable swagger conditionally * fix: site * Default false * Flag tests * fix * fix * template fixes * Fix * Fix * Fix * WIP * Formatted * Cleanup * Templates * BEGIN END SECTION * subshell exit code * Fix * Fix merge * WIP * Fix * Fix fmt * Fix * Generic api.md page * Fix merge * Link pages * Fix * Fix * Fix: links * Add icon * Write manifest file * Fix fmt * Fix: enterprise * Fix: Swagger.Enable * Fix: rename apidocs to apidoc * Fix: find -not -prune * Fix: json not available * Fix: rename Coderd API to Coder API * Fix: npm exec * Fix: api dir * Fix: by ID * Fix: string uuid * Fix: include deleted * Fix: indirect go.mod * Fix: source lib.sh * Fix: shellcheck * Fix: pushd popd * Fix: fmt * Fix: improve workspaces * Fix: swagger-enable * Fix * Fix: mention only HTTP 200 * Fix: IDs * Fix: https * Fix: icon * More APis * Fix: format swagger.json * Fix: SwaggerEndpoint * Fix: SCRIPT_DIR * Fix: PROJECT_ROOT * Fix: use code tags in schemas.md * Fix: examples * Fix: examples * Fix: improve format * Fix: date-time,enums * Fix: include_deleted * Fix: array of * Fix: parameter, response * Fix: string time or null * Workspaces: more docs * Workspaces: more docs * Fix: renderDisplayName * Fix: ActiveUserCount * Fix * Fix: typo * Templates: docs * Notice: incomplete
This commit is contained in:
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This script generates swagger description file and required Go docs files
|
||||
# from the coderd API.
|
||||
|
||||
set -euo pipefail
|
||||
# shellcheck source=scripts/lib.sh
|
||||
source "$(dirname "$(dirname "${BASH_SOURCE[0]}")")/lib.sh"
|
||||
|
||||
APIDOCGEN_DIR=$(dirname "${BASH_SOURCE[0]}")
|
||||
API_MD_TMP_FILE=$(mktemp /tmp/coder-apidocgen.XXXXXX)
|
||||
|
||||
cleanup() {
|
||||
rm -f "${API_MD_TMP_FILE}"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
log "Use temporary file: ${API_MD_TMP_FILE}"
|
||||
|
||||
pushd "${PROJECT_ROOT}"
|
||||
go run github.com/swaggo/swag/cmd/swag@v1.8.6 init \
|
||||
--generalInfo="coderd.go" \
|
||||
--dir="./coderd,./codersdk" \
|
||||
--output="./coderd/apidoc" \
|
||||
--outputTypes="go,json" \
|
||||
--parseDependency=true
|
||||
popd
|
||||
|
||||
pushd "${APIDOCGEN_DIR}"
|
||||
npm ci
|
||||
|
||||
# Make sure that widdershins is installed correctly.
|
||||
npm exec -- widdershins --version
|
||||
# Render the Markdown file.
|
||||
npm exec -- widdershins \
|
||||
--user_templates "./markdown-template" \
|
||||
--search false \
|
||||
--omitHeader true \
|
||||
--language_tabs "shell:curl" \
|
||||
--summary "../../coderd/apidoc/swagger.json" \
|
||||
--outfile "${API_MD_TMP_FILE}"
|
||||
# Perform the postprocessing
|
||||
go run postprocess/main.go -in-md-file-single "${API_MD_TMP_FILE}"
|
||||
popd
|
||||
@@ -0,0 +1,64 @@
|
||||
## Swagger / OpenAPI 2 and OpenAPI 3 template parameters
|
||||
|
||||
Note that properties of OpenAPI objects will be in OpenAPI 3.0 form, as
|
||||
Swagger / OpenAPI 2.0 definitions are converted automatically.
|
||||
|
||||
### Code templates
|
||||
|
||||
* `method` - the HTTP method of the operation (in lower-case)
|
||||
* `methodUpper` - the HTTP method of the operation (in upper-case)
|
||||
* `url` - the full URL of the operation (including protocol and host)
|
||||
* `consumes[]` - an array of MIME-types the operation consumes
|
||||
* `produces[]` - an array of MIME-types the operation produces
|
||||
* `operation` - the current operation object
|
||||
* `operationId` - the current operation id
|
||||
* `opName` - the operationId if set, otherwise the method + path
|
||||
* `tags[]` - the full list of tags applying to the operation
|
||||
* `security` - the security definitions applying to the operation
|
||||
* `resource` - the current tag/path object
|
||||
* `parameters[]` - an array of parameters for the operation (see below)
|
||||
* `queryString` - an example queryString, urlEncoded
|
||||
* `requiredQueryString` - an example queryString for `required:true` parameters
|
||||
* `queryParameters[]` - a subset of `parameters` that are `in:query`
|
||||
* `requiredParameters[]` - a subset of `queryParameters` that are `required:true`
|
||||
* `headerParameters[]` - a subset of `parameters` that are `in:header`
|
||||
* `allHeaders[]` - a concatenation of `headerParameters` and pseudo-parameters `Accept` and `Content-Type`, and optionally `Authorization` (the latter has an `isAuth` boolean property set true so it can be omitted in templates if desired
|
||||
|
||||
### Parameter template
|
||||
|
||||
* `parameters[]` - an array of [parameters](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#parameterObject), including the following pseudo-properties
|
||||
* `shortDesc` - a truncated version of the parameter description
|
||||
* `safeType` - a computed version of the parameter type, including Body and schema names
|
||||
* `originalType` - the original type of the parameter
|
||||
* `exampleValues` - an object containing examples for use in code-templates
|
||||
* `json` - example values in JSON compatible syntax
|
||||
* `object` - example values in raw object form (unquoted strings etc)
|
||||
* `depth` - a zero-based indicator of the depth of expanded request body parameters
|
||||
* `enums[]` - an array of (parameter)name/value pairs
|
||||
|
||||
### Responses template
|
||||
|
||||
* `responses[]` - an array of [responses](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#responseObject), including `status` and `meaning` properties
|
||||
|
||||
### Authentication template
|
||||
|
||||
* `authenticationStr` - a simple string of methods (and scopes where appropriate)
|
||||
* `securityDefinitions[]` - an array of applicable [securityDefinitions](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#securityRequirementObject)
|
||||
|
||||
### Schema Property template
|
||||
|
||||
* `schemaProperties[]` - an array of
|
||||
* `name`
|
||||
* `type`
|
||||
* `required`
|
||||
* `description`
|
||||
* `enums[]` - an array of (schema property)name/value pairs
|
||||
|
||||
### Common to all templates
|
||||
|
||||
* `openapi` - the top-level OpenAPI / Swagger document
|
||||
* `header` - the front-matter of the Slate/Shins markdown document
|
||||
* `host` - the (computed) host of the API
|
||||
* `protocol` - the default/first protocol of the API
|
||||
* `baseUrl` - the (computed) baseUrl of the API (including protocol and host)
|
||||
* `widdershins` - the contents of widdershins `package.json`
|
||||
@@ -0,0 +1 @@
|
||||
To perform this operation, you must be authenticated by means of one of the following methods: **{{= data.utils.getAuthenticationStr(data) }}**.
|
||||
@@ -0,0 +1,4 @@
|
||||
# Example request using curl
|
||||
curl -X {{=data.methodUpper}} http://coder-server:8080{{=data.url}}{{=data.requiredQueryString}}{{?data.allHeaders.length}} \{{?}}
|
||||
{{~data.allHeaders :p:index}} -H '{{=p.name}}: {{=p.exampleValues.object}}'{{?index < data.allHeaders.length-1}} \{{?}}
|
||||
{{~}}
|
||||
@@ -0,0 +1 @@
|
||||
{{= data.utils.inspect(data) }}
|
||||
@@ -0,0 +1,157 @@
|
||||
{{
|
||||
function renderSinglePropertyType(p) {
|
||||
if (!p.$ref) {
|
||||
return p.type;
|
||||
}
|
||||
|
||||
const pRef = p.$ref.replace("#/components/schemas/","");
|
||||
if (pRef == "codersdk.NullTime") {
|
||||
return "string(time) or `null`";
|
||||
}
|
||||
return "[" + pRef + "](#" + pRef.replace(".","").toLowerCase() + ")";
|
||||
}
|
||||
|
||||
function renderPropertyType(p) {
|
||||
if (p.type == "array") {
|
||||
return "array of " + renderSinglePropertyType(p.schema.items);
|
||||
}
|
||||
return renderSinglePropertyType(p);
|
||||
}
|
||||
|
||||
function renderDisplayName(p) {
|
||||
if (p.displayName == "» **additionalProperties**") {
|
||||
return "» `[any property]`";
|
||||
}
|
||||
if (p.displayName == "**additionalProperties**") {
|
||||
return "`[any property]`";
|
||||
}
|
||||
return "`" + p.displayName + "`";
|
||||
}
|
||||
|
||||
function renderDescription(p) {
|
||||
if (!p.description) {
|
||||
return "none";
|
||||
}
|
||||
|
||||
const toSnakeCase = str =>
|
||||
str
|
||||
.match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)
|
||||
.map(x => x.toLowerCase())
|
||||
.join('_');
|
||||
|
||||
const words = p.description.split(' ');
|
||||
if (words.length == 0) {
|
||||
return "none";
|
||||
}
|
||||
|
||||
const countUppercase = words[0].length - words[0].replace(/[A-Z]/g, '').length;
|
||||
if (countUppercase > 1) {
|
||||
const displayName = p.displayName.charAt(0).toUpperCase() + p.displayName.replaceAll("_", " ").toLowerCase().slice(1);
|
||||
return displayName + " " + words.slice(1).join(' ');
|
||||
}
|
||||
return p.description;
|
||||
}
|
||||
}}
|
||||
|
||||
{{? data.api.components && data.api.components.securitySchemes }}{{#def.security}}{{?}}
|
||||
|
||||
{{ for (var r in data.resources) { }}
|
||||
{{ data.resource = data.resources[r]; }}
|
||||
|
||||
<!-- APIDOCGEN: BEGIN SECTION -->
|
||||
{{= data.tags.section }}# {{= r}}
|
||||
|
||||
> This page is incomplete, stay tuned.
|
||||
|
||||
{{? data.resource.description }}{{= data.resource.description}}{{?}}
|
||||
|
||||
{{ for (var m in data.resource.methods) { }}
|
||||
{{ data.operationUniqueName = m; }}
|
||||
{{ data.method = data.resource.methods[m]; }}
|
||||
{{ data.operationUniqueSlug = data.method.slug; }}
|
||||
{{ data.operation = data.method.operation; }}
|
||||
{{= data.templates.operation(data) }}
|
||||
{{ } /* of methods */ }}
|
||||
|
||||
{{= data.tags.endSection }}
|
||||
{{ } /* of resources */ }}
|
||||
|
||||
{{? data.api.components && data.api.components.schemas }}
|
||||
{{= data.tags.section }}
|
||||
|
||||
<!-- APIDOCGEN: BEGIN SECTION -->
|
||||
# Schemas
|
||||
|
||||
> This page is incomplete, stay tuned.
|
||||
|
||||
{{ for (var s in data.components.schemas) {
|
||||
if (s == "codersdk.NullTime") {
|
||||
continue;
|
||||
}
|
||||
}}
|
||||
{{ var origSchema = data.components.schemas[s]; }}
|
||||
{{ var schema = data.api.components.schemas[s]; }}
|
||||
|
||||
{{= data.tags.section }}
|
||||
## {{=s}}
|
||||
|
||||
{{? data.options.yaml }}
|
||||
```yaml
|
||||
{{=data.utils.yaml.stringify(data.utils.getSample(schema,data.options,{quiet:true},data.api))}}
|
||||
{{??}}
|
||||
```json
|
||||
{{=data.utils.safejson(data.utils.getSample(schema,data.options,{quiet:true},data.api),null,2)}}
|
||||
{{?}}```
|
||||
|
||||
{{ var enums = []; }}
|
||||
{{ var blocks = data.utils.schemaToArray(origSchema,-1,{trim:true,join:true},data); }}
|
||||
{{ for (var block of blocks) {
|
||||
for (var p of block.rows) {
|
||||
if (p.schema && p.schema.enum) {
|
||||
for (var e of p.schema.enum) {
|
||||
enums.push({name:p.name,value:e});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
{{~ blocks :block}}
|
||||
{{? block.title }}{{= block.title}}{{= '\n\n'}}{{?}}
|
||||
{{? block.externalDocs}}
|
||||
<a href="{{=block.externalDocs.url}}">{{=block.externalDocs.description||'External documentation'}}</a>
|
||||
{{?}}
|
||||
|
||||
{{? block===blocks[0] }}
|
||||
{{= data.tags.section }}
|
||||
|
||||
### Properties
|
||||
{{?}}
|
||||
|
||||
{{? block.rows.length}}|Name|Type|Required|Restrictions|Description|
|
||||
|---|---|---|---|---|{{?}}
|
||||
{{~ block.rows :p}}|{{= renderDisplayName(p)}}|{{= renderPropertyType(p)}}|{{=p.required}}|{{=p.restrictions||'none'}}|{{= renderDescription(p)}}|
|
||||
{{~}}
|
||||
{{~}}
|
||||
{{? (blocks[0].rows.length === 0) && (blocks.length === 1) }}
|
||||
*None*
|
||||
{{?}}
|
||||
|
||||
{{? enums.length > 0 }}
|
||||
{{= data.tags.section }}
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
|Property|Value|
|
||||
|---|---|
|
||||
{{~ enums :e}}|{{=e.name}}|{{=data.utils.toPrimitive(e.value)}}|
|
||||
{{~}}
|
||||
|
||||
{{= data.tags.endSection }}
|
||||
{{?}}
|
||||
|
||||
{{= data.tags.endSection }}
|
||||
{{= data.tags.endSection }}
|
||||
{{ } /* of schemas */ }}
|
||||
|
||||
{{?}}
|
||||
@@ -0,0 +1,48 @@
|
||||
{{= data.tags.section }}
|
||||
|
||||
## {{= data.operationUniqueName}}
|
||||
|
||||
{{ data.methodUpper = data.method.verb.toUpperCase(); }}
|
||||
{{ data.url = data.utils.slashes(data.baseUrl + data.method.path); }}
|
||||
{{ data.parameters = data.operation.parameters; }}
|
||||
{{ data.enums = []; }}
|
||||
{{ data.utils.fakeProdCons(data); }}
|
||||
{{ data.utils.fakeBodyParameter(data); }}
|
||||
{{ data.utils.mergePathParameters(data); }}
|
||||
{{ data.utils.getParameters(data); }}
|
||||
|
||||
{{? data.options.codeSamples || data.operation["x-code-samples"] }}
|
||||
### Code samples
|
||||
|
||||
{{= data.utils.getCodeSamples(data)}}
|
||||
{{?}}
|
||||
|
||||
`{{= data.methodUpper}} {{=data.method.path}}`
|
||||
|
||||
{{? data.operation.summary && !data.options.tocSummary}}*{{= data.operation.summary }}*{{?}}
|
||||
|
||||
{{? data.operation.description}}{{= data.operation.description }}{{?}}
|
||||
|
||||
{{? data.operation.requestBody}}
|
||||
> Body parameter
|
||||
|
||||
{{? data.bodyParameter.exampleValues.description }}
|
||||
> {{= data.bodyParameter.exampleValues.description }}
|
||||
{{?}}
|
||||
|
||||
{{= data.utils.getBodyParameterExamples(data) }}
|
||||
{{?}}
|
||||
|
||||
{{? data.parameters && data.parameters.length }}
|
||||
{{#def.parameters}}
|
||||
{{?}}
|
||||
|
||||
{{#def.responses}}
|
||||
|
||||
{{ data.security = data.operation.security ? data.operation.security : data.api.security; }}
|
||||
{{? data.security && data.security.length }}
|
||||
{{#def.authentication}}
|
||||
{{??}}
|
||||
{{#def.authentication_none}}
|
||||
{{?}}
|
||||
{{= data.tags.endSection }}
|
||||
@@ -0,0 +1,50 @@
|
||||
{{
|
||||
function renderParameterType(p) {
|
||||
if (p.schema['x-widdershins-oldRef']) {
|
||||
const aType = p.schema['x-widdershins-oldRef'].replace("#/components/schemas/","");
|
||||
const href = aType.replace(".","").toLowerCase();
|
||||
return "[" + aType + "](schemas.md#" + href + ")";
|
||||
}
|
||||
return p.safeType;
|
||||
}
|
||||
}}
|
||||
{{= data.tags.section }}
|
||||
### Parameters
|
||||
|
||||
|Name|In|Type|Required|Description|
|
||||
|---|---|---|---|---|
|
||||
{{~ data.parameters :p}}|{{=p.name}}|{{=p.in}}|{{= renderParameterType(p)}}|{{=p.required}}|{{=p.shortDesc || 'none'}}|
|
||||
{{~}}
|
||||
|
||||
{{? data.longDescs }}
|
||||
#### Detailed descriptions
|
||||
{{~ data.parameters :p}}{{? p.shortDesc !== p.description}}
|
||||
**{{=p.name}}**: {{=p.description}}{{?}}
|
||||
{{~}}
|
||||
{{?}}
|
||||
|
||||
{{~ data.parameters :p}}
|
||||
|
||||
{{? p.schema && p.schema.enum }}
|
||||
{{~ p.schema.enum :e}}
|
||||
{{ var entry = {}; entry.name = p.name; entry.value = e; data.enums.push(entry); }}
|
||||
{{~}}
|
||||
{{?}}
|
||||
|
||||
{{? p.schema && p.schema.items && p.schema.items.enum }}
|
||||
{{~ p.schema.items.enum :e}}
|
||||
{{ var entry = {}; entry.name = p.name; entry.value = e; data.enums.push(entry); }}
|
||||
{{~}}
|
||||
{{?}}
|
||||
|
||||
{{~}}
|
||||
|
||||
{{? data.enums && data.enums.length }}
|
||||
#### Enumerated Values
|
||||
|
||||
|Parameter|Value|
|
||||
|---|---|
|
||||
{{~ data.enums :e}}|{{=e.name}}|{{=data.utils.toPrimitive(e.value)}}|
|
||||
{{~}}
|
||||
{{?}}
|
||||
{{= data.tags.endSection }}
|
||||
@@ -0,0 +1,113 @@
|
||||
{{
|
||||
function renderSingleResponseType(r) {
|
||||
var content;
|
||||
for (var ct in r.content) {
|
||||
content = r.content[ct];
|
||||
break;
|
||||
}
|
||||
if (!content) {
|
||||
return "no schema";
|
||||
}
|
||||
|
||||
var ref = content.schema["x-widdershins-oldRef"];
|
||||
if (!ref) {
|
||||
ref = content.schema.items["x-widdershins-oldRef"];
|
||||
}
|
||||
const aType = ref.replace("#/components/schemas/","");
|
||||
const href = aType.replace(".","").toLowerCase();
|
||||
return "[" + aType + "](schemas.md#" + href + ")";
|
||||
}
|
||||
|
||||
function renderResponseType(r) {
|
||||
if (r.type == "array") {
|
||||
return "array of " + renderSingleResponseType(r);
|
||||
}
|
||||
return renderSingleResponseType(r);
|
||||
}
|
||||
}}
|
||||
{{ data.responses = data.utils.getResponses(data); }}
|
||||
{{ data.responseSchemas = false; }}
|
||||
{{~ data.responses :response }}
|
||||
{{ if (response.content) data.responseSchemas = true; }}
|
||||
{{~}}
|
||||
|
||||
{{? data.responseSchemas }}
|
||||
### Example responses
|
||||
|
||||
{{= data.utils.getResponseExamples(data) }}
|
||||
{{?}}
|
||||
|
||||
{{= data.tags.section }}
|
||||
### Responses
|
||||
|
||||
|Status|Meaning|Description|Schema|
|
||||
|---|---|---|---|
|
||||
{{~ data.responses :r}}|{{=r.status}}|{{=r.meaning}}|{{=r.description || 'none'}}|{{= renderResponseType(r)}}|
|
||||
{{~}}
|
||||
|
||||
{{ data.responseSchemas = false; }}
|
||||
{{~ data.responses :response }}
|
||||
{{ if (response.content && !response.$ref && !data.utils.isPrimitive(response.type)) data.responseSchemas = true; }}
|
||||
{{~}}
|
||||
{{? data.responseSchemas }}
|
||||
<h3 id="{{=data.operationUniqueSlug}}-responseschema">Response Schema</h3>
|
||||
{{~ data.responses :response}}
|
||||
{{? response.content && !response.$ref && !data.utils.isPrimitive(response.type)}}
|
||||
{{? Object.keys(response.content).length }}
|
||||
{{ var responseKey = Object.keys(response.content)[0]; }}
|
||||
{{ var responseSchema = response.content[responseKey].schema; }}
|
||||
{{ var enums = []; }}
|
||||
{{ var blocks = data.utils.schemaToArray(responseSchema,0,{trim:true,join:true},data); }}
|
||||
{{ for (var block of blocks) {
|
||||
for (var p of block.rows) {
|
||||
if (p.schema && p.schema.enum) {
|
||||
for (var e of p.schema.enum) {
|
||||
enums.push({name:p.name,value:e});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
{{? blocks[0].rows.length || blocks[0].title }}
|
||||
Status Code **{{=response.status}}**
|
||||
|
||||
{{~ blocks :block}}
|
||||
{{? block.title }}*{{=block.title}}*
|
||||
{{?}}
|
||||
|Name|Type|Required|Restrictions|Description|
|
||||
|---|---|---|---|---|
|
||||
{{~block.rows :p}}|{{=p.displayName}}|{{? p.$ref}}`{{=p.$ref}}`{{?}}{{? !p.$ref}}{{=p.type}}{{?}}|{{=p.required}}|{{=p.restrictions||'none'}}|{{=p.description||'none'}}|
|
||||
{{~}}
|
||||
{{~}}
|
||||
{{?}}
|
||||
|
||||
{{? enums.length > 0 }}
|
||||
#### Enumerated Values
|
||||
|
||||
|Property|Value|
|
||||
|---|---|
|
||||
{{~ enums :e}}|{{=e.name}}|{{=data.utils.toPrimitive(e.value)}}|
|
||||
{{~}}
|
||||
|
||||
{{?}}
|
||||
{{?}}
|
||||
|
||||
{{ data.response = response; }}
|
||||
|
||||
{{?}}
|
||||
{{~}}
|
||||
{{?}}
|
||||
|
||||
{{ data.responseHeaders = data.utils.getResponseHeaders(data); }}
|
||||
{{? data.responseHeaders.length }}
|
||||
|
||||
### Response Headers
|
||||
|
||||
|Status|Header|Type|Format|Description|
|
||||
|---|---|---|---|---|
|
||||
{{~ data.responseHeaders :h}}|{{=h.status}}|{{=h.header}}|{{=h.type}}|{{=h.format||''}}|{{=h.description||'none'}}|
|
||||
{{~}}
|
||||
|
||||
{{?}}
|
||||
{{= data.tags.endSection }}
|
||||
@@ -0,0 +1,27 @@
|
||||
<!-- APIDOCGEN: BEGIN SECTION -->
|
||||
{{= data.tags.section }}# Authentication
|
||||
{{ for (var s in data.api.components.securitySchemes) { }}
|
||||
{{ var sd = data.api.components.securitySchemes[s]; }}
|
||||
{{? sd.type == 'apiKey' }}
|
||||
- API Key ({{=s}})
|
||||
- Parameter Name: **{{=sd.name}}**, in: {{=sd.in}}. {{=sd.description || ''}}
|
||||
{{?}}
|
||||
{{? sd.type == 'http'}}
|
||||
- HTTP Authentication, scheme: {{=sd.scheme}}{{? sd.description }}<br/>{{=sd.description}}{{?}}
|
||||
{{?}}
|
||||
{{? sd.type == 'oauth2'}}
|
||||
- oAuth2 authentication. {{=sd.description || ''}}
|
||||
{{ for (var f in sd.flows) { }}
|
||||
{{ var flow = sd.flows[f]; }}
|
||||
- Flow: {{=f}}
|
||||
{{? flow.authorizationUrl}} - Authorization URL = [{{=flow.authorizationUrl}}]({{=flow.authorizationUrl}}){{?}}
|
||||
{{? flow.tokenUrl}} - Token URL = [{{=flow.tokenUrl}}]({{=flow.tokenUrl}}){{?}}
|
||||
{{? flow.scopes && Object.keys(flow.scopes).length}}
|
||||
|Scope|Scope Description|
|
||||
|---|---|
|
||||
{{ for (var sc in flow.scopes) { }}|{{=sc}}|{{=data.utils.join(flow.scopes[sc])}}|
|
||||
{{ } /* of scopes */ }}
|
||||
{{?}}
|
||||
{{ } /* of flows */ }}
|
||||
{{?}}
|
||||
{{ } /* of securitySchemes */ }}
|
||||
Generated
+3369
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"widdershins": "^4.0.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
const (
|
||||
apiSubdir = "api"
|
||||
apiIndexFile = "index.md"
|
||||
apiIndexContent = `Get started with Coder API:
|
||||
|
||||
<children>
|
||||
This page is rendered on https://coder.com/docs/coder-oss/api. Refer to the other documents in the ` + "`" + `api/` + "`" + ` directory.
|
||||
</children>
|
||||
`
|
||||
)
|
||||
|
||||
var (
|
||||
docsDirectory string
|
||||
inMdFileSingle string
|
||||
|
||||
sectionSeparator = []byte("<!-- APIDOCGEN: BEGIN SECTION -->\n")
|
||||
nonAlphanumericRegex = regexp.MustCompile(`[^a-z0-9 ]+`)
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.Println("Postprocess API docs")
|
||||
|
||||
flag.StringVar(&docsDirectory, "docs-directory", "../../docs", "Path to Coder docs directory")
|
||||
flag.StringVar(&inMdFileSingle, "in-md-file-single", "", "Path to single Markdown file, output from widdershins.js")
|
||||
flag.Parse()
|
||||
|
||||
if inMdFileSingle == "" {
|
||||
flag.Usage()
|
||||
log.Fatal("missing value for in-md-file-single")
|
||||
}
|
||||
|
||||
sections, err := loadMarkdownSections()
|
||||
if err != nil {
|
||||
log.Fatal("can't load markdown sections: ", err)
|
||||
}
|
||||
|
||||
err = prepareDocsDirectory()
|
||||
if err != nil {
|
||||
log.Fatal("can't prepare docs directory: ", err)
|
||||
}
|
||||
|
||||
err = writeDocs(sections)
|
||||
if err != nil {
|
||||
log.Fatal("can't write docs directory: ", err)
|
||||
}
|
||||
|
||||
log.Println("Done")
|
||||
}
|
||||
|
||||
func loadMarkdownSections() ([][]byte, error) {
|
||||
log.Printf("Read the md-file-single: %s", inMdFileSingle)
|
||||
mdFile, err := os.ReadFile(inMdFileSingle)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("can't read the md-file-single: %w", err)
|
||||
}
|
||||
log.Printf("Read %dB", len(mdFile))
|
||||
|
||||
sections := bytes.Split(mdFile, sectionSeparator)
|
||||
if len(sections) < 2 {
|
||||
return nil, xerrors.Errorf("At least 1 section is expected: %w", err)
|
||||
}
|
||||
sections = sections[1:] // Skip the first element which is the empty byte array
|
||||
log.Printf("Loaded %d sections", len(sections))
|
||||
return sections, nil
|
||||
}
|
||||
|
||||
func prepareDocsDirectory() error {
|
||||
log.Println("Prepare docs directory")
|
||||
|
||||
apiPath := path.Join(docsDirectory, apiSubdir)
|
||||
|
||||
err := os.RemoveAll(apiPath)
|
||||
if err != nil {
|
||||
return xerrors.Errorf(`os.RemoveAll failed for "%s": %w`, apiPath, err)
|
||||
}
|
||||
|
||||
err = os.MkdirAll(apiPath, 0755)
|
||||
if err != nil {
|
||||
return xerrors.Errorf(`os.MkdirAll failed for "%s": %w`, apiPath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeDocs(sections [][]byte) error {
|
||||
log.Println("Write docs to destination")
|
||||
|
||||
apiDir := path.Join(docsDirectory, apiSubdir)
|
||||
err := os.WriteFile(path.Join(apiDir, apiIndexFile), []byte(apiIndexContent), 0644) // #nosec
|
||||
if err != nil {
|
||||
return xerrors.Errorf(`can't write the index file: %w`, err)
|
||||
}
|
||||
|
||||
type mdFile struct {
|
||||
title string
|
||||
path string
|
||||
}
|
||||
var mdFiles []mdFile
|
||||
|
||||
// Write .md files for grouped API method (Templates, Workspaces, etc.)
|
||||
for _, section := range sections {
|
||||
sectionName, err := extractSectionName(section)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("can't extract section name: %w", err)
|
||||
}
|
||||
log.Printf("Write section: %s", sectionName)
|
||||
|
||||
mdFilename := toMdFilename(sectionName)
|
||||
docPath := path.Join(apiDir, mdFilename)
|
||||
err = os.WriteFile(docPath, section, 0644) // #nosec
|
||||
if err != nil {
|
||||
return xerrors.Errorf(`can't write doc file "%s": %w`, docPath, err)
|
||||
}
|
||||
mdFiles = append(mdFiles, mdFile{
|
||||
title: sectionName,
|
||||
path: "./" + path.Join(apiSubdir, mdFilename),
|
||||
})
|
||||
}
|
||||
|
||||
// Update manifest.json
|
||||
type route struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
IconPath string `json:"icon_path,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
Children []route `json:"children,omitempty"`
|
||||
}
|
||||
|
||||
type manifest struct {
|
||||
Versions []string `json:"versions,omitempty"`
|
||||
Routes []route `json:"routes,omitempty"`
|
||||
}
|
||||
|
||||
manifestPath := path.Join(docsDirectory, "manifest.json")
|
||||
manifestFile, err := os.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("can't read manifest file: %w", err)
|
||||
}
|
||||
log.Printf("Read manifest file: %dB", len(manifestFile))
|
||||
|
||||
var m manifest
|
||||
err = json.Unmarshal(manifestFile, &m)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("json.Unmarshal failed: %w", err)
|
||||
}
|
||||
|
||||
for i, r := range m.Routes {
|
||||
if r.Title != "API" {
|
||||
continue
|
||||
}
|
||||
|
||||
var children []route
|
||||
for _, mdf := range mdFiles {
|
||||
docRoute := route{
|
||||
Title: mdf.title,
|
||||
Path: mdf.path,
|
||||
}
|
||||
children = append(children, docRoute)
|
||||
}
|
||||
|
||||
m.Routes[i].Children = children
|
||||
break
|
||||
}
|
||||
|
||||
manifestFile, err = json.MarshalIndent(m, "", " ")
|
||||
if err != nil {
|
||||
return xerrors.Errorf("json.Marshal failed: %w", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(manifestPath, manifestFile, 0644) // #nosec
|
||||
if err != nil {
|
||||
return xerrors.Errorf("can't write manifest file: %w", err)
|
||||
}
|
||||
log.Printf("Write manifest file: %dB", len(manifestFile))
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractSectionName(section []byte) (string, error) {
|
||||
scanner := bufio.NewScanner(bytes.NewReader(section))
|
||||
if !scanner.Scan() {
|
||||
return "", xerrors.Errorf("section header was expected")
|
||||
}
|
||||
|
||||
header := scanner.Text()[2:] // Skip #<space>
|
||||
return strings.TrimSpace(header), nil
|
||||
}
|
||||
|
||||
func toMdFilename(sectionName string) string {
|
||||
return nonAlphanumericRegex.ReplaceAllLiteralString(strings.ToLower(sectionName), "-") + ".md"
|
||||
}
|
||||
+1
-1
@@ -121,7 +121,7 @@ fatal() {
|
||||
trap 'fatal "Script encountered an error"' ERR
|
||||
|
||||
cdroot
|
||||
start_cmd API "" "${CODER_DEV_SHIM}" server --http-address 0.0.0.0:3000
|
||||
start_cmd API "" "${CODER_DEV_SHIM}" server --http-address 0.0.0.0:3000 --swagger-enable
|
||||
|
||||
echo '== Waiting for Coder to become ready'
|
||||
# Start the timeout in the background so interrupting this script
|
||||
|
||||
@@ -26,7 +26,7 @@ var (
|
||||
|
||||
func main() {
|
||||
flag.StringVar(&metricsFile, "metrics-file", "scripts/metricsdocgen/metrics", "Path to Prometheus metrics file")
|
||||
flag.StringVar(&prometheusDocFile, "prometheus-doc-file", "docs/admin/prometheus.md", "Path to prometheus doc file")
|
||||
flag.StringVar(&prometheusDocFile, "prometheus-doc-file", "docs/admin/prometheus.md", "Path to Prometheus doc file")
|
||||
flag.BoolVar(&dryRun, "dry-run", false, "Dry run")
|
||||
flag.Parse()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user