databricks-cli/bundle/schema/schema.go

280 lines
8.3 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"
)
// defines schema for a json object
type Schema struct {
// Type of the object
Type JavascriptType `json:"type,omitempty"`
2023-01-18 15:02:38 +00:00
// Description of the object. This is rendered as inline documentation in the
2023-01-19 10:34:55 +00:00
// IDE. This is manually injected here using schema.Docs
2023-01-18 15:02:38 +00:00
Description string `json:"description,omitempty"`
2023-01-19 14:25:17 +00:00
// Schemas for the fields of an struct. The keys are the first json tag.
2023-01-19 10:34:55 +00:00
// The values are the schema for the type of the field
Properties map[string]*Schema `json:"properties,omitempty"`
2023-01-19 14:25:17 +00:00
// The schema for all values of an array
Items *Schema `json:"items,omitempty"`
2023-01-19 14:25:17 +00:00
// The schema for any properties not mentioned in the Schema.Properties field.
2023-01-19 10:34:55 +00:00
// this validates Maps in bundle configuration
// OR
2023-01-19 14:25:17 +00:00
// A boolean type with value false. Setting false here validates that all
2023-01-19 10:34:55 +00:00
// properties in the yaml file have been defined in the json schema
//
// Its type during runtime will either be *Schema or bool
2023-01-19 14:25:17 +00:00
AdditionalProperties any `json:"additionalProperties,omitempty"`
2023-01-19 14:25:17 +00:00
// Required properties for the object. Any fields missing the "omitempty"
2023-01-19 10:34:55 +00:00
// tag will be included
Required []string `json:"required,omitempty"`
}
2023-01-19 14:37:47 +00:00
/*
This function translates golang types into json schema. Here is the mapping
between json schema types and golang types
2023-01-19 15:12:24 +00:00
2023-01-19 14:37:47 +00:00
- GolangType -> Javascript type / Json Schema2
2023-01-19 15:12:24 +00:00
2023-01-19 14:37:47 +00:00
- bool -> boolean
2023-01-19 15:12:24 +00:00
2023-01-19 14:37:47 +00:00
- string -> string
2023-01-19 15:12:24 +00:00
2023-01-19 14:37:47 +00:00
- int (all variants) -> number
2023-01-19 15:12:24 +00:00
2023-01-19 14:37:47 +00:00
- float (all variants) -> number
2023-01-19 15:12:24 +00:00
- map[string]MyStruct -> { type: object, additionalProperties: {}}
for details visit: https://json-schema.org/understanding-json-schema/reference/object.html#additional-properties
- []MyStruct -> {type: array, items: {}}
for details visit: https://json-schema.org/understanding-json-schema/reference/array.html#items
- []MyStruct -> {type: object, properties: {}, additionalProperties: false}
for details visit: https://json-schema.org/understanding-json-schema/reference/object.html#properties
2023-01-19 14:37:47 +00:00
*/
2023-01-18 15:02:38 +00:00
func NewSchema(golangType reflect.Type, docs *Docs) (*Schema, error) {
tracker := newTracker()
schema, err := safeToSchema(golangType, docs, "", tracker)
2023-01-13 03:52:25 +00:00
if err != nil {
return nil, tracker.errWithTrace(err.Error())
2023-01-13 03:52:25 +00:00
}
2023-01-18 15:02:38 +00:00
return schema, nil
2023-01-13 03:52:25 +00:00
}
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)
}
}
// A wrapper over toSchema function to detect cycles in the bundle config struct
func safeToSchema(golangType reflect.Type, docs *Docs, traceId string, tracker *tracker) (*Schema, error) {
// WE ERROR OUT IF THERE ARE CYCLES IN THE JSON SCHEMA
// There are mechanisms to deal with cycles though recursive identifiers in json
// schema. However if we use them, we would need to make sure we are able to detect
// cycles two properties (directly or indirectly) pointing to each other
//
// see: https://json-schema.org/understanding-json-schema/structuring.html#recursion
// for details
if tracker.hasCycle(golangType) {
2023-01-13 19:03:24 +00:00
return nil, fmt.Errorf("cycle detected")
}
2023-01-18 16:36:50 +00:00
tracker.step(golangType, traceId)
props, err := toSchema(golangType, docs, tracker)
if err != nil {
2023-01-13 19:03:24 +00:00
return nil, err
}
tracker.undoStep(golangType)
return props, nil
}
2023-01-19 14:25:17 +00:00
// This function returns all member fields of the provided type.
// If the type has embedded (aka anonymous) fields, this function traverses
// those in a breadth first manner
func getStructFields(golangType reflect.Type) []reflect.StructField {
fields := []reflect.StructField{}
bfsQueue := list.New()
for i := 0; i < golangType.NumField(); 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
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-16 01:23:35 +00:00
// params:
2023-01-19 15:12:24 +00:00
//
2023-01-19 14:33:32 +00:00
// golangType: golang type for which json schema properties to generate
// docs: Struct containing documentation to be injected into the json schema generated
// 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 toSchema(golangType reflect.Type, docs *Docs, tracker *tracker) (*Schema, error) {
2023-01-13 03:52:25 +00:00
// *Struct and Struct generate identical json schemas
if golangType.Kind() == reflect.Pointer {
return safeToSchema(golangType.Elem(), docs, "", tracker)
2023-01-13 19:03:24 +00:00
}
if golangType.Kind() == reflect.Interface {
return &Schema{}, 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
}
2023-01-18 15:02:38 +00:00
var description string
if docs != nil {
description = docs.Documentation
}
// case array/slice
var items *Schema
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
}
elemProps, err := safeToSchema(elemGolangType, docs, "", tracker)
2023-01-13 17:01:53 +00:00
if err != nil {
return nil, err
2023-01-13 17:01:53 +00:00
}
items = &Schema{
Type: elemJavascriptType,
Properties: elemProps.Properties,
AdditionalProperties: elemProps.AdditionalProperties,
Items: elemProps.Items,
Required: elemProps.Required,
2023-01-13 03:52:25 +00:00
}
}
// case map
var additionalProperties interface{}
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")
}
additionalProperties, err = safeToSchema(golangType.Elem(), docs, "", tracker)
if err != nil {
return nil, err
}
}
2023-01-13 11:09:19 +00:00
// case struct
properties := map[string]*Schema{}
required := []string{}
if golangType.Kind() == reflect.Struct {
children := getStructFields(golangType)
for _, child := range children {
// get child json tags
childJsonTag := strings.Split(child.Tag.Get("json"), ",")
childName := childJsonTag[0]
2023-01-13 19:03:24 +00:00
// skip children that have no json tags, the first json tag is ""
// or the first json tag is "-"
if childName == "" || childName == "-" {
continue
2023-01-13 11:09:19 +00:00
}
2023-01-18 15:02:38 +00:00
// get docs for the child if they exist
var childDocs *Docs
if docs != nil {
if val, ok := docs.Children[childName]; ok {
childDocs = &val
}
}
// compute if the child is a required field. Determined by the
// resence of "omitempty" in the json tags
hasOmitEmptyTag := false
for i := 1; i < len(childJsonTag); i++ {
if childJsonTag[i] == "omitempty" {
hasOmitEmptyTag = true
}
}
if !hasOmitEmptyTag {
required = append(required, childName)
}
// compute Schema.Properties for the child recursively
fieldProps, err := safeToSchema(child.Type, childDocs, childName, tracker)
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
}
2023-01-16 20:53:20 +00:00
properties[childName] = fieldProps
2023-01-13 03:52:25 +00:00
}
// set Schema.AdditionalProperties to false
additionalProperties = false
2023-01-13 03:52:25 +00:00
}
return &Schema{
Type: rootJavascriptType,
2023-01-18 15:02:38 +00:00
Description: description,
Items: items,
2023-01-16 20:53:20 +00:00
Properties: properties,
AdditionalProperties: additionalProperties,
Required: required,
2023-01-13 03:52:25 +00:00
}, nil
}