Codegen: Generate per-kind reference docs (#60416)

* Add docs generator

* Add json-to-md conversion

* Fix lint issues

* Remove check for kind type

* Disable prettier for generated docs

* Use schema ref names as identifiers for links & headers

* Display the default value (if so) in the description

* Undo 'draft:false' introduced by mistake

* Update pkg/codegen/jenny_docs.go

Co-authored-by: Jack Baldry <jack.baldry@grafana.com>

* Undraft and unlist kinds documentation  (#61476)

* Support running containers without root daemon

Signed-off-by: Jack Baldry <jack.baldry@grafana.com>

* Use section shortcode to automatically list child pages

Signed-off-by: Jack Baldry <jack.baldry@grafana.com>

* Undraft and unlist kinds documentation

This page and child pages are directly accessible but are not listed
in the table of contents.

Signed-off-by: Jack Baldry <jack.baldry@grafana.com>

* Add docs-preview to browse drafted pages

Signed-off-by: Jack Baldry <jack.baldry@grafana.com>

Signed-off-by: Jack Baldry <jack.baldry@grafana.com>

* Replace end of line and pipe characters in table codegen

* Remove draft status from generated docs

Signed-off-by: Jack Baldry <jack.baldry@grafana.com>
Co-authored-by: Joan López de la Franca Beltran <joanjan14@gmail.com>
Co-authored-by: Jack Baldry <jack.baldry@grafana.com>
Co-authored-by: Robert Horvath <robert.horvath@grafana.com>
pull/61274/head
Tania 2 years ago committed by GitHub
parent 59df361087
commit 045a12047f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .prettierignore
  2. 16
      docs/Makefile
  3. 10
      docs/sources/developers/kinds/_index.md
  4. 10
      docs/sources/developers/kinds/core/_index.md
  5. 219
      docs/sources/developers/kinds/core/dashboard/schema-reference.md
  6. 32
      docs/sources/developers/kinds/core/playlist/schema-reference.md
  7. 34
      docs/sources/developers/kinds/core/team/schema-reference.md
  8. 1
      go.mod
  9. 3
      go.sum
  10. 1
      kinds/gen.go
  11. 2
      pkg/codegen/generators.go
  12. 490
      pkg/codegen/jenny_docs.go
  13. 13
      pkg/codegen/tmpl/docs.tmpl

@ -33,3 +33,6 @@ public/openapi3.json
# Generated Kinds report
kinds/report.json
# Generated schema docs
docs/sources/developers/kinds/

@ -1,24 +1,28 @@
.PHONY: pull docs docs-quick docs-no-pull docs-test docs-local-static
PODMAN = $(shell if command -v podman &>/dev/null; then echo podman; else echo docker; fi)
IMAGE = grafana/docs-base:latest
CONTENT_PATH = /hugo/content/docs/grafana/next
LOCAL_STATIC_PATH = ../../website/static
PORT = 3002:3002
pull:
docker pull $(IMAGE)
$(PODMAN) pull $(IMAGE)
docs: pull
docker run -v $(shell pwd)/sources:$(CONTENT_PATH):Z -p $(PORT) --rm -it $(IMAGE) /bin/bash -c "make server"
$(PODMAN) run -v $(shell pwd)/sources:$(CONTENT_PATH):Z -p $(PORT) --rm -it $(IMAGE) /bin/bash -c "make server"
docs-preview: pull
$(PODMAN) run -v $(shell pwd)/sources:$(CONTENT_PATH):Z -p $(PORT) --rm -it $(IMAGE) /bin/bash -c "make server BUILD_DRAFTS=true"
docs-no-pull:
docker run -v $(shell pwd)/sources:$(CONTENT_PATH):Z -p $(PORT) --rm -it $(IMAGE) /bin/bash -c "make server"
$(PODMAN) run -v $(shell pwd)/sources:$(CONTENT_PATH):Z -p $(PORT) --rm -it $(IMAGE) /bin/bash -c "make server"
docs-test: pull
docker run -v $(shell pwd)/sources:$(CONTENT_PATH):Z --rm -it $(IMAGE) /bin/bash -c 'make prod'
$(PODMAN) run -v $(shell pwd)/sources:$(CONTENT_PATH):Z --rm -it $(IMAGE) /bin/bash -c 'make prod'
# expects that you have grafana/website checked out in same path as the grafana repo.
docs-local-static: pull
if [ ! -d "$(LOCAL_STATIC_PATH)" ]; then echo "local path (website project) $(LOCAL_STATIC_PATH) not found"]; exit 1; fi
docker run -v $(shell pwd)/sources:$(CONTENT_PATH):Z \
$(PODMAN) run -v $(shell pwd)/sources:$(CONTENT_PATH):Z \
-v $(shell pwd)/$(LOCAL_STATIC_PATH):/hugo/static:Z -p $(PORT) --rm -it $(IMAGE)

@ -0,0 +1,10 @@
---
title: Grafana schema
weight: 200
_build:
list: false
---
# Grafana schema
{{< section >}}

@ -0,0 +1,10 @@
---
title: Core kinds
weight: 200
---
# Grafana core kinds
Kinds that define Grafana’s core schematized object types - dashboards, datasources, users, etc.
{{< section >}}

@ -0,0 +1,219 @@
---
keywords:
- grafana
- schema
title: Dashboard kind
---
# Dashboard kind
### Maturity: merged
### Version: 0.0
## Properties
| Property | Type | Required | Description |
|------------------------|-----------------------------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `editable` | boolean | **Yes** | Whether a dashboard is editable or not. Default: `true`. |
| `graphTooltip` | integer | **Yes** | 0 for no shared crosshair or tooltip (default).<br/>1 for shared crosshair.<br/>2 for shared crosshair AND shared tooltip. Possible values are: `0`, `1`, `2`. Default: `0`. |
| `revision` | integer | **Yes** | Version of the current dashboard data Default: `-1`. |
| `schemaVersion` | integer | **Yes** | Version of the JSON schema, incremented each time a Grafana update brings<br/>changes to said schema.<br/>TODO this is the existing schema numbering system. It will be replaced by Thema's themaVersion Default: `36`. |
| `style` | string | **Yes** | Theme of dashboard. Possible values are: `dark`, `light`. Default: `dark`. |
| `annotations` | [object](#annotations) | No | TODO docs |
| `description` | string | No | Description of dashboard. |
| `fiscalYearStartMonth` | integer | No | TODO docs |
| `gnetId` | string | No | |
| `id` | integer | No | Unique numeric identifier for the dashboard.<br/>TODO must isolate or remove identifiers local to a Grafana instance...? |
| `links` | [DashboardLink](#dashboardlink)[] | No | TODO docs |
| `liveNow` | boolean | No | TODO docs |
| `panels` | [object](#panels)[] | No | |
| `refresh` | | No | TODO docs |
| `snapshot` | [Snapshot](#snapshot) | No | TODO docs |
| `tags` | string[] | No | Tags associated with dashboard. |
| `templating` | [object](#templating) | No | TODO docs |
| `time` | [object](#time) | No | Time range for dashboard, e.g. last 6 hours, last 7 days, etc |
| `timepicker` | [object](#timepicker) | No | TODO docs<br/>TODO this appears to be spread all over in the frontend. Concepts will likely need tidying in tandem with schema changes |
| `timezone` | string | No | Timezone of dashboard, Possible values are: `browser`, `utc`, ``. Default: `browser`. |
| `title` | string | No | Title of dashboard. |
| `uid` | string | No | Unique dashboard identifier that can be generated by anyone. string (8-40) |
| `version` | integer | No | Version of the dashboard, incremented each time the dashboard is updated. |
| `weekStart` | string | No | TODO docs |
## DashboardLink
FROM public/app/features/dashboard/state/DashboardModels.ts - ish
TODO docs
### Properties
| Property | Type | Required | Description |
|---------------|----------|----------|------------------------------------------------------|
| `asDropdown` | boolean | **Yes** | Default: `false`. |
| `icon` | string | **Yes** | |
| `includeVars` | boolean | **Yes** | Default: `false`. |
| `keepTime` | boolean | **Yes** | Default: `false`. |
| `tags` | string[] | **Yes** | |
| `targetBlank` | boolean | **Yes** | Default: `false`. |
| `title` | string | **Yes** | |
| `tooltip` | string | **Yes** | |
| `type` | string | **Yes** | TODO docs Possible values are: `link`, `dashboards`. |
| `url` | string | **Yes** | |
## Snapshot
TODO docs
### Properties
| Property | Type | Required | Description |
|---------------|---------|----------|-------------|
| `created` | string | **Yes** | TODO docs |
| `expires` | string | **Yes** | TODO docs |
| `externalUrl` | string | **Yes** | TODO docs |
| `external` | boolean | **Yes** | TODO docs |
| `id` | integer | **Yes** | TODO docs |
| `key` | string | **Yes** | TODO docs |
| `name` | string | **Yes** | TODO docs |
| `orgId` | integer | **Yes** | TODO docs |
| `updated` | string | **Yes** | TODO docs |
| `userId` | integer | **Yes** | TODO docs |
| `url` | string | No | TODO docs |
## annotations
TODO docs
### Properties
| Property | Type | Required | Description |
|----------|---------------------------------------|----------|-------------|
| `list` | [AnnotationQuery](#annotationquery)[] | No | |
### AnnotationQuery
TODO docs
FROM: AnnotationQuery in grafana-data/src/types/annotations.ts
#### Properties
| Property | Type | Required | Description |
|--------------|---------------------------------------|----------|-------------------------------------------------|
| `builtIn` | integer | **Yes** | Default: `0`. |
| `datasource` | [object](#datasource) | **Yes** | Datasource to use for annotation. |
| `enable` | boolean | **Yes** | Whether annotation is enabled. Default: `true`. |
| `showIn` | integer | **Yes** | Default: `0`. |
| `type` | string | **Yes** | Default: `dashboard`. |
| `hide` | boolean | No | Whether to hide annotation. Default: `false`. |
| `iconColor` | string | No | Annotation icon color. |
| `name` | string | No | Name of annotation. |
| `rawQuery` | string | No | Query for annotation data. |
| `target` | [AnnotationTarget](#annotationtarget) | No | TODO docs |
#### AnnotationTarget
TODO docs
##### Properties
| Property | Type | Required | Description |
|------------|----------|----------|-------------|
| `limit` | integer | **Yes** | |
| `matchAny` | boolean | **Yes** | |
| `tags` | string[] | **Yes** | |
| `type` | string | **Yes** | |
#### datasource
Datasource to use for annotation.
##### Properties
| Property | Type | Required | Description |
|----------|--------|----------|-------------|
| `type` | string | No | |
| `uid` | string | No | |
## panels
| Property | Type | Required | Description |
|----------|------|----------|-------------|
## templating
TODO docs
### Properties
| Property | Type | Required | Description |
|----------|-----------------------------------|----------|-------------|
| `list` | [VariableModel](#variablemodel)[] | No | |
### VariableModel
FROM: packages/grafana-data/src/types/templateVars.ts
TODO docs
TODO what about what's in public/app/features/types.ts?
TODO there appear to be a lot of different kinds of [template] vars here? if so need a disjunction
#### Properties
| Property | Type | Required | Description |
|----------------|---------------------------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `global` | boolean | **Yes** | Default: `false`. |
| `hide` | integer | **Yes** | Possible values are: `0`, `1`, `2`. |
| `id` | string | **Yes** | Default: `00000000-0000-0000-0000-000000000000`. |
| `index` | integer | **Yes** | Default: `-1`. |
| `name` | string | **Yes** | |
| `skipUrlSync` | boolean | **Yes** | Default: `false`. |
| `state` | string | **Yes** | Possible values are: `NotStarted`, `Loading`, `Streaming`, `Done`, `Error`. |
| `type` | string | **Yes** | FROM: packages/grafana-data/src/types/templateVars.ts<br/>TODO docs<br/>TODO this implies some wider pattern/discriminated union, probably? Possible values are: `query`, `adhoc`, `constant`, `datasource`, `interval`, `textbox`, `custom`, `system`. |
| `datasource` | [DataSourceRef](#datasourceref) | No | Ref to a DataSource instance |
| `description` | string | No | |
| `error` | [object](#error) | No | |
| `label` | string | No | |
| `query` | | No | TODO: Move this into a separated QueryVariableModel type |
| `rootStateKey` | string | No | |
#### DataSourceRef
Ref to a DataSource instance
##### Properties
| Property | Type | Required | Description |
|----------|--------|----------|------------------------------|
| `type` | string | No | The plugin type-id |
| `uid` | string | No | Specific datasource instance |
#### error
| Property | Type | Required | Description |
|----------|------|----------|-------------|
## time
Time range for dashboard, e.g. last 6 hours, last 7 days, etc
### Properties
| Property | Type | Required | Description |
|----------|--------|----------|--------------------|
| `from` | string | **Yes** | Default: `now-6h`. |
| `to` | string | **Yes** | Default: `now`. |
## timepicker
TODO docs
TODO this appears to be spread all over in the frontend. Concepts will likely need tidying in tandem with schema changes
### Properties
| Property | Type | Required | Description |
|---------------------|----------|----------|----------------------------------------------------------------------------------------|
| `collapse` | boolean | **Yes** | Whether timepicker is collapsed or not. Default: `false`. |
| `enable` | boolean | **Yes** | Whether timepicker is enabled or not. Default: `true`. |
| `hidden` | boolean | **Yes** | Whether timepicker is visible or not. Default: `false`. |
| `refresh_intervals` | string[] | **Yes** | Selectable intervals for auto-refresh. Default: `[5s 10s 30s 1m 5m 15m 30m 1h 2h 1d]`. |
| `time_options` | string[] | **Yes** | TODO docs Default: `[5m 15m 1h 6h 12h 24h 2d 7d 30d]`. |

@ -0,0 +1,32 @@
---
keywords:
- grafana
- schema
title: Playlist kind
---
# Playlist kind
### Maturity: merged
### Version: 0.0
## Properties
| Property | Type | Required | Description |
|------------|---------------------------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `interval` | string | **Yes** | Interval sets the time between switching views in a playlist.<br/>FIXME: Is this based on a standardized format or what options are available? Can datemath be used? Default: `5m`. |
| `name` | string | **Yes** | Name of the playlist. |
| `uid` | string | **Yes** | Unique playlist identifier. Generated on creation, either by the<br/>creator of the playlist of by the application. |
| `items` | [PlaylistItem](#playlistitem)[] | No | The ordered list of items that the playlist will iterate over.<br/>FIXME! This should not be optional, but changing it makes the godegen awkward |
## PlaylistItem
### Properties
| Property | Type | Required | Description |
|----------|--------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `type` | string | **Yes** | Type of the item. Possible values are: `dashboard_by_uid`, `dashboard_by_id`, `dashboard_by_tag`. |
| `value` | string | **Yes** | Value depends on type and describes the playlist item.<br/><br/> - dashboard_by_id: The value is an internal numerical identifier set by Grafana. This<br/> is not portable as the numerical identifier is non-deterministic between different instances.<br/> Will be replaced by dashboard_by_uid in the future. (deprecated)<br/> - dashboard_by_tag: The value is a tag which is set on any number of dashboards. All<br/> dashboards behind the tag will be added to the playlist.<br/> - dashboard_by_uid: The value is the dashboard UID |
| `title` | string | No | Title is an unused property -- it will be removed in the future |

@ -0,0 +1,34 @@
---
keywords:
- grafana
- schema
title: Team kind
---
# Team kind
### Maturity: merged
### Version: 0.0
## Properties
| Property | Type | Required | Description |
|-----------------|--------------------------|----------|----------------------------------------------------------|
| `created` | integer | **Yes** | Created indicates when the team was created. |
| `memberCount` | integer | **Yes** | MemberCount is the number of the team members. |
| `name` | string | **Yes** | Name of the team. |
| `orgId` | integer | **Yes** | OrgId is the ID of an organisation the team belongs to. |
| `permission` | integer | **Yes** | Possible values are: `0`, `1`, `2`, `4`. |
| `updated` | integer | **Yes** | Updated indicates when the team was updated. |
| `accessControl` | [object](#accesscontrol) | No | AccessControl metadata associated with a given resource. |
| `avatarUrl` | string | No | AvatarUrl is the team's avatar URL. |
| `email` | string | No | Email of the team. |
## accessControl
AccessControl metadata associated with a given resource.
| Property | Type | Required | Description |
|----------|------|----------|-------------|

@ -274,6 +274,7 @@ require (
require (
github.com/dave/dst v0.27.2
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f
github.com/parca-dev/parca v0.12.1
k8s.io/apimachinery v0.25.3
)

@ -2560,9 +2560,12 @@ github.com/xdg/scram v1.0.3/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49
github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
github.com/xdg/stringprep v1.0.3/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg=
github.com/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk=

@ -40,6 +40,7 @@ func main() {
codegen.BaseCoreRegistryJenny(filepath.Join("pkg", "registry", "corekind"), kindsys.GoCoreKindParentPath),
codegen.LatestMajorsOrXJenny(kindsys.TSCoreKindParentPath, codegen.TSTypesJenny{}),
codegen.TSVeneerIndexJenny(filepath.Join("packages", "grafana-schema", "src")),
codegen.DocsJenny(filepath.Join("docs", "sources", "developers", "kinds", "core")),
)
coreKindsGen.AddPostprocessors(codegen.SlashHeaderMapper("kinds/gen.go"))

@ -60,7 +60,7 @@ func SlashHeaderMapper(maingen string) codejen.FileMapper {
return func(f codejen.File) (codejen.File, error) {
// Never inject on certain filetypes, it's never valid
switch filepath.Ext(f.RelativePath) {
case ".json", ".yml", ".yaml":
case ".json", ".yml", ".yaml", ".md":
return f, nil
default:
buf := new(bytes.Buffer)

@ -0,0 +1,490 @@
package codegen
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"path"
"path/filepath"
"sort"
"strings"
"text/template"
"cuelang.org/go/cue/cuecontext"
"github.com/grafana/codejen"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/thema/encoding/jsonschema"
"github.com/olekukonko/tablewriter"
"github.com/xeipuuv/gojsonpointer"
)
func DocsJenny(docsPath string) OneToOne {
return docsJenny{
docsPath: docsPath,
}
}
type docsJenny struct {
docsPath string
}
func (j docsJenny) JennyName() string {
return "DocsJenny"
}
func (j docsJenny) Generate(decl *DeclForGen) (*codejen.File, error) {
f, err := jsonschema.GenerateSchema(decl.Lineage().Latest())
if err != nil {
return nil, fmt.Errorf("failed to generate json representation for the schema: %v", err)
}
b, err := cuecontext.New().BuildFile(f).MarshalJSON()
if err != nil {
return nil, fmt.Errorf("failed to marshal schema value to json: %v", err)
}
// We don't need entire json obj, only the value of components.schemas path
var obj struct {
Components struct {
Schemas json.RawMessage
}
}
err = json.Unmarshal(b, &obj)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal schema json: %v", err)
}
// fixes the references between the types within a json after making components.schema.<types> the root of the json
kindJsonStr := strings.Replace(string(obj.Components.Schemas), "#/components/schemas/", "#/", -1)
kindProps := decl.Properties.Common()
kindName := strings.ToLower(kindProps.Name)
data := templateData{
KindName: kindProps.Name,
KindVersion: decl.Lineage().Latest().Version().String(),
KindMaturity: string(kindProps.Maturity),
Markdown: "{{ .Markdown 1 }}",
}
tmpl, err := makeTemplate(data, "docs.tmpl")
if err != nil {
return nil, err
}
doc, err := jsonToMarkdown([]byte(kindJsonStr), string(tmpl), kindName)
if err != nil {
return nil, fmt.Errorf("failed to build markdown for kind %s: %v", kindName, err)
}
return codejen.NewFile(filepath.Join(j.docsPath, kindName, "schema-reference.md"), doc, j), nil
}
// makeTemplate pre-populates the template with the kind metadata
func makeTemplate(data templateData, tmpl string) ([]byte, error) {
buf := new(bytes.Buffer)
if err := tmpls.Lookup(tmpl).Execute(buf, data); err != nil {
return []byte{}, fmt.Errorf("failed to populate docs template with the kind metadata")
}
return buf.Bytes(), nil
}
type templateData struct {
KindName string
KindVersion string
KindMaturity string
Markdown string
}
// -------------------- JSON to Markdown conversion --------------------
// Copied from https://github.com/marcusolsson/json-schema-docs and slightly changed to fit the DocsJenny
type schema struct {
ID string `json:"$id,omitempty"`
Ref string `json:"$ref,omitempty"`
Schema string `json:"$schema,omitempty"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
Required []string `json:"required,omitempty"`
Type PropertyTypes `json:"type,omitempty"`
Properties map[string]*schema `json:"properties,omitempty"`
Items *schema `json:"items,omitempty"`
Definitions map[string]*schema `json:"definitions,omitempty"`
Enum []Any `json:"enum"`
Default any `json:"default"`
}
func jsonToMarkdown(jsonData []byte, tpl string, kindName string) ([]byte, error) {
sch, err := newSchema(jsonData, kindName)
if err != nil {
return []byte{}, err
}
t, err := template.New("markdown").Parse(tpl)
if err != nil {
return []byte{}, err
}
buf := new(bytes.Buffer)
err = t.Execute(buf, sch)
if err != nil {
return []byte{}, err
}
return buf.Bytes(), nil
}
func newSchema(b []byte, kindName string) (*schema, error) {
var data map[string]*schema
if err := json.Unmarshal(b, &data); err != nil {
return nil, err
}
// Needed for resolving in-schema references.
root, err := simplejson.NewJson(b)
if err != nil {
return nil, err
}
return resolveSchema(data[kindName], root)
}
// resolveSchema recursively resolves schemas.
func resolveSchema(schem *schema, root *simplejson.Json) (*schema, error) {
for _, prop := range schem.Properties {
if prop.Ref != "" {
tmp, err := resolveReference(prop.Ref, root)
if err != nil {
return nil, err
}
*prop = *tmp
}
foo, err := resolveSchema(prop, root)
if err != nil {
return nil, err
}
*prop = *foo
}
if schem.Items != nil {
if schem.Items.Ref != "" {
tmp, err := resolveReference(schem.Items.Ref, root)
if err != nil {
return nil, err
}
*schem.Items = *tmp
}
foo, err := resolveSchema(schem.Items, root)
if err != nil {
return nil, err
}
*schem.Items = *foo
}
return schem, nil
}
// resolveReference loads a schema from a $ref.
// If ref contains a hashtag (#), the part after represents a in-schema reference.
func resolveReference(ref string, root *simplejson.Json) (*schema, error) {
i := strings.Index(ref, "#")
if i != 0 {
return nil, fmt.Errorf("not in-schema reference: %s", ref)
}
return resolveInSchemaReference(ref[i+1:], root)
}
func resolveInSchemaReference(ref string, root *simplejson.Json) (*schema, error) {
// in-schema reference
pointer, err := gojsonpointer.NewJsonPointer(ref)
if err != nil {
return nil, err
}
v, _, err := pointer.Get(root.MustMap())
if err != nil {
return nil, err
}
var sch schema
b, err := json.Marshal(v)
if err != nil {
return nil, err
}
if err := json.Unmarshal(b, &sch); err != nil {
return nil, err
}
// Set the ref name as title
sch.Title = path.Base(ref)
return &sch, nil
}
// Markdown returns the Markdown representation of the schema.
//
// The level argument can be used to offset the heading levels. This can be
// useful if you want to add the schema under a subheading.
func (s schema) Markdown(level int) string {
if level < 1 {
level = 1
}
var buf bytes.Buffer
if s.Title != "" {
fmt.Fprintln(&buf, makeHeading(s.Title, level))
fmt.Fprintln(&buf)
}
if s.Description != "" {
fmt.Fprintln(&buf, s.Description)
if s.Default != nil {
fmt.Fprintf(&buf, "The default value is: `%v`.", s.Default)
}
fmt.Fprintln(&buf)
}
if len(s.Properties) > 0 {
fmt.Fprintln(&buf, makeHeading("Properties", level+1))
fmt.Fprintln(&buf)
}
printProperties(&buf, &s)
// Add padding.
fmt.Fprintln(&buf)
for _, obj := range findDefinitions(&s) {
fmt.Fprint(&buf, obj.Markdown(level+1))
}
return buf.String()
}
func makeHeading(heading string, level int) string {
if level < 0 {
return heading
}
if level <= 6 {
return strings.Repeat("#", level) + " " + heading
}
return fmt.Sprintf("**%s**", heading)
}
func findDefinitions(s *schema) []*schema {
// Gather all properties of object type so that we can generate the
// properties for them recursively.
var objs []*schema
for k, p := range s.Properties {
// Use the identifier as the title.
if p.Type.HasType(PropertyTypeObject) {
if len(p.Title) == 0 {
p.Title = k
}
objs = append(objs, p)
}
// If the property is an array of objects, use the name of the array
// property as the title.
if p.Type.HasType(PropertyTypeArray) {
if p.Items != nil {
if p.Items.Type.HasType(PropertyTypeObject) {
if len(p.Items.Title) == 0 {
p.Items.Title = k
}
objs = append(objs, p.Items)
}
}
}
}
// Sort the object schemas.
sort.Slice(objs, func(i, j int) bool {
return objs[i].Title < objs[j].Title
})
return objs
}
func printProperties(w io.Writer, s *schema) {
table := tablewriter.NewWriter(w)
table.SetHeader([]string{"Property", "Type", "Required", "Description"})
table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false})
table.SetCenterSeparator("|")
table.SetAutoFormatHeaders(false)
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
table.SetAutoWrapText(false)
// Buffer all property rows so that we can sort them before printing them.
var rows [][]string
for k, p := range s.Properties {
// Generate relative links for objects and arrays of objects.
var propType []string
for _, pt := range p.Type {
switch pt {
case PropertyTypeObject:
name, anchor := propNameAndAnchor(k, p.Title)
propType = append(propType, fmt.Sprintf("[%s](#%s)", name, anchor))
case PropertyTypeArray:
if p.Items != nil {
for _, pi := range p.Items.Type {
if pi == PropertyTypeObject {
name, anchor := propNameAndAnchor(k, p.Items.Title)
propType = append(propType, fmt.Sprintf("[%s](#%s)[]", name, anchor))
} else {
propType = append(propType, fmt.Sprintf("%s[]", pi))
}
}
} else {
propType = append(propType, string(pt))
}
default:
propType = append(propType, string(pt))
}
}
var propTypeStr string
if len(propType) == 1 {
propTypeStr = propType[0]
} else if len(propType) == 2 {
propTypeStr = strings.Join(propType, " or ")
} else if len(propType) > 2 {
propTypeStr = fmt.Sprintf("%s, or %s", strings.Join(propType[:len(propType)-1], ", "), propType[len(propType)-1])
}
// Emphasize required properties.
var required string
if in(s.Required, k) {
required = "**Yes**"
} else {
required = "No"
}
desc := p.Description
if len(p.Enum) > 0 {
var vals []string
for _, e := range p.Enum {
vals = append(vals, e.String())
}
desc += " Possible values are: `" + strings.Join(vals, "`, `") + "`."
}
if p.Default != nil {
desc += fmt.Sprintf(" Default: `%v`.", p.Default)
}
rows = append(rows, []string{fmt.Sprintf("`%s`", k), propTypeStr, required, formatForTable(desc)})
}
// Sort by the required column, then by the name column.
sort.Slice(rows, func(i, j int) bool {
if rows[i][2] < rows[j][2] {
return true
}
if rows[i][2] > rows[j][2] {
return false
}
return rows[i][0] < rows[j][0]
})
table.AppendBulk(rows)
table.Render()
}
func propNameAndAnchor(prop, title string) (string, string) {
if len(title) > 0 {
return title, strings.ToLower(title)
}
return string(PropertyTypeObject), strings.ToLower(prop)
}
// in returns true if a string slice contains a specific string.
func in(strs []string, str string) bool {
for _, s := range strs {
if s == str {
return true
}
}
return false
}
// formatForTable returns string usable in a Markdown table.
// It trims white spaces, replaces new lines and pipe characters.
func formatForTable(in string) string {
s := strings.TrimSpace(in)
s = strings.ReplaceAll(s, "\n", "<br/>")
s = strings.ReplaceAll(s, "|", "&#124;")
return s
}
type PropertyTypes []PropertyType
func (pts *PropertyTypes) HasType(pt PropertyType) bool {
for _, t := range *pts {
if t == pt {
return true
}
}
return false
}
func (pts *PropertyTypes) UnmarshalJSON(data []byte) error {
var value interface{}
if err := json.Unmarshal(data, &value); err != nil {
return err
}
switch val := value.(type) {
case string:
*pts = []PropertyType{PropertyType(val)}
return nil
case []interface{}:
var pt []PropertyType
for _, t := range val {
s, ok := t.(string)
if !ok {
return errors.New("unsupported property type")
}
pt = append(pt, PropertyType(s))
}
*pts = pt
default:
return errors.New("unsupported property type")
}
return nil
}
type PropertyType string
const (
PropertyTypeString PropertyType = "string"
PropertyTypeNumber PropertyType = "number"
PropertyTypeBoolean PropertyType = "boolean"
PropertyTypeObject PropertyType = "object"
PropertyTypeArray PropertyType = "array"
PropertyTypeNull PropertyType = "null"
)
type Any struct {
value interface{}
}
func (u *Any) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, &u.value); err != nil {
return err
}
return nil
}
func (u *Any) String() string {
return fmt.Sprintf("%v", u.value)
}

@ -0,0 +1,13 @@
---
keywords:
- grafana
- schema
title: {{ .KindName }} kind
---
# {{ .KindName }} kind
### Maturity: {{ .KindMaturity }}
### Version: {{ .KindVersion }}
{{ .Markdown }}
Loading…
Cancel
Save