Improve JSON schema

This commit is contained in:
Shreyas Goenka 2024-08-20 15:34:49 +02:00
parent 242d4b51ed
commit 75f252e51c
No known key found for this signature in database
GPG Key ID: 92A07DF49CCB0622
6 changed files with 454 additions and 218 deletions

View File

@ -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
} // }

View File

@ -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 == ""
} }

View File

@ -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:

View File

@ -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 {

View File

@ -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
}

View File

@ -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"