mirror of https://github.com/databricks/cli.git
229 lines
6.4 KiB
Go
229 lines
6.4 KiB
Go
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
|
|
Type string
|
|
}
|
|
|
|
type attributeNode struct {
|
|
Title string
|
|
Type string
|
|
Description string
|
|
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
|
|
circular bool
|
|
}
|
|
|
|
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 {
|
|
rootProps = append(rootProps, rootProp{k, v, true, false})
|
|
}
|
|
nodes := make([]rootNode, 0, len(rootProps))
|
|
visited := make(map[string]bool)
|
|
|
|
for i := 0; i < len(rootProps); i++ {
|
|
item := rootProps[i]
|
|
k := item.k
|
|
v := item.v
|
|
|
|
if visited[k] {
|
|
continue
|
|
}
|
|
visited[k] = true
|
|
|
|
v = resolveRefs(v, refs)
|
|
node := rootNode{
|
|
Title: k,
|
|
Description: getDescription(v, item.topLevel),
|
|
TopLevel: item.topLevel,
|
|
Example: getExample(v),
|
|
Type: getHumanReadableType(v.Type),
|
|
}
|
|
|
|
hasProperties := len(v.Properties) > 0
|
|
if hasProperties {
|
|
node.Attributes = getAttributes(v.Properties, refs, ownFields, k, item.circular)
|
|
}
|
|
|
|
mapValueType := getMapValueType(v, refs)
|
|
if mapValueType != nil {
|
|
d := getDescription(mapValueType, true)
|
|
if d != "" {
|
|
node.Description = d
|
|
}
|
|
if node.Example == "" {
|
|
node.Example = getExample(mapValueType)
|
|
}
|
|
node.ObjectKeyAttributes = getAttributes(mapValueType.Properties, refs, ownFields, getMapKeyPrefix(k), item.circular)
|
|
}
|
|
|
|
arrayItemType := resolveRefs(v.Items, refs)
|
|
if arrayItemType != nil {
|
|
node.ArrayItemAttributes = getAttributes(arrayItemType.Properties, refs, ownFields, k, item.circular)
|
|
}
|
|
|
|
nodes = append(nodes, node)
|
|
|
|
// 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
|
|
}
|
|
|
|
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>"
|
|
}
|
|
|
|
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",
|
|
}
|
|
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)
|
|
typeString := getHumanReadableType(v.Type)
|
|
if typeString == "" {
|
|
typeString = "Any"
|
|
}
|
|
var reference string
|
|
if isReferenceType(v, refs, ownFields) && !circular {
|
|
reference = prefix + "." + k
|
|
}
|
|
attributes = append(attributes, attributeNode{
|
|
Title: k,
|
|
Type: typeString,
|
|
Description: getDescription(v, true),
|
|
Link: reference,
|
|
})
|
|
}
|
|
sort.Slice(attributes, func(i, j int) bool {
|
|
return attributes[i].Title < attributes[j].Title
|
|
})
|
|
return attributes
|
|
}
|
|
|
|
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 {
|
|
if i := strings.Index(ref, "github.com"); i >= 0 {
|
|
ref = ref[i:]
|
|
}
|
|
_, isCustomField := ownFields[ref]
|
|
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 {
|
|
if v.Reference != nil && !shouldExtract(*v.Reference, ownFields) {
|
|
continue
|
|
}
|
|
v = resolveRefs(v, refs)
|
|
if v.Type == "object" || v.Type == "array" {
|
|
nodes = append(nodes, rootProp{prefix + "." + k, v, false, isCycleField(k)})
|
|
}
|
|
}
|
|
return nodes
|
|
}
|
|
|
|
func isCycleField(field string) bool {
|
|
return field == "for_each_task"
|
|
}
|
|
|
|
func getExample(v *jsonschema.Schema) string {
|
|
examples := v.Examples
|
|
if len(examples) == 0 {
|
|
return ""
|
|
}
|
|
return examples[0].(string)
|
|
}
|