databricks-cli/bundle/schema/schema.go

193 lines
5.4 KiB
Go
Raw Normal View History

2023-01-13 03:52:25 +00:00
package schema
import (
"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 11:09:19 +00:00
Type JsType `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 11:09:19 +00:00
Type JsType `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 {
Type JsType `json:"type"`
2023-01-13 17:01:53 +00:00
Properities map[string]*Property `json:"properties,omitempty"`
2023-01-13 03:52:25 +00:00
}
func NewSchema(golangType reflect.Type) (*Schema, error) {
traceSet := map[reflect.Type]struct{}{}
traceSlice := []reflect.Type{}
rootProp, err := toProperity(golangType, traceSet, traceSlice)
2023-01-13 03:52:25 +00:00
if err != nil {
return nil, err
}
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
}
type JsType string
const (
Invalid JsType = "invalid"
Boolean = "boolean"
String = "string"
Number = "number"
Object = "object"
Array = "array"
)
func javascriptType(golangType reflect.Type) (JsType, 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
// TODO: add support for pattern properities to account for maps
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
func errWithTrace(prefix string, trace []reflect.Type) error {
traceString := ""
for _, golangType := range trace {
traceString += " -> " + golangType.Name()
2023-01-13 03:52:25 +00:00
}
return fmt.Errorf("[ERROR] " + prefix + " type 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-13 11:09:19 +00:00
// TODO: add doc string explaining numHistoryOccurances
func toProperity(golangType reflect.Type, traceSet map[reflect.Type]struct{}, traceSlice []reflect.Type) (*Property, error) {
traceSlice = append(traceSlice, golangType)
2023-01-13 03:52:25 +00:00
// *Struct and Struct generate identical json schemas
if golangType.Kind() == reflect.Pointer {
return toProperity(golangType.Elem(), traceSet, traceSlice)
2023-01-13 03:52:25 +00:00
}
rootJavascriptType, err := javascriptType(golangType)
2023-01-13 03:52:25 +00:00
if err != nil {
2023-01-13 17:01:53 +00:00
return nil, errWithTrace(err.Error(), traceSlice)
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 {
2023-01-13 17:01:53 +00:00
return nil, errWithTrace(err.Error(), traceSlice)
2023-01-13 03:52:25 +00:00
}
2023-01-13 17:01:53 +00:00
// detect cycles. Fail if a cycle is detected
// TODO: Add references here for cycles
_, ok := traceSet[elemGolangType]
if ok {
fmt.Println("[DEBUG] traceSet: ", traceSet)
return nil, errWithTrace("cycle detected", traceSlice)
}
// add current child field to history
traceSet[elemGolangType] = struct{}{}
elemProps, err := toProperity(elemGolangType, traceSet, traceSlice)
if err != nil {
return nil, errWithTrace(err.Error(), traceSlice)
}
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,
Properities: elemProps.Properities,
2023-01-13 03:52:25 +00:00
}
}
2023-01-13 17:01:53 +00:00
// var additionalProperties *Property
// if golangType.Kind() == reflect.Map {
// additionalProperties =
// }
2023-01-13 11:09:19 +00:00
2023-01-13 17:01:53 +00:00
properities := map[string]*Property{}
if golangType.Kind() == reflect.Struct {
for i := 0; i < golangType.NumField(); i++ {
child := golangType.Field(i)
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
// skip non json annotated fields
if childName == "" {
continue
2023-01-13 11:09:19 +00:00
}
// detect cycles. Fail if a cycle is detected
// TODO: Add references here for cycles
_, ok := traceSet[child.Type]
if ok {
fmt.Println("[DEBUG] traceSet: ", traceSet)
return nil, errWithTrace("cycle detected", traceSlice)
}
// add current child field to history
traceSet[child.Type] = struct{}{}
// recursively compute properties for this child field
fieldProps, err := toProperity(child.Type, traceSet, traceSlice)
2023-01-13 11:09:19 +00:00
2023-01-13 03:52:25 +00:00
if err != nil {
2023-01-13 17:01:53 +00:00
return nil, errWithTrace(err.Error(), traceSlice)
2023-01-13 03:52:25 +00:00
}
// traversal complete, delete child from history
delete(traceSet, child.Type)
properities[childName] = fieldProps
2023-01-13 03:52:25 +00:00
}
}
traceSlice = traceSlice[:len(traceSlice)-1]
2023-01-13 03:52:25 +00:00
return &Property{
Type: rootJavascriptType,
Items: items,
Properities: properities,
2023-01-13 03:52:25 +00:00
}, nil
}