Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for reprompts if user input does not match template schema #946

Merged
merged 31 commits into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
3eec419
Make to/from string methods private to the jsonschema package
shreyas-goenka Oct 31, 2023
f765a16
-
shreyas-goenka Oct 31, 2023
e0b178b
-
shreyas-goenka Oct 31, 2023
0eb5020
Merge remote-tracking branch 'origin' into refactor-schema-string-met…
shreyas-goenka Nov 3, 2023
1c0b074
Add reprompting when user input does not match template schema
shreyas-goenka Nov 3, 2023
abbe612
Merge remote-tracking branch 'origin' into repromt-on-validation-error
shreyas-goenka Nov 6, 2023
e1401f7
friendly error messages
shreyas-goenka Nov 6, 2023
e8a3d49
infinite retries
shreyas-goenka Nov 6, 2023
cd0547f
merge
shreyas-goenka Nov 6, 2023
d1c2776
cleanup
shreyas-goenka Nov 6, 2023
3fad026
an -> a
shreyas-goenka Nov 6, 2023
da51c73
merge
shreyas-goenka Dec 4, 2023
e59a30f
small rename
shreyas-goenka Dec 5, 2023
0d24e39
refine error message
shreyas-goenka Dec 5, 2023
a3dfe0b
rename var
shreyas-goenka Dec 5, 2023
0852e42
Merge remote-tracking branch 'origin' into repromt-on-validation-error
shreyas-goenka Dec 6, 2023
03ca489
PatternMatchFailedError -> PatternMatchError
shreyas-goenka Dec 6, 2023
7070cf7
indicated -> indicates
shreyas-goenka Dec 6, 2023
7b283e0
remove parse comment at the wrong place
shreyas-goenka Dec 6, 2023
46d45dd
Merge remote-tracking branch 'origin' into repromt-on-validation-error
shreyas-goenka Dec 11, 2023
eb55055
remove custom catching of the error
shreyas-goenka Dec 11, 2023
5dff694
PatternMatchError -> patternMatchError
shreyas-goenka Dec 11, 2023
b8e4a73
add comment
shreyas-goenka Dec 11, 2023
b98af23
comments
shreyas-goenka Dec 11, 2023
90e2d1d
add retry for selection UI
shreyas-goenka Dec 11, 2023
a5b839c
-
shreyas-goenka Dec 11, 2023
c0ec638
-
shreyas-goenka Dec 11, 2023
9defc3d
-
shreyas-goenka Dec 11, 2023
61f5354
refactor to promptOnce
shreyas-goenka Dec 22, 2023
2ecdf4e
Address comment
shreyas-goenka Dec 22, 2023
effce0f
-
shreyas-goenka Dec 22, 2023
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
54 changes: 48 additions & 6 deletions libs/jsonschema/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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)
shreyas-goenka marked this conversation as resolved.
Show resolved Hide resolved
}

func validatePatternMatch(name string, value any, propertySchema *Schema) error {
if propertySchema.Pattern == "" {
// Return early if no pattern is specified
Expand All @@ -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)
}
4 changes: 2 additions & 2 deletions libs/jsonschema/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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\"")
Expand Down
81 changes: 56 additions & 25 deletions libs/template/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package template

import (
"context"
"errors"
"fmt"

"github.com/databricks/cli/libs/cmdio"
Expand All @@ -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
Expand Down Expand Up @@ -143,6 +152,44 @@ 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
var err error
shreyas-goenka marked this conversation as resolved.
Show resolved Hide resolved
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 {
userInput, err = cmdio.Ask(c.ctx, description, defaultVal)
if err != nil {
return err
}
}

// Convert user input string back to a Go value
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() {
Expand Down Expand Up @@ -171,38 +218,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 {
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 {
shreyas-goenka marked this conversation as resolved.
Show resolved Hide resolved
err = c.promptOnce(property, name, defaultVal, description)
if err == nil {
break
}
} else {
userInput, err = cmdio.Ask(c.ctx, description, defaultVal)
if err != nil {
return err
if errors.As(err, &retriableError{}) {
shreyas-goenka marked this conversation as resolved.
Show resolved Hide resolved
continue
}
}

// 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
}
}
Expand Down