diff --git a/libs/jsonschema/utils.go b/libs/jsonschema/utils.go index 9e65ed069d..ff9b88312b 100644 --- a/libs/jsonschema/utils.go +++ b/libs/jsonschema/utils.go @@ -7,6 +7,19 @@ import ( "strconv" ) +// This error indicates an failure to parse a string as a particular JSON schema type. +type parseStringError struct { + // Expected JSON schema type for the value + ExpectedType Type + + // The string value that failed to parse + Value string +} + +func (e parseStringError) Error() string { + return fmt.Sprintf("%q is not a %s", e.Value, e.ExpectedType) +} + // function to check whether a float value represents an integer func isIntegerValue(v float64) bool { return v == float64(int64(v)) @@ -108,11 +121,40 @@ func fromString(s string, T Type) (any, error) { // Return more readable error incase of a syntax error if errors.Is(err, strconv.ErrSyntax) { - return nil, fmt.Errorf("could not parse %q as a %s: %w", s, T, err) + return nil, parseStringError{ + ExpectedType: T, + Value: s, + } } return v, err } +// Error indicates a value entered by the user failed to match the pattern specified +// in the template schema. +type patternMatchError struct { + // The name of the property that failed to match the pattern + PropertyName string + + // The value of the property that failed to match the pattern + PropertyValue any + + // The regex pattern that the property value failed to match + Pattern string + + // Failure message to display to the user, if specified in the template + // schema + FailureMessage string +} + +func (e patternMatchError) Error() string { + // If custom user error message is defined, return error with the custom message + msg := e.FailureMessage + if msg == "" { + msg = fmt.Sprintf("Expected to match regex pattern: %s", e.Pattern) + } + return fmt.Sprintf("invalid value for %s: %q. %s", e.PropertyName, e.PropertyValue, msg) +} + func validatePatternMatch(name string, value any, propertySchema *Schema) error { if propertySchema.Pattern == "" { // Return early if no pattern is specified @@ -134,10 +176,10 @@ func validatePatternMatch(name string, value any, propertySchema *Schema) error return nil } - // If custom user error message is defined, return error with the custom message - msg := propertySchema.PatternMatchFailureMessage - if msg == "" { - msg = fmt.Sprintf("Expected to match regex pattern: %s", propertySchema.Pattern) + return patternMatchError{ + PropertyName: name, + PropertyValue: value, + Pattern: propertySchema.Pattern, + FailureMessage: propertySchema.PatternMatchFailureMessage, } - return fmt.Errorf("invalid value for %s: %q. %s", name, value, msg) } diff --git a/libs/jsonschema/utils_test.go b/libs/jsonschema/utils_test.go index b036a23f0c..89200dae33 100644 --- a/libs/jsonschema/utils_test.go +++ b/libs/jsonschema/utils_test.go @@ -110,10 +110,10 @@ func TestTemplateFromString(t *testing.T) { assert.EqualError(t, err, "cannot parse string as object of type array. Value of string: \"qrt\"") _, err = fromString("abc", IntegerType) - assert.EqualError(t, err, "could not parse \"abc\" as a integer: strconv.ParseInt: parsing \"abc\": invalid syntax") + assert.EqualError(t, err, "\"abc\" is not a integer") _, err = fromString("1.0", IntegerType) - assert.EqualError(t, err, "could not parse \"1.0\" as a integer: strconv.ParseInt: parsing \"1.0\": invalid syntax") + assert.EqualError(t, err, "\"1.0\" is not a integer") _, err = fromString("1.0", "foobar") assert.EqualError(t, err, "unknown json schema type: \"foobar\"") diff --git a/libs/template/config.go b/libs/template/config.go index 2b4d19d14a..49434754e3 100644 --- a/libs/template/config.go +++ b/libs/template/config.go @@ -2,6 +2,7 @@ package template import ( "context" + "errors" "fmt" "github.com/databricks/cli/libs/cmdio" @@ -12,6 +13,14 @@ import ( // The latest template schema version supported by the CLI const latestSchemaVersion = 1 +type retriableError struct { + err error +} + +func (e retriableError) Error() string { + return e.err.Error() +} + type config struct { ctx context.Context values map[string]any @@ -143,6 +152,45 @@ func (c *config) skipPrompt(p jsonschema.Property, r *renderer) (bool, error) { return true, nil } +func (c *config) promptOnce(property *jsonschema.Schema, name, defaultVal, description string) error { + var userInput string + if property.Enum != nil { + // List options for the user to select from + options, err := property.EnumStringSlice() + if err != nil { + return err + } + userInput, err = cmdio.AskSelect(c.ctx, description, options) + if err != nil { + return err + } + } else { + var err error + userInput, err = cmdio.Ask(c.ctx, description, defaultVal) + if err != nil { + return err + } + } + + // Convert user input string back to a Go value + var err error + c.values[name], err = property.ParseString(userInput) + if err != nil { + // Show error and retry if validation fails + cmdio.LogString(c.ctx, fmt.Sprintf("Validation failed: %s", err.Error())) + return retriableError{err: err} + } + + // Validate the partial config which includes the new value + err = c.schema.ValidateInstance(c.values) + if err != nil { + // Show error and retry if validation fails + cmdio.LogString(c.ctx, fmt.Sprintf("Validation failed: %s", err.Error())) + return retriableError{err: err} + } + return 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() { @@ -171,40 +219,23 @@ func (c *config) promptForValues(r *renderer) error { } } + // Compute description for the prompt description, err := r.executeTemplate(property.Description) if err != nil { return err } - // Get user input by running the prompt - var userInput string - if property.Enum != nil { - // convert list of enums to string slice - enums, err := property.EnumStringSlice() - if err != nil { - return err - } - userInput, err = cmdio.AskSelect(c.ctx, description, enums) - if err != nil { - return err + // We wrap this function in a retry loop to allow retries when the user + // entered value is invalid. + for { + err = c.promptOnce(property, name, defaultVal, description) + if err == nil { + break } - } else { - userInput, err = cmdio.Ask(c.ctx, description, defaultVal) - if err != nil { + if !errors.As(err, &retriableError{}) { return err } } - - // Convert user input string back to a value - c.values[name], err = property.ParseString(userInput) - if err != nil { - return err - } - - // Validate the partial config based on this update - if err := c.schema.ValidateInstance(c.values); err != nil { - return err - } } return nil }