databricks-cli/bundle/schema/schema.go

257 lines
7.5 KiB
Go
Raw Normal View History

2023-01-13 03:52:25 +00:00
package schema
import (
"container/list"
2023-01-13 03:52:25 +00:00
"fmt"
"reflect"
"strings"
)
2023-01-13 11:09:19 +00:00
const MaxHistoryOccurances = 3
// TODO: should omit empty denote non required fields in the json schema?
2023-01-13 03:52:25 +00:00
type Schema struct {
2023-01-13 18:40:09 +00:00
Type JavascriptType `json:"type"`
Properities map[string]*Property `json:"properties,omitempty"`
AdditionalProperities *Property `json:"additionalProperties,omitempty"`
2023-01-13 03:52:25 +00:00
}
type Property struct {
2023-01-13 18:40:09 +00:00
Type JavascriptType `json:"type"`
2023-01-13 16:28:46 +00:00
Items *Item `json:"items,omitempty"`
Properities map[string]*Property `json:"properties,omitempty"`
AdditionalProperities *Property `json:"additionalProperties,omitempty"`
2023-01-13 03:52:25 +00:00
}
// TODO: panic for now, add support for adding schemas to $defs in case of cycles
2023-01-13 03:52:25 +00:00
type Item struct {
2023-01-13 18:40:09 +00:00
Type JavascriptType `json:"type"`
Properities map[string]*Property `json:"properties,omitempty"`
2023-01-13 03:52:25 +00:00
}
func NewSchema(golangType reflect.Type) (*Schema, error) {
2023-01-13 19:03:24 +00:00
seenTypes := map[reflect.Type]struct{}{}
debugTrace := list.New()
2023-01-16 01:23:35 +00:00
rootProp, err := toProperty(golangType, seenTypes, debugTrace)
2023-01-13 03:52:25 +00:00
if err != nil {
2023-01-13 19:03:24 +00:00
return nil, errWithTrace(err.Error(), debugTrace)
2023-01-13 03:52:25 +00:00
}
return &Schema{
2023-01-13 11:09:19 +00:00
Type: rootProp.Type,
Properities: rootProp.Properities,
AdditionalProperities: rootProp.AdditionalProperities,
2023-01-13 03:52:25 +00:00
}, nil
}
// TODO: add tests for errors being triggered
2023-01-13 19:03:24 +00:00
type JavascriptType string
2023-01-13 03:52:25 +00:00
const (
2023-01-13 19:03:24 +00:00
Invalid JavascriptType = "invalid"
Boolean JavascriptType = "boolean"
String JavascriptType = "string"
Number JavascriptType = "number"
Object JavascriptType = "object"
Array JavascriptType = "array"
2023-01-13 03:52:25 +00:00
)
2023-01-13 18:40:09 +00:00
func javascriptType(golangType reflect.Type) (JavascriptType, error) {
switch golangType.Kind() {
2023-01-13 03:52:25 +00:00
case reflect.Bool:
return Boolean, nil
case reflect.String:
return String, nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Float32, reflect.Float64:
2023-01-13 03:52:25 +00:00
return Number, nil
case reflect.Struct:
return Object, nil
case reflect.Map:
if golangType.Key().Kind() != reflect.String {
return Invalid, fmt.Errorf("only strings map keys are valid. key type: %v", golangType.Key().Kind())
2023-01-13 03:52:25 +00:00
}
return Object, nil
case reflect.Array, reflect.Slice:
return Array, nil
default:
return Invalid, fmt.Errorf("unhandled golang type: %s", golangType)
}
}
2023-01-13 17:01:53 +00:00
// TODO: add a simple test for this
2023-01-13 18:37:35 +00:00
func errWithTrace(prefix string, trace *list.List) error {
2023-01-13 19:03:24 +00:00
traceString := "root"
2023-01-13 18:37:35 +00:00
curr := trace.Front()
for curr.Next() != nil {
2023-01-13 19:03:24 +00:00
traceString += " -> " + curr.Value.(string)
2023-01-13 18:37:35 +00:00
curr = curr.Next()
2023-01-13 03:52:25 +00:00
}
2023-01-13 19:03:24 +00:00
return fmt.Errorf("[ERROR] " + prefix + ". traveral trace: " + traceString)
2023-01-13 03:52:25 +00:00
}
// TODO: handle case of self referential pointers in structs
2023-01-13 17:01:53 +00:00
// TODO: add handling of embedded types
// TODO: add tests for the error cases, forcefully triggering them
2023-01-13 03:52:25 +00:00
2023-01-16 01:23:35 +00:00
// A wrapper over toProperty function with checks for an cycles to avoid being
// stuck in an loop when traversing the config struct
2023-01-13 19:03:24 +00:00
func safeToProperty(golangType reflect.Type, seenTypes map[reflect.Type]struct{}, debugTrace *list.List) (*Property, error) {
// detect cycles. Fail if a cycle is detected
// TODO: Add references here for cycles
// TODO: move this check somewhere nicer
2023-01-13 19:03:24 +00:00
_, ok := seenTypes[golangType]
if ok {
2023-01-13 19:03:24 +00:00
fmt.Println("[DEBUG] traceSet: ", seenTypes)
return nil, fmt.Errorf("cycle detected")
}
// add current child field to history
2023-01-13 19:03:24 +00:00
seenTypes[golangType] = struct{}{}
2023-01-16 01:23:35 +00:00
props, err := toProperty(golangType, seenTypes, debugTrace)
if err != nil {
2023-01-13 19:03:24 +00:00
return nil, err
}
2023-01-13 19:03:24 +00:00
delete(seenTypes, golangType)
return props, nil
}
// travels anonymous embedded fields in a bfs manner to give us a list of all
// member fields of a struct
// simple Tree based traversal will take place because embbedded fields cannot
// form a cycle
2023-01-16 01:23:35 +00:00
// Adds the member fields of golangType to the passed slice. Needed because
// golangType can contain embedded fields (aka anonymous)
//
// The function traverses the embedded fields in a breadth first manner
//
// params:
// fields: slice to which member fields of golangType will be added to
func addStructFields(fields []reflect.StructField, golangType reflect.Type) []reflect.StructField {
bfsQueue := list.New()
2023-01-13 18:25:42 +00:00
for i := golangType.NumField() - 1; i >= 0; i-- {
bfsQueue.PushBack(golangType.Field(i))
}
for bfsQueue.Len() > 0 {
front := bfsQueue.Front()
field := front.Value.(reflect.StructField)
bfsQueue.Remove(front)
if !field.Anonymous {
fields = append(fields, field)
continue
}
2023-01-13 18:25:42 +00:00
// TODO: add test case for pointer too
fieldType := field.Type
if fieldType.Kind() == reflect.Pointer {
fieldType = fieldType.Elem()
}
for i := 0; i < fieldType.NumField(); i++ {
bfsQueue.PushBack(fieldType.Field(i))
}
}
return fields
}
2023-01-13 19:03:24 +00:00
// TODO: add ignore for -
2023-01-16 01:23:35 +00:00
// params:
// golangType: golang type for which json schema properties to generate
// seenTypes : set of golang types already seen in path during recursion.
// Used to identify cycles.
// debugTrace: linked list of golang types encounted. In case of errors this
// helps log where the error originated from
func toProperty(golangType reflect.Type, seenTypes map[reflect.Type]struct{}, debugTrace *list.List) (*Property, error) {
2023-01-13 03:52:25 +00:00
// *Struct and Struct generate identical json schemas
2023-01-13 19:03:24 +00:00
// TODO: add test case for pointer
if golangType.Kind() == reflect.Pointer {
2023-01-16 01:23:35 +00:00
return toProperty(golangType.Elem(), seenTypes, debugTrace)
2023-01-13 19:03:24 +00:00
}
// TODO: add test case for interfaces
if golangType.Kind() == reflect.Interface {
return nil, nil
2023-01-13 03:52:25 +00:00
}
rootJavascriptType, err := javascriptType(golangType)
2023-01-13 03:52:25 +00:00
if err != nil {
return nil, err
2023-01-13 03:52:25 +00:00
}
// case array/slice
2023-01-13 03:52:25 +00:00
var items *Item
if golangType.Kind() == reflect.Array || golangType.Kind() == reflect.Slice {
2023-01-13 17:01:53 +00:00
elemGolangType := golangType.Elem()
elemJavascriptType, err := javascriptType(elemGolangType)
2023-01-13 03:52:25 +00:00
if err != nil {
return nil, err
2023-01-13 17:01:53 +00:00
}
2023-01-13 19:03:24 +00:00
elemProps, err := safeToProperty(elemGolangType, seenTypes, debugTrace)
2023-01-13 17:01:53 +00:00
if err != nil {
return nil, err
2023-01-13 17:01:53 +00:00
}
2023-01-13 03:52:25 +00:00
items = &Item{
2023-01-13 17:01:53 +00:00
// TODO: Add a test for slice of object
Type: elemJavascriptType,
2023-01-13 17:01:53 +00:00
Properities: elemProps.Properities,
2023-01-13 03:52:25 +00:00
}
}
// case map
var additionalProperties *Property
if golangType.Kind() == reflect.Map {
if golangType.Key().Kind() != reflect.String {
2023-01-13 19:03:24 +00:00
return nil, fmt.Errorf("only string keyed maps allowed")
}
2023-01-13 19:03:24 +00:00
additionalProperties, err = safeToProperty(golangType.Elem(), seenTypes, debugTrace)
if err != nil {
return nil, err
}
}
2023-01-13 11:09:19 +00:00
// case struct
2023-01-13 17:01:53 +00:00
properities := map[string]*Property{}
if golangType.Kind() == reflect.Struct {
children := []reflect.StructField{}
children = addStructFields(children, golangType)
for _, child := range children {
2023-01-13 11:09:19 +00:00
// compute child properties
childJsonTag := child.Tag.Get("json")
childName := strings.Split(childJsonTag, ",")[0]
2023-01-13 11:09:19 +00:00
2023-01-13 19:03:24 +00:00
// add current field to debug trace
debugTrace.PushBack(childName)
// skip non json annotated fields
if childName == "" {
continue
2023-01-13 11:09:19 +00:00
}
// recursively compute properties for this child field
2023-01-13 19:03:24 +00:00
fieldProps, err := safeToProperty(child.Type, seenTypes, debugTrace)
2023-01-13 03:52:25 +00:00
if err != nil {
2023-01-13 19:03:24 +00:00
return nil, err
2023-01-13 03:52:25 +00:00
}
properities[childName] = fieldProps
2023-01-13 19:03:24 +00:00
// remove current field from debug trace
back := debugTrace.Back()
debugTrace.Remove(back)
2023-01-13 03:52:25 +00:00
}
}
return &Property{
Type: rootJavascriptType,
Items: items,
Properities: properities,
AdditionalProperities: additionalProperties,
2023-01-13 03:52:25 +00:00
}, nil
}