mirror of https://github.com/databricks/cli.git
Add JSON schema validation for input template parameters (#598)
## Changes This PR: 1. Adds code for reading template configs and validating them against a JSON schema. 2. Moves the json schema struct in `bundle/schema` to a separate library package. This struct is now reused for validating template configs. ## Tests Unit tests
This commit is contained in:
parent
fc8729d162
commit
5df8935de4
|
@ -8,6 +8,7 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/databricks/cli/bundle/config"
|
"github.com/databricks/cli/bundle/config"
|
||||||
|
"github.com/databricks/cli/libs/jsonschema"
|
||||||
"github.com/databricks/databricks-sdk-go/openapi"
|
"github.com/databricks/databricks-sdk-go/openapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -39,7 +40,7 @@ func BundleDocs(openapiSpecPath string) (*Docs, error) {
|
||||||
}
|
}
|
||||||
openapiReader := &OpenapiReader{
|
openapiReader := &OpenapiReader{
|
||||||
OpenapiSpec: spec,
|
OpenapiSpec: spec,
|
||||||
Memo: make(map[string]*Schema),
|
Memo: make(map[string]*jsonschema.Schema),
|
||||||
}
|
}
|
||||||
resourcesDocs, err := openapiReader.ResourcesDocs()
|
resourcesDocs, err := openapiReader.ResourcesDocs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -88,22 +89,22 @@ func initializeBundleDocs() (*Docs, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// *Docs are a subset of *Schema, this function selects that subset
|
// *Docs are a subset of *Schema, this function selects that subset
|
||||||
func schemaToDocs(schema *Schema) *Docs {
|
func schemaToDocs(jsonSchema *jsonschema.Schema) *Docs {
|
||||||
// terminate recursion if schema is nil
|
// terminate recursion if schema is nil
|
||||||
if schema == nil {
|
if jsonSchema == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
docs := &Docs{
|
docs := &Docs{
|
||||||
Description: schema.Description,
|
Description: jsonSchema.Description,
|
||||||
}
|
}
|
||||||
if len(schema.Properties) > 0 {
|
if len(jsonSchema.Properties) > 0 {
|
||||||
docs.Properties = make(map[string]*Docs)
|
docs.Properties = make(map[string]*Docs)
|
||||||
}
|
}
|
||||||
for k, v := range schema.Properties {
|
for k, v := range jsonSchema.Properties {
|
||||||
docs.Properties[k] = schemaToDocs(v)
|
docs.Properties[k] = schemaToDocs(v)
|
||||||
}
|
}
|
||||||
docs.Items = schemaToDocs(schema.Items)
|
docs.Items = schemaToDocs(jsonSchema.Items)
|
||||||
if additionalProperties, ok := schema.AdditionalProperties.(*Schema); ok {
|
if additionalProperties, ok := jsonSchema.AdditionalProperties.(*jsonschema.Schema); ok {
|
||||||
docs.AdditionalProperties = schemaToDocs(additionalProperties)
|
docs.AdditionalProperties = schemaToDocs(additionalProperties)
|
||||||
}
|
}
|
||||||
return docs
|
return docs
|
||||||
|
|
|
@ -4,30 +4,31 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/libs/jsonschema"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSchemaToDocs(t *testing.T) {
|
func TestSchemaToDocs(t *testing.T) {
|
||||||
schema := &Schema{
|
jsonSchema := &jsonschema.Schema{
|
||||||
Type: "object",
|
Type: "object",
|
||||||
Description: "root doc",
|
Description: "root doc",
|
||||||
Properties: map[string]*Schema{
|
Properties: map[string]*jsonschema.Schema{
|
||||||
"foo": {Type: "number", Description: "foo doc"},
|
"foo": {Type: "number", Description: "foo doc"},
|
||||||
"bar": {Type: "string"},
|
"bar": {Type: "string"},
|
||||||
"octave": {
|
"octave": {
|
||||||
Type: "object",
|
Type: "object",
|
||||||
AdditionalProperties: &Schema{Type: "number"},
|
AdditionalProperties: &jsonschema.Schema{Type: "number"},
|
||||||
Description: "octave docs",
|
Description: "octave docs",
|
||||||
},
|
},
|
||||||
"scales": {
|
"scales": {
|
||||||
Type: "object",
|
Type: "object",
|
||||||
Description: "scale docs",
|
Description: "scale docs",
|
||||||
Items: &Schema{Type: "string"},
|
Items: &jsonschema.Schema{Type: "string"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
docs := schemaToDocs(schema)
|
docs := schemaToDocs(jsonSchema)
|
||||||
docsJson, err := json.MarshalIndent(docs, " ", " ")
|
docsJson, err := json.MarshalIndent(docs, " ", " ")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
|
|
@ -5,17 +5,18 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/libs/jsonschema"
|
||||||
"github.com/databricks/databricks-sdk-go/openapi"
|
"github.com/databricks/databricks-sdk-go/openapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OpenapiReader struct {
|
type OpenapiReader struct {
|
||||||
OpenapiSpec *openapi.Specification
|
OpenapiSpec *openapi.Specification
|
||||||
Memo map[string]*Schema
|
Memo map[string]*jsonschema.Schema
|
||||||
}
|
}
|
||||||
|
|
||||||
const SchemaPathPrefix = "#/components/schemas/"
|
const SchemaPathPrefix = "#/components/schemas/"
|
||||||
|
|
||||||
func (reader *OpenapiReader) readOpenapiSchema(path string) (*Schema, error) {
|
func (reader *OpenapiReader) readOpenapiSchema(path string) (*jsonschema.Schema, error) {
|
||||||
schemaKey := strings.TrimPrefix(path, SchemaPathPrefix)
|
schemaKey := strings.TrimPrefix(path, SchemaPathPrefix)
|
||||||
|
|
||||||
// return early if we already have a computed schema
|
// return early if we already have a computed schema
|
||||||
|
@ -35,7 +36,7 @@ func (reader *OpenapiReader) readOpenapiSchema(path string) (*Schema, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
jsonSchema := &Schema{}
|
jsonSchema := &jsonschema.Schema{}
|
||||||
err = json.Unmarshal(bytes, jsonSchema)
|
err = json.Unmarshal(bytes, jsonSchema)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -50,7 +51,7 @@ func (reader *OpenapiReader) readOpenapiSchema(path string) (*Schema, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
additionalProperties := &Schema{}
|
additionalProperties := &jsonschema.Schema{}
|
||||||
err = json.Unmarshal(b, additionalProperties)
|
err = json.Unmarshal(b, additionalProperties)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -65,7 +66,7 @@ func (reader *OpenapiReader) readOpenapiSchema(path string) (*Schema, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// safe againt loops in refs
|
// safe againt loops in refs
|
||||||
func (reader *OpenapiReader) safeResolveRefs(root *Schema, tracker *tracker) (*Schema, error) {
|
func (reader *OpenapiReader) safeResolveRefs(root *jsonschema.Schema, tracker *tracker) (*jsonschema.Schema, error) {
|
||||||
if root.Reference == nil {
|
if root.Reference == nil {
|
||||||
return reader.traverseSchema(root, tracker)
|
return reader.traverseSchema(root, tracker)
|
||||||
}
|
}
|
||||||
|
@ -100,9 +101,9 @@ func (reader *OpenapiReader) safeResolveRefs(root *Schema, tracker *tracker) (*S
|
||||||
return root, err
|
return root, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (reader *OpenapiReader) traverseSchema(root *Schema, tracker *tracker) (*Schema, error) {
|
func (reader *OpenapiReader) traverseSchema(root *jsonschema.Schema, tracker *tracker) (*jsonschema.Schema, error) {
|
||||||
// case primitive (or invalid)
|
// case primitive (or invalid)
|
||||||
if root.Type != Object && root.Type != Array {
|
if root.Type != jsonschema.ObjectType && root.Type != jsonschema.ArrayType {
|
||||||
return root, nil
|
return root, nil
|
||||||
}
|
}
|
||||||
// only root references are resolved
|
// only root references are resolved
|
||||||
|
@ -128,9 +129,9 @@ func (reader *OpenapiReader) traverseSchema(root *Schema, tracker *tracker) (*Sc
|
||||||
root.Items = itemsSchema
|
root.Items = itemsSchema
|
||||||
}
|
}
|
||||||
// case map
|
// case map
|
||||||
additionionalProperties, ok := root.AdditionalProperties.(*Schema)
|
additionalProperties, ok := root.AdditionalProperties.(*jsonschema.Schema)
|
||||||
if ok && additionionalProperties != nil {
|
if ok && additionalProperties != nil {
|
||||||
valueSchema, err := reader.safeResolveRefs(additionionalProperties, tracker)
|
valueSchema, err := reader.safeResolveRefs(additionalProperties, tracker)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -139,7 +140,7 @@ func (reader *OpenapiReader) traverseSchema(root *Schema, tracker *tracker) (*Sc
|
||||||
return root, nil
|
return root, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (reader *OpenapiReader) readResolvedSchema(path string) (*Schema, error) {
|
func (reader *OpenapiReader) readResolvedSchema(path string) (*jsonschema.Schema, error) {
|
||||||
root, err := reader.readOpenapiSchema(path)
|
root, err := reader.readOpenapiSchema(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/libs/jsonschema"
|
||||||
"github.com/databricks/databricks-sdk-go/openapi"
|
"github.com/databricks/databricks-sdk-go/openapi"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
@ -47,7 +48,7 @@ func TestReadSchemaForObject(t *testing.T) {
|
||||||
spec := &openapi.Specification{}
|
spec := &openapi.Specification{}
|
||||||
reader := &OpenapiReader{
|
reader := &OpenapiReader{
|
||||||
OpenapiSpec: spec,
|
OpenapiSpec: spec,
|
||||||
Memo: make(map[string]*Schema),
|
Memo: make(map[string]*jsonschema.Schema),
|
||||||
}
|
}
|
||||||
err := json.Unmarshal([]byte(specString), spec)
|
err := json.Unmarshal([]byte(specString), spec)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -105,7 +106,7 @@ func TestReadSchemaForArray(t *testing.T) {
|
||||||
spec := &openapi.Specification{}
|
spec := &openapi.Specification{}
|
||||||
reader := &OpenapiReader{
|
reader := &OpenapiReader{
|
||||||
OpenapiSpec: spec,
|
OpenapiSpec: spec,
|
||||||
Memo: make(map[string]*Schema),
|
Memo: make(map[string]*jsonschema.Schema),
|
||||||
}
|
}
|
||||||
err := json.Unmarshal([]byte(specString), spec)
|
err := json.Unmarshal([]byte(specString), spec)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -151,7 +152,7 @@ func TestReadSchemaForMap(t *testing.T) {
|
||||||
spec := &openapi.Specification{}
|
spec := &openapi.Specification{}
|
||||||
reader := &OpenapiReader{
|
reader := &OpenapiReader{
|
||||||
OpenapiSpec: spec,
|
OpenapiSpec: spec,
|
||||||
Memo: make(map[string]*Schema),
|
Memo: make(map[string]*jsonschema.Schema),
|
||||||
}
|
}
|
||||||
err := json.Unmarshal([]byte(specString), spec)
|
err := json.Unmarshal([]byte(specString), spec)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -200,7 +201,7 @@ func TestRootReferenceIsResolved(t *testing.T) {
|
||||||
spec := &openapi.Specification{}
|
spec := &openapi.Specification{}
|
||||||
reader := &OpenapiReader{
|
reader := &OpenapiReader{
|
||||||
OpenapiSpec: spec,
|
OpenapiSpec: spec,
|
||||||
Memo: make(map[string]*Schema),
|
Memo: make(map[string]*jsonschema.Schema),
|
||||||
}
|
}
|
||||||
err := json.Unmarshal([]byte(specString), spec)
|
err := json.Unmarshal([]byte(specString), spec)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -250,7 +251,7 @@ func TestSelfReferenceLoopErrors(t *testing.T) {
|
||||||
spec := &openapi.Specification{}
|
spec := &openapi.Specification{}
|
||||||
reader := &OpenapiReader{
|
reader := &OpenapiReader{
|
||||||
OpenapiSpec: spec,
|
OpenapiSpec: spec,
|
||||||
Memo: make(map[string]*Schema),
|
Memo: make(map[string]*jsonschema.Schema),
|
||||||
}
|
}
|
||||||
err := json.Unmarshal([]byte(specString), spec)
|
err := json.Unmarshal([]byte(specString), spec)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -284,7 +285,7 @@ func TestCrossReferenceLoopErrors(t *testing.T) {
|
||||||
spec := &openapi.Specification{}
|
spec := &openapi.Specification{}
|
||||||
reader := &OpenapiReader{
|
reader := &OpenapiReader{
|
||||||
OpenapiSpec: spec,
|
OpenapiSpec: spec,
|
||||||
Memo: make(map[string]*Schema),
|
Memo: make(map[string]*jsonschema.Schema),
|
||||||
}
|
}
|
||||||
err := json.Unmarshal([]byte(specString), spec)
|
err := json.Unmarshal([]byte(specString), spec)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -329,7 +330,7 @@ func TestReferenceResolutionForMapInObject(t *testing.T) {
|
||||||
spec := &openapi.Specification{}
|
spec := &openapi.Specification{}
|
||||||
reader := &OpenapiReader{
|
reader := &OpenapiReader{
|
||||||
OpenapiSpec: spec,
|
OpenapiSpec: spec,
|
||||||
Memo: make(map[string]*Schema),
|
Memo: make(map[string]*jsonschema.Schema),
|
||||||
}
|
}
|
||||||
err := json.Unmarshal([]byte(specString), spec)
|
err := json.Unmarshal([]byte(specString), spec)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -399,7 +400,7 @@ func TestReferenceResolutionForArrayInObject(t *testing.T) {
|
||||||
spec := &openapi.Specification{}
|
spec := &openapi.Specification{}
|
||||||
reader := &OpenapiReader{
|
reader := &OpenapiReader{
|
||||||
OpenapiSpec: spec,
|
OpenapiSpec: spec,
|
||||||
Memo: make(map[string]*Schema),
|
Memo: make(map[string]*jsonschema.Schema),
|
||||||
}
|
}
|
||||||
err := json.Unmarshal([]byte(specString), spec)
|
err := json.Unmarshal([]byte(specString), spec)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
@ -5,41 +5,10 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/libs/jsonschema"
|
||||||
)
|
)
|
||||||
|
|
||||||
// defines schema for a json object
|
|
||||||
type Schema struct {
|
|
||||||
// Type of the object
|
|
||||||
Type JavascriptType `json:"type,omitempty"`
|
|
||||||
|
|
||||||
// Description of the object. This is rendered as inline documentation in the
|
|
||||||
// IDE. This is manually injected here using schema.Docs
|
|
||||||
Description string `json:"description,omitempty"`
|
|
||||||
|
|
||||||
// Schemas for the fields of an struct. The keys are the first json tag.
|
|
||||||
// The values are the schema for the type of the field
|
|
||||||
Properties map[string]*Schema `json:"properties,omitempty"`
|
|
||||||
|
|
||||||
// The schema for all values of an array
|
|
||||||
Items *Schema `json:"items,omitempty"`
|
|
||||||
|
|
||||||
// The schema for any properties not mentioned in the Schema.Properties field.
|
|
||||||
// this validates maps[string]any in bundle configuration
|
|
||||||
// OR
|
|
||||||
// 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
|
|
||||||
//
|
|
||||||
// Its type during runtime will either be *Schema or bool
|
|
||||||
AdditionalProperties any `json:"additionalProperties,omitempty"`
|
|
||||||
|
|
||||||
// Required properties for the object. Any fields missing the "omitempty"
|
|
||||||
// json tag will be included
|
|
||||||
Required []string `json:"required,omitempty"`
|
|
||||||
|
|
||||||
// URI to a json schema
|
|
||||||
Reference *string `json:"$ref,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
||||||
//
|
//
|
||||||
|
@ -61,7 +30,7 @@ type Schema struct {
|
||||||
//
|
//
|
||||||
// - []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) (*Schema, error) {
|
func New(golangType reflect.Type, docs *Docs) (*jsonschema.Schema, error) {
|
||||||
tracker := newTracker()
|
tracker := newTracker()
|
||||||
schema, err := safeToSchema(golangType, docs, "", tracker)
|
schema, err := safeToSchema(golangType, docs, "", tracker)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -70,39 +39,28 @@ func New(golangType reflect.Type, docs *Docs) (*Schema, error) {
|
||||||
return schema, nil
|
return schema, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type JavascriptType string
|
func jsonSchemaType(golangType reflect.Type) (jsonschema.Type, error) {
|
||||||
|
|
||||||
const (
|
|
||||||
Invalid JavascriptType = "invalid"
|
|
||||||
Boolean JavascriptType = "boolean"
|
|
||||||
String JavascriptType = "string"
|
|
||||||
Number JavascriptType = "number"
|
|
||||||
Object JavascriptType = "object"
|
|
||||||
Array JavascriptType = "array"
|
|
||||||
)
|
|
||||||
|
|
||||||
func javascriptType(golangType reflect.Type) (JavascriptType, error) {
|
|
||||||
switch golangType.Kind() {
|
switch golangType.Kind() {
|
||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
return Boolean, nil
|
return jsonschema.BooleanType, nil
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
return String, 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 Number, nil
|
return jsonschema.NumberType, nil
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
return Object, nil
|
return jsonschema.ObjectType, nil
|
||||||
case reflect.Map:
|
case reflect.Map:
|
||||||
if golangType.Key().Kind() != reflect.String {
|
if golangType.Key().Kind() != reflect.String {
|
||||||
return Invalid, 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 Object, nil
|
return jsonschema.ObjectType, nil
|
||||||
case reflect.Array, reflect.Slice:
|
case reflect.Array, reflect.Slice:
|
||||||
return Array, nil
|
return jsonschema.ArrayType, nil
|
||||||
default:
|
default:
|
||||||
return Invalid, fmt.Errorf("unhandled golang type: %s", golangType)
|
return jsonschema.InvalidType, fmt.Errorf("unhandled golang type: %s", golangType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,7 +79,7 @@ func javascriptType(golangType reflect.Type) (JavascriptType, 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) (*Schema, error) {
|
func safeToSchema(golangType reflect.Type, docs *Docs, traceId string, tracker *tracker) (*jsonschema.Schema, error) {
|
||||||
// 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
|
||||||
|
@ -174,29 +132,29 @@ func getStructFields(golangType reflect.Type) []reflect.StructField {
|
||||||
return fields
|
return fields
|
||||||
}
|
}
|
||||||
|
|
||||||
func toSchema(golangType reflect.Type, docs *Docs, tracker *tracker) (*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 &Schema{}, nil
|
return &jsonschema.Schema{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
rootJavascriptType, err := javascriptType(golangType)
|
rootJavascriptType, err := jsonSchemaType(golangType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
schema := &Schema{Type: rootJavascriptType}
|
jsonSchema := &jsonschema.Schema{Type: rootJavascriptType}
|
||||||
|
|
||||||
if docs != nil {
|
if docs != nil {
|
||||||
schema.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 := javascriptType(elemGolangType)
|
elemJavascriptType, err := jsonSchemaType(elemGolangType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -208,7 +166,7 @@ func toSchema(golangType reflect.Type, docs *Docs, tracker *tracker) (*Schema, e
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
schema.Items = &Schema{
|
jsonSchema.Items = &jsonschema.Schema{
|
||||||
Type: elemJavascriptType,
|
Type: elemJavascriptType,
|
||||||
Properties: elemProps.Properties,
|
Properties: elemProps.Properties,
|
||||||
AdditionalProperties: elemProps.AdditionalProperties,
|
AdditionalProperties: elemProps.AdditionalProperties,
|
||||||
|
@ -226,7 +184,7 @@ func toSchema(golangType reflect.Type, docs *Docs, tracker *tracker) (*Schema, e
|
||||||
if docs != nil {
|
if docs != nil {
|
||||||
childDocs = docs.AdditionalProperties
|
childDocs = docs.AdditionalProperties
|
||||||
}
|
}
|
||||||
schema.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
|
||||||
}
|
}
|
||||||
|
@ -235,7 +193,7 @@ func toSchema(golangType reflect.Type, docs *Docs, tracker *tracker) (*Schema, e
|
||||||
// case struct
|
// case struct
|
||||||
if golangType.Kind() == reflect.Struct {
|
if golangType.Kind() == reflect.Struct {
|
||||||
children := getStructFields(golangType)
|
children := getStructFields(golangType)
|
||||||
properties := map[string]*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")
|
||||||
|
@ -281,10 +239,10 @@ func toSchema(golangType reflect.Type, docs *Docs, tracker *tracker) (*Schema, e
|
||||||
properties[childName] = fieldProps
|
properties[childName] = fieldProps
|
||||||
}
|
}
|
||||||
|
|
||||||
schema.AdditionalProperties = false
|
jsonSchema.AdditionalProperties = false
|
||||||
schema.Properties = properties
|
jsonSchema.Properties = properties
|
||||||
schema.Required = required
|
jsonSchema.Required = required
|
||||||
}
|
}
|
||||||
|
|
||||||
return schema, nil
|
return jsonSchema, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
package jsonschema
|
||||||
|
|
||||||
|
// defines schema for a json object
|
||||||
|
type Schema struct {
|
||||||
|
// Type of the object
|
||||||
|
Type Type `json:"type,omitempty"`
|
||||||
|
|
||||||
|
// Description of the object. This is rendered as inline documentation in the
|
||||||
|
// IDE. This is manually injected here using schema.Docs
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
|
||||||
|
// Schemas for the fields of an struct. The keys are the first json tag.
|
||||||
|
// The values are the schema for the type of the field
|
||||||
|
Properties map[string]*Schema `json:"properties,omitempty"`
|
||||||
|
|
||||||
|
// The schema for all values of an array
|
||||||
|
Items *Schema `json:"items,omitempty"`
|
||||||
|
|
||||||
|
// The schema for any properties not mentioned in the Schema.Properties field.
|
||||||
|
// this validates maps[string]any in bundle configuration
|
||||||
|
// OR
|
||||||
|
// 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
|
||||||
|
//
|
||||||
|
// Its type during runtime will either be *Schema or bool
|
||||||
|
AdditionalProperties any `json:"additionalProperties,omitempty"`
|
||||||
|
|
||||||
|
// Required properties for the object. Any fields missing the "omitempty"
|
||||||
|
// json tag will be included
|
||||||
|
Required []string `json:"required,omitempty"`
|
||||||
|
|
||||||
|
// URI to a json schema
|
||||||
|
Reference *string `json:"$ref,omitempty"`
|
||||||
|
|
||||||
|
// Default value for the property / object
|
||||||
|
Default any `json:"default,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Type string
|
||||||
|
|
||||||
|
const (
|
||||||
|
InvalidType Type = "invalid"
|
||||||
|
BooleanType Type = "boolean"
|
||||||
|
StringType Type = "string"
|
||||||
|
NumberType Type = "number"
|
||||||
|
ObjectType Type = "object"
|
||||||
|
ArrayType Type = "array"
|
||||||
|
IntegerType Type = "integer"
|
||||||
|
)
|
|
@ -0,0 +1,121 @@
|
||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/libs/jsonschema"
|
||||||
|
)
|
||||||
|
|
||||||
|
// function to check whether a float value represents an integer
|
||||||
|
func isIntegerValue(v float64) bool {
|
||||||
|
return v == float64(int(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// cast value to integer for config values that are floats but are supposed to be
|
||||||
|
// integers according to the schema
|
||||||
|
//
|
||||||
|
// Needed because the default json unmarshaler for maps converts all numbers to floats
|
||||||
|
func castFloatConfigValuesToInt(config map[string]any, jsonSchema *jsonschema.Schema) error {
|
||||||
|
for k, v := range config {
|
||||||
|
// error because all config keys should be defined in schema too
|
||||||
|
fieldInfo, ok := jsonSchema.Properties[k]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("%s is not defined as an input parameter for the template", k)
|
||||||
|
}
|
||||||
|
// skip non integer fields
|
||||||
|
if fieldInfo.Type != jsonschema.IntegerType {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert floating point type values to integer
|
||||||
|
switch floatVal := v.(type) {
|
||||||
|
case float32:
|
||||||
|
if !isIntegerValue(float64(floatVal)) {
|
||||||
|
return fmt.Errorf("expected %s to have integer value but it is %v", k, v)
|
||||||
|
}
|
||||||
|
config[k] = int(floatVal)
|
||||||
|
case float64:
|
||||||
|
if !isIntegerValue(floatVal) {
|
||||||
|
return fmt.Errorf("expected %s to have integer value but it is %v", k, v)
|
||||||
|
}
|
||||||
|
config[k] = int(floatVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func assignDefaultConfigValues(config map[string]any, schema *jsonschema.Schema) error {
|
||||||
|
for k, v := range schema.Properties {
|
||||||
|
if _, ok := config[k]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if v.Default == nil {
|
||||||
|
return fmt.Errorf("input parameter %s is not defined in config", k)
|
||||||
|
}
|
||||||
|
config[k] = v.Default
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateConfigValueTypes(config map[string]any, schema *jsonschema.Schema) error {
|
||||||
|
// validate types defined in config
|
||||||
|
for k, v := range config {
|
||||||
|
fieldInfo, ok := schema.Properties[k]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("%s is not defined as an input parameter for the template", k)
|
||||||
|
}
|
||||||
|
err := validateType(v, fieldInfo.Type)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("incorrect type for %s. %w", k, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadSchema(path string) (*jsonschema.Schema, error) {
|
||||||
|
schemaBytes, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
schema := &jsonschema.Schema{}
|
||||||
|
err = json.Unmarshal(schemaBytes, schema)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return schema, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadConfig(path string, jsonSchema *jsonschema.Schema) (map[string]any, error) {
|
||||||
|
// Read config file
|
||||||
|
var config map[string]any
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(b, &config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign default value to any fields that do not have a value yet
|
||||||
|
err = assignDefaultConfigValues(config, jsonSchema)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// cast any fields that are supposed to be integers. The json unmarshalling
|
||||||
|
// for a generic map converts all numbers to floating point
|
||||||
|
err = castFloatConfigValuesToInt(config, jsonSchema)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate config according to schema
|
||||||
|
err = validateConfigValueTypes(config, jsonSchema)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return config, nil
|
||||||
|
}
|
|
@ -0,0 +1,274 @@
|
||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/libs/jsonschema"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testSchema(t *testing.T) *jsonschema.Schema {
|
||||||
|
schemaJson := `{
|
||||||
|
"properties": {
|
||||||
|
"int_val": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"float_val": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"bool_val": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"string_val": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
var jsonSchema jsonschema.Schema
|
||||||
|
err := json.Unmarshal([]byte(schemaJson), &jsonSchema)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return &jsonSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplateSchemaIsInteger(t *testing.T) {
|
||||||
|
assert.False(t, isIntegerValue(1.1))
|
||||||
|
assert.False(t, isIntegerValue(0.1))
|
||||||
|
assert.False(t, isIntegerValue(-0.1))
|
||||||
|
|
||||||
|
assert.True(t, isIntegerValue(-1.0))
|
||||||
|
assert.True(t, isIntegerValue(0.0))
|
||||||
|
assert.True(t, isIntegerValue(2.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplateSchemaCastFloatToInt(t *testing.T) {
|
||||||
|
// define schema for config
|
||||||
|
jsonSchema := testSchema(t)
|
||||||
|
|
||||||
|
// define the config
|
||||||
|
configJson := `{
|
||||||
|
"int_val": 1,
|
||||||
|
"float_val": 2,
|
||||||
|
"bool_val": true,
|
||||||
|
"string_val": "main hoon na"
|
||||||
|
}`
|
||||||
|
var config map[string]any
|
||||||
|
err := json.Unmarshal([]byte(configJson), &config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// assert types before casting, checking that the integer was indeed loaded
|
||||||
|
// as a floating point
|
||||||
|
assert.IsType(t, float64(0), config["int_val"])
|
||||||
|
assert.IsType(t, float64(0), config["float_val"])
|
||||||
|
assert.IsType(t, true, config["bool_val"])
|
||||||
|
assert.IsType(t, "abc", config["string_val"])
|
||||||
|
|
||||||
|
err = castFloatConfigValuesToInt(config, jsonSchema)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// assert type after casting, that the float value was converted to an integer
|
||||||
|
// for int_val.
|
||||||
|
assert.IsType(t, int(0), config["int_val"])
|
||||||
|
assert.IsType(t, float64(0), config["float_val"])
|
||||||
|
assert.IsType(t, true, config["bool_val"])
|
||||||
|
assert.IsType(t, "abc", config["string_val"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplateSchemaCastFloatToIntFailsForUnknownTypes(t *testing.T) {
|
||||||
|
// define schema for config
|
||||||
|
schemaJson := `{
|
||||||
|
"properties": {
|
||||||
|
"foo": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
var jsonSchema jsonschema.Schema
|
||||||
|
err := json.Unmarshal([]byte(schemaJson), &jsonSchema)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// define the config
|
||||||
|
configJson := `{
|
||||||
|
"bar": true
|
||||||
|
}`
|
||||||
|
var config map[string]any
|
||||||
|
err = json.Unmarshal([]byte(configJson), &config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = castFloatConfigValuesToInt(config, &jsonSchema)
|
||||||
|
assert.ErrorContains(t, err, "bar is not defined as an input parameter for the template")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplateSchemaCastFloatToIntFailsWhenWithNonIntValues(t *testing.T) {
|
||||||
|
// define schema for config
|
||||||
|
schemaJson := `{
|
||||||
|
"properties": {
|
||||||
|
"foo": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
var jsonSchema jsonschema.Schema
|
||||||
|
err := json.Unmarshal([]byte(schemaJson), &jsonSchema)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// define the config
|
||||||
|
configJson := `{
|
||||||
|
"foo": 1.1
|
||||||
|
}`
|
||||||
|
var config map[string]any
|
||||||
|
err = json.Unmarshal([]byte(configJson), &config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = castFloatConfigValuesToInt(config, &jsonSchema)
|
||||||
|
assert.ErrorContains(t, err, "expected foo to have integer value but it is 1.1")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplateSchemaValidateType(t *testing.T) {
|
||||||
|
// assert validation passing
|
||||||
|
err := validateType(int(0), jsonschema.IntegerType)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = validateType(int32(1), jsonschema.IntegerType)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = validateType(int64(1), jsonschema.IntegerType)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = validateType(float32(1.1), jsonschema.NumberType)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = validateType(float64(1.2), jsonschema.NumberType)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = validateType(int(1), jsonschema.NumberType)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = validateType(false, jsonschema.BooleanType)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = validateType("abc", jsonschema.StringType)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// assert validation failing for integers
|
||||||
|
err = validateType(float64(1.2), jsonschema.IntegerType)
|
||||||
|
assert.ErrorContains(t, err, "expected type integer, but value is 1.2")
|
||||||
|
err = validateType(true, jsonschema.IntegerType)
|
||||||
|
assert.ErrorContains(t, err, "expected type integer, but value is true")
|
||||||
|
err = validateType("abc", jsonschema.IntegerType)
|
||||||
|
assert.ErrorContains(t, err, "expected type integer, but value is \"abc\"")
|
||||||
|
|
||||||
|
// assert validation failing for floats
|
||||||
|
err = validateType(true, jsonschema.NumberType)
|
||||||
|
assert.ErrorContains(t, err, "expected type float, but value is true")
|
||||||
|
err = validateType("abc", jsonschema.NumberType)
|
||||||
|
assert.ErrorContains(t, err, "expected type float, but value is \"abc\"")
|
||||||
|
|
||||||
|
// assert validation failing for boolean
|
||||||
|
err = validateType(int(1), jsonschema.BooleanType)
|
||||||
|
assert.ErrorContains(t, err, "expected type boolean, but value is 1")
|
||||||
|
err = validateType(float64(1), jsonschema.BooleanType)
|
||||||
|
assert.ErrorContains(t, err, "expected type boolean, but value is 1")
|
||||||
|
err = validateType("abc", jsonschema.BooleanType)
|
||||||
|
assert.ErrorContains(t, err, "expected type boolean, but value is \"abc\"")
|
||||||
|
|
||||||
|
// assert validation failing for string
|
||||||
|
err = validateType(int(1), jsonschema.StringType)
|
||||||
|
assert.ErrorContains(t, err, "expected type string, but value is 1")
|
||||||
|
err = validateType(float64(1), jsonschema.StringType)
|
||||||
|
assert.ErrorContains(t, err, "expected type string, but value is 1")
|
||||||
|
err = validateType(false, jsonschema.StringType)
|
||||||
|
assert.ErrorContains(t, err, "expected type string, but value is false")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplateSchemaValidateConfig(t *testing.T) {
|
||||||
|
// define schema for config
|
||||||
|
jsonSchema := testSchema(t)
|
||||||
|
|
||||||
|
// define the config
|
||||||
|
config := map[string]any{
|
||||||
|
"int_val": 1,
|
||||||
|
"float_val": 1.1,
|
||||||
|
"bool_val": true,
|
||||||
|
"string_val": "abc",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := validateConfigValueTypes(config, jsonSchema)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplateSchemaValidateConfigFailsForUnknownField(t *testing.T) {
|
||||||
|
// define schema for config
|
||||||
|
jsonSchema := testSchema(t)
|
||||||
|
|
||||||
|
// define the config
|
||||||
|
config := map[string]any{
|
||||||
|
"foo": 1,
|
||||||
|
"float_val": 1.1,
|
||||||
|
"bool_val": true,
|
||||||
|
"string_val": "abc",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := validateConfigValueTypes(config, jsonSchema)
|
||||||
|
assert.ErrorContains(t, err, "foo is not defined as an input parameter for the template")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplateSchemaValidateConfigFailsForWhenIncorrectTypes(t *testing.T) {
|
||||||
|
// define schema for config
|
||||||
|
jsonSchema := testSchema(t)
|
||||||
|
|
||||||
|
// define the config
|
||||||
|
config := map[string]any{
|
||||||
|
"int_val": 1,
|
||||||
|
"float_val": 1.1,
|
||||||
|
"bool_val": "true",
|
||||||
|
"string_val": "abc",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := validateConfigValueTypes(config, jsonSchema)
|
||||||
|
assert.ErrorContains(t, err, "incorrect type for bool_val. expected type boolean, but value is \"true\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplateSchemaValidateConfigFailsForWhenMissingInputParams(t *testing.T) {
|
||||||
|
// define schema for config
|
||||||
|
schemaJson := `{
|
||||||
|
"properties": {
|
||||||
|
"int_val": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"string_val": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
var jsonSchema jsonschema.Schema
|
||||||
|
err := json.Unmarshal([]byte(schemaJson), &jsonSchema)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// define the config
|
||||||
|
config := map[string]any{
|
||||||
|
"int_val": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = assignDefaultConfigValues(config, &jsonSchema)
|
||||||
|
assert.ErrorContains(t, err, "input parameter string_val is not defined in config")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplateDefaultAssignment(t *testing.T) {
|
||||||
|
// define schema for config
|
||||||
|
schemaJson := `{
|
||||||
|
"properties": {
|
||||||
|
"foo": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
var jsonSchema jsonschema.Schema
|
||||||
|
err := json.Unmarshal([]byte(schemaJson), &jsonSchema)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// define the config
|
||||||
|
config := map[string]any{}
|
||||||
|
|
||||||
|
err = assignDefaultConfigValues(config, &jsonSchema)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 1.0, config["foo"])
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/libs/jsonschema"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type validator func(v any) error
|
||||||
|
|
||||||
|
func validateType(v any, fieldType jsonschema.Type) error {
|
||||||
|
validateFunc, ok := validators[fieldType]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return validateFunc(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateString(v any) error {
|
||||||
|
if _, ok := v.(string); !ok {
|
||||||
|
return fmt.Errorf("expected type string, but value is %#v", v)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateBoolean(v any) error {
|
||||||
|
if _, ok := v.(bool); !ok {
|
||||||
|
return fmt.Errorf("expected type boolean, but value is %#v", v)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateNumber(v any) error {
|
||||||
|
if !slices.Contains([]reflect.Kind{reflect.Float32, reflect.Float64, reflect.Int,
|
||||||
|
reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint,
|
||||||
|
reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64},
|
||||||
|
reflect.TypeOf(v).Kind()) {
|
||||||
|
return fmt.Errorf("expected type float, but value is %#v", v)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateInteger(v any) error {
|
||||||
|
if !slices.Contains([]reflect.Kind{reflect.Int, reflect.Int8, reflect.Int16,
|
||||||
|
reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16,
|
||||||
|
reflect.Uint32, reflect.Uint64},
|
||||||
|
reflect.TypeOf(v).Kind()) {
|
||||||
|
return fmt.Errorf("expected type integer, but value is %#v", v)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var validators map[jsonschema.Type]validator = map[jsonschema.Type]validator{
|
||||||
|
jsonschema.StringType: validateString,
|
||||||
|
jsonschema.BooleanType: validateBoolean,
|
||||||
|
jsonschema.IntegerType: validateInteger,
|
||||||
|
jsonschema.NumberType: validateNumber,
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidatorString(t *testing.T) {
|
||||||
|
err := validateString("abc")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = validateString(1)
|
||||||
|
assert.ErrorContains(t, err, "expected type string, but value is 1")
|
||||||
|
|
||||||
|
err = validateString(true)
|
||||||
|
assert.ErrorContains(t, err, "expected type string, but value is true")
|
||||||
|
|
||||||
|
err = validateString("false")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidatorBoolean(t *testing.T) {
|
||||||
|
err := validateBoolean(true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = validateBoolean(1)
|
||||||
|
assert.ErrorContains(t, err, "expected type boolean, but value is 1")
|
||||||
|
|
||||||
|
err = validateBoolean("abc")
|
||||||
|
assert.ErrorContains(t, err, "expected type boolean, but value is \"abc\"")
|
||||||
|
|
||||||
|
err = validateBoolean("false")
|
||||||
|
assert.ErrorContains(t, err, "expected type boolean, but value is \"false\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidatorNumber(t *testing.T) {
|
||||||
|
err := validateNumber(true)
|
||||||
|
assert.ErrorContains(t, err, "expected type float, but value is true")
|
||||||
|
|
||||||
|
err = validateNumber(int32(1))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = validateNumber(int64(1))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = validateNumber(float32(1))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = validateNumber(float64(1))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = validateNumber("abc")
|
||||||
|
assert.ErrorContains(t, err, "expected type float, but value is \"abc\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidatorInt(t *testing.T) {
|
||||||
|
err := validateInteger(true)
|
||||||
|
assert.ErrorContains(t, err, "expected type integer, but value is true")
|
||||||
|
|
||||||
|
err = validateInteger(int32(1))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = validateInteger(int64(1))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = validateInteger(float32(1))
|
||||||
|
assert.ErrorContains(t, err, "expected type integer, but value is 1")
|
||||||
|
|
||||||
|
err = validateInteger(float64(1))
|
||||||
|
assert.ErrorContains(t, err, "expected type integer, but value is 1")
|
||||||
|
|
||||||
|
err = validateInteger("abc")
|
||||||
|
assert.ErrorContains(t, err, "expected type integer, but value is \"abc\"")
|
||||||
|
}
|
Loading…
Reference in New Issue