mirror of https://github.com/databricks/cli.git
Improve JSON schema
This commit is contained in:
parent
242d4b51ed
commit
75f252e51c
|
@ -1,26 +1,23 @@
|
||||||
package schema
|
package schema
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"container/list"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/databricks/cli/libs/dyn/dynvar"
|
"github.com/databricks/cli/libs/dyn/dynvar"
|
||||||
"github.com/databricks/cli/libs/jsonschema"
|
"github.com/databricks/cli/libs/jsonschema"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Fields tagged "readonly" should not be emitted in the schema as they are
|
// // 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.
|
// // computed at runtime, and should not be assigned a value by the bundle author.
|
||||||
const readonlyTag = "readonly"
|
// const readonlyTag = "readonly"
|
||||||
|
|
||||||
// Annotation for internal bundle fields that should not be exposed to customers.
|
// // 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.
|
// // Fields can be tagged as "internal" to remove them from the generated schema.
|
||||||
const internalTag = "internal"
|
// const internalTag = "internal"
|
||||||
|
|
||||||
// Annotation for bundle fields that have been deprecated.
|
// // Annotation for bundle fields that have been deprecated.
|
||||||
// Fields tagged as "deprecated" are removed/omitted from the generated schema.
|
// // Fields tagged as "deprecated" are removed/omitted from the generated schema.
|
||||||
const deprecatedTag = "deprecated"
|
// const deprecatedTag = "deprecated"
|
||||||
|
|
||||||
// This function translates golang types into json schema. Here is the mapping
|
// This function translates golang types into json schema. Here is the mapping
|
||||||
// between json schema types and golang types
|
// between json schema types and golang types
|
||||||
|
@ -44,38 +41,61 @@ const deprecatedTag = "deprecated"
|
||||||
// - []MyStruct -> {type: object, properties: {}, additionalProperties: false}
|
// - []MyStruct -> {type: object, properties: {}, additionalProperties: false}
|
||||||
// for details visit: https://json-schema.org/understanding-json-schema/reference/object.html#properties
|
// for details visit: https://json-schema.org/understanding-json-schema/reference/object.html#properties
|
||||||
func New(golangType reflect.Type, docs *Docs) (*jsonschema.Schema, error) {
|
func New(golangType reflect.Type, docs *Docs) (*jsonschema.Schema, error) {
|
||||||
tracker := newTracker()
|
|
||||||
schema, err := safeToSchema(golangType, docs, "", tracker)
|
s, err := jsonschema.FromType(golangType, jsonschema.FromTypeOptions{
|
||||||
|
Transform: 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:
|
||||||
|
Pattern: dynvar.VariableRegex,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, tracker.errWithTrace(err.Error(), "root")
|
return nil, err
|
||||||
}
|
}
|
||||||
return schema, nil
|
return &s, nil
|
||||||
|
|
||||||
|
// tracker := newTracker()
|
||||||
|
// schema, err := safeToSchema(golangType, docs, "", tracker)
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, tracker.errWithTrace(err.Error(), "root")
|
||||||
|
// }
|
||||||
|
// return schema, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func jsonSchemaType(golangType reflect.Type) (jsonschema.Type, error) {
|
// func jsonSchemaType(golangType reflect.Type) (jsonschema.Type, error) {
|
||||||
switch golangType.Kind() {
|
// switch golangType.Kind() {
|
||||||
case reflect.Bool:
|
// case reflect.Bool:
|
||||||
return jsonschema.BooleanType, nil
|
// return jsonschema.BooleanType, nil
|
||||||
case reflect.String:
|
// case reflect.String:
|
||||||
return jsonschema.StringType, nil
|
// return jsonschema.StringType, nil
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
// case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
// reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
||||||
reflect.Float32, reflect.Float64:
|
// reflect.Float32, reflect.Float64:
|
||||||
|
|
||||||
return jsonschema.NumberType, nil
|
// return jsonschema.NumberType, nil
|
||||||
case reflect.Struct:
|
// case reflect.Struct:
|
||||||
return jsonschema.ObjectType, nil
|
// return jsonschema.ObjectType, nil
|
||||||
case reflect.Map:
|
// case reflect.Map:
|
||||||
if golangType.Key().Kind() != reflect.String {
|
// 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.InvalidType, fmt.Errorf("only strings map keys are valid. key type: %v", golangType.Key().Kind())
|
||||||
}
|
// }
|
||||||
return jsonschema.ObjectType, nil
|
// return jsonschema.ObjectType, nil
|
||||||
case reflect.Array, reflect.Slice:
|
// case reflect.Array, reflect.Slice:
|
||||||
return jsonschema.ArrayType, nil
|
// return jsonschema.ArrayType, nil
|
||||||
default:
|
// default:
|
||||||
return jsonschema.InvalidType, fmt.Errorf("unhandled golang type: %s", golangType)
|
// return jsonschema.InvalidType, fmt.Errorf("unhandled golang type: %s", golangType)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// A wrapper over toSchema function to:
|
// A wrapper over toSchema function to:
|
||||||
// 1. Detect cycles in the bundle config struct.
|
// 1. Detect cycles in the bundle config struct.
|
||||||
|
@ -92,196 +112,196 @@ func jsonSchemaType(golangType reflect.Type) (jsonschema.Type, error) {
|
||||||
// like array, map or no json tags
|
// like array, map or no json tags
|
||||||
//
|
//
|
||||||
// - tracker: Keeps track of types / traceIds seen during recursive traversal
|
// - tracker: Keeps track of types / traceIds seen during recursive traversal
|
||||||
func safeToSchema(golangType reflect.Type, docs *Docs, traceId string, tracker *tracker) (*jsonschema.Schema, error) {
|
// 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
|
// // 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
|
// // support for recursive types is added to the schema generator. PR: https://github.com/databricks/cli/pull/1204
|
||||||
if traceId == "for_each_task" {
|
// if traceId == "for_each_task" {
|
||||||
return &jsonschema.Schema{
|
// return &jsonschema.Schema{
|
||||||
Type: jsonschema.ObjectType,
|
// Type: jsonschema.ObjectType,
|
||||||
}, nil
|
// }, nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
// WE ERROR OUT IF THERE ARE CYCLES IN THE JSON SCHEMA
|
// // WE ERROR OUT IF THERE ARE CYCLES IN THE JSON SCHEMA
|
||||||
// There are mechanisms to deal with cycles though recursive identifiers in json
|
// // 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
|
// // 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
|
// // cycles where two properties (directly or indirectly) pointing to each other
|
||||||
//
|
// //
|
||||||
// see: https://json-schema.org/understanding-json-schema/structuring.html#recursion
|
// // see: https://json-schema.org/understanding-json-schema/structuring.html#recursion
|
||||||
// for details
|
// // for details
|
||||||
if tracker.hasCycle(golangType) {
|
// if tracker.hasCycle(golangType) {
|
||||||
return nil, fmt.Errorf("cycle detected")
|
// return nil, fmt.Errorf("cycle detected")
|
||||||
}
|
// }
|
||||||
|
|
||||||
tracker.push(golangType, traceId)
|
// tracker.push(golangType, traceId)
|
||||||
props, err := toSchema(golangType, docs, tracker)
|
// props, err := toSchema(golangType, docs, tracker)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return nil, err
|
// return nil, err
|
||||||
}
|
// }
|
||||||
tracker.pop(golangType)
|
// tracker.pop(golangType)
|
||||||
return props, nil
|
// return props, nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
// This function returns all member fields of the provided type.
|
// This function returns all member fields of the provided type.
|
||||||
// If the type has embedded (aka anonymous) fields, this function traverses
|
// If the type has embedded (aka anonymous) fields, this function traverses
|
||||||
// those in a breadth first manner
|
// those in a breadth first manner
|
||||||
func getStructFields(golangType reflect.Type) []reflect.StructField {
|
// func getStructFields(golangType reflect.Type) []reflect.StructField {
|
||||||
fields := []reflect.StructField{}
|
// fields := []reflect.StructField{}
|
||||||
bfsQueue := list.New()
|
// bfsQueue := list.New()
|
||||||
|
|
||||||
for i := 0; i < golangType.NumField(); i++ {
|
// for i := 0; i < golangType.NumField(); i++ {
|
||||||
bfsQueue.PushBack(golangType.Field(i))
|
// bfsQueue.PushBack(golangType.Field(i))
|
||||||
}
|
// }
|
||||||
for bfsQueue.Len() > 0 {
|
// for bfsQueue.Len() > 0 {
|
||||||
front := bfsQueue.Front()
|
// front := bfsQueue.Front()
|
||||||
field := front.Value.(reflect.StructField)
|
// field := front.Value.(reflect.StructField)
|
||||||
bfsQueue.Remove(front)
|
// bfsQueue.Remove(front)
|
||||||
|
|
||||||
if !field.Anonymous {
|
// if !field.Anonymous {
|
||||||
fields = append(fields, field)
|
// fields = append(fields, field)
|
||||||
continue
|
// continue
|
||||||
}
|
// }
|
||||||
|
|
||||||
fieldType := field.Type
|
// fieldType := field.Type
|
||||||
if fieldType.Kind() == reflect.Pointer {
|
// if fieldType.Kind() == reflect.Pointer {
|
||||||
fieldType = fieldType.Elem()
|
// fieldType = fieldType.Elem()
|
||||||
}
|
// }
|
||||||
|
|
||||||
for i := 0; i < fieldType.NumField(); i++ {
|
// for i := 0; i < fieldType.NumField(); i++ {
|
||||||
bfsQueue.PushBack(fieldType.Field(i))
|
// bfsQueue.PushBack(fieldType.Field(i))
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
return fields
|
// return fields
|
||||||
}
|
// }
|
||||||
|
|
||||||
func toSchema(golangType reflect.Type, docs *Docs, tracker *tracker) (*jsonschema.Schema, error) {
|
// func toSchema(golangType reflect.Type, docs *Docs, tracker *tracker) (*jsonschema.Schema, error) {
|
||||||
// *Struct and Struct generate identical json schemas
|
// // *Struct and Struct generate identical json schemas
|
||||||
if golangType.Kind() == reflect.Pointer {
|
// if golangType.Kind() == reflect.Pointer {
|
||||||
return safeToSchema(golangType.Elem(), docs, "", tracker)
|
// return safeToSchema(golangType.Elem(), docs, "", tracker)
|
||||||
}
|
// }
|
||||||
if golangType.Kind() == reflect.Interface {
|
// if golangType.Kind() == reflect.Interface {
|
||||||
return &jsonschema.Schema{}, nil
|
// return &jsonschema.Schema{}, nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
rootJavascriptType, err := jsonSchemaType(golangType)
|
// rootJavascriptType, err := jsonSchemaType(golangType)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return nil, err
|
// return nil, err
|
||||||
}
|
// }
|
||||||
jsonSchema := &jsonschema.Schema{Type: rootJavascriptType}
|
// jsonSchema := &jsonschema.Schema{Type: rootJavascriptType}
|
||||||
|
|
||||||
// If the type is a non-string primitive, then we allow it to be a string
|
// // 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).
|
// // provided it's a pure variable reference (ie only a single variable reference).
|
||||||
if rootJavascriptType == jsonschema.BooleanType || rootJavascriptType == jsonschema.NumberType {
|
// if rootJavascriptType == jsonschema.BooleanType || rootJavascriptType == jsonschema.NumberType {
|
||||||
jsonSchema = &jsonschema.Schema{
|
// jsonSchema = &jsonschema.Schema{
|
||||||
AnyOf: []*jsonschema.Schema{
|
// AnyOf: []*jsonschema.Schema{
|
||||||
{
|
// {
|
||||||
Type: rootJavascriptType,
|
// Type: rootJavascriptType,
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
Type: jsonschema.StringType,
|
// Type: jsonschema.StringType,
|
||||||
Pattern: dynvar.VariableRegex,
|
// Pattern: dynvar.VariableRegex,
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
if docs != nil {
|
// if docs != nil {
|
||||||
jsonSchema.Description = docs.Description
|
// jsonSchema.Description = docs.Description
|
||||||
}
|
// }
|
||||||
|
|
||||||
// case array/slice
|
// // case array/slice
|
||||||
if golangType.Kind() == reflect.Array || golangType.Kind() == reflect.Slice {
|
// if golangType.Kind() == reflect.Array || golangType.Kind() == reflect.Slice {
|
||||||
elemGolangType := golangType.Elem()
|
// elemGolangType := golangType.Elem()
|
||||||
elemJavascriptType, err := jsonSchemaType(elemGolangType)
|
// elemJavascriptType, err := jsonSchemaType(elemGolangType)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return nil, err
|
// return nil, err
|
||||||
}
|
// }
|
||||||
var childDocs *Docs
|
// var childDocs *Docs
|
||||||
if docs != nil {
|
// if docs != nil {
|
||||||
childDocs = docs.Items
|
// childDocs = docs.Items
|
||||||
}
|
// }
|
||||||
elemProps, err := safeToSchema(elemGolangType, childDocs, "", tracker)
|
// elemProps, err := safeToSchema(elemGolangType, childDocs, "", tracker)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return nil, err
|
// return nil, err
|
||||||
}
|
// }
|
||||||
jsonSchema.Items = &jsonschema.Schema{
|
// jsonSchema.Items = &jsonschema.Schema{
|
||||||
Type: elemJavascriptType,
|
// Type: elemJavascriptType,
|
||||||
Properties: elemProps.Properties,
|
// Properties: elemProps.Properties,
|
||||||
AdditionalProperties: elemProps.AdditionalProperties,
|
// AdditionalProperties: elemProps.AdditionalProperties,
|
||||||
Items: elemProps.Items,
|
// Items: elemProps.Items,
|
||||||
Required: elemProps.Required,
|
// Required: elemProps.Required,
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// case map
|
// // case map
|
||||||
if golangType.Kind() == reflect.Map {
|
// if golangType.Kind() == reflect.Map {
|
||||||
if golangType.Key().Kind() != reflect.String {
|
// if golangType.Key().Kind() != reflect.String {
|
||||||
return nil, fmt.Errorf("only string keyed maps allowed")
|
// return nil, fmt.Errorf("only string keyed maps allowed")
|
||||||
}
|
// }
|
||||||
var childDocs *Docs
|
// var childDocs *Docs
|
||||||
if docs != nil {
|
// if docs != nil {
|
||||||
childDocs = docs.AdditionalProperties
|
// childDocs = docs.AdditionalProperties
|
||||||
}
|
// }
|
||||||
jsonSchema.AdditionalProperties, err = safeToSchema(golangType.Elem(), childDocs, "", tracker)
|
// jsonSchema.AdditionalProperties, err = safeToSchema(golangType.Elem(), childDocs, "", tracker)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return nil, err
|
// return nil, err
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// case struct
|
// // case struct
|
||||||
if golangType.Kind() == reflect.Struct {
|
// if golangType.Kind() == reflect.Struct {
|
||||||
children := getStructFields(golangType)
|
// children := getStructFields(golangType)
|
||||||
properties := map[string]*jsonschema.Schema{}
|
// properties := map[string]*jsonschema.Schema{}
|
||||||
required := []string{}
|
// required := []string{}
|
||||||
for _, child := range children {
|
// for _, child := range children {
|
||||||
bundleTag := child.Tag.Get("bundle")
|
// bundleTag := child.Tag.Get("bundle")
|
||||||
// Fields marked as "readonly", "internal" or "deprecated" are skipped
|
// // Fields marked as "readonly", "internal" or "deprecated" are skipped
|
||||||
// while generating the schema
|
// // while generating the schema
|
||||||
if bundleTag == readonlyTag || bundleTag == internalTag || bundleTag == deprecatedTag {
|
// if bundleTag == readonlyTag || bundleTag == internalTag || bundleTag == deprecatedTag {
|
||||||
continue
|
// continue
|
||||||
}
|
// }
|
||||||
|
|
||||||
// get child json tags
|
// // get child json tags
|
||||||
childJsonTag := strings.Split(child.Tag.Get("json"), ",")
|
// childJsonTag := strings.Split(child.Tag.Get("json"), ",")
|
||||||
childName := childJsonTag[0]
|
// childName := childJsonTag[0]
|
||||||
|
|
||||||
// skip children that have no json tags, the first json tag is ""
|
// // skip children that have no json tags, the first json tag is ""
|
||||||
// or the first json tag is "-"
|
// // or the first json tag is "-"
|
||||||
if childName == "" || childName == "-" {
|
// if childName == "" || childName == "-" {
|
||||||
continue
|
// continue
|
||||||
}
|
// }
|
||||||
|
|
||||||
// get docs for the child if they exist
|
// // get docs for the child if they exist
|
||||||
var childDocs *Docs
|
// var childDocs *Docs
|
||||||
if docs != nil {
|
// if docs != nil {
|
||||||
if val, ok := docs.Properties[childName]; ok {
|
// if val, ok := docs.Properties[childName]; ok {
|
||||||
childDocs = val
|
// childDocs = val
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// compute if the child is a required field. Determined by the
|
// // compute if the child is a required field. Determined by the
|
||||||
// presence of "omitempty" in the json tags
|
// // presence of "omitempty" in the json tags
|
||||||
hasOmitEmptyTag := false
|
// hasOmitEmptyTag := false
|
||||||
for i := 1; i < len(childJsonTag); i++ {
|
// for i := 1; i < len(childJsonTag); i++ {
|
||||||
if childJsonTag[i] == "omitempty" {
|
// if childJsonTag[i] == "omitempty" {
|
||||||
hasOmitEmptyTag = true
|
// hasOmitEmptyTag = true
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
if !hasOmitEmptyTag {
|
// if !hasOmitEmptyTag {
|
||||||
required = append(required, childName)
|
// required = append(required, childName)
|
||||||
}
|
// }
|
||||||
|
|
||||||
// compute Schema.Properties for the child recursively
|
// // compute Schema.Properties for the child recursively
|
||||||
fieldProps, err := safeToSchema(child.Type, childDocs, childName, tracker)
|
// fieldProps, err := safeToSchema(child.Type, childDocs, childName, tracker)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return nil, err
|
// return nil, err
|
||||||
}
|
// }
|
||||||
properties[childName] = fieldProps
|
// properties[childName] = fieldProps
|
||||||
}
|
// }
|
||||||
|
|
||||||
jsonSchema.AdditionalProperties = false
|
// jsonSchema.AdditionalProperties = false
|
||||||
jsonSchema.Properties = properties
|
// jsonSchema.Properties = properties
|
||||||
jsonSchema.Required = required
|
// jsonSchema.Required = required
|
||||||
}
|
// }
|
||||||
|
|
||||||
return jsonSchema, nil
|
// return jsonSchema, nil
|
||||||
}
|
// }
|
||||||
|
|
|
@ -18,11 +18,11 @@ func (c pathComponent) Index() int {
|
||||||
return c.index
|
return c.index
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c pathComponent) isKey() bool {
|
func (c pathComponent) IsKey() bool {
|
||||||
return c.key != ""
|
return c.key != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c pathComponent) isIndex() bool {
|
func (c pathComponent) IsIndex() bool {
|
||||||
return c.key == ""
|
return c.key == ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,9 +14,9 @@ type cannotTraverseNilError struct {
|
||||||
func (e cannotTraverseNilError) Error() string {
|
func (e cannotTraverseNilError) Error() string {
|
||||||
component := e.p[len(e.p)-1]
|
component := e.p[len(e.p)-1]
|
||||||
switch {
|
switch {
|
||||||
case component.isKey():
|
case component.IsKey():
|
||||||
return fmt.Sprintf("expected a map to index %q, found nil", e.p)
|
return fmt.Sprintf("expected a map to index %q, found nil", e.p)
|
||||||
case component.isIndex():
|
case component.IsIndex():
|
||||||
return fmt.Sprintf("expected a sequence to index %q, found nil", e.p)
|
return fmt.Sprintf("expected a sequence to index %q, found nil", e.p)
|
||||||
default:
|
default:
|
||||||
panic("invalid component")
|
panic("invalid component")
|
||||||
|
@ -90,7 +90,7 @@ func (component pathComponent) visit(v Value, prefix Path, suffix Pattern, opts
|
||||||
path := append(prefix, component)
|
path := append(prefix, component)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case component.isKey():
|
case component.IsKey():
|
||||||
// Expect a map to be set if this is a key.
|
// Expect a map to be set if this is a key.
|
||||||
switch v.Kind() {
|
switch v.Kind() {
|
||||||
case KindMap:
|
case KindMap:
|
||||||
|
@ -129,7 +129,7 @@ func (component pathComponent) visit(v Value, prefix Path, suffix Pattern, opts
|
||||||
l: v.l,
|
l: v.l,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
case component.isIndex():
|
case component.IsIndex():
|
||||||
// Expect a sequence to be set if this is an index.
|
// Expect a sequence to be set if this is an index.
|
||||||
switch v.Kind() {
|
switch v.Kind() {
|
||||||
case KindSequence:
|
case KindSequence:
|
||||||
|
|
|
@ -32,7 +32,7 @@ func SetByPath(v Value, p Path, nv Value) (Value, error) {
|
||||||
path := append(prefix, component)
|
path := append(prefix, component)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case component.isKey():
|
case component.IsKey():
|
||||||
// Expect a map to be set if this is a key.
|
// Expect a map to be set if this is a key.
|
||||||
m, ok := v.AsMap()
|
m, ok := v.AsMap()
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -48,7 +48,7 @@ func SetByPath(v Value, p Path, nv Value) (Value, error) {
|
||||||
l: v.l,
|
l: v.l,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
case component.isIndex():
|
case component.IsIndex():
|
||||||
// Expect a sequence to be set if this is an index.
|
// Expect a sequence to be set if this is an index.
|
||||||
s, ok := v.AsSequence()
|
s, ok := v.AsSequence()
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
|
@ -0,0 +1,204 @@
|
||||||
|
package jsonschema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"container/list"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var InvalidSchema = Schema{
|
||||||
|
Type: InvalidType,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
|
||||||
|
// Annotation for bundle fields that have been deprecated.
|
||||||
|
// Fields tagged as "deprecated" are removed/omitted from the generated schema.
|
||||||
|
const deprecatedTag = "deprecated"
|
||||||
|
|
||||||
|
// TODO: Test what happens with invalid cycles? Do integration tests fail?
|
||||||
|
// TODO: Call out in the PR description that recursive types like "for_each_task"
|
||||||
|
// are now supported.
|
||||||
|
|
||||||
|
type FromTypeOptions struct {
|
||||||
|
// Transformation function to apply after generating the schema.
|
||||||
|
Transform func(s Schema) Schema
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Skip generating schema for interface fields.
|
||||||
|
func FromType(typ reflect.Type, opts FromTypeOptions) (Schema, error) {
|
||||||
|
// Dereference pointers if necessary.
|
||||||
|
for typ.Kind() == reflect.Ptr {
|
||||||
|
typ = typ.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
// An interface value can never be serialized from text, and thus is explicitly
|
||||||
|
// set to null and disallowed in the schema.
|
||||||
|
if typ.Kind() == reflect.Interface {
|
||||||
|
return Schema{Type: NullType}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var res Schema
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// TODO: Narrow down the number of Go types handled here.
|
||||||
|
switch typ.Kind() {
|
||||||
|
case reflect.Struct:
|
||||||
|
res, err = fromTypeStruct(typ, opts)
|
||||||
|
case reflect.Slice:
|
||||||
|
res, err = fromTypeSlice(typ, opts)
|
||||||
|
case reflect.Map:
|
||||||
|
res, err = fromTypeMap(typ, opts)
|
||||||
|
// TODO: Should the primitive functions below be inlined?
|
||||||
|
case reflect.String:
|
||||||
|
res = Schema{Type: StringType}
|
||||||
|
case reflect.Bool:
|
||||||
|
res = Schema{Type: BooleanType}
|
||||||
|
// case reflect.Int, reflect.Int32, reflect.Int64:
|
||||||
|
// res = Schema{Type: IntegerType}
|
||||||
|
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:
|
||||||
|
res = Schema{Type: NumberType}
|
||||||
|
default:
|
||||||
|
return InvalidSchema, fmt.Errorf("unsupported type: %s", typ.Kind())
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return InvalidSchema, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Transform != nil {
|
||||||
|
res = opts.Transform(res)
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromTypeStruct(typ reflect.Type, opts FromTypeOptions) (Schema, error) {
|
||||||
|
if typ.Kind() != reflect.Struct {
|
||||||
|
return InvalidSchema, fmt.Errorf("expected struct, got %s", typ.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
res := Schema{
|
||||||
|
Type: ObjectType,
|
||||||
|
|
||||||
|
Properties: make(map[string]*Schema),
|
||||||
|
|
||||||
|
// TODO: Confirm that empty arrays are not serialized.
|
||||||
|
Required: []string{},
|
||||||
|
|
||||||
|
AdditionalProperties: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
structFields := getStructFields(typ)
|
||||||
|
|
||||||
|
for _, structField := range structFields {
|
||||||
|
bundleTags := strings.Split(structField.Tag.Get("bundle"), ",")
|
||||||
|
// Fields marked as "readonly", "internal" or "deprecated" are skipped
|
||||||
|
// while generating the schema
|
||||||
|
if slices.Contains(bundleTags, readonlyTag) ||
|
||||||
|
slices.Contains(bundleTags, internalTag) ||
|
||||||
|
slices.Contains(bundleTags, deprecatedTag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonTags := strings.Split(structField.Tag.Get("json"), ",")
|
||||||
|
// Do not include fields in the schema that will not be serialized during
|
||||||
|
// JSON marshalling.
|
||||||
|
if jsonTags[0] == "" || jsonTags[0] == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// "omitempty" tags in the Go SDK structs represent fields that not are
|
||||||
|
// required to be present in the API payload. Thus its absence in the
|
||||||
|
// tags list indicates that the field is required.
|
||||||
|
if !slices.Contains(jsonTags, "omitempty") {
|
||||||
|
res.Required = append(res.Required, jsonTags[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := FromType(structField.Type, opts)
|
||||||
|
if err != nil {
|
||||||
|
return InvalidSchema, err
|
||||||
|
}
|
||||||
|
res.Properties[jsonTags[0]] = &s
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromTypeSlice(typ reflect.Type, opts FromTypeOptions) (Schema, error) {
|
||||||
|
if typ.Kind() != reflect.Slice {
|
||||||
|
return InvalidSchema, fmt.Errorf("expected slice, got %s", typ.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
res := Schema{
|
||||||
|
Type: ArrayType,
|
||||||
|
}
|
||||||
|
|
||||||
|
items, err := FromType(typ.Elem(), opts)
|
||||||
|
if err != nil {
|
||||||
|
return InvalidSchema, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Items = &items
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromTypeMap(typ reflect.Type, opts FromTypeOptions) (Schema, error) {
|
||||||
|
if typ.Kind() != reflect.Map {
|
||||||
|
return InvalidSchema, fmt.Errorf("expected map, got %s", typ.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
if typ.Key().Kind() != reflect.String {
|
||||||
|
return InvalidSchema, fmt.Errorf("found map with non-string key: %v", typ.Key())
|
||||||
|
}
|
||||||
|
|
||||||
|
res := Schema{
|
||||||
|
Type: ObjectType,
|
||||||
|
}
|
||||||
|
|
||||||
|
additionalProperties, err := FromType(typ.Elem(), opts)
|
||||||
|
if err != nil {
|
||||||
|
return InvalidSchema, err
|
||||||
|
}
|
||||||
|
res.AdditionalProperties = additionalProperties
|
||||||
|
return res, nil
|
||||||
|
}
|
|
@ -13,6 +13,13 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// defines schema for a json object
|
// defines schema for a json object
|
||||||
|
// TODO: Remove pointers from properties and AnyOf.
|
||||||
|
// TODO: Can / should we emulate dyn.V here in having a readonly model for the data
|
||||||
|
// structure? Makes it easier to reason about.
|
||||||
|
//
|
||||||
|
// Any performance issues can be addressed by storing the schema
|
||||||
|
//
|
||||||
|
// as an embedded file.
|
||||||
type Schema struct {
|
type Schema struct {
|
||||||
// Type of the object
|
// Type of the object
|
||||||
Type Type `json:"type,omitempty"`
|
Type Type `json:"type,omitempty"`
|
||||||
|
@ -23,6 +30,7 @@ type Schema struct {
|
||||||
|
|
||||||
// Expected value for the JSON object. The object value must be equal to this
|
// Expected value for the JSON object. The object value must be equal to this
|
||||||
// field if it's specified in the schema.
|
// field if it's specified in the schema.
|
||||||
|
// TODO: Generics here? OR maybe a type from the reflection package.
|
||||||
Const any `json:"const,omitempty"`
|
Const any `json:"const,omitempty"`
|
||||||
|
|
||||||
// Schemas for the fields of an struct. The keys are the first json tag.
|
// Schemas for the fields of an struct. The keys are the first json tag.
|
||||||
|
@ -38,7 +46,8 @@ type Schema struct {
|
||||||
// A boolean type with value false. Setting false here validates that all
|
// A boolean type with value false. Setting false here validates that all
|
||||||
// properties in the config have been defined in the json schema as properties
|
// properties in the config have been defined in the json schema as properties
|
||||||
//
|
//
|
||||||
// Its type during runtime will either be *Schema or bool
|
// Its type during runtime will either be Schema or bool
|
||||||
|
// TODO: Generics to represent either a Schema{} or a bool.
|
||||||
AdditionalProperties any `json:"additionalProperties,omitempty"`
|
AdditionalProperties any `json:"additionalProperties,omitempty"`
|
||||||
|
|
||||||
// Required properties for the object. Any fields missing the "omitempty"
|
// Required properties for the object. Any fields missing the "omitempty"
|
||||||
|
@ -63,7 +72,7 @@ type Schema struct {
|
||||||
Extension
|
Extension
|
||||||
|
|
||||||
// Schema that must match any of the schemas in the array
|
// Schema that must match any of the schemas in the array
|
||||||
AnyOf []*Schema `json:"anyOf,omitempty"`
|
AnyOf []Schema `json:"anyOf,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default value defined in a JSON Schema, represented as a string.
|
// Default value defined in a JSON Schema, represented as a string.
|
||||||
|
@ -120,7 +129,10 @@ func (s *Schema) SetByPath(path string, v Schema) error {
|
||||||
type Type string
|
type Type string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// Default zero value of a schema. This does not correspond to a type in the
|
||||||
|
// JSON schema spec and is an internal type defined for convenience.
|
||||||
InvalidType Type = "invalid"
|
InvalidType Type = "invalid"
|
||||||
|
NullType Type = "null"
|
||||||
BooleanType Type = "boolean"
|
BooleanType Type = "boolean"
|
||||||
StringType Type = "string"
|
StringType Type = "string"
|
||||||
NumberType Type = "number"
|
NumberType Type = "number"
|
||||||
|
|
Loading…
Reference in New Issue