github.com/databricks/cli@v0.203.0/bundle/schema/schema.go (about) 1 package schema 2 3 import ( 4 "container/list" 5 "fmt" 6 "reflect" 7 "strings" 8 9 "github.com/databricks/cli/libs/jsonschema" 10 ) 11 12 // This function translates golang types into json schema. Here is the mapping 13 // between json schema types and golang types 14 // 15 // - GolangType -> Javascript type / Json Schema2 16 // 17 // - bool -> boolean 18 // 19 // - string -> string 20 // 21 // - int (all variants) -> number 22 // 23 // - float (all variants) -> number 24 // 25 // - map[string]MyStruct -> { type: object, additionalProperties: {}} 26 // for details visit: https://json-schema.org/understanding-json-schema/reference/object.html#additional-properties 27 // 28 // - []MyStruct -> {type: array, items: {}} 29 // for details visit: https://json-schema.org/understanding-json-schema/reference/array.html#items 30 // 31 // - []MyStruct -> {type: object, properties: {}, additionalProperties: false} 32 // for details visit: https://json-schema.org/understanding-json-schema/reference/object.html#properties 33 func New(golangType reflect.Type, docs *Docs) (*jsonschema.Schema, error) { 34 tracker := newTracker() 35 schema, err := safeToSchema(golangType, docs, "", tracker) 36 if err != nil { 37 return nil, tracker.errWithTrace(err.Error(), "root") 38 } 39 return schema, nil 40 } 41 42 func jsonSchemaType(golangType reflect.Type) (jsonschema.Type, error) { 43 switch golangType.Kind() { 44 case reflect.Bool: 45 return jsonschema.BooleanType, nil 46 case reflect.String: 47 return jsonschema.StringType, nil 48 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, 49 reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, 50 reflect.Float32, reflect.Float64: 51 52 return jsonschema.NumberType, nil 53 case reflect.Struct: 54 return jsonschema.ObjectType, nil 55 case reflect.Map: 56 if golangType.Key().Kind() != reflect.String { 57 return jsonschema.InvalidType, fmt.Errorf("only strings map keys are valid. key type: %v", golangType.Key().Kind()) 58 } 59 return jsonschema.ObjectType, nil 60 case reflect.Array, reflect.Slice: 61 return jsonschema.ArrayType, nil 62 default: 63 return jsonschema.InvalidType, fmt.Errorf("unhandled golang type: %s", golangType) 64 } 65 } 66 67 // A wrapper over toSchema function to: 68 // 1. Detect cycles in the bundle config struct. 69 // 2. Update tracker 70 // 71 // params: 72 // 73 // - golangType: Golang type to generate json schema for 74 // 75 // - docs: Contains documentation to be injected into the generated json schema 76 // 77 // - traceId: An identifier for the current type, to trace recursive traversal. 78 // Its value is the first json tag in case of struct fields and "" in other cases 79 // like array, map or no json tags 80 // 81 // - tracker: Keeps track of types / traceIds seen during recursive traversal 82 func safeToSchema(golangType reflect.Type, docs *Docs, traceId string, tracker *tracker) (*jsonschema.Schema, error) { 83 // WE ERROR OUT IF THERE ARE CYCLES IN THE JSON SCHEMA 84 // There are mechanisms to deal with cycles though recursive identifiers in json 85 // schema. However if we use them, we would need to make sure we are able to detect 86 // cycles where two properties (directly or indirectly) pointing to each other 87 // 88 // see: https://json-schema.org/understanding-json-schema/structuring.html#recursion 89 // for details 90 if tracker.hasCycle(golangType) { 91 return nil, fmt.Errorf("cycle detected") 92 } 93 94 tracker.push(golangType, traceId) 95 props, err := toSchema(golangType, docs, tracker) 96 if err != nil { 97 return nil, err 98 } 99 tracker.pop(golangType) 100 return props, nil 101 } 102 103 // This function returns all member fields of the provided type. 104 // If the type has embedded (aka anonymous) fields, this function traverses 105 // those in a breadth first manner 106 func getStructFields(golangType reflect.Type) []reflect.StructField { 107 fields := []reflect.StructField{} 108 bfsQueue := list.New() 109 110 for i := 0; i < golangType.NumField(); i++ { 111 bfsQueue.PushBack(golangType.Field(i)) 112 } 113 for bfsQueue.Len() > 0 { 114 front := bfsQueue.Front() 115 field := front.Value.(reflect.StructField) 116 bfsQueue.Remove(front) 117 118 if !field.Anonymous { 119 fields = append(fields, field) 120 continue 121 } 122 123 fieldType := field.Type 124 if fieldType.Kind() == reflect.Pointer { 125 fieldType = fieldType.Elem() 126 } 127 128 for i := 0; i < fieldType.NumField(); i++ { 129 bfsQueue.PushBack(fieldType.Field(i)) 130 } 131 } 132 return fields 133 } 134 135 func toSchema(golangType reflect.Type, docs *Docs, tracker *tracker) (*jsonschema.Schema, error) { 136 // *Struct and Struct generate identical json schemas 137 if golangType.Kind() == reflect.Pointer { 138 return safeToSchema(golangType.Elem(), docs, "", tracker) 139 } 140 if golangType.Kind() == reflect.Interface { 141 return &jsonschema.Schema{}, nil 142 } 143 144 rootJavascriptType, err := jsonSchemaType(golangType) 145 if err != nil { 146 return nil, err 147 } 148 jsonSchema := &jsonschema.Schema{Type: rootJavascriptType} 149 150 if docs != nil { 151 jsonSchema.Description = docs.Description 152 } 153 154 // case array/slice 155 if golangType.Kind() == reflect.Array || golangType.Kind() == reflect.Slice { 156 elemGolangType := golangType.Elem() 157 elemJavascriptType, err := jsonSchemaType(elemGolangType) 158 if err != nil { 159 return nil, err 160 } 161 var childDocs *Docs 162 if docs != nil { 163 childDocs = docs.Items 164 } 165 elemProps, err := safeToSchema(elemGolangType, childDocs, "", tracker) 166 if err != nil { 167 return nil, err 168 } 169 jsonSchema.Items = &jsonschema.Schema{ 170 Type: elemJavascriptType, 171 Properties: elemProps.Properties, 172 AdditionalProperties: elemProps.AdditionalProperties, 173 Items: elemProps.Items, 174 Required: elemProps.Required, 175 } 176 } 177 178 // case map 179 if golangType.Kind() == reflect.Map { 180 if golangType.Key().Kind() != reflect.String { 181 return nil, fmt.Errorf("only string keyed maps allowed") 182 } 183 var childDocs *Docs 184 if docs != nil { 185 childDocs = docs.AdditionalProperties 186 } 187 jsonSchema.AdditionalProperties, err = safeToSchema(golangType.Elem(), childDocs, "", tracker) 188 if err != nil { 189 return nil, err 190 } 191 } 192 193 // case struct 194 if golangType.Kind() == reflect.Struct { 195 children := getStructFields(golangType) 196 properties := map[string]*jsonschema.Schema{} 197 required := []string{} 198 for _, child := range children { 199 bundleTag := child.Tag.Get("bundle") 200 if bundleTag == "readonly" { 201 continue 202 } 203 204 // get child json tags 205 childJsonTag := strings.Split(child.Tag.Get("json"), ",") 206 childName := childJsonTag[0] 207 208 // skip children that have no json tags, the first json tag is "" 209 // or the first json tag is "-" 210 if childName == "" || childName == "-" { 211 continue 212 } 213 214 // get docs for the child if they exist 215 var childDocs *Docs 216 if docs != nil { 217 if val, ok := docs.Properties[childName]; ok { 218 childDocs = val 219 } 220 } 221 222 // compute if the child is a required field. Determined by the 223 // presence of "omitempty" in the json tags 224 hasOmitEmptyTag := false 225 for i := 1; i < len(childJsonTag); i++ { 226 if childJsonTag[i] == "omitempty" { 227 hasOmitEmptyTag = true 228 } 229 } 230 if !hasOmitEmptyTag { 231 required = append(required, childName) 232 } 233 234 // compute Schema.Properties for the child recursively 235 fieldProps, err := safeToSchema(child.Type, childDocs, childName, tracker) 236 if err != nil { 237 return nil, err 238 } 239 properties[childName] = fieldProps 240 } 241 242 jsonSchema.AdditionalProperties = false 243 jsonSchema.Properties = properties 244 jsonSchema.Required = required 245 } 246 247 return jsonSchema, nil 248 }