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:
Marcin Tojek
2022-12-19 18:43:46 +01:00
committed by GitHub
parent f239ca7ee3
commit dc6d271293
41 changed files with 9272 additions and 76 deletions
@@ -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 */ }}