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: if hasProperties { newProps = append(newProps, extractNodes(k, v.Properties, refs, ownFields)...) } // Adds node with definition for the type of array item. Example: // permissions: // - 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: // : 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 + "." } 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) }