From 13181fc5a74b7af0b19963dda6ba83fa6fdfd24a Mon Sep 17 00:00:00 2001 From: Ilya Kuznetsov Date: Wed, 15 Jan 2025 22:10:39 +0100 Subject: [PATCH] fix: More ducmentation + tests --- bundle/internal/docs/markdown.go | 4 +- bundle/internal/docs/nodes.go | 76 ++++++++++++------ bundle/internal/docs/nodes_test.go | 120 +++++++++++++++++++++++++++++ bundle/internal/docs/refs.go | 14 ++-- 4 files changed, 185 insertions(+), 29 deletions(-) create mode 100644 bundle/internal/docs/nodes_test.go diff --git a/bundle/internal/docs/markdown.go b/bundle/internal/docs/markdown.go index 9ab7d0981..6e3b42b65 100644 --- a/bundle/internal/docs/markdown.go +++ b/bundle/internal/docs/markdown.go @@ -87,13 +87,13 @@ func buildAttributeTable(m *markdownRenderer, attributes []attributeNode) *markd func formatDescription(a attributeNode) string { s := strings.ReplaceAll(a.Description, "\n", " ") - if a.Reference != "" { + if a.Link != "" { if strings.HasSuffix(s, ".") { s += " " } else if s != "" { s += ". " } - s += fmt.Sprintf("See [_](#%s).", a.Reference) + s += fmt.Sprintf("See [_](#%s).", a.Link) } return s } diff --git a/bundle/internal/docs/nodes.go b/bundle/internal/docs/nodes.go index 81f2e1443..68ed86450 100644 --- a/bundle/internal/docs/nodes.go +++ b/bundle/internal/docs/nodes.go @@ -24,7 +24,7 @@ type attributeNode struct { Title string Type string Description string - Reference string + Link string } type rootProp struct { @@ -61,6 +61,7 @@ func buildNodes(s jsonschema.Schema, refs map[string]*jsonschema.Schema, ownFiel continue } visited[k] = true + v = resolveRefs(v, refs) node := rootNode{ Title: k, @@ -70,37 +71,56 @@ func buildNodes(s jsonschema.Schema, refs map[string]*jsonschema.Schema, ownFiel Type: getHumanReadableType(v.Type), } - node.Attributes = getAttributes(v.Properties, refs, ownFields, k, item.circular) - if !item.circular { - rootProps = append(rootProps, extractNodes(k, v.Properties, refs, ownFields)...) + hasProperties := len(v.Properties) > 0 + if hasProperties { + node.Attributes = getAttributes(v.Properties, refs, ownFields, k, item.circular) } - additionalProps, ok := v.AdditionalProperties.(*jsonschema.Schema) - if ok { - objectKeyType := resolveRefs(additionalProps, refs) - d := getDescription(objectKeyType, true) + mapValueType := getMapValueType(v, refs) + if mapValueType != nil { + d := getDescription(mapValueType, true) if d != "" { node.Description = d } - if len(node.Example) == 0 { - node.Example = getExample(objectKeyType) - } - prefix := k + "." - node.ObjectKeyAttributes = getAttributes(objectKeyType.Properties, refs, ownFields, prefix, item.circular) - if !item.circular { - rootProps = append(rootProps, extractNodes(prefix, objectKeyType.Properties, refs, ownFields)...) + if node.Example == "" { + node.Example = getExample(mapValueType) } + node.ObjectKeyAttributes = getAttributes(mapValueType.Properties, refs, ownFields, getMapKeyPrefix(k), item.circular) } - if v.Items != nil { - arrayItemType := resolveRefs(v.Items, refs) + arrayItemType := resolveRefs(v.Items, refs) + if arrayItemType != nil { node.ArrayItemAttributes = getAttributes(arrayItemType.Properties, refs, ownFields, k, item.circular) - if !item.circular { - rootProps = append(rootProps, extractNodes(k, arrayItemType.Properties, refs, ownFields)...) - } } 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 { @@ -109,6 +129,18 @@ func buildNodes(s jsonschema.Schema, refs map[string]*jsonschema.Schema, ownFiel 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") @@ -143,7 +175,7 @@ func getAttributes(props, refs map[string]*jsonschema.Schema, ownFields map[stri Title: k, Type: typeString, Description: getDescription(v, true), - Reference: reference, + Link: reference, }) } sort.Slice(attributes, func(i, j int) bool { @@ -172,7 +204,7 @@ func shouldExtract(ref string, ownFields map[string]bool) bool { func extractNodes(prefix string, props, refs map[string]*jsonschema.Schema, ownFields map[string]bool) []rootProp { nodes := []rootProp{} for k, v := range props { - if !shouldExtract(*v.Reference, ownFields) { + if v.Reference != nil && !shouldExtract(*v.Reference, ownFields) { continue } v = resolveRefs(v, refs) diff --git a/bundle/internal/docs/nodes_test.go b/bundle/internal/docs/nodes_test.go new file mode 100644 index 000000000..bdb2ce9db --- /dev/null +++ b/bundle/internal/docs/nodes_test.go @@ -0,0 +1,120 @@ +package main + +import ( + "testing" + + "github.com/databricks/cli/libs/jsonschema" + "github.com/stretchr/testify/assert" +) + +func TestBuildNodes_ChildExpansion(t *testing.T) { + tests := []struct { + name string + schema jsonschema.Schema + refs map[string]*jsonschema.Schema + ownFields map[string]bool + wantNodes []rootNode + }{ + { + name: "array expansion", + schema: jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "list": { + Type: "array", + Items: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "listSub": {Reference: strPtr("#/$defs/github.com/listSub")}, + }, + }, + }, + }, + }, + refs: map[string]*jsonschema.Schema{ + "github.com/listSub": {Type: "array", Items: &jsonschema.Schema{Type: "object", Properties: map[string]*jsonschema.Schema{"subField": {Type: "string"}}}}, + }, + ownFields: map[string]bool{"github.com/listSub": true}, + wantNodes: []rootNode{ + { + Title: "list", + TopLevel: true, + Type: "Sequence", + ArrayItemAttributes: []attributeNode{ + {Title: "listSub", Type: "Sequence", Link: "list.listSub"}, + }, + }, + { + Title: "list.listSub", + Type: "Sequence", + ArrayItemAttributes: []attributeNode{ + {Title: "subField", Type: "String"}, + }, + }, + }, + }, + { + name: "map expansion", + schema: jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "myMap": { + Type: "object", + AdditionalProperties: &jsonschema.Schema{ + Reference: strPtr("#/$defs/github.com/myMap"), + Properties: map[string]*jsonschema.Schema{ + "mapSub": {Type: "object", Reference: strPtr("#/$defs/github.com/mapSub")}, + }, + }, + }, + }, + }, + refs: map[string]*jsonschema.Schema{ + "github.com/myMap": { + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "mapSub": {Type: "boolean", Reference: strPtr("#/$defs/github.com/mapSub")}, + }, + }, + "github.com/mapSub": { + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "deepSub": {Type: "boolean"}, + }, + }, + }, + ownFields: map[string]bool{ + "github.com/myMap": true, + "github.com/mapSub": true, + }, + wantNodes: []rootNode{ + { + Title: "myMap", + TopLevel: true, + Type: "Map", + ObjectKeyAttributes: []attributeNode{ + {Title: "mapSub", Type: "Map", Link: "myMap..mapSub"}, + }, + }, + { + Title: "myMap..mapSub", + Type: "Map", + Attributes: []attributeNode{ + {Title: "deepSub", Type: "Boolean"}, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := buildNodes(tt.schema, tt.refs, tt.ownFields) + assert.Equal(t, tt.wantNodes, got) + }) + } +} + +func strPtr(s string) *string { + return &s +} diff --git a/bundle/internal/docs/refs.go b/bundle/internal/docs/refs.go index 2cc613fbb..ca45e6ab2 100644 --- a/bundle/internal/docs/refs.go +++ b/bundle/internal/docs/refs.go @@ -7,7 +7,7 @@ import ( "github.com/databricks/cli/libs/jsonschema" ) -func isReferenceType(v *jsonschema.Schema, refs map[string]*jsonschema.Schema, customFields map[string]bool) bool { +func isReferenceType(v *jsonschema.Schema, refs map[string]*jsonschema.Schema, ownFields map[string]bool) bool { if v.Type != "object" && v.Type != "array" { return false } @@ -21,7 +21,7 @@ func isReferenceType(v *jsonschema.Schema, refs map[string]*jsonschema.Schema, c } } props := resolveAdditionalProperties(v) - if !isInOwnFields(props, customFields) { + if !isInOwnFields(props, ownFields) { return false } if props != nil { @@ -32,9 +32,9 @@ func isReferenceType(v *jsonschema.Schema, refs map[string]*jsonschema.Schema, c return false } -func isInOwnFields(node *jsonschema.Schema, customFields map[string]bool) bool { +func isInOwnFields(node *jsonschema.Schema, ownFields map[string]bool) bool { if node != nil && node.Reference != nil { - return customFields[getRefType(node)] + return ownFields[getRefType(node)] } return true } @@ -51,8 +51,11 @@ func resolveAdditionalProperties(v *jsonschema.Schema) *jsonschema.Schema { } func resolveRefs(s *jsonschema.Schema, schemas map[string]*jsonschema.Schema) *jsonschema.Schema { - node := s + if s == nil { + return nil + } + node := s description := s.Description markdownDescription := s.MarkdownDescription examples := s.Examples @@ -62,6 +65,7 @@ func resolveRefs(s *jsonschema.Schema, schemas map[string]*jsonschema.Schema) *j newNode, ok := schemas[ref] if !ok { log.Printf("schema %s not found", ref) + break } if description == "" {