{{- /* Determine if Path for requested GVR is at /api or /apis based on emptiness of group */ -}}
{{- $prefix := (ternary "/api" (join "" "/apis/" $.GVR.Group) (not $.GVR.Group)) -}}
{{- /* Search both cluster-scoped and namespaced-scoped paths for the GVR to find its GVK */ -}}
{{- /* Also search for paths with {name} component in case the list path is missing */ -}}
{{- /* Looks for the following paths: */ -}}
{{- /* /apis/<group>/<version>/<resource> */ -}}
{{- /* /apis/<group>/<version>/<resource>/{name} */ -}}
{{- /* /apis/<group>/<version>/namespaces/{namespace}/<resource> */ -}}
{{- /* /apis/<group>/<version>/namespaces/{namespace}/<resource>/{name} */ -}}
{{- /* Also search for get verb paths in case list verb is missing */ -}}
{{- $clusterScopedSearchPath := join "/" $prefix $.GVR.Version $.GVR.Resource -}}
{{- $clusterScopedNameSearchPath := join "/" $prefix $.GVR.Version $.GVR.Resource "{name}" -}}
{{- $namespaceScopedSearchPath := join "/" $prefix $.GVR.Version "namespaces" "{namespace}" $.GVR.Resource -}}
{{- $namespaceScopedNameSearchPath := join "/" $prefix $.GVR.Version "namespaces" "{namespace}" $.GVR.Resource "{name}" -}}
{{- $gvk := "" -}}
{{- /* Pull GVK from operation */ -}}
{{- range $index, $searchPath := (list $clusterScopedSearchPath $clusterScopedNameSearchPath $namespaceScopedSearchPath $namespaceScopedNameSearchPath) -}}
{{- with $resourcePathElement := index $.Document "paths" $searchPath -}}
{{- range $methodIndex, $method := (list "get" "post" "put" "patch" "delete") -}}
{{- with $resourceMethodPathElement := index $resourcePathElement $method -}}
{{- with $gvk = index $resourceMethodPathElement "x-kubernetes-group-version-kind" -}}
{{- break -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- with $gvk -}}
{{- if $gvk.group -}}
GROUP: {{ $gvk.group }}{{"\n" -}}
{{- end -}}
KIND: {{ $gvk.kind}}{{"\n" -}}
VERSION: {{ $gvk.version }}{{"\n" -}}
{{- "\n" -}}
{{- with include "schema" (dict "gvk" $gvk "Document" $.Document "FieldPath" $.FieldPath "Recursive" $.Recursive) -}}
{{- . -}}
{{- else -}}
{{- throw "error: GVK %v not found in OpenAPI schema" $gvk -}}
{{- end -}}
{{- else -}}
{{- throw "error: GVR (%v) not found in OpenAPI schema" $.GVR.String -}}
{{- end -}}
{{- "\n" -}}
{{- /*
Finds a schema with the given GVK and prints its explain output or empty string
if GVK was not found
Takes dictionary as argument with keys:
gvk: openapiv3 JSON schema
Document: entire doc
FieldPath: field path to follow
Recursive: print recursive
*/ -}}
{{- define "schema" -}}
{{- /* Find definition with this GVK by filtering out the components/schema with the given x-kubernetes-group-version-kind */ -}}
{{- range index $.Document "components" "schemas" -}}
{{- if contains (index . "x-kubernetes-group-version-kind") $.gvk -}}
{{- with include "output" (set $ "schema" .) -}}
{{- . -}}
{{- else -}}
{{- $fieldName := (index $.FieldPath (sub (len $.FieldPath) 1)) -}}
{{- throw "error: field \"%v\" does not exist" $fieldName}}
{{- end -}}
{{- break -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- /*
Follows FieldPath until the FieldPath is empty. Then prints field name and field
list of resultant schema. If field path is not found. Prints nothing.
Example output:
FIELD: spec
DESCRIPTION:
<template "description">
FIELDS:
<template "fieldList">
Takes dictionary as argument with keys:
schema: openapiv3 JSON schema
history: map[string]int
Document: entire doc
FieldPath: field path to follow
Recursive: print recursive
*/ -}}
{{- define "output" -}}
{{- $refString := or (index $.schema "$ref") "" -}}
{{- $nextContext := set $ "history" (set $.history $refString 1) -}}
{{- $resolved := or (resolveRef $refString $.Document) $.schema -}}
{{- if not $.FieldPath -}}
DESCRIPTION:{{- "\n" -}}
{{- or (include "description" (dict "schema" $resolved "Document" $.Document)) "<empty>" | wrap 76 | indent 4 -}}{{- "\n" -}}
{{- with include "fieldList" (dict "schema" $resolved "level" 1 "Document" $.Document "Recursive" $.Recursive) -}}
FIELDS:{{- "\n" -}}
{{- . -}}
{{- end -}}
{{- else if and $refString (index $.history $refString) -}}
{{- /* Stop and do nothing. Hit a cycle */ -}}
{{- else if and $resolved.properties (index $resolved.properties (first $.FieldPath)) -}}
{{- /* Schema has this property directly. Traverse to next schema */ -}}
{{- $subschema := index $resolved.properties (first $.FieldPath) -}}
{{- if eq 1 (len $.FieldPath) -}}
{{- /* TODO: The original explain would say RESOURCE instead of FIELD here under some circumstances */ -}}
FIELD: {{first $.FieldPath}} <{{ template "typeGuess" (dict "schema" $subschema "Document" $.Document) }}>{{"\n"}}
{{- template "extractEnum" (dict "schema" $subschema "Document" $.Document "isLongView" true "limit" -1) -}}{{"\n"}}
{{- "\n" -}}
{{- end -}}
{{- template "output" (set $nextContext "history" (dict) "FieldPath" (slice $.FieldPath 1) "schema" $subschema ) -}}
{{- else if $resolved.items -}}
{{- /* If the schema is an array then traverse the array item fields */ -}}
{{- template "output" (set $nextContext "schema" $resolved.items) -}}
{{- else if and $resolved.additionalProperties (not (eq "bool" (printf "%T" $resolved.additionalProperties))) -}}
{{- /* If the schema is a map then traverse the map item fields */ -}}
{{- template "output" (set $nextContext "schema" $resolved.additionalProperties) -}}
{{- else -}}
{{- /* Last thing to try is all the alternatives in the allOf case */ -}}
{{- /* Stop when one of the alternatives has an output at all */ -}}
{{- range $index, $subschema := $resolved.allOf -}}
{{- with include "output" (set $nextContext "schema" $subschema) -}}
{{- . -}}
{{- break -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- /*
Prints list of fields of a given open api schema in following form:
field1 <type> -required-
DESCRIPTION
field2 <type> -required-
DESCRIPTION
or if Recursive is enabled:
field1 <type> -required-
subfield1 <type>
subsubfield1 <type>
subsubfield2 <type>
subfield2 <type>
field2 <type> -required-
subfield1 <type>
subfield2 <type>
Follows refs for field traversal. If there are cycles in the schema, they are
detected and traversal ends.
Takes dictionary as argument with keys:
schema: openapiv3 JSON schema
level: indentation level
history: map[string]int containing all ref names so far
Document: entire doc
Recursive: print recursive
*/ -}}
{{- define "fieldList" -}}
{{- /* Resolve schema if it is a ref */}}
{{- /* If this is a ref seen before, then ignore it */}}
{{- $refString := or (index $.schema "$ref") "" }}
{{- if and $refString (index (or $.history (dict)) $refString) -}}
{{- /* Do nothing for cycle */}}
{{- else -}}
{{- $nextContext := set $ "history" (set $.history $refString 1) -}}
{{- $resolved := or (resolveRef $refString $.Document) $.schema -}}
{{- range $k, $v := $resolved.properties -}}
{{- template "fieldDetail" (dict "name" $k "schema" $resolved "short" $.Recursive "level" $.level "Document" $.Document) -}}
{{- if $.Recursive -}}
{{- /* Check if we already know about this element */}}
{{- template "fieldList" (set $nextContext "schema" $v "level" (add $.level 1)) -}}
{{- end -}}
{{- end -}}
{{- range $resolved.allOf -}}
{{- template "fieldList" (set $nextContext "schema" .)}}
{{- end -}}
{{- if $resolved.items}}{{- template "fieldList" (set $nextContext "schema" $resolved.items)}}{{end}}
{{- if and $resolved.additionalProperties (not (eq "bool" (printf "%T" $resolved.additionalProperties))) -}}
{{- template "fieldList" (set $nextContext "schema" $resolved.additionalProperties)}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- /*
Prints a single field of the given schema
Optionally prints in a one-line style
Takes dictionary as argument with keys:
schema: openapiv3 JSON schema which contains the field
name: name of field
short: limit printing to a single-line summary
level: indentation amount
Document: openapi document
*/ -}}
{{- define "fieldDetail" -}}
{{- $level := or $.level 0 -}}
{{- $indentAmount := mul $level 2 -}}
{{- $fieldSchema := index $.schema.properties $.name -}}
{{- $.name | indent $indentAmount -}}{{"\t"}}<{{ template "typeGuess" (dict "schema" $fieldSchema "Document" $.Document) }}>
{{- if contains $.schema.required $.name}} -required-{{- end -}}
{{- template "extractEnum" (dict "schema" $fieldSchema "Document" $.Document "isLongView" false "limit" 4 "indentAmount" $indentAmount) -}}
{{- "\n" -}}
{{- if not $.short -}}
{{- or $fieldSchema.description "<no description>" | wrap (sub 78 $indentAmount) | indent (add $indentAmount 2) -}}{{- "\n" -}}
{{- "\n" -}}
{{- end -}}
{{- end -}}
{{- /*
Prints the description of the given OpenAPI v3 schema. Also walks through all
sibling schemas to the provided schema and prints the description of those schemas
too
Takes dictionary as argument with keys:
schema: openapiv3 JSON schema
Document: document to resolve refs within
*/ -}}
{{- define "description" -}}
{{- with or (resolveRef (index $.schema "$ref") $.Document) $.schema -}}
{{- if .description -}}
{{- .description -}}
{{- "\n" -}}
{{- end -}}
{{- range .allOf -}}
{{- template "description" (set $ "schema" .)}}
{{- end -}}
{{- if .items -}}
{{- template "description" (set $ "schema" .items) -}}
{{- end -}}
{{- if and .additionalProperties (not (eq "bool" (printf "%T" .additionalProperties))) -}}
{{- template "description" (set $ "schema" .additionalProperties) -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- /* Renders a shortstring representing an interpretation of what is the "type"
of a subschema e.g.:
`string` `number`, `Object`, `[]Object`, `map[string]string`, etc.
Serves as a more helpful type hint than raw typical openapi `type` field
Takes dictionary as argument with keys:
schema: openapiv3 JSON schema
Document: openapi document
*/ -}}
{{- define "typeGuess" -}}
{{- with $.schema -}}
{{- if .items -}}
[]{{template "typeGuess" (set $ "schema" .items)}}
{{- else if and .additionalProperties (not (eq "bool" (printf "%T" .additionalProperties))) -}}
map[string]{{template "typeGuess" (set $ "schema" .additionalProperties)}}
{{- else if and .allOf (not .properties) (eq (len .allOf) 1) -}}
{{- /* If allOf has a single element and there are no direct
properties on the schema, defer to the allOf */ -}}
{{- template "typeGuess" (set $ "schema" (first .allOf)) -}}
{{- else if index . "$ref"}}
{{- /* Parse the #!/components/schemas/io.k8s.Kind string into just the Kind name */ -}}
{{- $ref := index . "$ref" -}}
{{- /* Look up ref schema to see primitive type. Only put the ref type name if it is an object. */ -}}
{{- $refSchema := resolveRef $ref $.Document -}}
{{- if (or (not $refSchema) (or (not $refSchema.type) (eq $refSchema.type "object"))) -}}
{{- $name := split $ref "/" | last -}}
{{- or (split $name "." | last) "Object" -}}
{{- else if $refSchema.type -}}
{{- or $refSchema.type "Object" -}}
{{- else -}}
{{- or .type "Object" -}}
{{- end -}}
{{- else -}}
{{/* Old explain used capitalized "Object". Just follow suit */}}
{{- if eq .type "object" -}}Object
{{- else -}}{{- or .type "Object" -}}{{- end -}}
{{- end -}}
{{- else -}}
{{- fail "expected schema argument to subtemplate 'typeguess'" -}}
{{- end -}}
{{- end -}}
{{- /* Check if there is any enum returns it in this format e.g.:
ENUM: "", BlockDevice, CharDevice, Directory
enum: "", BlockDevice, CharDevice, Directory
Can change the style of enum in future by modifying this function
Takes dictionary as argument with keys:
schema: openapiv3 JSON schema
Document: openapi document
isLongView: (boolean) Simple view: long list of all fields. Long view: all details of one field
limit: (int) truncate the amount of enums that can be printed in simple view, -1 means all items
indentAmount: intent of the beginning enum line in longform view
*/ -}}
{{- define "extractEnum" -}}
{{- with $.schema -}}
{{- if .enum -}}
{{- $enumLen := len .enum -}}
{{- $limit := or $.limit -1 -}}
{{- if eq $.isLongView false -}}
{{- "\n" -}}
{{- "" | indent $.indentAmount -}}
{{- "enum: " -}}
{{- else -}}
{{- "ENUM:" -}}
{{- end -}}
{{- range $index, $element := .enum -}}
{{- /* Prints , .... and return the range when it goes over the limit */ -}}
{{- if and (gt $limit -1) (ge $index $limit) -}}
{{- ", ...." -}}
{{- break -}}
{{- end -}}
{{- /* Use to reflect "" when we see empty string */ -}}
{{- $elementType := printf "%T" $element -}}
{{- /* Print out either `, ` or `\n ` based of the view */ -}}
{{- /* Simple view */ -}}
{{- if and (gt $index 0) (eq $.isLongView false) -}}
{{- ", " -}}
{{- /* Long view */ -}}
{{- else if eq $.isLongView true -}}
{{- "\n" -}}{{- "" | indent 4 -}}
{{- end -}}
{{- /* Convert empty string to `""` for more clarification */ -}}
{{- if and (eq "string" $elementType) (eq $element "") -}}
{{- `""` -}}
{{- else -}}
{{- $element -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}