databricks-cli/bundle/internal/docs/nodes.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

229 lines
6.4 KiB
Go
Raw Normal View History

package main
import (
"sort"
"strings"
"github.com/databricks/cli/libs/jsonschema"
)
// rootNode is an intermediate representation of resolved JSON-schema item that is used to generate documentation
// Every schema node goes follows this conversion `JSON-schema -> rootNode -> markdown text`
type rootNode struct {
Title string
Description string
Attributes []attributeNode
Example string
ObjectKeyAttributes []attributeNode
ArrayItemAttributes []attributeNode
TopLevel bool
2025-01-03 12:52:59 +00:00
Type string
}
type attributeNode struct {
Title string
Type string
Description string
2025-01-15 21:10:39 +00:00
Link string
}
type rootProp struct {
// k is the name of the property
k string
// v is the corresponding json-schema node
v *jsonschema.Schema
// topLevel is true only for direct properties of the schema of root type (e.g. config.Root or config.Resources)
// Example: config.Root has .
topLevel bool
// circular indicates if property was added by recursive type, e.g. task.for_each_task.task.for_each_task
// These entries don't expand further and don't add any new nodes from their properties
2025-01-13 14:55:49 +00:00
circular bool
}
2025-01-10 11:52:20 +00:00
const MapType = "Map"
// buildNodes converts JSON-schema to a flat list of rootNode items that are then used to generate markdown documentation
// It recursively traverses the schema expanding the resulting list with new items for every properties of nodes `object` and `array` type
func buildNodes(s jsonschema.Schema, refs map[string]*jsonschema.Schema, ownFields map[string]bool) []rootNode {
rootProps := []rootProp{}
for k, v := range s.Properties {
2025-01-13 14:55:49 +00:00
rootProps = append(rootProps, rootProp{k, v, true, false})
}
nodes := make([]rootNode, 0, len(rootProps))
2025-01-10 11:52:20 +00:00
visited := make(map[string]bool)
for i := 0; i < len(rootProps); i++ {
2024-12-18 19:53:38 +00:00
item := rootProps[i]
k := item.k
v := item.v
2025-01-10 11:52:20 +00:00
if visited[k] {
continue
}
visited[k] = true
2025-01-15 21:10:39 +00:00
v = resolveRefs(v, refs)
node := rootNode{
Title: k,
2024-12-18 19:53:38 +00:00
Description: getDescription(v, item.topLevel),
TopLevel: item.topLevel,
2025-01-02 15:13:24 +00:00
Example: getExample(v),
2025-01-03 12:52:59 +00:00
Type: getHumanReadableType(v.Type),
}
2025-01-15 21:10:39 +00:00
hasProperties := len(v.Properties) > 0
if hasProperties {
node.Attributes = getAttributes(v.Properties, refs, ownFields, k, item.circular)
2025-01-13 14:55:49 +00:00
}
2025-01-15 21:10:39 +00:00
mapValueType := getMapValueType(v, refs)
if mapValueType != nil {
d := getDescription(mapValueType, true)
2025-01-10 11:52:20 +00:00
if d != "" {
node.Description = d
}
2025-01-15 21:10:39 +00:00
if node.Example == "" {
node.Example = getExample(mapValueType)
2025-01-13 14:55:49 +00:00
}
2025-01-15 21:10:39 +00:00
node.ObjectKeyAttributes = getAttributes(mapValueType.Properties, refs, ownFields, getMapKeyPrefix(k), item.circular)
}
2025-01-15 21:10:39 +00:00
arrayItemType := resolveRefs(v.Items, refs)
if arrayItemType != nil {
node.ArrayItemAttributes = getAttributes(arrayItemType.Properties, refs, ownFields, k, item.circular)
}
2025-01-13 14:28:41 +00:00
nodes = append(nodes, node)
2025-01-15 21:10:39 +00:00
// Whether we should add new root props from the children of the current JSON-schema node to include their definitions to this document
shouldAddNewProps := !item.circular
if shouldAddNewProps {
newProps := []rootProp{}
// Adds node with definition for the properties. Example:
// bundle:
// prop-name: <value>
if hasProperties {
newProps = append(newProps, extractNodes(k, v.Properties, refs, ownFields)...)
}
// Adds node with definition for the type of array item. Example:
// permissions:
// - <item>
if arrayItemType != nil {
newProps = append(newProps, extractNodes(k, arrayItemType.Properties, refs, ownFields)...)
}
// Adds node with definition for the type of the Map value. Example:
// targets:
// <key>: <value>
if mapValueType != nil {
newProps = append(newProps, extractNodes(getMapKeyPrefix(k), mapValueType.Properties, refs, ownFields)...)
}
rootProps = append(rootProps, newProps...)
}
}
sort.Slice(nodes, func(i, j int) bool {
return nodes[i].Title < nodes[j].Title
})
return nodes
}
2025-01-15 21:10:39 +00:00
func getMapValueType(v *jsonschema.Schema, refs map[string]*jsonschema.Schema) *jsonschema.Schema {
additionalProps, ok := v.AdditionalProperties.(*jsonschema.Schema)
if ok {
return resolveRefs(additionalProps, refs)
}
return nil
}
func getMapKeyPrefix(s string) string {
return s + ".<name>"
}
2025-01-03 12:52:59 +00:00
func removePluralForm(s string) string {
if strings.HasSuffix(s, "s") {
return strings.TrimSuffix(s, "s")
}
return s
}
func getHumanReadableType(t jsonschema.Type) string {
typesMapping := map[string]string{
"string": "String",
"integer": "Integer",
"boolean": "Boolean",
"array": "Sequence",
"object": "Map",
}
2025-01-03 12:52:59 +00:00
return typesMapping[string(t)]
}
func getAttributes(props, refs map[string]*jsonschema.Schema, ownFields map[string]bool, prefix string, circular bool) []attributeNode {
attributes := []attributeNode{}
for k, v := range props {
v = resolveRefs(v, refs)
2025-01-03 12:52:59 +00:00
typeString := getHumanReadableType(v.Type)
if typeString == "" {
typeString = "Any"
}
2025-01-10 11:52:20 +00:00
var reference string
if isReferenceType(v, refs, ownFields) && !circular {
2025-01-10 11:52:20 +00:00
reference = prefix + "." + k
}
attributes = append(attributes, attributeNode{
Title: k,
Type: typeString,
2024-12-18 19:53:38 +00:00
Description: getDescription(v, true),
2025-01-15 21:10:39 +00:00
Link: reference,
})
}
sort.Slice(attributes, func(i, j int) bool {
return attributes[i].Title < attributes[j].Title
})
return attributes
}
2024-12-18 19:53:38 +00:00
func getDescription(s *jsonschema.Schema, allowMarkdown bool) string {
if allowMarkdown && s.MarkdownDescription != "" {
return s.MarkdownDescription
}
return s.Description
}
func shouldExtract(ref string, ownFields map[string]bool) bool {
2025-01-10 11:52:20 +00:00
if i := strings.Index(ref, "github.com"); i >= 0 {
ref = ref[i:]
}
_, isCustomField := ownFields[ref]
2024-12-18 19:53:38 +00:00
return isCustomField
}
// extractNodes returns a list of rootProp items for all properties of the json-schema node that should be extracted based on context
// E.g. we extract all propert
func extractNodes(prefix string, props, refs map[string]*jsonschema.Schema, ownFields map[string]bool) []rootProp {
nodes := []rootProp{}
for k, v := range props {
2025-01-15 21:10:39 +00:00
if v.Reference != nil && !shouldExtract(*v.Reference, ownFields) {
2024-12-18 19:53:38 +00:00
continue
}
v = resolveRefs(v, refs)
2025-01-10 11:52:20 +00:00
if v.Type == "object" || v.Type == "array" {
2025-01-13 14:55:49 +00:00
nodes = append(nodes, rootProp{prefix + "." + k, v, false, isCycleField(k)})
}
}
return nodes
}
2025-01-02 15:13:24 +00:00
2025-01-13 14:55:49 +00:00
func isCycleField(field string) bool {
return field == "for_each_task"
}
2025-01-02 15:13:24 +00:00
func getExample(v *jsonschema.Schema) string {
examples := v.Examples
if len(examples) == 0 {
return ""
}
return examples[0].(string)
}