From 045a12047f71a45e3c21ad11875f9b1a0d02c846 Mon Sep 17 00:00:00 2001 From: Tania Date: Mon, 16 Jan 2023 13:55:40 +0100 Subject: [PATCH] Codegen: Generate per-kind reference docs (#60416) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * Undraft and unlist kinds documentation (#61476) * Support running containers without root daemon Signed-off-by: Jack Baldry * Use section shortcode to automatically list child pages Signed-off-by: Jack Baldry * 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 * Add docs-preview to browse drafted pages Signed-off-by: Jack Baldry Signed-off-by: Jack Baldry * Replace end of line and pipe characters in table codegen * Remove draft status from generated docs Signed-off-by: Jack Baldry Co-authored-by: Joan López de la Franca Beltran Co-authored-by: Jack Baldry Co-authored-by: Robert Horvath --- .prettierignore | 3 + docs/Makefile | 16 +- docs/sources/developers/kinds/_index.md | 10 + docs/sources/developers/kinds/core/_index.md | 10 + .../kinds/core/dashboard/schema-reference.md | 219 ++++++++ .../kinds/core/playlist/schema-reference.md | 32 ++ .../kinds/core/team/schema-reference.md | 34 ++ go.mod | 1 + go.sum | 3 + kinds/gen.go | 1 + pkg/codegen/generators.go | 2 +- pkg/codegen/jenny_docs.go | 490 ++++++++++++++++++ pkg/codegen/tmpl/docs.tmpl | 13 + 13 files changed, 827 insertions(+), 7 deletions(-) create mode 100644 docs/sources/developers/kinds/_index.md create mode 100644 docs/sources/developers/kinds/core/_index.md create mode 100644 docs/sources/developers/kinds/core/dashboard/schema-reference.md create mode 100644 docs/sources/developers/kinds/core/playlist/schema-reference.md create mode 100644 docs/sources/developers/kinds/core/team/schema-reference.md create mode 100644 pkg/codegen/jenny_docs.go create mode 100644 pkg/codegen/tmpl/docs.tmpl diff --git a/.prettierignore b/.prettierignore index cc9c39938ad..62700e93ea4 100644 --- a/.prettierignore +++ b/.prettierignore @@ -33,3 +33,6 @@ public/openapi3.json # Generated Kinds report kinds/report.json + +# Generated schema docs +docs/sources/developers/kinds/ diff --git a/docs/Makefile b/docs/Makefile index 4f553a5e878..96dc60c463f 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -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) diff --git a/docs/sources/developers/kinds/_index.md b/docs/sources/developers/kinds/_index.md new file mode 100644 index 00000000000..cdb07012a0c --- /dev/null +++ b/docs/sources/developers/kinds/_index.md @@ -0,0 +1,10 @@ +--- +title: Grafana schema +weight: 200 +_build: + list: false +--- + +# Grafana schema + +{{< section >}} diff --git a/docs/sources/developers/kinds/core/_index.md b/docs/sources/developers/kinds/core/_index.md new file mode 100644 index 00000000000..9e1eedc72aa --- /dev/null +++ b/docs/sources/developers/kinds/core/_index.md @@ -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 >}} diff --git a/docs/sources/developers/kinds/core/dashboard/schema-reference.md b/docs/sources/developers/kinds/core/dashboard/schema-reference.md new file mode 100644 index 00000000000..5fc5f9be873 --- /dev/null +++ b/docs/sources/developers/kinds/core/dashboard/schema-reference.md @@ -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).
1 for shared crosshair.
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
changes to said schema.
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.
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
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
TODO docs
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]`. | + + diff --git a/docs/sources/developers/kinds/core/playlist/schema-reference.md b/docs/sources/developers/kinds/core/playlist/schema-reference.md new file mode 100644 index 00000000000..918c9cb05ca --- /dev/null +++ b/docs/sources/developers/kinds/core/playlist/schema-reference.md @@ -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.
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
creator of the playlist of by the application. | +| `items` | [PlaylistItem](#playlistitem)[] | No | The ordered list of items that the playlist will iterate over.
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.

- dashboard_by_id: The value is an internal numerical identifier set by Grafana. This
is not portable as the numerical identifier is non-deterministic between different instances.
Will be replaced by dashboard_by_uid in the future. (deprecated)
- dashboard_by_tag: The value is a tag which is set on any number of dashboards. All
dashboards behind the tag will be added to the playlist.
- dashboard_by_uid: The value is the dashboard UID | +| `title` | string | No | Title is an unused property -- it will be removed in the future | + + diff --git a/docs/sources/developers/kinds/core/team/schema-reference.md b/docs/sources/developers/kinds/core/team/schema-reference.md new file mode 100644 index 00000000000..6da29527078 --- /dev/null +++ b/docs/sources/developers/kinds/core/team/schema-reference.md @@ -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 | +|----------|------|----------|-------------| + + diff --git a/go.mod b/go.mod index d420673b13d..12483f6bfa8 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index b921e920314..10e99e1a63a 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/kinds/gen.go b/kinds/gen.go index 9a06c592c39..3c437c7049d 100644 --- a/kinds/gen.go +++ b/kinds/gen.go @@ -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")) diff --git a/pkg/codegen/generators.go b/pkg/codegen/generators.go index 037766d13d7..1c6d0743f9f 100644 --- a/pkg/codegen/generators.go +++ b/pkg/codegen/generators.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) diff --git a/pkg/codegen/jenny_docs.go b/pkg/codegen/jenny_docs.go new file mode 100644 index 00000000000..a2cb7667ad9 --- /dev/null +++ b/pkg/codegen/jenny_docs.go @@ -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. 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", "
") + 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) +} diff --git a/pkg/codegen/tmpl/docs.tmpl b/pkg/codegen/tmpl/docs.tmpl new file mode 100644 index 00000000000..7f135c97cea --- /dev/null +++ b/pkg/codegen/tmpl/docs.tmpl @@ -0,0 +1,13 @@ +--- +keywords: + - grafana + - schema +title: {{ .KindName }} kind +--- + +# {{ .KindName }} kind + +### Maturity: {{ .KindMaturity }} +### Version: {{ .KindVersion }} + +{{ .Markdown }}