mirror of https://github.com/databricks/cli.git
Add support for `anyOf` to `skip_prompt_if` (#1133)
## Changes This PR: Introduces `anyOf` to `skip_prompt_if`. This allows you to make OR conditionals for skipping prompts during template initialization. ## Tests Added unit test and confirmed existing ones still work. Also tested manually. --------- Co-authored-by: Shreyas Goenka <shreyas.goenka@databricks.com>
This commit is contained in:
parent
14abcb3ad7
commit
ce8cfef19d
|
@ -24,10 +24,7 @@ func (s *Schema) LoadInstance(path string) (map[string]any, error) {
|
||||||
// We convert integer properties from float64 to int64 here.
|
// We convert integer properties from float64 to int64 here.
|
||||||
for name, v := range instance {
|
for name, v := range instance {
|
||||||
propertySchema, ok := s.Properties[name]
|
propertySchema, ok := s.Properties[name]
|
||||||
if !ok {
|
if !ok || propertySchema.Type != IntegerType {
|
||||||
continue
|
|
||||||
}
|
|
||||||
if propertySchema.Type != IntegerType {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
integerValue, err := toInteger(v)
|
integerValue, err := toInteger(v)
|
||||||
|
@ -47,6 +44,8 @@ func (s *Schema) ValidateInstance(instance map[string]any) error {
|
||||||
s.validateRequired,
|
s.validateRequired,
|
||||||
s.validateTypes,
|
s.validateTypes,
|
||||||
s.validatePattern,
|
s.validatePattern,
|
||||||
|
s.validateConst,
|
||||||
|
s.validateAnyOf,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fn := range validations {
|
for _, fn := range validations {
|
||||||
|
@ -103,10 +102,7 @@ func (s *Schema) validateTypes(instance map[string]any) error {
|
||||||
func (s *Schema) validateEnum(instance map[string]any) error {
|
func (s *Schema) validateEnum(instance map[string]any) error {
|
||||||
for k, v := range instance {
|
for k, v := range instance {
|
||||||
fieldInfo, ok := s.Properties[k]
|
fieldInfo, ok := s.Properties[k]
|
||||||
if !ok {
|
if !ok || fieldInfo.Enum == nil {
|
||||||
continue
|
|
||||||
}
|
|
||||||
if fieldInfo.Enum == nil {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !slices.Contains(fieldInfo.Enum, v) {
|
if !slices.Contains(fieldInfo.Enum, v) {
|
||||||
|
@ -129,3 +125,38 @@ func (s *Schema) validatePattern(instance map[string]any) error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Schema) validateConst(instance map[string]any) error {
|
||||||
|
for k, v := range instance {
|
||||||
|
fieldInfo, ok := s.Properties[k]
|
||||||
|
if !ok || fieldInfo.Const == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if v != fieldInfo.Const {
|
||||||
|
return fmt.Errorf("expected value of property %s to be %v. Found: %v", k, fieldInfo.Const, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validates that the instance matches at least one of the schemas in anyOf
|
||||||
|
// For more information, see https://json-schema.org/understanding-json-schema/reference/combining#anyof.
|
||||||
|
func (s *Schema) validateAnyOf(instance map[string]any) error {
|
||||||
|
if s.AnyOf == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// According to the JSON schema RFC, anyOf must contain at least one schema.
|
||||||
|
// https://json-schema.org/draft/2020-12/json-schema-core
|
||||||
|
if len(s.AnyOf) == 0 {
|
||||||
|
return fmt.Errorf("anyOf must contain at least one schema")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, anyOf := range s.AnyOf {
|
||||||
|
err := anyOf.ValidateInstance(instance)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("instance does not match any of the schemas in anyOf")
|
||||||
|
}
|
||||||
|
|
|
@ -222,3 +222,106 @@ func TestValidateInstanceForMultiplePatterns(t *testing.T) {
|
||||||
assert.EqualError(t, schema.validatePattern(invalidInstanceValue), "invalid value for bar: \"xyz\". Expected to match regex pattern: ^[d-f]+$")
|
assert.EqualError(t, schema.validatePattern(invalidInstanceValue), "invalid value for bar: \"xyz\". Expected to match regex pattern: ^[d-f]+$")
|
||||||
assert.EqualError(t, schema.ValidateInstance(invalidInstanceValue), "invalid value for bar: \"xyz\". Expected to match regex pattern: ^[d-f]+$")
|
assert.EqualError(t, schema.ValidateInstance(invalidInstanceValue), "invalid value for bar: \"xyz\". Expected to match regex pattern: ^[d-f]+$")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateInstanceForConst(t *testing.T) {
|
||||||
|
schema, err := Load("./testdata/instance-validate/test-schema-const.json")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Valid values for both foo and bar
|
||||||
|
validInstance := map[string]any{
|
||||||
|
"foo": "abc",
|
||||||
|
"bar": "def",
|
||||||
|
}
|
||||||
|
assert.NoError(t, schema.validateConst(validInstance))
|
||||||
|
assert.NoError(t, schema.ValidateInstance(validInstance))
|
||||||
|
|
||||||
|
// Empty instance
|
||||||
|
emptyInstanceValue := map[string]any{}
|
||||||
|
assert.NoError(t, schema.validateConst(emptyInstanceValue))
|
||||||
|
assert.NoError(t, schema.ValidateInstance(emptyInstanceValue))
|
||||||
|
|
||||||
|
// Missing value for bar
|
||||||
|
missingInstanceValue := map[string]any{
|
||||||
|
"foo": "abc",
|
||||||
|
}
|
||||||
|
assert.NoError(t, schema.validateConst(missingInstanceValue))
|
||||||
|
assert.NoError(t, schema.ValidateInstance(missingInstanceValue))
|
||||||
|
|
||||||
|
// Valid value for bar, invalid value for foo
|
||||||
|
invalidInstanceValue := map[string]any{
|
||||||
|
"foo": "xyz",
|
||||||
|
"bar": "def",
|
||||||
|
}
|
||||||
|
assert.EqualError(t, schema.validateConst(invalidInstanceValue), "expected value of property foo to be abc. Found: xyz")
|
||||||
|
assert.EqualError(t, schema.ValidateInstance(invalidInstanceValue), "expected value of property foo to be abc. Found: xyz")
|
||||||
|
|
||||||
|
// Valid value for foo, invalid value for bar
|
||||||
|
invalidInstanceValue = map[string]any{
|
||||||
|
"foo": "abc",
|
||||||
|
"bar": "xyz",
|
||||||
|
}
|
||||||
|
assert.EqualError(t, schema.validateConst(invalidInstanceValue), "expected value of property bar to be def. Found: xyz")
|
||||||
|
assert.EqualError(t, schema.ValidateInstance(invalidInstanceValue), "expected value of property bar to be def. Found: xyz")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateInstanceForEmptySchema(t *testing.T) {
|
||||||
|
schema, err := Load("./testdata/instance-validate/test-empty-anyof.json")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Valid values for both foo and bar
|
||||||
|
validInstance := map[string]any{
|
||||||
|
"foo": "abc",
|
||||||
|
"bar": "abc",
|
||||||
|
}
|
||||||
|
assert.ErrorContains(t, schema.validateAnyOf(validInstance), "anyOf must contain at least one schema")
|
||||||
|
assert.ErrorContains(t, schema.ValidateInstance(validInstance), "anyOf must contain at least one schema")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateInstanceForAnyOf(t *testing.T) {
|
||||||
|
schema, err := Load("./testdata/instance-validate/test-schema-anyof.json")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Valid values for both foo and bar
|
||||||
|
validInstance := map[string]any{
|
||||||
|
"foo": "abc",
|
||||||
|
"bar": "abc",
|
||||||
|
}
|
||||||
|
assert.NoError(t, schema.validateAnyOf(validInstance))
|
||||||
|
assert.NoError(t, schema.ValidateInstance(validInstance))
|
||||||
|
|
||||||
|
// Valid values for bar
|
||||||
|
validInstance = map[string]any{
|
||||||
|
"foo": "abc",
|
||||||
|
"bar": "def",
|
||||||
|
}
|
||||||
|
assert.NoError(t, schema.validateAnyOf(validInstance))
|
||||||
|
assert.NoError(t, schema.ValidateInstance(validInstance))
|
||||||
|
|
||||||
|
// Empty instance. Invalid because "foo" is required.
|
||||||
|
emptyInstanceValue := map[string]any{}
|
||||||
|
assert.ErrorContains(t, schema.validateAnyOf(emptyInstanceValue), "instance does not match any of the schemas in anyOf")
|
||||||
|
assert.ErrorContains(t, schema.ValidateInstance(emptyInstanceValue), "instance does not match any of the schemas in anyOf")
|
||||||
|
|
||||||
|
// Missing values for bar, invalid value for foo. Passes because only "foo"
|
||||||
|
// is required in second condition.
|
||||||
|
missingInstanceValue := map[string]any{
|
||||||
|
"foo": "xyz",
|
||||||
|
}
|
||||||
|
assert.NoError(t, schema.validateAnyOf(missingInstanceValue))
|
||||||
|
assert.NoError(t, schema.ValidateInstance(missingInstanceValue))
|
||||||
|
|
||||||
|
// Valid value for bar, invalid value for foo
|
||||||
|
invalidInstanceValue := map[string]any{
|
||||||
|
"foo": "xyz",
|
||||||
|
"bar": "abc",
|
||||||
|
}
|
||||||
|
assert.EqualError(t, schema.validateAnyOf(invalidInstanceValue), "instance does not match any of the schemas in anyOf")
|
||||||
|
assert.EqualError(t, schema.ValidateInstance(invalidInstanceValue), "instance does not match any of the schemas in anyOf")
|
||||||
|
|
||||||
|
// Invalid value for both
|
||||||
|
invalidInstanceValue = map[string]any{
|
||||||
|
"bar": "xyz",
|
||||||
|
}
|
||||||
|
assert.EqualError(t, schema.validateAnyOf(invalidInstanceValue), "instance does not match any of the schemas in anyOf")
|
||||||
|
assert.EqualError(t, schema.ValidateInstance(invalidInstanceValue), "instance does not match any of the schemas in anyOf")
|
||||||
|
}
|
||||||
|
|
|
@ -60,6 +60,9 @@ type Schema struct {
|
||||||
|
|
||||||
// Extension embeds our custom JSON schema extensions.
|
// Extension embeds our custom JSON schema extensions.
|
||||||
Extension
|
Extension
|
||||||
|
|
||||||
|
// Schema that must match any of the schemas in the array
|
||||||
|
AnyOf []*Schema `json:"anyOf,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default value defined in a JSON Schema, represented as a string.
|
// Default value defined in a JSON Schema, represented as a string.
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"anyOf": []
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"foo": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "abc"
|
||||||
|
},
|
||||||
|
"bar": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "abc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"foo",
|
||||||
|
"bar"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"bar": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "def"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"foo"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"foo": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "abc"
|
||||||
|
},
|
||||||
|
"bar": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "def"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -125,12 +125,9 @@ func (c *config) skipPrompt(p jsonschema.Property, r *renderer) (bool, error) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if conditions specified by template author for skipping the prompt
|
// Validate the partial config against skip_prompt_if schema
|
||||||
// are satisfied. If they are not, we have to prompt for a user input.
|
validationErr := p.Schema.SkipPromptIf.ValidateInstance(c.values)
|
||||||
for name, property := range p.Schema.SkipPromptIf.Properties {
|
if validationErr != nil {
|
||||||
if v, ok := c.values[name]; ok && v == property.Const {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -340,6 +340,7 @@ func TestPromptIsSkipped(t *testing.T) {
|
||||||
Const: 123,
|
Const: 123,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Required: []string{"abc", "def"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -398,3 +399,121 @@ func TestPromptIsSkipped(t *testing.T) {
|
||||||
assert.True(t, skip)
|
assert.True(t, skip)
|
||||||
assert.Equal(t, "hello-world", c.values["xyz"])
|
assert.Equal(t, "hello-world", c.values["xyz"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPromptIsSkippedAnyOf(t *testing.T) {
|
||||||
|
c := config{
|
||||||
|
ctx: context.Background(),
|
||||||
|
values: make(map[string]any),
|
||||||
|
schema: &jsonschema.Schema{
|
||||||
|
Properties: map[string]*jsonschema.Schema{
|
||||||
|
"abc": {
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
"def": {
|
||||||
|
Type: "integer",
|
||||||
|
},
|
||||||
|
"xyz": {
|
||||||
|
Type: "string",
|
||||||
|
Default: "hello-world",
|
||||||
|
Extension: jsonschema.Extension{
|
||||||
|
SkipPromptIf: &jsonschema.Schema{
|
||||||
|
AnyOf: []*jsonschema.Schema{
|
||||||
|
{
|
||||||
|
Properties: map[string]*jsonschema.Schema{
|
||||||
|
"abc": {
|
||||||
|
Const: "foobar",
|
||||||
|
},
|
||||||
|
"def": {
|
||||||
|
Const: 123,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Required: []string{"abc", "def"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Properties: map[string]*jsonschema.Schema{
|
||||||
|
"abc": {
|
||||||
|
Const: "barfoo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// No skip condition defined. Prompt should not be skipped.
|
||||||
|
skip, err := c.skipPrompt(jsonschema.Property{
|
||||||
|
Name: "abc",
|
||||||
|
Schema: c.schema.Properties["abc"],
|
||||||
|
}, testRenderer())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, skip)
|
||||||
|
|
||||||
|
// Values do not match skip condition. Prompt should not be skipped.
|
||||||
|
c.values = map[string]any{
|
||||||
|
"abc": "foobar",
|
||||||
|
"def": 1234,
|
||||||
|
}
|
||||||
|
skip, err = c.skipPrompt(jsonschema.Property{
|
||||||
|
Name: "xyz",
|
||||||
|
Schema: c.schema.Properties["xyz"],
|
||||||
|
}, testRenderer())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, skip)
|
||||||
|
assert.NotContains(t, c.values, "xyz")
|
||||||
|
|
||||||
|
// def is missing value. Prompt should not be skipped.
|
||||||
|
c.values = map[string]any{
|
||||||
|
"abc": "foobar",
|
||||||
|
}
|
||||||
|
_, err = c.skipPrompt(jsonschema.Property{
|
||||||
|
Name: "xyz",
|
||||||
|
Schema: c.schema.Properties["xyz"],
|
||||||
|
}, testRenderer())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, skip)
|
||||||
|
assert.NotContains(t, c.values, "xyz")
|
||||||
|
|
||||||
|
// abc is missing value. Prompt should be skipped because abc is optional
|
||||||
|
// in second condition.
|
||||||
|
c.values = map[string]any{
|
||||||
|
"def": 123,
|
||||||
|
}
|
||||||
|
skip, err = c.skipPrompt(jsonschema.Property{
|
||||||
|
Name: "xyz",
|
||||||
|
Schema: c.schema.Properties["xyz"],
|
||||||
|
}, testRenderer())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, skip)
|
||||||
|
assert.Equal(t, "hello-world", c.values["xyz"])
|
||||||
|
|
||||||
|
// Values match skip condition. Prompt should be skipped. Default value should
|
||||||
|
// be assigned to "xyz".
|
||||||
|
c.values = map[string]any{
|
||||||
|
"abc": "foobar",
|
||||||
|
"def": 123,
|
||||||
|
}
|
||||||
|
skip, err = c.skipPrompt(jsonschema.Property{
|
||||||
|
Name: "xyz",
|
||||||
|
Schema: c.schema.Properties["xyz"],
|
||||||
|
}, testRenderer())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, skip)
|
||||||
|
assert.Equal(t, "hello-world", c.values["xyz"])
|
||||||
|
|
||||||
|
// Values match skip condition. Prompt should be skipped. Default value should
|
||||||
|
// be assigned to "xyz".
|
||||||
|
c.values = map[string]any{
|
||||||
|
"abc": "barfoo",
|
||||||
|
}
|
||||||
|
skip, err = c.skipPrompt(jsonschema.Property{
|
||||||
|
Name: "xyz",
|
||||||
|
Schema: c.schema.Properties["xyz"],
|
||||||
|
}, testRenderer())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, skip)
|
||||||
|
assert.Equal(t, "hello-world", c.values["xyz"])
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue