mirror of https://github.com/databricks/cli.git
more progress on the internal functionality
This commit is contained in:
parent
43325fdd0a
commit
6d2f88282f
|
@ -42,23 +42,21 @@ import (
|
||||||
// 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) {
|
||||||
|
|
||||||
s, err := jsonschema.FromType(golangType, jsonschema.FromTypeOptions{
|
s, err := jsonschema.FromType(golangType, func(s jsonschema.Schema) jsonschema.Schema {
|
||||||
Transform: func(s jsonschema.Schema) jsonschema.Schema {
|
if s.Type == jsonschema.NumberType || s.Type == jsonschema.BooleanType {
|
||||||
if s.Type == jsonschema.NumberType || s.Type == jsonschema.BooleanType {
|
s = jsonschema.Schema{
|
||||||
s = jsonschema.Schema{
|
AnyOf: []jsonschema.Schema{
|
||||||
AnyOf: []jsonschema.Schema{
|
s,
|
||||||
s,
|
{
|
||||||
{
|
Type: jsonschema.StringType,
|
||||||
Type: jsonschema.StringType,
|
// TODO: Narrow down the scope of the regex match.
|
||||||
// TODO: Narrow down the scope of the regex match.
|
// Also likely need to rename this variable.
|
||||||
// Also likely need to rename this variable.
|
Pattern: dynvar.VariableRegex,
|
||||||
Pattern: dynvar.VariableRegex,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
return s
|
}
|
||||||
},
|
return s
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -2,6 +2,7 @@ package schema
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -12,6 +13,11 @@ import (
|
||||||
func TestIntSchema(t *testing.T) {
|
func TestIntSchema(t *testing.T) {
|
||||||
var elemInt int
|
var elemInt int
|
||||||
|
|
||||||
|
type Bae struct{}
|
||||||
|
|
||||||
|
typ := reflect.TypeOf(Bae{})
|
||||||
|
fmt.Println(typ.PkgPath())
|
||||||
|
|
||||||
expected :=
|
expected :=
|
||||||
`{
|
`{
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
|
|
|
@ -3,11 +3,13 @@ package jsonschema
|
||||||
import (
|
import (
|
||||||
"container/list"
|
"container/list"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO: Maybe can be removed?
|
||||||
var InvalidSchema = Schema{
|
var InvalidSchema = Schema{
|
||||||
Type: InvalidType,
|
Type: InvalidType,
|
||||||
}
|
}
|
||||||
|
@ -28,56 +30,130 @@ const deprecatedTag = "deprecated"
|
||||||
// TODO: Call out in the PR description that recursive types like "for_each_task"
|
// TODO: Call out in the PR description that recursive types like "for_each_task"
|
||||||
// are now supported.
|
// are now supported.
|
||||||
|
|
||||||
type FromTypeOptions struct {
|
type constructor struct {
|
||||||
// Transformation function to apply after generating the schema.
|
// Map of typ.PkgPath() + "." + typ.Name() to the schema for that type.
|
||||||
Transform func(s Schema) Schema
|
// Example key: github.com/databricks/databricks-sdk-go/service/jobs.JobSettings
|
||||||
|
definitions map[string]Schema
|
||||||
|
|
||||||
|
// Transformation function to apply after generating a node in the schema.
|
||||||
|
fn func(s Schema) Schema
|
||||||
|
}
|
||||||
|
|
||||||
|
// The $defs block in a JSON schema cannot contain "/", otherwise it will not be
|
||||||
|
// correctly parsed by a JSON schema validator. So we replace "/" with an additional
|
||||||
|
// level of nesting in the output map.
|
||||||
|
//
|
||||||
|
// For example:
|
||||||
|
// {"a/b/c": "value"} is converted to {"a": {"b": {"c": "value"}}}
|
||||||
|
func (c *constructor) nestedDefinitions() any {
|
||||||
|
if len(c.definitions) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
res := make(map[string]any)
|
||||||
|
for k, v := range c.definitions {
|
||||||
|
parts := strings.Split(k, "/")
|
||||||
|
cur := res
|
||||||
|
for i, p := range parts {
|
||||||
|
if i == len(parts)-1 {
|
||||||
|
cur[p] = v
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := cur[p]; !ok {
|
||||||
|
cur[p] = make(map[string]any)
|
||||||
|
}
|
||||||
|
cur = cur[p].(map[string]any)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Skip generating schema for interface fields.
|
// TODO: Skip generating schema for interface fields.
|
||||||
func FromType(typ reflect.Type, opts FromTypeOptions) (Schema, error) {
|
func FromType(typ reflect.Type, fn func(s Schema) Schema) (Schema, error) {
|
||||||
|
c := constructor{
|
||||||
|
definitions: make(map[string]Schema),
|
||||||
|
fn: fn,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.walk(typ)
|
||||||
|
if err != nil {
|
||||||
|
return InvalidSchema, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res := c.definitions[typePath(typ)]
|
||||||
|
// No need to include the root type in the definitions.
|
||||||
|
delete(c.definitions, typePath(typ))
|
||||||
|
res.Definitions = c.nestedDefinitions()
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func typePath(typ reflect.Type) string {
|
||||||
|
// For built-in types, return the type name directly.
|
||||||
|
if typ.PkgPath() == "" {
|
||||||
|
return typ.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join([]string{typ.PkgPath(), typ.Name()}, ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: would a worked based model fit better here? Is this internal API not
|
||||||
|
// the right fit?
|
||||||
|
func (c *constructor) walk(typ reflect.Type) error {
|
||||||
// Dereference pointers if necessary.
|
// Dereference pointers if necessary.
|
||||||
for typ.Kind() == reflect.Ptr {
|
for typ.Kind() == reflect.Ptr {
|
||||||
typ = typ.Elem()
|
typ = typ.Elem()
|
||||||
}
|
}
|
||||||
|
|
||||||
// An interface value can never be serialized from text, and thus is explicitly
|
typPath := typePath(typ)
|
||||||
// set to null and disallowed in the schema.
|
|
||||||
if typ.Kind() == reflect.Interface {
|
// Return value directly if it's already been processed.
|
||||||
return Schema{Type: NullType}, nil
|
if _, ok := c.definitions[typPath]; ok {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var res Schema
|
var s Schema
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// TODO: Narrow down the number of Go types handled here.
|
// TODO: Narrow / widen down the number of Go types handled here.
|
||||||
switch typ.Kind() {
|
switch typ.Kind() {
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
res, err = fromTypeStruct(typ, opts)
|
s, err = c.fromTypeStruct(typ)
|
||||||
case reflect.Slice:
|
case reflect.Slice:
|
||||||
res, err = fromTypeSlice(typ, opts)
|
s, err = c.fromTypeSlice(typ)
|
||||||
case reflect.Map:
|
case reflect.Map:
|
||||||
res, err = fromTypeMap(typ, opts)
|
s, err = c.fromTypeMap(typ)
|
||||||
// TODO: Should the primitive functions below be inlined?
|
// TODO: Should the primitive functions below be inlined?
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
res = Schema{Type: StringType}
|
s = Schema{Type: StringType}
|
||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
res = Schema{Type: BooleanType}
|
s = Schema{Type: BooleanType}
|
||||||
// TODO: Add comment about reduced coverage of primitive Go types in the code paths here.
|
// TODO: Add comment about reduced coverage of primitive Go types in the code paths here.
|
||||||
case reflect.Int:
|
case reflect.Int:
|
||||||
res = Schema{Type: IntegerType}
|
s = Schema{Type: IntegerType}
|
||||||
case reflect.Float32, reflect.Float64:
|
case reflect.Float32, reflect.Float64:
|
||||||
res = Schema{Type: NumberType}
|
s = Schema{Type: NumberType}
|
||||||
|
case reflect.Interface:
|
||||||
|
// An interface value can never be serialized from text, and thus is explicitly
|
||||||
|
// set to null and disallowed in the schema.
|
||||||
|
s = Schema{Type: NullType}
|
||||||
default:
|
default:
|
||||||
return InvalidSchema, fmt.Errorf("unsupported type: %s", typ.Kind())
|
return fmt.Errorf("unsupported type: %s", typ.Kind())
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return InvalidSchema, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.Transform != nil {
|
if c.fn != nil {
|
||||||
res = opts.Transform(res)
|
s = c.fn(s)
|
||||||
}
|
}
|
||||||
return res, nil
|
|
||||||
|
// Store definition for the type if it's part of a Go package and not a built-in type.
|
||||||
|
// TODO: Apply transformation at the end, to all definitions instead of
|
||||||
|
// during recursive traversal?
|
||||||
|
c.definitions[typPath] = s
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function returns all member fields of the provided type.
|
// This function returns all member fields of the provided type.
|
||||||
|
@ -112,7 +188,7 @@ func getStructFields(golangType reflect.Type) []reflect.StructField {
|
||||||
return fields
|
return fields
|
||||||
}
|
}
|
||||||
|
|
||||||
func fromTypeStruct(typ reflect.Type, opts FromTypeOptions) (Schema, error) {
|
func (c *constructor) fromTypeStruct(typ reflect.Type) (Schema, error) {
|
||||||
if typ.Kind() != reflect.Struct {
|
if typ.Kind() != reflect.Struct {
|
||||||
return InvalidSchema, fmt.Errorf("expected struct, got %s", typ.Kind())
|
return InvalidSchema, fmt.Errorf("expected struct, got %s", typ.Kind())
|
||||||
}
|
}
|
||||||
|
@ -129,7 +205,6 @@ func fromTypeStruct(typ reflect.Type, opts FromTypeOptions) (Schema, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
structFields := getStructFields(typ)
|
structFields := getStructFields(typ)
|
||||||
|
|
||||||
for _, structField := range structFields {
|
for _, structField := range structFields {
|
||||||
bundleTags := strings.Split(structField.Tag.Get("bundle"), ",")
|
bundleTags := strings.Split(structField.Tag.Get("bundle"), ",")
|
||||||
// Fields marked as "readonly", "internal" or "deprecated" are skipped
|
// Fields marked as "readonly", "internal" or "deprecated" are skipped
|
||||||
|
@ -143,7 +218,7 @@ func fromTypeStruct(typ reflect.Type, opts FromTypeOptions) (Schema, error) {
|
||||||
jsonTags := strings.Split(structField.Tag.Get("json"), ",")
|
jsonTags := strings.Split(structField.Tag.Get("json"), ",")
|
||||||
// Do not include fields in the schema that will not be serialized during
|
// Do not include fields in the schema that will not be serialized during
|
||||||
// JSON marshalling.
|
// JSON marshalling.
|
||||||
if jsonTags[0] == "" || jsonTags[0] == "-" {
|
if jsonTags[0] == "" || jsonTags[0] == "-" || !structField.IsExported() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// "omitempty" tags in the Go SDK structs represent fields that not are
|
// "omitempty" tags in the Go SDK structs represent fields that not are
|
||||||
|
@ -153,11 +228,19 @@ func fromTypeStruct(typ reflect.Type, opts FromTypeOptions) (Schema, error) {
|
||||||
res.Required = append(res.Required, jsonTags[0])
|
res.Required = append(res.Required, jsonTags[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
s, err := FromType(structField.Type, opts)
|
// Trigger call to fromType, to recursively generate definitions for
|
||||||
|
// the struct field.
|
||||||
|
err := c.walk(structField.Type)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return InvalidSchema, err
|
return InvalidSchema, err
|
||||||
}
|
}
|
||||||
res.Properties[jsonTags[0]] = &s
|
|
||||||
|
typPath := typePath(structField.Type)
|
||||||
|
refPath := path.Join("#/$defs", typPath)
|
||||||
|
// For non-built-in types, refer to the definition.
|
||||||
|
res.Properties[jsonTags[0]] = &Schema{
|
||||||
|
Reference: &refPath,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
|
@ -165,7 +248,7 @@ func fromTypeStruct(typ reflect.Type, opts FromTypeOptions) (Schema, error) {
|
||||||
|
|
||||||
// TODO: Add comments explaining the translation between struct, map, slice and
|
// TODO: Add comments explaining the translation between struct, map, slice and
|
||||||
// the JSON schema representation.
|
// the JSON schema representation.
|
||||||
func fromTypeSlice(typ reflect.Type, opts FromTypeOptions) (Schema, error) {
|
func (c *constructor) fromTypeSlice(typ reflect.Type) (Schema, error) {
|
||||||
if typ.Kind() != reflect.Slice {
|
if typ.Kind() != reflect.Slice {
|
||||||
return InvalidSchema, fmt.Errorf("expected slice, got %s", typ.Kind())
|
return InvalidSchema, fmt.Errorf("expected slice, got %s", typ.Kind())
|
||||||
}
|
}
|
||||||
|
@ -174,16 +257,24 @@ func fromTypeSlice(typ reflect.Type, opts FromTypeOptions) (Schema, error) {
|
||||||
Type: ArrayType,
|
Type: ArrayType,
|
||||||
}
|
}
|
||||||
|
|
||||||
items, err := FromType(typ.Elem(), opts)
|
// Trigger call to fromType, to recursively generate definitions for
|
||||||
|
// the slice element.
|
||||||
|
err := c.walk(typ.Elem())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return InvalidSchema, err
|
return InvalidSchema, err
|
||||||
}
|
}
|
||||||
|
|
||||||
res.Items = &items
|
typPath := typePath(typ.Elem())
|
||||||
|
refPath := path.Join("#/$defs", typPath)
|
||||||
|
|
||||||
|
// For non-built-in types, refer to the definition
|
||||||
|
res.Items = &Schema{
|
||||||
|
Reference: &refPath,
|
||||||
|
}
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fromTypeMap(typ reflect.Type, opts FromTypeOptions) (Schema, error) {
|
func (c *constructor) fromTypeMap(typ reflect.Type) (Schema, error) {
|
||||||
if typ.Kind() != reflect.Map {
|
if typ.Kind() != reflect.Map {
|
||||||
return InvalidSchema, fmt.Errorf("expected map, got %s", typ.Kind())
|
return InvalidSchema, fmt.Errorf("expected map, got %s", typ.Kind())
|
||||||
}
|
}
|
||||||
|
@ -196,10 +287,19 @@ func fromTypeMap(typ reflect.Type, opts FromTypeOptions) (Schema, error) {
|
||||||
Type: ObjectType,
|
Type: ObjectType,
|
||||||
}
|
}
|
||||||
|
|
||||||
additionalProperties, err := FromType(typ.Elem(), opts)
|
// Trigger call to fromType, to recursively generate definitions for
|
||||||
|
// the map value.
|
||||||
|
err := c.walk(typ.Elem())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return InvalidSchema, err
|
return InvalidSchema, err
|
||||||
}
|
}
|
||||||
res.AdditionalProperties = additionalProperties
|
|
||||||
|
typPath := typePath(typ.Elem())
|
||||||
|
refPath := path.Join("#/$defs", typPath)
|
||||||
|
|
||||||
|
// For non-built-in types, refer to the definition
|
||||||
|
res.AdditionalProperties = &Schema{
|
||||||
|
Reference: &refPath,
|
||||||
|
}
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
package jsonschema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFromTypeBasic(t *testing.T) {
|
||||||
|
type myStruct struct {
|
||||||
|
V string `json:"v"`
|
||||||
|
}
|
||||||
|
|
||||||
|
strRef := "#/$defs/string"
|
||||||
|
boolRef := "#/$defs/bool"
|
||||||
|
intRef := "#/$defs/int"
|
||||||
|
|
||||||
|
tcases := []struct {
|
||||||
|
name string
|
||||||
|
typ reflect.Type
|
||||||
|
expected Schema
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "int",
|
||||||
|
typ: reflect.TypeOf(int(0)),
|
||||||
|
expected: Schema{
|
||||||
|
Type: "integer",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "string",
|
||||||
|
typ: reflect.TypeOf(""),
|
||||||
|
expected: Schema{
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bool",
|
||||||
|
typ: reflect.TypeOf(true),
|
||||||
|
expected: Schema{
|
||||||
|
Type: "boolean",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "float64",
|
||||||
|
typ: reflect.TypeOf(float64(0)),
|
||||||
|
expected: Schema{
|
||||||
|
Type: "number",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "struct",
|
||||||
|
typ: reflect.TypeOf(myStruct{}),
|
||||||
|
expected: Schema{
|
||||||
|
Type: "object",
|
||||||
|
Definitions: map[string]any{
|
||||||
|
"string": Schema{
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]*Schema{
|
||||||
|
"v": {
|
||||||
|
Reference: &strRef,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AdditionalProperties: false,
|
||||||
|
Required: []string{"v"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "slice",
|
||||||
|
typ: reflect.TypeOf([]bool{}),
|
||||||
|
expected: Schema{
|
||||||
|
Type: "array",
|
||||||
|
Definitions: map[string]any{
|
||||||
|
"bool": Schema{
|
||||||
|
Type: "boolean",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Items: &Schema{
|
||||||
|
Reference: &boolRef,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "map",
|
||||||
|
typ: reflect.TypeOf(map[string]int{}),
|
||||||
|
expected: Schema{
|
||||||
|
Type: "object",
|
||||||
|
Definitions: map[string]any{
|
||||||
|
"int": Schema{
|
||||||
|
Type: "integer",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AdditionalProperties: &Schema{
|
||||||
|
Reference: &intRef,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tcases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
s, err := FromType(tc.typ, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, tc.expected, s)
|
||||||
|
|
||||||
|
// jsonSchema, err := json.MarshalIndent(s, " ", " ")
|
||||||
|
// assert.NoError(t, err)
|
||||||
|
|
||||||
|
// expectedJson, err := json.MarshalIndent(tc.expected, " ", " ")
|
||||||
|
// assert.NoError(t, err)
|
||||||
|
|
||||||
|
// t.Log("[DEBUG] actual: ", string(jsonSchema))
|
||||||
|
// t.Log("[DEBUG] expected: ", string(expectedJson))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,9 @@ import (
|
||||||
//
|
//
|
||||||
// as an embedded file.
|
// as an embedded file.
|
||||||
type Schema struct {
|
type Schema struct {
|
||||||
|
// TODO: Comments for this field
|
||||||
|
Definitions any `json:"$defs,omitempty"`
|
||||||
|
|
||||||
// Type of the object
|
// Type of the object
|
||||||
Type Type `json:"type,omitempty"`
|
Type Type `json:"type,omitempty"`
|
||||||
|
|
||||||
|
@ -55,6 +58,7 @@ type Schema struct {
|
||||||
Required []string `json:"required,omitempty"`
|
Required []string `json:"required,omitempty"`
|
||||||
|
|
||||||
// URI to a json schema
|
// URI to a json schema
|
||||||
|
// TODO: Would be nice to make items as well as this a non-pointer.
|
||||||
Reference *string `json:"$ref,omitempty"`
|
Reference *string `json:"$ref,omitempty"`
|
||||||
|
|
||||||
// Default value for the property / object
|
// Default value for the property / object
|
||||||
|
|
Loading…
Reference in New Issue