mirror of https://github.com/databricks/cli.git
more iteration
This commit is contained in:
parent
535f670868
commit
11dfdc036d
|
@ -1,42 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/databricks/cli/bundle/schema"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Println("Usage: go run main.go <output-file>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Output file, to write the generated schema descriptions to.
|
||||
outputFile := os.Args[1]
|
||||
|
||||
// Input file, the databricks openapi spec.
|
||||
inputFile := os.Getenv("DATABRICKS_OPENAPI_SPEC")
|
||||
if inputFile == "" {
|
||||
log.Fatal("DATABRICKS_OPENAPI_SPEC environment variable not set")
|
||||
}
|
||||
|
||||
// Generate the schema descriptions.
|
||||
docs, err := schema.UpdateBundleDescriptions(inputFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
result, err := json.MarshalIndent(docs, "", " ")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Write the schema descriptions to the output file.
|
||||
err = os.WriteFile(outputFile, result, 0644)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
|
||||
"github.com/databricks/cli/bundle/config"
|
||||
"github.com/databricks/cli/libs/jsonschema"
|
||||
)
|
||||
|
||||
func interpolationPattern(s string) string {
|
||||
return fmt.Sprintf(`\$\{(%s(\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\[[0-9]+\])*)*(\[[0-9]+\])*)\}`, s)
|
||||
}
|
||||
|
||||
func addInterpolationPatterns(_ reflect.Type, s jsonschema.Schema) jsonschema.Schema {
|
||||
switch s.Type {
|
||||
case jsonschema.ArrayType, jsonschema.ObjectType:
|
||||
// arrays and objects can have complex variable values specified.
|
||||
return jsonschema.Schema{
|
||||
AnyOf: []jsonschema.Schema{s, {
|
||||
Type: jsonschema.StringType,
|
||||
// TODO: Are multi-level complex variable references supported?
|
||||
Pattern: interpolationPattern("var"),
|
||||
}},
|
||||
}
|
||||
case jsonschema.StringType, jsonschema.IntegerType, jsonschema.NumberType, jsonschema.BooleanType:
|
||||
// primitives can have variable values, or references like ${bundle.xyz}
|
||||
// or ${workspace.xyz}
|
||||
// TODO: Followup, do not allow references like ${} in the schema unless
|
||||
// they are of the permitted patterns?
|
||||
return jsonschema.Schema{
|
||||
AnyOf: []jsonschema.Schema{s,
|
||||
// TODO: Add "resources" here
|
||||
{Type: jsonschema.StringType, Pattern: interpolationPattern("bundle")},
|
||||
{Type: jsonschema.StringType, Pattern: interpolationPattern("workspace")},
|
||||
{Type: jsonschema.StringType, Pattern: interpolationPattern("var")},
|
||||
},
|
||||
}
|
||||
default:
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add a couple of end to end tests that the bundle schema generated is
|
||||
// correct.
|
||||
// TODO: Call out in the PR description that recursive types like "for_each_task"
|
||||
// are now supported. Manually test for_each_task.
|
||||
// TODO: The bundle_descriptions.json file contains a bunch of custom descriptions
|
||||
// as well. Make sure to pull those in.
|
||||
// TODO: Add unit tests for all permutations of structs, maps and slices for the FromType
|
||||
// method.
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Println("Usage: go run main.go <output-file>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Output file, where the generated JSON schema will be written to.
|
||||
outputFile := os.Args[1]
|
||||
|
||||
// Input file, the databricks openapi spec.
|
||||
inputFile := os.Getenv("DATABRICKS_OPENAPI_SPEC")
|
||||
if inputFile == "" {
|
||||
log.Fatal("DATABRICKS_OPENAPI_SPEC environment variable not set")
|
||||
}
|
||||
|
||||
p, err := newParser(inputFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Generate the JSON schema from the bundle Go struct.
|
||||
s, err := jsonschema.FromType(reflect.TypeOf(config.Root{}), []func(reflect.Type, jsonschema.Schema) jsonschema.Schema{
|
||||
p.addDescriptions,
|
||||
p.addEnums,
|
||||
addInterpolationPatterns,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(s, "", " ")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Write the schema descriptions to the output file.
|
||||
err = os.WriteFile(outputFile, b, 0644)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/databricks/cli/libs/jsonschema"
|
||||
)
|
||||
|
||||
type Components struct {
|
||||
Schemas map[string]jsonschema.Schema `json:"schemas,omitempty"`
|
||||
}
|
||||
|
||||
type Specification struct {
|
||||
Components Components `json:"components"`
|
||||
}
|
||||
|
||||
type openapiParser struct {
|
||||
ref map[string]jsonschema.Schema
|
||||
}
|
||||
|
||||
func newParser(path string) (*openapiParser, error) {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
spec := Specification{}
|
||||
err = json.Unmarshal(b, &spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p := &openapiParser{}
|
||||
p.ref = spec.Components.Schemas
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// This function finds any JSON schemas that were defined in the OpenAPI spec
|
||||
// that correspond to the given Go SDK type. It looks both at the type itself
|
||||
// and any embedded types within it.
|
||||
func (p *openapiParser) findRef(typ reflect.Type) (jsonschema.Schema, bool) {
|
||||
typs := []reflect.Type{typ}
|
||||
|
||||
// If the type is a struct, the corresponding Go SDK struct might be embedded
|
||||
// in it. We need to check for those as well.
|
||||
if typ.Kind() == reflect.Struct {
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
if !typ.Field(i).Anonymous {
|
||||
continue
|
||||
}
|
||||
|
||||
// Deference current type if it's a pointer.
|
||||
ctyp := typ.Field(i).Type
|
||||
for ctyp.Kind() == reflect.Ptr {
|
||||
ctyp = ctyp.Elem()
|
||||
}
|
||||
|
||||
typs = append(typs, ctyp)
|
||||
}
|
||||
}
|
||||
|
||||
for _, ctyp := range typs {
|
||||
// Skip if it's not a Go SDK type.
|
||||
if !strings.HasPrefix(ctyp.PkgPath(), "github.com/databricks/databricks-sdk-go") {
|
||||
continue
|
||||
}
|
||||
|
||||
pkgName := path.Base(ctyp.PkgPath())
|
||||
k := fmt.Sprintf("%s.%s", pkgName, ctyp.Name())
|
||||
|
||||
// Skip if the type is not in the openapi spec.
|
||||
_, ok := p.ref[k]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Return the first Go SDK type found in the openapi spec.
|
||||
return p.ref[k], true
|
||||
}
|
||||
|
||||
return jsonschema.Schema{}, false
|
||||
}
|
||||
|
||||
// Use the OpenAPI spec to load descriptions for the given type.
|
||||
func (p *openapiParser) addDescriptions(typ reflect.Type, s jsonschema.Schema) jsonschema.Schema {
|
||||
ref, ok := p.findRef(typ)
|
||||
if !ok {
|
||||
return s
|
||||
}
|
||||
|
||||
s.Description = ref.Description
|
||||
|
||||
// Iterate over properties to load descriptions. This is not needed for any
|
||||
// OpenAPI spec generated from protobufs, which are guaranteed to be one level
|
||||
// deep.
|
||||
// Needed for any hand-written OpenAPI specs.
|
||||
for k, v := range s.Properties {
|
||||
if refProp, ok := ref.Properties[k]; ok {
|
||||
v.Description = refProp.Description
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Use the OpenAPI spec add enum values for the given type.
|
||||
func (p *openapiParser) addEnums(typ reflect.Type, s jsonschema.Schema) jsonschema.Schema {
|
||||
ref, ok := p.findRef(typ)
|
||||
if !ok {
|
||||
return s
|
||||
}
|
||||
|
||||
s.Enum = append(s.Enum, ref.Enum...)
|
||||
|
||||
// Iterate over properties to load enums. This is not needed for any
|
||||
// OpenAPI spec generated from protobufs, which are guaranteed to be one level
|
||||
// deep.
|
||||
// Needed for any hand-written OpenAPI specs.
|
||||
for k, v := range s.Properties {
|
||||
if refProp, ok := ref.Properties[k]; ok {
|
||||
v.Enum = append(v.Enum, refProp.Enum...)
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
|
@ -3,7 +3,6 @@ package schema
|
|||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/databricks/cli/libs/dyn/dynvar"
|
||||
"github.com/databricks/cli/libs/jsonschema"
|
||||
)
|
||||
|
||||
|
@ -42,26 +41,28 @@ import (
|
|||
// for details visit: https://json-schema.org/understanding-json-schema/reference/object.html#properties
|
||||
func New(golangType reflect.Type, docs *Docs) (*jsonschema.Schema, error) {
|
||||
|
||||
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.VariableRegex,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return s
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &s, nil
|
||||
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
|
||||
|
||||
// tracker := newTracker()
|
||||
// schema, err := safeToSchema(golangType, docs, "", tracker)
|
||||
|
|
|
@ -6,9 +6,9 @@ import (
|
|||
"github.com/databricks/cli/libs/dyn"
|
||||
)
|
||||
|
||||
const VariableRegex = `\$\{([a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\[[0-9]+\])*)*(\[[0-9]+\])*)\}`
|
||||
const ReferenceRegex = `\$\{([a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\[[0-9]+\])*)*(\[[0-9]+\])*)\}`
|
||||
|
||||
var re = regexp.MustCompile(VariableRegex)
|
||||
var re = regexp.MustCompile(ReferenceRegex)
|
||||
|
||||
// ref represents a variable reference.
|
||||
// It is a string [dyn.Value] contained in a larger [dyn.Value].
|
||||
|
|
|
@ -3,17 +3,13 @@ package jsonschema
|
|||
import (
|
||||
"container/list"
|
||||
"fmt"
|
||||
"maps"
|
||||
"path"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TODO: Maybe can be removed?
|
||||
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"
|
||||
|
@ -26,19 +22,17 @@ const internalTag = "internal"
|
|||
// 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 constructor struct {
|
||||
// Map of typ.PkgPath() + "." + typ.Name() to the schema for that type.
|
||||
// Example key: github.com/databricks/databricks-sdk-go/service/jobs.JobSettings
|
||||
definitions map[string]Schema
|
||||
|
||||
seen map[string]struct{}
|
||||
// Map of typ.PkgPath() + "." + typ.Name() to the corresponding type. Used to
|
||||
// track types that have been seen to avoid infinite recursion.
|
||||
seen map[string]reflect.Type
|
||||
|
||||
// Transformation function to apply after generating a node in the schema.
|
||||
fn func(s Schema) Schema
|
||||
// The root type for which the schema is being generated.
|
||||
root reflect.Type
|
||||
}
|
||||
|
||||
// The $defs block in a JSON schema cannot contain "/", otherwise it will not be
|
||||
|
@ -47,13 +41,19 @@ type constructor struct {
|
|||
//
|
||||
// For example:
|
||||
// {"a/b/c": "value"} is converted to {"a": {"b": {"c": "value"}}}
|
||||
func (c *constructor) nestedDefinitions() any {
|
||||
if len(c.definitions) == 0 {
|
||||
func (c *constructor) Definitions() any {
|
||||
defs := maps.Clone(c.definitions)
|
||||
|
||||
// Remove the root type from the definitions. No need to include it in the
|
||||
// definitions.
|
||||
delete(defs, typePath(c.root))
|
||||
|
||||
if len(defs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
res := make(map[string]any)
|
||||
for k, v := range c.definitions {
|
||||
for k, v := range defs {
|
||||
parts := strings.Split(k, "/")
|
||||
cur := res
|
||||
for i, p := range parts {
|
||||
|
@ -72,47 +72,74 @@ func (c *constructor) nestedDefinitions() any {
|
|||
return res
|
||||
}
|
||||
|
||||
// TODO: Skip generating schema for interface fields.
|
||||
func FromType(typ reflect.Type, fn func(s Schema) Schema) (Schema, error) {
|
||||
// FromType converts a reflect.Type to a jsonschema.Schema. Nodes in the final JSON
|
||||
// schema are guaranteed to be one level deep, which is done using defining $defs
|
||||
// for every Go type and referring them using $ref in the corresponding node in
|
||||
// the JSON schema.
|
||||
//
|
||||
// fns is a list of transformation functions that will be applied to all $defs
|
||||
// in the schema.
|
||||
func FromType(typ reflect.Type, fns []func(typ reflect.Type, s Schema) Schema) (Schema, error) {
|
||||
c := constructor{
|
||||
definitions: make(map[string]Schema),
|
||||
seen: make(map[string]struct{}),
|
||||
fn: fn,
|
||||
seen: make(map[string]reflect.Type),
|
||||
root: typ,
|
||||
}
|
||||
|
||||
err := c.walk(typ)
|
||||
if err != nil {
|
||||
return InvalidSchema, err
|
||||
return Schema{}, err
|
||||
}
|
||||
|
||||
for k, v := range c.definitions {
|
||||
for _, fn := range fns {
|
||||
c.definitions[k] = fn(c.seen[k], v)
|
||||
}
|
||||
}
|
||||
|
||||
res := c.definitions[typePath(typ)]
|
||||
// No need to include the root type in the definitions.
|
||||
delete(c.definitions, typePath(typ))
|
||||
res.Definitions = c.nestedDefinitions()
|
||||
res.Definitions = c.Definitions()
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// typePath computes a unique string representation of the type. $ref in the generated
|
||||
// JSON schema will refer to this path. See TestTypePath for examples outputs.
|
||||
func typePath(typ reflect.Type) string {
|
||||
// Pointers have a typ.Name() of "". Dereference them to get the underlying type.
|
||||
for typ.Kind() == reflect.Ptr {
|
||||
typ = typ.Elem()
|
||||
}
|
||||
|
||||
// typ.Name() resolves to "" for any type.
|
||||
if typ.Kind() == reflect.Interface {
|
||||
return "interface"
|
||||
}
|
||||
|
||||
// For built-in types, return the type name directly.
|
||||
if typ.PkgPath() == "" {
|
||||
return typ.Name()
|
||||
// Recursively call typePath, to handle slices of slices / maps.
|
||||
if typ.Kind() == reflect.Slice {
|
||||
return path.Join("slice", typePath(typ.Elem()))
|
||||
}
|
||||
|
||||
return strings.Join([]string{typ.PkgPath(), typ.Name()}, ".")
|
||||
if typ.Kind() == reflect.Map {
|
||||
if typ.Key().Kind() != reflect.String {
|
||||
panic(fmt.Sprintf("found map with non-string key: %v", typ.Key()))
|
||||
}
|
||||
|
||||
// Recursively call typePath, to handle maps of maps / slices.
|
||||
return path.Join("map", typePath(typ.Elem()))
|
||||
}
|
||||
|
||||
switch {
|
||||
case typ.PkgPath() != "" && typ.Name() != "":
|
||||
return typ.PkgPath() + "." + typ.Name()
|
||||
case typ.Name() != "":
|
||||
return typ.Name()
|
||||
default:
|
||||
panic("unexpected empty type name for type: " + typ.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: would a worked based model fit better here? Is this internal API not
|
||||
// the right fit?
|
||||
// Walk the Go type, generating $defs for every type encountered, and populating
|
||||
// the corresponding $ref in the JSON schema.
|
||||
func (c *constructor) walk(typ reflect.Type) error {
|
||||
// Dereference pointers if necessary.
|
||||
for typ.Kind() == reflect.Ptr {
|
||||
|
@ -121,10 +148,11 @@ func (c *constructor) walk(typ reflect.Type) error {
|
|||
|
||||
typPath := typePath(typ)
|
||||
|
||||
// Keep track of seen types to avoid infinite recursion.
|
||||
if _, ok := c.seen[typPath]; !ok {
|
||||
c.seen[typPath] = struct{}{}
|
||||
// Return early if the type has already been seen, to avoid infinite recursion.
|
||||
if _, ok := c.seen[typPath]; ok {
|
||||
return nil
|
||||
}
|
||||
c.seen[typPath] = typ
|
||||
|
||||
// Return early directly if it's already been processed.
|
||||
if _, ok := c.definitions[typPath]; ok {
|
||||
|
@ -134,7 +162,6 @@ func (c *constructor) walk(typ reflect.Type) error {
|
|||
var s Schema
|
||||
var err error
|
||||
|
||||
// TODO: Narrow / widen down the number of Go types handled here.
|
||||
switch typ.Kind() {
|
||||
case reflect.Struct:
|
||||
s, err = c.fromTypeStruct(typ)
|
||||
|
@ -142,20 +169,20 @@ func (c *constructor) walk(typ reflect.Type) error {
|
|||
s, err = c.fromTypeSlice(typ)
|
||||
case reflect.Map:
|
||||
s, err = c.fromTypeMap(typ)
|
||||
// TODO: Should the primitive functions below be inlined?
|
||||
case reflect.String:
|
||||
s = Schema{Type: StringType}
|
||||
case reflect.Bool:
|
||||
s = Schema{Type: BooleanType}
|
||||
// TODO: Add comment about reduced coverage of primitive Go types in the code paths here.
|
||||
case reflect.Int:
|
||||
case reflect.Int, reflect.Int32, reflect.Int64:
|
||||
s = Schema{Type: IntegerType}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
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}
|
||||
// Interface or any types are not serialized to JSON by the default JSON
|
||||
// unmarshaller (json.Unmarshal). They likely thus are parsed by the
|
||||
// dynamic configuration tree and we should support arbitary values here.
|
||||
// Eg: variables.default can be anything.
|
||||
s = Schema{}
|
||||
default:
|
||||
return fmt.Errorf("unsupported type: %s", typ.Kind())
|
||||
}
|
||||
|
@ -163,13 +190,7 @@ func (c *constructor) walk(typ reflect.Type) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if c.fn != nil {
|
||||
s = c.fn(s)
|
||||
}
|
||||
|
||||
// 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?
|
||||
// Store the computed JSON schema for the type.
|
||||
c.definitions[typPath] = s
|
||||
return nil
|
||||
}
|
||||
|
@ -206,20 +227,15 @@ func getStructFields(typ reflect.Type) []reflect.StructField {
|
|||
return fields
|
||||
}
|
||||
|
||||
// TODO: get rid of the errors here and panic instead?
|
||||
func (c *constructor) fromTypeStruct(typ reflect.Type) (Schema, error) {
|
||||
if typ.Kind() != reflect.Struct {
|
||||
return InvalidSchema, fmt.Errorf("expected struct, got %s", typ.Kind())
|
||||
return Schema{}, 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{},
|
||||
|
||||
Type: ObjectType,
|
||||
Properties: make(map[string]*Schema),
|
||||
Required: []string{},
|
||||
AdditionalProperties: false,
|
||||
}
|
||||
|
||||
|
@ -240,6 +256,7 @@ func (c *constructor) fromTypeStruct(typ reflect.Type) (Schema, error) {
|
|||
if jsonTags[0] == "" || jsonTags[0] == "-" || !structField.IsExported() {
|
||||
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.
|
||||
|
@ -247,19 +264,16 @@ func (c *constructor) fromTypeStruct(typ reflect.Type) (Schema, error) {
|
|||
res.Required = append(res.Required, jsonTags[0])
|
||||
}
|
||||
|
||||
// Walk the fields of the struct.
|
||||
typPath := typePath(structField.Type)
|
||||
// Only walk if the type has not been seen yet.
|
||||
if _, ok := c.seen[typPath]; !ok {
|
||||
// Trigger call to fromType, to recursively generate definitions for
|
||||
// the struct field.
|
||||
err := c.walk(structField.Type)
|
||||
if err != nil {
|
||||
return InvalidSchema, err
|
||||
}
|
||||
err := c.walk(structField.Type)
|
||||
if err != nil {
|
||||
return Schema{}, err
|
||||
}
|
||||
|
||||
// For every property in the struct, add a $ref to the corresponding
|
||||
// $defs block.
|
||||
refPath := path.Join("#/$defs", typPath)
|
||||
// For non-built-in types, refer to the definition.
|
||||
res.Properties[jsonTags[0]] = &Schema{
|
||||
Reference: &refPath,
|
||||
}
|
||||
|
@ -268,11 +282,9 @@ func (c *constructor) fromTypeStruct(typ reflect.Type) (Schema, error) {
|
|||
return res, nil
|
||||
}
|
||||
|
||||
// TODO: Add comments explaining the translation between struct, map, slice and
|
||||
// the JSON schema representation.
|
||||
func (c *constructor) fromTypeSlice(typ reflect.Type) (Schema, error) {
|
||||
if typ.Kind() != reflect.Slice {
|
||||
return InvalidSchema, fmt.Errorf("expected slice, got %s", typ.Kind())
|
||||
return Schema{}, fmt.Errorf("expected slice, got %s", typ.Kind())
|
||||
}
|
||||
|
||||
res := Schema{
|
||||
|
@ -280,19 +292,16 @@ func (c *constructor) fromTypeSlice(typ reflect.Type) (Schema, error) {
|
|||
}
|
||||
|
||||
typPath := typePath(typ.Elem())
|
||||
// Only walk if the type has not been seen yet.
|
||||
if _, ok := c.seen[typPath]; !ok {
|
||||
// Trigger call to fromType, to recursively generate definitions for
|
||||
// the slice element.
|
||||
err := c.walk(typ.Elem())
|
||||
if err != nil {
|
||||
return InvalidSchema, err
|
||||
}
|
||||
|
||||
// Walk the slice element type.
|
||||
err := c.walk(typ.Elem())
|
||||
if err != nil {
|
||||
return Schema{}, err
|
||||
}
|
||||
|
||||
refPath := path.Join("#/$defs", typPath)
|
||||
|
||||
// For non-built-in types, refer to the definition
|
||||
// Add a $ref to the corresponding $defs block for the slice element type.
|
||||
res.Items = &Schema{
|
||||
Reference: &refPath,
|
||||
}
|
||||
|
@ -301,11 +310,11 @@ func (c *constructor) fromTypeSlice(typ reflect.Type) (Schema, error) {
|
|||
|
||||
func (c *constructor) fromTypeMap(typ reflect.Type) (Schema, error) {
|
||||
if typ.Kind() != reflect.Map {
|
||||
return InvalidSchema, fmt.Errorf("expected map, got %s", typ.Kind())
|
||||
return Schema{}, 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())
|
||||
return Schema{}, fmt.Errorf("found map with non-string key: %v", typ.Key())
|
||||
}
|
||||
|
||||
res := Schema{
|
||||
|
@ -313,19 +322,16 @@ func (c *constructor) fromTypeMap(typ reflect.Type) (Schema, error) {
|
|||
}
|
||||
|
||||
typPath := typePath(typ.Elem())
|
||||
// Only walk if the type has not been seen yet.
|
||||
if _, ok := c.seen[typPath]; !ok {
|
||||
// Trigger call to fromType, to recursively generate definitions for
|
||||
// the map value.
|
||||
err := c.walk(typ.Elem())
|
||||
if err != nil {
|
||||
return InvalidSchema, err
|
||||
}
|
||||
|
||||
// Walk the map value type.
|
||||
err := c.walk(typ.Elem())
|
||||
if err != nil {
|
||||
return Schema{}, err
|
||||
}
|
||||
|
||||
refPath := path.Join("#/$defs", typPath)
|
||||
|
||||
// For non-built-in types, refer to the definition
|
||||
// Add a $ref to the corresponding $defs block for the map value type.
|
||||
res.AdditionalProperties = &Schema{
|
||||
Reference: &refPath,
|
||||
}
|
||||
|
|
|
@ -67,9 +67,7 @@ func TestFromTypeBasic(t *testing.T) {
|
|||
expected: Schema{
|
||||
Type: "object",
|
||||
Definitions: map[string]any{
|
||||
"interface": Schema{
|
||||
Type: "null",
|
||||
},
|
||||
"interface": Schema{},
|
||||
"string": Schema{
|
||||
Type: "string",
|
||||
},
|
||||
|
@ -160,6 +158,8 @@ func TestGetStructFields(t *testing.T) {
|
|||
assert.Equal(t, "B", fields[2].Name)
|
||||
}
|
||||
|
||||
// TODO: Add other case coverage for all the tests below
|
||||
|
||||
func TestFromTypeNested(t *testing.T) {
|
||||
type Inner struct {
|
||||
S string `json:"s"`
|
||||
|
@ -222,7 +222,7 @@ func TestFromTypeNested(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "struct as a map value",
|
||||
typ: reflect.TypeOf(map[string]Inner{}),
|
||||
typ: reflect.TypeOf(map[string]*Inner{}),
|
||||
expected: Schema{
|
||||
Type: "object",
|
||||
Definitions: expectedDefinitions,
|
||||
|
@ -252,7 +252,6 @@ func TestFromTypeNested(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Call out in the PR description that recursive Go types are supported.
|
||||
func TestFromTypeRecursive(t *testing.T) {
|
||||
fooRef := "#/$defs/github.com/databricks/cli/libs/jsonschema/test_types.Foo"
|
||||
barRef := "#/$defs/github.com/databricks/cli/libs/jsonschema/test_types.Bar"
|
||||
|
@ -353,8 +352,76 @@ func TestFromTypeSelfReferential(t *testing.T) {
|
|||
assert.Equal(t, expected, s)
|
||||
}
|
||||
|
||||
// TODO: Add coverage for all errors returned by FromType.
|
||||
func TestFromTypeError(t *testing.T) {
|
||||
type mapOfInts map[int]int
|
||||
_, err := FromType(reflect.TypeOf(mapOfInts{}), nil)
|
||||
assert.EqualError(t, err, "found map with non-string key: int")
|
||||
|
||||
assert.PanicsWithValue(t, "found map with non-string key: int", func() {
|
||||
FromType(reflect.TypeOf(mapOfInts{}), nil)
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Add test that the fn argument ot from_type works as expected.
|
||||
|
||||
func TestTypePath(t *testing.T) {
|
||||
type myStruct struct{}
|
||||
|
||||
tcases := []struct {
|
||||
typ reflect.Type
|
||||
path string
|
||||
}{
|
||||
{
|
||||
typ: reflect.TypeOf(""),
|
||||
path: "string",
|
||||
},
|
||||
{
|
||||
typ: reflect.TypeOf(int(0)),
|
||||
path: "int",
|
||||
},
|
||||
{
|
||||
typ: reflect.TypeOf(true),
|
||||
path: "bool",
|
||||
},
|
||||
{
|
||||
typ: reflect.TypeOf(float64(0)),
|
||||
path: "float64",
|
||||
},
|
||||
{
|
||||
typ: reflect.TypeOf(myStruct{}),
|
||||
path: "github.com/databricks/cli/libs/jsonschema.myStruct",
|
||||
},
|
||||
{
|
||||
typ: reflect.TypeOf([]int{}),
|
||||
path: "slice/int",
|
||||
},
|
||||
{
|
||||
typ: reflect.TypeOf(map[string]int{}),
|
||||
path: "map/int",
|
||||
},
|
||||
{
|
||||
typ: reflect.TypeOf([]myStruct{}),
|
||||
path: "slice/github.com/databricks/cli/libs/jsonschema.myStruct",
|
||||
},
|
||||
{
|
||||
typ: reflect.TypeOf([][]map[string]map[string]myStruct{}),
|
||||
path: "slice/slice/map/map/github.com/databricks/cli/libs/jsonschema.myStruct",
|
||||
},
|
||||
{
|
||||
typ: reflect.TypeOf(map[string]myStruct{}),
|
||||
path: "map/github.com/databricks/cli/libs/jsonschema.myStruct",
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: support arbitary depth of maps and slices. Also add validation
|
||||
// in this function that non-string keys are not allowed.
|
||||
for _, tc := range tcases {
|
||||
t.Run(tc.typ.String(), func(t *testing.T) {
|
||||
assert.Equal(t, tc.path, typePath(tc.typ))
|
||||
})
|
||||
}
|
||||
|
||||
// Maps with non-string keys should panic.
|
||||
assert.PanicsWithValue(t, "found map with non-string key: int", func() {
|
||||
typePath(reflect.TypeOf(map[int]int{}))
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue