Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions libs/jsonschema/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ type Extension struct {
// schema will fail.
MinDatabricksCliVersion string `json:"min_databricks_cli_version,omitempty"`

// Skip prompting if this schema is satisfied by the configuration already present. In
// that case the default value of the property is used instead.
SkipPromptIf *Schema `json:"skip_prompt_if,omitempty"`

// Version of the schema. This is used to determine if the schema is
// compatible with the current CLI version.
Version *int `json:"version,omitempty"`
Expand Down
33 changes: 33 additions & 0 deletions libs/jsonschema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ type Schema struct {
// IDE. This is manually injected here using schema.Docs
Description string `json:"description,omitempty"`

// Expected value for the JSON object. The object value must be equal to this
// field if it's specified in the schema.
Const any `json:"const,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"`
Expand Down Expand Up @@ -118,6 +122,18 @@ func (schema *Schema) validateSchemaDefaultValueTypes() error {
return nil
}

func (schema *Schema) validateConstValueTypes() error {
for name, property := range schema.Properties {
if property.Const == nil {
continue
}
if err := validateType(property.Const, property.Type); err != nil {
return fmt.Errorf("type validation for const value of property %s failed: %w", name, err)
}
}
return nil
}

// Validate enum field values for properties are consistent with types.
func (schema *Schema) validateSchemaEnumValueTypes() error {
for name, property := range schema.Properties {
Expand Down Expand Up @@ -203,14 +219,25 @@ func (schema *Schema) validateSchemaMinimumCliVersion(currentVersion string) fun
}
}

func (schema *Schema) validateSchemaSkippedPropertiesHaveDefaults() error {
for name, property := range schema.Properties {
if property.SkipPromptIf != nil && property.Default == nil {
return fmt.Errorf("property %q has a skip_prompt_if clause but no default value", name)
}
}
return nil
}

func (schema *Schema) validate() error {
for _, fn := range []func() error{
schema.validateSchemaPropertyTypes,
schema.validateSchemaDefaultValueTypes,
schema.validateSchemaEnumValueTypes,
schema.validateConstValueTypes,
schema.validateSchemaDefaultValueIsInEnums,
schema.validateSchemaPattern,
schema.validateSchemaMinimumCliVersion("v" + build.GetInfo().Version),
schema.validateSchemaSkippedPropertiesHaveDefaults,
} {
err := fn()
if err != nil {
Expand Down Expand Up @@ -248,6 +275,12 @@ func Load(path string) (*Schema, error) {
return nil, fmt.Errorf("failed to parse default value for property %s: %w", name, err)
}
}
if property.Const != nil {
property.Const, err = toInteger(property.Const)
if err != nil {
return nil, fmt.Errorf("failed to parse const value for property %s: %w", name, err)
}
}
for i, enum := range property.Enum {
property.Enum[i], err = toInteger(enum)
if err != nil {
Expand Down
55 changes: 55 additions & 0 deletions libs/jsonschema/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func TestSchemaLoadIntegers(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, int64(1), schema.Properties["abc"].Default)
assert.Equal(t, []any{int64(1), int64(2), int64(3)}, schema.Properties["abc"].Enum)
assert.Equal(t, int64(5), schema.Properties["def"].Const)
}

func TestSchemaLoadIntegersWithInvalidDefault(t *testing.T) {
Expand All @@ -60,6 +61,11 @@ func TestSchemaLoadIntegersWithInvalidEnums(t *testing.T) {
assert.EqualError(t, err, "failed to parse enum value 2.4 at index 1 for property abc: expected integer value, got: 2.4")
}

func TestSchemaLoadIntergersWithInvalidConst(t *testing.T) {
_, err := Load("./testdata/schema-load-int/schema-invalid-const.json")
assert.EqualError(t, err, "failed to parse const value for property def: expected integer value, got: 5.1")
}

func TestSchemaValidateDefaultType(t *testing.T) {
invalidSchema := &Schema{
Properties: map[string]*Schema{
Expand Down Expand Up @@ -250,3 +256,52 @@ func TestValidateSchemaMinimumCliVersion(t *testing.T) {
err = s.validateSchemaMinimumCliVersion("v0.0.0-dev")()
assert.NoError(t, err)
}

func TestValidateSchemaConstTypes(t *testing.T) {
s := &Schema{
Properties: map[string]*Schema{
"foo": {
Type: "string",
Const: "abc",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are valid types for Const? Only strings and integers?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Const can be any type, including null or objects according to the JSON schema spec: https://json-schema.org/draft/2020-12/json-schema-validation#name-const

Copy link
Copy Markdown
Contributor Author

@shreyas-goenka shreyas-goenka Nov 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For mlops-stacks, we only the capability to validate const strings right now.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shreyas-goenka what if I define the following const, does it work?

"properties": {
        "def": {
            "type": "float",
            "const": 5.1
        }
    }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried it out. Does not work for float because of precision issues. (5.1 vs 5.099).

Also does not work for integers due to type not being correct (basically const is parsed as a float rather than an int). I can fix it in a followup PR.

},
},
}
err := s.validate()
assert.NoError(t, err)

s = &Schema{
Properties: map[string]*Schema{
"foo": {
Type: "string",
Const: 123,
},
},
}
err = s.validate()
assert.EqualError(t, err, "type validation for const value of property foo failed: expected type string, but value is 123")
}

func TestValidateSchemaSkippedPropertiesHaveDefaults(t *testing.T) {
s := &Schema{
Properties: map[string]*Schema{
"foo": {
Type: "string",
Extension: Extension{SkipPromptIf: &Schema{}},
},
},
}
err := s.validate()
assert.EqualError(t, err, "property \"foo\" has a skip_prompt_if clause but no default value")

s = &Schema{
Properties: map[string]*Schema{
"foo": {
Type: "string",
Default: "abc",
Extension: Extension{SkipPromptIf: &Schema{}},
},
},
}
err = s.validate()
assert.NoError(t, err)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"type": "object",
"properties": {
"def": {
"type": "integer",
"const": 5.1
}
}
}
4 changes: 4 additions & 0 deletions libs/jsonschema/testdata/schema-load-int/schema-valid.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
"type": "integer",
"default": 1,
"enum": [1,2,3]
},
"def": {
"type": "integer",
"const": 5
}
}
}
47 changes: 44 additions & 3 deletions libs/template/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,20 +105,61 @@ func (c *config) assignDefaultValues(r *renderer) error {
return nil
}

func (c *config) skipPrompt(p jsonschema.Property, r *renderer) (bool, error) {
// Config already has a value assigned. We don't have to prompt for a user input.
if _, ok := c.values[p.Name]; ok {
return true, nil
}

if p.Schema.SkipPromptIf == nil {
return false, nil
}

// Check if conditions specified by template author for skipping the prompt
// are satisfied. If they are not, we have to prompt for a user input.
for name, property := range p.Schema.SkipPromptIf.Properties {
if v, ok := c.values[name]; ok && v == property.Const {
continue
}
return false, nil
}

if p.Schema.Default == nil {
return false, fmt.Errorf("property %s has skip_prompt_if set but no default value", p.Name)
}

// Assign default value to property if we are skipping it.
if p.Schema.Type != jsonschema.StringType {
c.values[p.Name] = p.Schema.Default
return true, nil
}

// Execute the default value as a template and assign it to the property.
var err error
c.values[p.Name], err = r.executeTemplate(p.Schema.Default.(string))
if err != nil {
return false, err
}
return true, nil
}

// Prompts user for values for properties that do not have a value set yet
func (c *config) promptForValues(r *renderer) error {
for _, p := range c.schema.OrderedProperties() {
name := p.Name
property := p.Schema

// Config already has a value assigned
if _, ok := c.values[name]; ok {
// Skip prompting if we can.
skip, err := c.skipPrompt(p, r)
if err != nil {
return err
}
if skip {
continue
}

// Compute default value to display by converting it to a string
var defaultVal string
var err error
if property.Default != nil {
defaultValRaw, err := property.DefaultString()
if err != nil {
Expand Down
Loading