Introduce experimental OpenAPI shortcode

This commit is contained in:
Alex Shpak
2026-05-22 14:06:48 +02:00
parent 118c5354bb
commit 1d38cd0183
8 changed files with 683 additions and 35 deletions
+261
View File
@@ -0,0 +1,261 @@
{
"openapi": "3.1.0",
"info": {
"title": "Tic Tac Toe",
"description": "This API allows writing down marks on a Tic Tac Toe board\nand requesting the state of the board or of individual squares.\n",
"version": "1.0.0"
},
"tags": [
{
"name": "Gameplay"
}
],
"paths": {
"/board": {
"get": {
"summary": "Get the whole board",
"description": "Retrieves the current state of the board and the winner.",
"tags": ["Gameplay"],
"operationId": "get-board",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/status"
}
}
}
}
},
"security": [
{
"defaultApiKey": []
},
{
"app2AppOauth": ["board:read"]
}
]
}
},
"/board/{row}/{column}": {
"parameters": [
{
"$ref": "#/components/parameters/rowParam"
},
{
"$ref": "#/components/parameters/columnParam"
}
],
"get": {
"summary": "Get a single board square",
"description": "Retrieves the requested square.",
"tags": ["Gameplay"],
"operationId": "get-square",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/mark"
}
}
}
},
"400": {
"description": "The provided parameters are incorrect",
"content": {
"text/html": {
"schema": {
"$ref": "#/components/schemas/errorMessage"
},
"example": "Illegal coordinates"
}
}
}
},
"security": [
{
"bearerHttpAuthentication": []
},
{
"user2AppOauth": ["board:read"]
}
]
},
"put": {
"summary": "Set a single board square",
"description": "Places a mark on the board and retrieves the whole board and the winner (if any).",
"tags": ["Gameplay"],
"operationId": "put-square",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/mark"
}
}
}
},
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/status"
}
}
}
},
"400": {
"description": "The provided parameters are incorrect",
"content": {
"text/html": {
"schema": {
"$ref": "#/components/schemas/errorMessage"
},
"examples": {
"illegalCoordinates": {
"value": "Illegal coordinates."
},
"notEmpty": {
"value": "Square is not empty."
},
"invalidMark": {
"value": "Invalid Mark (X or O)."
}
}
}
}
}
},
"security": [
{
"bearerHttpAuthentication": []
},
{
"user2AppOauth": ["board:write"]
}
]
}
}
},
"components": {
"parameters": {
"rowParam": {
"description": "Board row (vertical coordinate)",
"name": "row",
"in": "path",
"required": true,
"schema": {
"$ref": "#/components/schemas/coordinate"
}
},
"columnParam": {
"description": "Board column (horizontal coordinate)",
"name": "column",
"in": "path",
"required": true,
"schema": {
"$ref": "#/components/schemas/coordinate"
}
}
},
"schemas": {
"errorMessage": {
"type": "string",
"maxLength": 256,
"description": "A text message describing an error"
},
"coordinate": {
"type": "integer",
"minimum": 1,
"maximum": 3,
"example": 1
},
"mark": {
"type": "string",
"enum": [".", "X", "O"],
"description": "Possible values for a board square. `.` means empty square.",
"example": "."
},
"board": {
"type": "array",
"maxItems": 3,
"minItems": 3,
"items": {
"type": "array",
"maxItems": 3,
"minItems": 3,
"items": {
"$ref": "#/components/schemas/mark"
}
}
},
"winner": {
"type": "string",
"enum": [".", "X", "O"],
"description": "Winner of the game. `.` means nobody has won yet.",
"example": "."
},
"status": {
"type": "object",
"properties": {
"winner": {
"$ref": "#/components/schemas/winner"
},
"board": {
"$ref": "#/components/schemas/board"
}
}
}
},
"securitySchemes": {
"defaultApiKey": {
"description": "API key provided in console",
"type": "apiKey",
"name": "api-key",
"in": "header"
},
"basicHttpAuthentication": {
"description": "Basic HTTP Authentication",
"type": "http",
"scheme": "Basic"
},
"bearerHttpAuthentication": {
"description": "Bearer token using a JWT",
"type": "http",
"scheme": "Bearer",
"bearerFormat": "JWT"
},
"app2AppOauth": {
"type": "oauth2",
"flows": {
"clientCredentials": {
"tokenUrl": "https://learn.openapis.org/oauth/2.0/token",
"scopes": {
"board:read": "Read the board"
}
}
}
},
"user2AppOauth": {
"type": "oauth2",
"flows": {
"authorizationCode": {
"authorizationUrl": "https://learn.openapis.org/oauth/2.0/auth",
"tokenUrl": "https://learn.openapis.org/oauth/2.0/token",
"scopes": {
"board:read": "Read the board",
"board:write": "Write to the board"
}
}
}
}
}
}
}
+13 -33
View File
@@ -5,62 +5,42 @@ layout: landing
<br /> <br />
# Hugo Book {anchor=false} # Hugo Book {anchor=false}
{{% columns %}}
- Documentation theme for [Hugo](https://gohugo.io) as simple as plain book.
Hugo-Book theme is designed to be easy to read, write, navigate and maintain. Documentation theme for [Hugo](https://gohugo.io) as simple as plain book.
It is an attempt to create a sustainable web project.
{{<button href="/docs/getting-started/introduction">}}Explore{{</button>}} Hugo-Book theme is designed to be easy to read, write, navigate and maintain.
It is an attempt to create a sustainable web project.
- ```shell {{<button href="/docs/getting-started/introduction">}}Explore{{</button>}}
# TL;DR
git clone https://github.com/alex-shpak/hugo-book-starter documentation
cd documentation
git submodule update --init --remote
hugo server --minify
```
{{% /columns %}}
<br />
<br /> <br />
{{% columns %}} {{% columns %}}
- {{< card >}} - ### Probably fast {anchor=false}
### Probably fast {anchor=false}
Built on Hugo static site generator. "The worlds fastest framework for building websites". Built on Hugo static site generator. "The worlds fastest framework for building websites".
{{< /card >}}
- {{< card >}} - ### 50% JS free {anchor=false}
### 50% JS free {anchor=false}
All important features are working even with JavaScript disabled in the browser, including interactive shortcodes. All important features are working even with JavaScript disabled in the browser, including interactive shortcodes.
{{< /card >}}
- {{< card >}} - ### Minimalistic {anchor=false}
### Minimalistic {anchor=false}
For real, it is very minimalistic, black on white. No extra tools are needed to build the site, only Hugo. No heavy JS or CSS frameworks included. For real, it is very minimalistic, black on white. No extra tools are needed to build the site, only Hugo. No heavy JS or CSS frameworks included.
{{< /card >}}
{{% /columns %}} {{% /columns %}}
{{% columns %}} {{% columns %}}
- {{< card >}} - ### Shortcodes {anchor=false}
### Shortcodes {anchor=false}
Pretty good shortcodes are included to enhance content: Pretty good shortcodes are included to enhance content:
[Columns](/docs/content/shortcodes/columns/), [Columns](/docs/content/shortcodes/columns/),
[Cards](/docs/content/shortcodes/cards/), [Cards](/docs/content/shortcodes/experimental/cards/),
[Tabs](/docs/content/shortcodes/tabs/), [Tabs](/docs/content/shortcodes/tabs/),
[Images](/docs/content/shortcodes/images/), [Images](/docs/content/shortcodes/experimental/images/),
[Asciinema](/docs/content/shortcodes/asciinema/), [Asciinema](/docs/content/shortcodes/asciinema/),
[KaTex](/docs/content/shortcodes/katex/), [KaTex](/docs/content/shortcodes/katex/),
[Mermaid](/docs/content/shortcodes/mermaid/) and others. [Mermaid](/docs/content/shortcodes/mermaid/) and others.
{{< /card >}}
- {{< card >}} - ### Even more {anchor=false}
### Even more {anchor=false}
Do people actually read these? I thought it was just a visual filler. But there is full-text search and multi-language support. Do people actually read these? I thought it was just a visual filler. But there is full-text search and multi-language support.
{{< /card >}}
- {{< card >}} - ### Made to be extendable {anchor=false}
### Made to be extendable {anchor=false}
There are multiple points to inject own styles and templates to make it your own. There are multiple points to inject own styles and templates to make it your own.
{{< /card >}}
{{% /columns %}} {{% /columns %}}
@@ -0,0 +1,3 @@
---
bookCollapseSection: true
---
@@ -1,6 +1,6 @@
# Cards # Cards
Content blocks with optional images and links. Often used with [Columns](/docs/content/shortcodes/columns) for grid layouts. Content blocks with optional images and links. Often used with [Columns](/docs/content/shortcodes/columns/) for grid layouts.
## Syntax ## Syntax
@@ -18,7 +18,7 @@ Markdown content
Cards can display an image above the content. Cards can display an image above the content.
{{< /card >}} {{< /card >}}
- {{< card href="/docs/content/shortcodes/cards" >}} - {{< card href="/docs/content/shortcodes/experimental/cards/" >}}
### With Link {anchor=false} ### With Link {anchor=false}
When `href` is set, the entire card becomes clickable. When `href` is set, the entire card becomes clickable.
{{< /card >}} {{< /card >}}
@@ -0,0 +1,19 @@
---
title: OpenAPI
---
# OpenAPI
Renders an OpenAPI 3.x specification as a documentation page. Operations are grouped by their first tag.
## Syntax
```tpl
{{%/* openapi src="spec.json" */%}}
```
The spec file is loaded via `resources.Get`, so the path is relative to the site's `assets/` directory. JSON and YAML are both supported.
## Example
{{% openapi src="tictactoe.json" %}}
+385
View File
@@ -0,0 +1,385 @@
{{- $source := or (.Get "src") (.Get 0) -}}
{{- with resources.Get $source -}}
{{- $raw := . -}}
{{- if or (strings.HasSuffix $source ".yaml") (strings.HasSuffix $source ".yml") -}}
{{- $raw = resources.FromString (printf "%s.json" $source) (. | transform.Unmarshal | jsonify) -}}
{{- end -}}
{{- $spec := openapi3.Unmarshal $raw -}}
{{ with $spec.Info }}{{ template "openapi-info" (dict "info" . "externalDocs" $spec.ExternalDocs) }}{{ end }}
{{ with $spec.Servers }}{{ template "openapi-servers" (dict "servers" .) }}{{ end }}
{{ with $spec.Components }}{{ with .SecuritySchemes }}{{ template "openapi-security" (dict "schemes" .) }}{{ end }}{{ end }}
{{ template "openapi-operations" (dict "spec" $spec) }}
{{ with $spec.Components }}{{ with .Schemas }}{{ template "openapi-models" (dict "schemas" .) }}{{ end }}{{ end }}
{{- else -}}
{{- errorf "openapi shortcode: resource %q not found" $source -}}
{{- end -}}
{{- /* ========== Info ========== */ -}}
{{- define "openapi-info" -}}
{{- $info := .info -}}
{{- $externalDocs := .externalDocs }}
{{ with $info.Description }}{{ . }}{{ end }}
{{ $meta := slice -}}
{{- with $info.Version }}{{ $meta = $meta | append (printf "`v%s`" .) }}{{ end -}}
{{- with $info.License }}{{ if .URL }}{{ $meta = $meta | append (printf "[%s](%s)" .Name .URL) }}{{ else }}{{ $meta = $meta | append .Name }}{{ end }}{{ end -}}
{{- with $info.Contact }}{{ with .Email }}{{ $meta = $meta | append (printf "[%s](mailto:%s)" . .) }}{{ end }}{{ end -}}
{{- with $info.TermsOfService }}{{ $meta = $meta | append (printf "[Terms of service](%s)" .) }}{{ end -}}
{{- with $externalDocs }}{{ $meta = $meta | append (printf "[%s](%s)" (or .Description .URL) .URL) }}{{ end }}
{{ delimit $meta " \n" | safeHTML }}
{{- end -}}
{{- /* ========== Servers ========== */ -}}
{{- define "openapi-servers" }}
## Servers
{{ range .servers }}
{{ with .Description }}{{ . | chomp | replaceRE "\n" " " }}{{ else }}Server{{ end }}
: [`{{ .URL }}`]({{ .URL }})
{{- range $name, $var := .Variables }}
: `{{ $name }}`: default `{{ $var.Default }}`{{ with $var.Enum }}, enum: {{ delimit . ", " }}{{ end }}{{ with $var.Description }}, {{ . | chomp | replaceRE "\n" " " }}{{ end }}
{{- end }}
{{ end -}}
{{- end -}}
{{- /* ========== Security ========== */ -}}
{{- define "openapi-security" -}}
{{- $schemes := .schemes }}
## Security
{{ range $name, $schemeRef := $schemes }}
{{- with $schemeRef.Value }}
{{ with .Description }}{{ . | chomp | replaceRE "\n" " " }}
: `{{ $name }}`
{{ else }}`{{ $name }}`
{{ end -}}
: `{{ .Type }}`{{ with .Scheme }} `{{ . }}`{{ end }}{{ with .In }} in `{{ . }}`{{ end }}{{ with .Name }} (`{{ . }}`){{ end }}{{ with .BearerFormat }} `{{ . }}`{{ end }}{{ with .OpenIdConnectUrl }}, discovery: [{{ . }}]({{ . }}){{ end }}
{{- end }}
{{ end -}}
{{- end -}}
{{- /* ========== Operations (grouped by tag) ========== */ -}}
{{- define "openapi-operations" -}}
{{- $spec := .spec -}}
{{- $methods := slice "GET" "POST" "PUT" "PATCH" "DELETE" "HEAD" "OPTIONS" "TRACE" -}}
{{- $locations := slice
(dict "in" "path" "label" "Path parameters")
(dict "in" "query" "label" "Query parameters")
(dict "in" "header" "label" "Header parameters")
(dict "in" "cookie" "label" "Cookie parameters") -}}
{{- /* Bucket each operation under every tag it declares */ -}}
{{- $byTag := dict -}}
{{- range $path, $pathItem := $spec.Paths.Map -}}
{{- range $method := $methods -}}
{{- with index $pathItem.Operations $method -}}
{{- $tags := .Tags -}}
{{- if not $tags }}{{ $tags = slice "API" }}{{ end -}}
{{- $entry := dict "path" $path "method" $method "operation" . "pathItem" $pathItem -}}
{{- range $tag := $tags -}}
{{- $bucket := index $byTag $tag | default slice -}}
{{- $byTag = merge $byTag (dict $tag ($bucket | append $entry)) -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end }}
{{ range $tag, $entries := $byTag }}
## {{ $tag | title }}
{{ range $entry := $entries }}
{{ template "openapi-operation" (dict "spec" $spec "path" $entry.path "method" $entry.method "operation" $entry.operation "pathItem" $entry.pathItem "locations" $locations) }}
{{- end -}}
{{- end }}
{{- end -}}
{{- /* ========== Operation ========== */ -}}
{{- define "openapi-operation" -}}
{{- $spec := .spec -}}
{{- $path := .path -}}
{{- $method := .method -}}
{{- $operation := .operation -}}
{{- $pathItem := .pathItem -}}
{{- $locations := .locations }}
{{- $title := or $operation.Summary (printf "`%s %s`" (upper $method) $path) -}}
{{- if $operation.Deprecated }}{{ $title = printf "~~%s~~" $title }}{{ end }}
### {{ $title }}{{ with $operation.OperationID }} {id="op-{{ anchorize . }}"}{{ end }}
{{ with $operation.Description }}{{ . }}
{{ end -}}
{{- with $operation.ExternalDocs }}[{{ with .Description }}{{ . | chomp | replaceRE "\n" " " }}{{ else }}{{ .URL }}{{ end }}]({{ .URL }})
{{ end -}}
{{- $security := $operation.Security -}}
{{- if not $security }}{{ $security = $spec.Security }}{{ end }}
{{ $requirementStrings := slice -}}
{{- range $requirement := $security -}}
{{- $schemeLinks := slice -}}
{{- range $schemeName, $_ := $requirement -}}
{{- $schemeLinks = $schemeLinks | append (printf "[%s](#security)" $schemeName) -}}
{{- end -}}
{{- if $schemeLinks }}{{ $requirementStrings = $requirementStrings | append (delimit $schemeLinks " and ") }}{{ end -}}
{{- end }}
#### Request{{ if $requirementStrings }} (requires {{ delimit $requirementStrings ", " }}){{ end }}
```tpl
{{ upper $method }} {{ $path }}
```
{{- /* Dedup: operation-level overrides path-level by name+in */ -}}
{{- $opKeys := dict -}}
{{- range $operation.Parameters }}{{ with .Value }}{{ $opKeys = merge $opKeys (dict (printf "%s:%s" .In .Name) true) }}{{ end }}{{ end -}}
{{- $allParams := slice -}}
{{- range $pp := $pathItem.Parameters }}{{ with $pp.Value }}{{ if not (isset $opKeys (printf "%s:%s" .In .Name)) }}{{ $allParams = $allParams | append $pp }}{{ end }}{{ end }}{{ end -}}
{{- $allParams = append $allParams $operation.Parameters -}}
{{ template "openapi-parameters" (dict "allParams" $allParams "locations" $locations) }}
{{- with $operation.RequestBody }}{{ with .Value -}}
{{ template "openapi-request-body" (dict "requestBody" .) }}
{{- end }}{{ end }}
{{- with $operation.Responses -}}
{{ template "openapi-responses" (dict "responses" .) }}
{{- end -}}
{{- end -}}
{{- /* ========== Parameters ========== */ -}}
{{- define "openapi-parameters" -}}
{{- $allParams := .allParams -}}
{{- range .locations -}}
{{- $location := .in -}}
{{- $params := slice -}}
{{- range $allParams -}}
{{- with .Value }}{{ if eq .In $location }}{{ $params = $params | append . }}{{ end }}{{ end -}}
{{- end }}
{{- if $params }}
#### {{ .label }}
| Parameter name | Value | Description | Additional |
|----------------|-------|-------------|------------|
{{- range $params }}
| `{{ .Name }}` | {{ with .Schema }}{{ template "openapi-type" . }}{{ end }} | {{ with .Description }}{{ . | chomp | replaceRE "\n" " " }}{{ end }} | {{ template "openapi-param-extras" . }} |
{{- end }}
{{- end -}}
{{- end -}}
{{- end -}}
{{- define "openapi-param-extras" -}}
{{- $bits := slice -}}
{{- if .Required }}{{ $bits = $bits | append "Required" }}{{ end -}}
{{- if .Deprecated }}{{ $bits = $bits | append "Deprecated" }}{{ end -}}
{{- $example := .Example -}}
{{- $default := "" -}}
{{- with .Schema }}{{ with .Value -}}
{{- with .Default }}{{ $default = . }}{{ end -}}
{{- if not $example }}{{ with .Example }}{{ $example = . }}{{ end }}{{ end -}}
{{- end }}{{ end -}}
{{- with $default }}{{ $bits = $bits | append (printf "Default: `%v`" .) }}{{ end -}}
{{- with $example }}{{ $bits = $bits | append (printf "Example: `%v`" .) }}{{ end -}}
{{- delimit $bits ", " -}}
{{- end -}}
{{- /* ========== Request body ========== */ -}}
{{- define "openapi-request-body" -}}
{{- with .requestBody -}}
{{- if or .Description .Content }}
#### Request body{{ if .Required }} (required){{ end }}
{{ with .Description }}{{ . }}{{ end }}
{{ range $mediaType, $obj := .Content }}
{{ template "openapi-media-examples" (dict "mediaType" $mediaType "examples" $obj.Examples "example" $obj.Example) }}
{{- end -}}
{{ template "openapi-schema-summary" (dict "content" .Content) }}
{{- end -}}
{{- end -}}
{{- end -}}
{{- /* ========== Responses ========== */ -}}
{{- define "openapi-responses" -}}
{{- $responses := .responses -}}
{{- with $responses }}
#### Responses
{{ range $code, $codeRef := .Map -}}
{{- with $codeRef.Value }}
`{{ $code }}`{{ with .Description }}: {{ . | chomp | replaceRE "\n" " " }}{{ end }}
{{ with .Content }}{{ range $mediaType, $obj := . }}
{{ template "openapi-media-examples" (dict "mediaType" $mediaType "examples" $obj.Examples "example" $obj.Example) }}
{{- end -}}
{{ template "openapi-schema-summary" (dict "content" .) }}
{{ end -}}
{{ template "openapi-headers-detail" (dict "code" $code "headers" .Headers) }}
{{- end }}
{{ end -}}
{{- end -}}
{{- end -}}
{{- define "openapi-headers-detail" -}}
{{- $code := .code -}}
{{- with .headers }}
`{{ $code }}` response headers:
| Name | Type | Description |
|------|------|-------------|
{{- range $name, $hdrRef := . }}
{{- with $hdrRef.Value }}
| `{{ $name }}` | {{ with .Schema }}{{ template "openapi-type" . }}{{ end }} | {{ with .Description }}{{ . | chomp | replaceRE "\n" " " }}{{ end }} |
{{- end -}}
{{- end }}
{{- end -}}
{{- end -}}
{{- /* ========== Media examples (author-provided only) ========== */ -}}
{{- define "openapi-media-examples" -}}
{{- $mediaType := .mediaType -}}
{{- $examples := .examples -}}
{{- $example := .example -}}
{{- if $examples -}}
{{- range $name, $exRef := $examples }}
{{- with $exRef.Value }}
{{ with .Summary }}{{ . }}{{ else }}{{ $name }}{{ end }}{{ with .Description }} - {{ . | chomp | replaceRE "\n" " " }}{{ end }}
```jsonc {filename="{{ $mediaType }}"}
{{ jsonify (dict "indent" " ") .Value | safeHTML }}
```
{{- end -}}
{{- end -}}
{{- else if $example }}
```jsonc {filename="{{ $mediaType }}"}
{{ jsonify (dict "indent" " ") $example | safeHTML }}
```
{{- end -}}
{{- end -}}
{{- /* ========== Schema summary (type caption + embedded properties table) ========== */ -}}
{{- define "openapi-schema-summary" -}}
{{- $schemaRef := "" -}}
{{- range $mt, $obj := .content -}}
{{- if not $schemaRef }}{{ with $obj.Schema }}{{ $schemaRef = . }}{{ end }}{{ end -}}
{{- end -}}
{{- with $schemaRef }}
Schema: {{ template "openapi-type" . }}
{{- /* Unwrap arrays so `array of Pet` embeds the Pet table */ -}}
{{- $value := $schemaRef.Value -}}
{{- $itemRef := $schemaRef -}}
{{- if and $value (not $value.Properties) $value.Items -}}
{{- $itemRef = $value.Items -}}
{{- end -}}
{{- with $itemRef.Value -}}
{{- if or .Properties .AllOf }}
{{ template "openapi-properties-table" (dict "schema" $itemRef) }}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- /* ========== Models ========== */ -}}
{{- define "openapi-models" -}}
{{- $schemas := .schemas }}
## Models
{{ range $name, $schemaRef := $schemas -}}
{{- with $schemaRef.Value }}
### {{ $name }} {id="model-{{ $name | anchorize }}"}
{{ with .Description }}{{ . | chomp }}
{{ end -}}
{{- $parents := slice -}}
{{- range .AllOf }}{{ if .Ref }}{{ $parents = $parents | append . }}{{ end }}{{ end -}}
{{- with $parents }}Inherits from {{ range $i, $p := . }}{{ if $i }}, {{ end }}{{ template "openapi-schema-link" $p }}{{ end }}.
{{ end -}}
{{- if or .Properties .AllOf -}}
{{ template "openapi-properties-table" (dict "schema" $schemaRef) }}
{{- else -}}
Type: {{ template "openapi-type" $schemaRef }}{{ with .Enum }} (enum: {{ range $i, $v := . }}{{ if $i }}, {{ end }}`{{ $v }}`{{ end }}){{ end }}
{{- end }}
{{- end }}
{{ end -}}
{{- end -}}
{{- /* ========== Properties table ========== */ -}}
{{- define "openapi-properties-table" -}}
{{- $schemaRef := .schema -}}
{{- with $schemaRef.Value -}}
{{- $props := dict -}}
{{- $required := slice -}}
{{- with .Properties }}{{ $props = merge $props . }}{{ end -}}
{{- with .Required }}{{ $required = $required | append . }}{{ end -}}
{{- range .AllOf -}}
{{- if not .Ref -}}
{{- with .Value -}}
{{- with .Properties }}{{ $props = merge $props . }}{{ end -}}
{{- with .Required }}{{ $required = $required | append . }}{{ end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- if $props }}
| Property | Type | Required | Description |
|----------|------|----------|-------------|
{{- range $name, $prop := $props -}}
{{- $isRequired := in $required $name }}
| `{{ $name }}` | {{ template "openapi-type" $prop }} | {{ if $isRequired }}yes{{ else }}no{{ end }} | {{ with $prop.Value.Description }}{{ . | chomp | replaceRE "\n" " " }}{{ end }} |
{{- end }}
{{- end -}}
{{- end -}}
{{- end -}}
{{- /* ========== Type cell (handles ref, array, primitive) ========== */ -}}
{{- define "openapi-type" -}}
{{- if .Ref -}}
{{- template "openapi-schema-link" . -}}
{{- else -}}
{{- with .Value -}}
{{- $t := "" -}}{{- with .Type }}{{ with .Slice }}{{ $t = index . 0 }}{{ end }}{{ end -}}
{{- if eq $t "array" -}}
array of {{ template "openapi-type" .Items }}
{{- else if $t -}}
{{ $t }}{{ with .Format }} ({{ . }}){{ end }}
{{- else if .Enum -}}
enum
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- /* ========== Schema link (#model-name) ========== */ -}}
{{- define "openapi-schema-link" -}}
{{- $parts := split .Ref "/" -}}
{{- $name := index $parts (sub (len $parts) 1) -}}
[{{ $name }}](#model-{{ $name | anchorize }})
{{- end -}}