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(def *DefForGen) (*codejen.File, error) { f, err := jsonschema.GenerateSchema(def.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 { Info struct { Title string } 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 := def.Properties.Common() data := templateData{ KindName: kindProps.Name, KindVersion: def.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), obj.Info.Title) if err != nil { return nil, fmt.Errorf("failed to build markdown for kind %s: %v", kindProps.Name, err) } return codejen.NewFile(filepath.Join(j.docsPath, strings.ToLower(kindProps.Name), "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. 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]) } // Emphasize required properties. var required string if in(s.Required, k) { required = "**Yes**" } else { required = "No" } desc := p.Description if len(p.Enum) > 0 { vals := make([]string, 0, len(p.Enum)) 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) }