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"
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
|
|
@ -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\"")
|
||||
|
|
|
@ -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,39 +219,22 @@ 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 {
|
||||
// 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
|
||||
}
|
||||
if !errors.As(err, &retriableError{}) {
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue