mirror of https://github.com/databricks/cli.git
Add support for reprompts if user input does not match template schema (#946)
## Changes This PR adds retry logic to user input prompts, prompting users again if the value does not match the requirements specified in the bundle template schema. ## Tests Manually. Here's an example UX. The first prompt expects an integer and the second one a string made only from the letters "defg" ``` shreyas.goenka@THW32HFW6T cli % cli bundle init ~/mlops-stack Please enter an integer [123]: abc Validation failed: "abc" is not a integer Please enter an integer [123]: 123 Please enter a string [dddd]: apple Validation failed: invalid value for input_root_dir: "apple". Only characters the 'd', 'e', 'f', 'g' are allowed ```
This commit is contained in:
parent
fa3c8b1017
commit
f2408eda62
|
@ -7,6 +7,19 @@ import (
|
||||||
"strconv"
|
"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
|
// function to check whether a float value represents an integer
|
||||||
func isIntegerValue(v float64) bool {
|
func isIntegerValue(v float64) bool {
|
||||||
return v == float64(int64(v))
|
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
|
// Return more readable error incase of a syntax error
|
||||||
if errors.Is(err, strconv.ErrSyntax) {
|
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
|
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 {
|
func validatePatternMatch(name string, value any, propertySchema *Schema) error {
|
||||||
if propertySchema.Pattern == "" {
|
if propertySchema.Pattern == "" {
|
||||||
// Return early if no pattern is specified
|
// Return early if no pattern is specified
|
||||||
|
@ -134,10 +176,10 @@ func validatePatternMatch(name string, value any, propertySchema *Schema) error
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If custom user error message is defined, return error with the custom message
|
return patternMatchError{
|
||||||
msg := propertySchema.PatternMatchFailureMessage
|
PropertyName: name,
|
||||||
if msg == "" {
|
PropertyValue: value,
|
||||||
msg = fmt.Sprintf("Expected to match regex pattern: %s", propertySchema.Pattern)
|
Pattern: propertySchema.Pattern,
|
||||||
|
FailureMessage: propertySchema.PatternMatchFailureMessage,
|
||||||
}
|
}
|
||||||
return fmt.Errorf("invalid value for %s: %q. %s", name, value, msg)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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\"")
|
assert.EqualError(t, err, "cannot parse string as object of type array. Value of string: \"qrt\"")
|
||||||
|
|
||||||
_, err = fromString("abc", IntegerType)
|
_, 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)
|
_, 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")
|
_, err = fromString("1.0", "foobar")
|
||||||
assert.EqualError(t, err, "unknown json schema type: \"foobar\"")
|
assert.EqualError(t, err, "unknown json schema type: \"foobar\"")
|
||||||
|
|
|
@ -2,6 +2,7 @@ package template
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/databricks/cli/libs/cmdio"
|
"github.com/databricks/cli/libs/cmdio"
|
||||||
|
@ -12,6 +13,14 @@ import (
|
||||||
// The latest template schema version supported by the CLI
|
// The latest template schema version supported by the CLI
|
||||||
const latestSchemaVersion = 1
|
const latestSchemaVersion = 1
|
||||||
|
|
||||||
|
type retriableError struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e retriableError) Error() string {
|
||||||
|
return e.err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
values map[string]any
|
values map[string]any
|
||||||
|
@ -143,6 +152,45 @@ func (c *config) skipPrompt(p jsonschema.Property, r *renderer) (bool, error) {
|
||||||
return true, nil
|
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
|
// Prompts user for values for properties that do not have a value set yet
|
||||||
func (c *config) promptForValues(r *renderer) error {
|
func (c *config) promptForValues(r *renderer) error {
|
||||||
for _, p := range c.schema.OrderedProperties() {
|
for _, p := range c.schema.OrderedProperties() {
|
||||||
|
@ -171,39 +219,22 @@ func (c *config) promptForValues(r *renderer) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compute description for the prompt
|
||||||
description, err := r.executeTemplate(property.Description)
|
description, err := r.executeTemplate(property.Description)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user input by running the prompt
|
// We wrap this function in a retry loop to allow retries when the user
|
||||||
var userInput string
|
// entered value is invalid.
|
||||||
if property.Enum != nil {
|
for {
|
||||||
// convert list of enums to string slice
|
err = c.promptOnce(property, name, defaultVal, description)
|
||||||
enums, err := property.EnumStringSlice()
|
if err == nil {
|
||||||
if err != nil {
|
break
|
||||||
|
}
|
||||||
|
if !errors.As(err, &retriableError{}) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
userInput, err = cmdio.AskSelect(c.ctx, description, enums)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
userInput, err = cmdio.Ask(c.ctx, description, defaultVal)
|
|
||||||
if err != nil {
|
|
||||||
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
|
return nil
|
||||||
|
|
Loading…
Reference in New Issue