From 3c01ae2c9ef87d92259f72dd1bba4f4466ae431f Mon Sep 17 00:00:00 2001 From: Tania Date: Thu, 2 Feb 2023 13:02:29 +0100 Subject: [PATCH] Docs: Render map type fields in generated docs (#62022) * Render map type in docs * Generate * Clenaup rendering properties * Minor updates * Update maps rendering --- .../core/serviceaccount/schema-reference.md | 35 ++-- .../kinds/core/team/schema-reference.md | 29 ++- pkg/codegen/jenny_docs.go | 182 ++++++++++++------ 3 files changed, 149 insertions(+), 97 deletions(-) diff --git a/docs/sources/developers/kinds/core/serviceaccount/schema-reference.md b/docs/sources/developers/kinds/core/serviceaccount/schema-reference.md index 7a9b487e6d1..ba92626deac 100644 --- a/docs/sources/developers/kinds/core/serviceaccount/schema-reference.md +++ b/docs/sources/developers/kinds/core/serviceaccount/schema-reference.md @@ -13,26 +13,19 @@ title: ServiceAccount kind ## Properties -| Property | Type | Required | Description | -|-----------------|--------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------| -| `avatarUrl` | string | **Yes** | AvatarUrl is the service account's avatar URL. It allows the frontend to display a picture in front
of the service account. | -| `id` | integer | **Yes** | ID is the unique identifier of the service account in the database. | -| `isDisabled` | boolean | **Yes** | IsDisabled indicates if the service account is disabled. | -| `login` | string | **Yes** | Login of the service account. | -| `name` | string | **Yes** | Name of the service account. | -| `orgId` | integer | **Yes** | OrgId is the ID of an organisation the service account belongs to. | -| `role` | string | **Yes** | OrgRole is a Grafana Organization Role which can be 'Viewer', 'Editor', 'Admin'. Possible values are: `Admin`, `Editor`, `Viewer`. | -| `tokens` | integer | **Yes** | Tokens is the number of active tokens for the service account.
Tokens are used to authenticate the service account against Grafana. | -| `accessControl` | [object](#accesscontrol) | No | AccessControl metadata associated with a given resource. | -| `created` | integer | No | Created indicates when the service account was created. | -| `teams` | string[] | No | Teams is a list of teams the service account belongs to. | -| `updated` | integer | No | Updated indicates when the service account was updated. | - -## accessControl - -AccessControl metadata associated with a given resource. - -| Property | Type | Required | Description | -|----------|------|----------|-------------| +| Property | Type | Required | Description | +|-----------------|--------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------| +| `avatarUrl` | string | **Yes** | AvatarUrl is the service account's avatar URL. It allows the frontend to display a picture in front
of the service account. | +| `id` | integer | **Yes** | ID is the unique identifier of the service account in the database. | +| `isDisabled` | boolean | **Yes** | IsDisabled indicates if the service account is disabled. | +| `login` | string | **Yes** | Login of the service account. | +| `name` | string | **Yes** | Name of the service account. | +| `orgId` | integer | **Yes** | OrgId is the ID of an organisation the service account belongs to. | +| `role` | string | **Yes** | OrgRole is a Grafana Organization Role which can be 'Viewer', 'Editor', 'Admin'. Possible values are: `Admin`, `Editor`, `Viewer`. | +| `tokens` | integer | **Yes** | Tokens is the number of active tokens for the service account.
Tokens are used to authenticate the service account against Grafana. | +| `accessControl` | map[string]boolean | No | AccessControl metadata associated with a given resource. | +| `created` | integer | No | Created indicates when the service account was created. | +| `teams` | string[] | No | Teams is a list of teams the service account belongs to. | +| `updated` | integer | No | Updated indicates when the service account was updated. | diff --git a/docs/sources/developers/kinds/core/team/schema-reference.md b/docs/sources/developers/kinds/core/team/schema-reference.md index e58402f70d4..36a89101065 100644 --- a/docs/sources/developers/kinds/core/team/schema-reference.md +++ b/docs/sources/developers/kinds/core/team/schema-reference.md @@ -13,23 +13,16 @@ title: Team kind ## 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 | -|----------|------|----------|-------------| +| 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` | map[string]boolean | 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. | diff --git a/pkg/codegen/jenny_docs.go b/pkg/codegen/jenny_docs.go index 6718bd67d77..a802e9dc74e 100644 --- a/pkg/codegen/jenny_docs.go +++ b/pkg/codegen/jenny_docs.go @@ -108,18 +108,44 @@ type templateData struct { // 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"` + 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"` + AdditionalProperties *schema `json:"additionalProperties"` + Default any `json:"default"` +} + +func renderMapType(props *schema) string { + if props == nil { + return "" + } + + if props.Type.HasType(PropertyTypeObject) { + name, anchor := propNameAndAnchor(props.Title, props.Title) + return fmt.Sprintf("[%s](#%s)", name, anchor) + } + + if props.AdditionalProperties != nil { + return "map[string]" + renderMapType(props.AdditionalProperties) + } + + if props.Items != nil { + return "[]" + renderMapType(props.Items) + } + + var types []string + for _, t := range props.Type { + types = append(types, string(t)) + } + return strings.Join(types, ", ") } func jsonToMarkdown(jsonData []byte, tpl string, kindName string) ([]byte, error) { @@ -189,6 +215,21 @@ func resolveSchema(schem *schema, root *simplejson.Json) (*schema, error) { *schem.Items = *foo } + if schem.AdditionalProperties != nil { + if schem.AdditionalProperties.Ref != "" { + tmp, err := resolveReference(schem.AdditionalProperties.Ref, root) + if err != nil { + return nil, err + } + *schem.AdditionalProperties = *tmp + } + foo, err := resolveSchema(schem.AdditionalProperties, root) + if err != nil { + return nil, err + } + *schem.AdditionalProperties = *foo + } + return schem, nil } @@ -242,7 +283,7 @@ func (s schema) Markdown(level int) string { var buf bytes.Buffer - if s.Title != "" { + if s.Title != "" && s.AdditionalProperties == nil { fmt.Fprintln(&buf, makeHeading(s.Title, level)) fmt.Fprintln(&buf) } @@ -289,9 +330,9 @@ func findDefinitions(s *schema) []*schema { // properties for them recursively. var objs []*schema - for k, p := range s.Properties { - // Use the identifier as the title. - if p.Type.HasType(PropertyTypeObject) { + definition := func(k string, p *schema) { + if p.Type.HasType(PropertyTypeObject) && p.AdditionalProperties == nil { + // Use the identifier as the title. if len(p.Title) == 0 { p.Title = k } @@ -312,6 +353,15 @@ func findDefinitions(s *schema) []*schema { } } + for k, p := range s.Properties { + // If a property has AdditionalProperties, then it's a map + if p.AdditionalProperties != nil { + definition(k, p.AdditionalProperties) + } + + definition(k, p) + } + // Sort the object schemas. sort.Slice(objs, func(i, j int) bool { return objs[i].Title < objs[j].Title @@ -331,67 +381,36 @@ func printProperties(w io.Writer, s *schema) { // Buffer all property rows so that we can sort them before printing them. rows := make([][]string, 0, len(s.Properties)) - for k, p := range s.Properties { - // Generate relative links for objects and arrays of objects. - propType := make([]string, 0, len(p.Type)) - 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]) - } + for key, val := range s.Properties { + typeStr := propTypeStr(key, val) // Emphasize required properties. var required string - if in(s.Required, k) { + if in(s.Required, key) { required = "**Yes**" } else { required = "No" } - desc := p.Description + desc := val.Description - if len(p.Enum) > 0 { - vals := make([]string, 0, len(p.Enum)) - for _, e := range p.Enum { + if len(val.Enum) > 0 { + vals := make([]string, 0, len(val.Enum)) + for _, e := range val.Enum { vals = append(vals, e.String()) } desc += " Possible values are: `" + strings.Join(vals, "`, `") + "`." } - if p.Default != nil { - desc += fmt.Sprintf(" Default: `%v`.", p.Default) + if val.Default != nil { + desc += fmt.Sprintf(" Default: `%v`.", val.Default) } - rows = append(rows, []string{fmt.Sprintf("`%s`", k), propTypeStr, required, formatForTable(desc)}) + rows = append(rows, []string{fmt.Sprintf("`%s`", key), typeStr, required, formatForTable(desc)}) } - // Sort by the required column, then by the name column. + // Sort by the required column, then by the key column. sort.Slice(rows, func(i, j int) bool { if rows[i][2] < rows[j][2] { return true @@ -406,6 +425,53 @@ func printProperties(w io.Writer, s *schema) { table.Render() } +func propTypeStr(propName string, propValue *schema) string { + // If the property has AdditionalProperties, it is most likely a map type + if propValue.AdditionalProperties != nil { + mapValue := renderMapType(propValue.AdditionalProperties) + return "map[string]" + mapValue + } + + propType := make([]string, 0, len(propValue.Type)) + // Generate relative links for objects and arrays of objects. + for _, pt := range propValue.Type { + switch pt { + case PropertyTypeObject: + name, anchor := propNameAndAnchor(propName, propValue.Title) + propType = append(propType, fmt.Sprintf("[%s](#%s)", name, anchor)) + case PropertyTypeArray: + if propValue.Items != nil { + for _, pi := range propValue.Items.Type { + if pi == PropertyTypeObject { + name, anchor := propNameAndAnchor(propName, propValue.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)) + } + } + + if len(propType) == 0 { + return "" + } + + if len(propType) == 1 { + return propType[0] + } + + if len(propType) == 2 { + return strings.Join(propType, " or ") + } + + return fmt.Sprintf("%s, or %s", strings.Join(propType[:len(propType)-1], ", "), propType[len(propType)-1]) +} + func propNameAndAnchor(prop, title string) (string, string) { if len(title) > 0 { return title, strings.ToLower(title)