mirror of https://github.com/grafana/grafana
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
parent
59df361087
commit
045a12047f
@ -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 | |
||||
|----------|------|----------|-------------| |
||||
|
||||
|
@ -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, "|", "|") |
||||
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…
Reference in new issue