2023-01-20 15:55:44 +00:00
|
|
|
package schema
|
|
|
|
|
|
|
|
import (
|
|
|
|
"reflect"
|
|
|
|
|
2023-08-01 14:09:27 +00:00
|
|
|
"github.com/databricks/cli/libs/jsonschema"
|
|
|
|
)
|
2023-01-20 15:55:44 +00:00
|
|
|
|
2024-08-20 13:34:49 +00:00
|
|
|
// // Fields tagged "readonly" should not be emitted in the schema as they are
|
|
|
|
// // computed at runtime, and should not be assigned a value by the bundle author.
|
|
|
|
// const readonlyTag = "readonly"
|
2023-08-10 10:03:52 +00:00
|
|
|
|
2024-08-20 13:34:49 +00:00
|
|
|
// // Annotation for internal bundle fields that should not be exposed to customers.
|
|
|
|
// // Fields can be tagged as "internal" to remove them from the generated schema.
|
|
|
|
// const internalTag = "internal"
|
2023-08-10 10:03:52 +00:00
|
|
|
|
2024-08-20 13:34:49 +00:00
|
|
|
// // Annotation for bundle fields that have been deprecated.
|
|
|
|
// // Fields tagged as "deprecated" are removed/omitted from the generated schema.
|
|
|
|
// const deprecatedTag = "deprecated"
|
2023-12-06 10:45:18 +00:00
|
|
|
|
2023-01-20 15:55:44 +00:00
|
|
|
// This function translates golang types into json schema. Here is the mapping
|
|
|
|
// between json schema types and golang types
|
|
|
|
//
|
|
|
|
// - GolangType -> Javascript type / Json Schema2
|
|
|
|
//
|
|
|
|
// - bool -> boolean
|
|
|
|
//
|
|
|
|
// - string -> string
|
|
|
|
//
|
|
|
|
// - int (all variants) -> number
|
|
|
|
//
|
|
|
|
// - float (all variants) -> number
|
|
|
|
//
|
|
|
|
// - 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-08-01 14:09:27 +00:00
|
|
|
func New(golangType reflect.Type, docs *Docs) (*jsonschema.Schema, error) {
|
2024-08-20 13:34:49 +00:00
|
|
|
|
2024-08-26 18:16:45 +00:00
|
|
|
return nil, nil
|
|
|
|
|
|
|
|
// s, err := jsonschema.FromType(golangType, func(s jsonschema.Schema) jsonschema.Schema {
|
|
|
|
// if s.Type == jsonschema.NumberType || s.Type == jsonschema.BooleanType {
|
|
|
|
// s = jsonschema.Schema{
|
|
|
|
// AnyOf: []jsonschema.Schema{
|
|
|
|
// s,
|
|
|
|
// {
|
|
|
|
// Type: jsonschema.StringType,
|
|
|
|
// // TODO: Narrow down the scope of the regex match.
|
|
|
|
// // Also likely need to rename this variable.
|
|
|
|
// Pattern: dynvar.ReferenceRegex,
|
|
|
|
// },
|
|
|
|
// },
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// return s
|
|
|
|
// })
|
|
|
|
// if err != nil {
|
|
|
|
// return nil, err
|
|
|
|
// }
|
|
|
|
// return &s, nil
|
2023-01-20 15:55:44 +00:00
|
|
|
|
2024-08-20 13:34:49 +00:00
|
|
|
// tracker := newTracker()
|
|
|
|
// schema, err := safeToSchema(golangType, docs, "", tracker)
|
|
|
|
// if err != nil {
|
|
|
|
// return nil, tracker.errWithTrace(err.Error(), "root")
|
|
|
|
// }
|
|
|
|
// return schema, nil
|
2023-01-20 15:55:44 +00:00
|
|
|
}
|
|
|
|
|
2024-08-20 13:34:49 +00:00
|
|
|
// func jsonSchemaType(golangType reflect.Type) (jsonschema.Type, error) {
|
|
|
|
// switch golangType.Kind() {
|
|
|
|
// case reflect.Bool:
|
|
|
|
// return jsonschema.BooleanType, nil
|
|
|
|
// case reflect.String:
|
|
|
|
// return jsonschema.StringType, 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:
|
|
|
|
|
|
|
|
// return jsonschema.NumberType, nil
|
|
|
|
// case reflect.Struct:
|
|
|
|
// return jsonschema.ObjectType, nil
|
|
|
|
// case reflect.Map:
|
|
|
|
// if golangType.Key().Kind() != reflect.String {
|
|
|
|
// return jsonschema.InvalidType, fmt.Errorf("only strings map keys are valid. key type: %v", golangType.Key().Kind())
|
|
|
|
// }
|
|
|
|
// return jsonschema.ObjectType, nil
|
|
|
|
// case reflect.Array, reflect.Slice:
|
|
|
|
// return jsonschema.ArrayType, nil
|
|
|
|
// default:
|
|
|
|
// return jsonschema.InvalidType, fmt.Errorf("unhandled golang type: %s", golangType)
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
|
2023-01-20 15:55:44 +00:00
|
|
|
// A wrapper over toSchema function to:
|
|
|
|
// 1. Detect cycles in the bundle config struct.
|
|
|
|
// 2. Update tracker
|
|
|
|
//
|
|
|
|
// params:
|
|
|
|
//
|
|
|
|
// - golangType: Golang type to generate json schema for
|
|
|
|
//
|
|
|
|
// - docs: Contains documentation to be injected into the generated json schema
|
|
|
|
//
|
|
|
|
// - traceId: An identifier for the current type, to trace recursive traversal.
|
|
|
|
// Its value is the first json tag in case of struct fields and "" in other cases
|
|
|
|
// like array, map or no json tags
|
|
|
|
//
|
|
|
|
// - tracker: Keeps track of types / traceIds seen during recursive traversal
|
2024-08-20 13:34:49 +00:00
|
|
|
// func safeToSchema(golangType reflect.Type, docs *Docs, traceId string, tracker *tracker) (*jsonschema.Schema, error) {
|
|
|
|
// // HACK to unblock CLI release (13th Feb 2024). This is temporary until proper
|
|
|
|
// // support for recursive types is added to the schema generator. PR: https://github.com/databricks/cli/pull/1204
|
|
|
|
// if traceId == "for_each_task" {
|
|
|
|
// return &jsonschema.Schema{
|
|
|
|
// Type: jsonschema.ObjectType,
|
|
|
|
// }, nil
|
|
|
|
// }
|
2024-02-13 14:13:47 +00:00
|
|
|
|
2024-08-20 13:34:49 +00:00
|
|
|
// // 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 where 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) {
|
|
|
|
// return nil, fmt.Errorf("cycle detected")
|
|
|
|
// }
|
2023-01-20 15:55:44 +00:00
|
|
|
|
2024-08-20 13:34:49 +00:00
|
|
|
// tracker.push(golangType, traceId)
|
|
|
|
// props, err := toSchema(golangType, docs, tracker)
|
|
|
|
// if err != nil {
|
|
|
|
// return nil, err
|
|
|
|
// }
|
|
|
|
// tracker.pop(golangType)
|
|
|
|
// return props, nil
|
|
|
|
// }
|
2023-01-20 15:55:44 +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
|
2024-08-20 13:34:49 +00:00
|
|
|
// func getStructFields(golangType reflect.Type) []reflect.StructField {
|
|
|
|
// fields := []reflect.StructField{}
|
|
|
|
// bfsQueue := list.New()
|
2023-01-20 15:55:44 +00:00
|
|
|
|
2024-08-20 13:34:49 +00:00
|
|
|
// 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)
|
2023-01-20 15:55:44 +00:00
|
|
|
|
2024-08-20 13:34:49 +00:00
|
|
|
// if !field.Anonymous {
|
|
|
|
// fields = append(fields, field)
|
|
|
|
// continue
|
|
|
|
// }
|
2023-01-20 15:55:44 +00:00
|
|
|
|
2024-08-20 13:34:49 +00:00
|
|
|
// fieldType := field.Type
|
|
|
|
// if fieldType.Kind() == reflect.Pointer {
|
|
|
|
// fieldType = fieldType.Elem()
|
|
|
|
// }
|
2024-04-25 11:20:45 +00:00
|
|
|
|
2024-08-20 13:34:49 +00:00
|
|
|
// for i := 0; i < fieldType.NumField(); i++ {
|
|
|
|
// bfsQueue.PushBack(fieldType.Field(i))
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// return fields
|
|
|
|
// }
|
2023-01-20 15:55:44 +00:00
|
|
|
|
2024-08-20 13:34:49 +00:00
|
|
|
// func toSchema(golangType reflect.Type, docs *Docs, tracker *tracker) (*jsonschema.Schema, error) {
|
|
|
|
// // *Struct and Struct generate identical json schemas
|
|
|
|
// if golangType.Kind() == reflect.Pointer {
|
|
|
|
// return safeToSchema(golangType.Elem(), docs, "", tracker)
|
|
|
|
// }
|
|
|
|
// if golangType.Kind() == reflect.Interface {
|
|
|
|
// return &jsonschema.Schema{}, nil
|
|
|
|
// }
|
2023-01-20 15:55:44 +00:00
|
|
|
|
2024-08-20 13:34:49 +00:00
|
|
|
// rootJavascriptType, err := jsonSchemaType(golangType)
|
|
|
|
// if err != nil {
|
|
|
|
// return nil, err
|
|
|
|
// }
|
|
|
|
// jsonSchema := &jsonschema.Schema{Type: rootJavascriptType}
|
2023-01-20 15:55:44 +00:00
|
|
|
|
2024-08-20 13:34:49 +00:00
|
|
|
// // If the type is a non-string primitive, then we allow it to be a string
|
|
|
|
// // provided it's a pure variable reference (ie only a single variable reference).
|
|
|
|
// if rootJavascriptType == jsonschema.BooleanType || rootJavascriptType == jsonschema.NumberType {
|
|
|
|
// jsonSchema = &jsonschema.Schema{
|
|
|
|
// AnyOf: []*jsonschema.Schema{
|
|
|
|
// {
|
|
|
|
// Type: rootJavascriptType,
|
|
|
|
// },
|
|
|
|
// {
|
|
|
|
// Type: jsonschema.StringType,
|
|
|
|
// Pattern: dynvar.VariableRegex,
|
|
|
|
// },
|
|
|
|
// },
|
|
|
|
// }
|
|
|
|
// }
|
2023-04-04 10:16:07 +00:00
|
|
|
|
2024-08-20 13:34:49 +00:00
|
|
|
// if docs != nil {
|
|
|
|
// jsonSchema.Description = docs.Description
|
|
|
|
// }
|
2023-01-20 15:55:44 +00:00
|
|
|
|
2024-08-20 13:34:49 +00:00
|
|
|
// // case array/slice
|
|
|
|
// if golangType.Kind() == reflect.Array || golangType.Kind() == reflect.Slice {
|
|
|
|
// elemGolangType := golangType.Elem()
|
|
|
|
// elemJavascriptType, err := jsonSchemaType(elemGolangType)
|
|
|
|
// if err != nil {
|
|
|
|
// return nil, err
|
|
|
|
// }
|
|
|
|
// var childDocs *Docs
|
|
|
|
// if docs != nil {
|
|
|
|
// childDocs = docs.Items
|
|
|
|
// }
|
|
|
|
// elemProps, err := safeToSchema(elemGolangType, childDocs, "", tracker)
|
|
|
|
// if err != nil {
|
|
|
|
// return nil, err
|
|
|
|
// }
|
|
|
|
// jsonSchema.Items = &jsonschema.Schema{
|
|
|
|
// Type: elemJavascriptType,
|
|
|
|
// Properties: elemProps.Properties,
|
|
|
|
// AdditionalProperties: elemProps.AdditionalProperties,
|
|
|
|
// Items: elemProps.Items,
|
|
|
|
// Required: elemProps.Required,
|
|
|
|
// }
|
|
|
|
// }
|
2023-01-20 15:55:44 +00:00
|
|
|
|
2024-08-20 13:34:49 +00:00
|
|
|
// // case map
|
|
|
|
// if golangType.Kind() == reflect.Map {
|
|
|
|
// if golangType.Key().Kind() != reflect.String {
|
|
|
|
// return nil, fmt.Errorf("only string keyed maps allowed")
|
|
|
|
// }
|
|
|
|
// var childDocs *Docs
|
|
|
|
// if docs != nil {
|
|
|
|
// childDocs = docs.AdditionalProperties
|
|
|
|
// }
|
|
|
|
// jsonSchema.AdditionalProperties, err = safeToSchema(golangType.Elem(), childDocs, "", tracker)
|
|
|
|
// if err != nil {
|
|
|
|
// return nil, err
|
|
|
|
// }
|
|
|
|
// }
|
2023-01-20 15:55:44 +00:00
|
|
|
|
2024-08-20 13:34:49 +00:00
|
|
|
// // case struct
|
|
|
|
// if golangType.Kind() == reflect.Struct {
|
|
|
|
// children := getStructFields(golangType)
|
|
|
|
// properties := map[string]*jsonschema.Schema{}
|
|
|
|
// required := []string{}
|
|
|
|
// for _, child := range children {
|
|
|
|
// bundleTag := child.Tag.Get("bundle")
|
|
|
|
// // Fields marked as "readonly", "internal" or "deprecated" are skipped
|
|
|
|
// // while generating the schema
|
|
|
|
// if bundleTag == readonlyTag || bundleTag == internalTag || bundleTag == deprecatedTag {
|
|
|
|
// continue
|
|
|
|
// }
|
2023-01-20 15:55:44 +00:00
|
|
|
|
2024-08-20 13:34:49 +00:00
|
|
|
// // get child json tags
|
|
|
|
// childJsonTag := strings.Split(child.Tag.Get("json"), ",")
|
|
|
|
// childName := childJsonTag[0]
|
2023-01-20 15:55:44 +00:00
|
|
|
|
2024-08-20 13:34:49 +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-20 15:55:44 +00:00
|
|
|
|
2024-08-20 13:34:49 +00:00
|
|
|
// // get docs for the child if they exist
|
|
|
|
// var childDocs *Docs
|
|
|
|
// if docs != nil {
|
|
|
|
// if val, ok := docs.Properties[childName]; ok {
|
|
|
|
// childDocs = val
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
|
|
|
|
// // compute if the child is a required field. Determined by the
|
|
|
|
// // presence 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)
|
|
|
|
// if err != nil {
|
|
|
|
// return nil, err
|
|
|
|
// }
|
|
|
|
// properties[childName] = fieldProps
|
|
|
|
// }
|
|
|
|
|
|
|
|
// jsonSchema.AdditionalProperties = false
|
|
|
|
// jsonSchema.Properties = properties
|
|
|
|
// jsonSchema.Required = required
|
|
|
|
// }
|
|
|
|
|
|
|
|
// return jsonSchema, nil
|
|
|
|
// }
|