2023-09-07 14:36:06 +00:00
|
|
|
package jsonschema
|
2023-08-07 13:14:25 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2023-09-25 09:53:38 +00:00
|
|
|
"regexp"
|
2023-08-07 13:14:25 +00:00
|
|
|
"strconv"
|
|
|
|
)
|
|
|
|
|
2023-12-22 15:43:08 +00:00
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
2023-08-07 13:14:25 +00:00
|
|
|
// function to check whether a float value represents an integer
|
|
|
|
func isIntegerValue(v float64) bool {
|
|
|
|
return v == float64(int64(v))
|
|
|
|
}
|
|
|
|
|
|
|
|
func toInteger(v any) (int64, error) {
|
|
|
|
switch typedVal := v.(type) {
|
|
|
|
// cast float to int
|
|
|
|
case float32:
|
|
|
|
if !isIntegerValue(float64(typedVal)) {
|
|
|
|
return 0, fmt.Errorf("expected integer value, got: %v", v)
|
|
|
|
}
|
|
|
|
return int64(typedVal), nil
|
|
|
|
case float64:
|
|
|
|
if !isIntegerValue(typedVal) {
|
|
|
|
return 0, fmt.Errorf("expected integer value, got: %v", v)
|
|
|
|
}
|
|
|
|
return int64(typedVal), nil
|
|
|
|
|
|
|
|
// pass through common integer cases
|
|
|
|
case int:
|
|
|
|
return int64(typedVal), nil
|
|
|
|
case int32:
|
|
|
|
return int64(typedVal), nil
|
|
|
|
case int64:
|
|
|
|
return typedVal, nil
|
|
|
|
|
|
|
|
default:
|
|
|
|
return 0, fmt.Errorf("cannot convert %#v to an integer", v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-06 15:05:17 +00:00
|
|
|
func toString(v any, T Type) (string, error) {
|
2023-08-07 13:14:25 +00:00
|
|
|
switch T {
|
2023-09-07 14:36:06 +00:00
|
|
|
case BooleanType:
|
2023-08-07 13:14:25 +00:00
|
|
|
boolVal, ok := v.(bool)
|
|
|
|
if !ok {
|
|
|
|
return "", fmt.Errorf("expected bool, got: %#v", v)
|
|
|
|
}
|
|
|
|
return strconv.FormatBool(boolVal), nil
|
2023-09-07 14:36:06 +00:00
|
|
|
case StringType:
|
2023-08-07 13:14:25 +00:00
|
|
|
strVal, ok := v.(string)
|
|
|
|
if !ok {
|
|
|
|
return "", fmt.Errorf("expected string, got: %#v", v)
|
|
|
|
}
|
|
|
|
return strVal, nil
|
2023-09-07 14:36:06 +00:00
|
|
|
case NumberType:
|
2023-08-07 13:14:25 +00:00
|
|
|
floatVal, ok := v.(float64)
|
|
|
|
if !ok {
|
|
|
|
return "", fmt.Errorf("expected float, got: %#v", v)
|
|
|
|
}
|
|
|
|
return strconv.FormatFloat(floatVal, 'f', -1, 64), nil
|
2023-09-07 14:36:06 +00:00
|
|
|
case IntegerType:
|
2023-08-07 13:14:25 +00:00
|
|
|
intVal, err := toInteger(v)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return strconv.FormatInt(intVal, 10), nil
|
2023-09-07 14:36:06 +00:00
|
|
|
case ArrayType, ObjectType:
|
2023-08-07 13:14:25 +00:00
|
|
|
return "", fmt.Errorf("cannot format object of type %s as a string. Value of object: %#v", T, v)
|
2023-08-15 14:28:04 +00:00
|
|
|
default:
|
|
|
|
return "", fmt.Errorf("unknown json schema type: %q", T)
|
2023-08-07 13:14:25 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-06 15:05:17 +00:00
|
|
|
func toStringSlice(arr []any, T Type) ([]string, error) {
|
2023-09-08 12:07:22 +00:00
|
|
|
res := []string{}
|
|
|
|
for _, v := range arr {
|
2023-11-06 15:05:17 +00:00
|
|
|
s, err := toString(v, T)
|
2023-09-08 12:07:22 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
res = append(res, s)
|
|
|
|
}
|
|
|
|
return res, nil
|
|
|
|
}
|
|
|
|
|
2023-11-06 15:05:17 +00:00
|
|
|
func fromString(s string, T Type) (any, error) {
|
2023-09-07 14:36:06 +00:00
|
|
|
if T == StringType {
|
2023-08-07 13:14:25 +00:00
|
|
|
return s, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Variables to store value and error from parsing
|
|
|
|
var v any
|
|
|
|
var err error
|
|
|
|
|
|
|
|
switch T {
|
2023-09-07 14:36:06 +00:00
|
|
|
case BooleanType:
|
2023-08-07 13:14:25 +00:00
|
|
|
v, err = strconv.ParseBool(s)
|
2023-09-07 14:36:06 +00:00
|
|
|
case NumberType:
|
2023-08-07 13:14:25 +00:00
|
|
|
v, err = strconv.ParseFloat(s, 32)
|
2023-09-07 14:36:06 +00:00
|
|
|
case IntegerType:
|
2023-08-07 13:14:25 +00:00
|
|
|
v, err = strconv.ParseInt(s, 10, 64)
|
2023-09-07 14:36:06 +00:00
|
|
|
case ArrayType, ObjectType:
|
2023-08-07 13:14:25 +00:00
|
|
|
return "", fmt.Errorf("cannot parse string as object of type %s. Value of string: %q", T, s)
|
2023-08-15 14:28:04 +00:00
|
|
|
default:
|
|
|
|
return "", fmt.Errorf("unknown json schema type: %q", T)
|
2023-08-07 13:14:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Return more readable error incase of a syntax error
|
|
|
|
if errors.Is(err, strconv.ErrSyntax) {
|
2023-12-22 15:43:08 +00:00
|
|
|
return nil, parseStringError{
|
|
|
|
ExpectedType: T,
|
|
|
|
Value: s,
|
|
|
|
}
|
2023-08-07 13:14:25 +00:00
|
|
|
}
|
|
|
|
return v, err
|
|
|
|
}
|
2023-09-25 09:53:38 +00:00
|
|
|
|
2023-12-22 15:43:08 +00:00
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
2023-11-06 15:05:17 +00:00
|
|
|
func validatePatternMatch(name string, value any, propertySchema *Schema) error {
|
2023-09-25 09:53:38 +00:00
|
|
|
if propertySchema.Pattern == "" {
|
|
|
|
// Return early if no pattern is specified
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Expect type of value to be a string
|
|
|
|
stringValue, ok := value.(string)
|
|
|
|
if !ok {
|
|
|
|
return fmt.Errorf("invalid value for %s: %v. Expected a value of type string", name, value)
|
|
|
|
}
|
|
|
|
|
|
|
|
match, err := regexp.MatchString(propertySchema.Pattern, stringValue)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if match {
|
|
|
|
// successful match
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-12-22 15:43:08 +00:00
|
|
|
return patternMatchError{
|
|
|
|
PropertyName: name,
|
|
|
|
PropertyValue: value,
|
|
|
|
Pattern: propertySchema.Pattern,
|
|
|
|
FailureMessage: propertySchema.PatternMatchFailureMessage,
|
2023-09-25 09:53:38 +00:00
|
|
|
}
|
|
|
|
}
|