Skip to content

Instantly share code, notes, and snippets.

@jpbetz
Last active July 20, 2023 18:08
Show Gist options
  • Save jpbetz/039a7eabe8d7e33852ca8552102b955a to your computer and use it in GitHub Desktop.
Save jpbetz/039a7eabe8d7e33852ca8552102b955a to your computer and use it in GitHub Desktop.
// ValidFieldPath validates that jsonPath is a valid JSON Path containing only field and map accessors
// that are valid for the given schema, and returns a field.Path representation of the validated jsonPath or an error.
func ValidFieldPath(jsonPath string, schema *schema.Structural) (validFieldPath *field.Path, err error) {
appendToPath := func(name string) error {
if schema.AdditionalProperties != nil {
validFieldPath = validFieldPath.Key(name)
schema = schema.AdditionalProperties.Structural
} else if schema.Properties != nil {
validFieldPath = validFieldPath.Child(name)
val, ok := schema.Properties[name]
if !ok {
return fmt.Errorf("does not refer to a valid field")
}
schema = &val
} else {
return fmt.Errorf("does not refer to a valid field")
}
return nil
}
validFieldPath = nil
scanner := bufio.NewScanner(strings.NewReader(jsonPath))
// configure the scanner to split the string into tokens.
// The three delimiters ('.', '[', ']') will be returned as single char tokens.
// All other text between delimiters is returned as string tokens.
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
if len(data) > 0 {
for i := 0; i < len(data); i++ {
// If in a single quoted string, look for the end of string
// ignoring delimiters.
if data[0] == '\'' {
if i > 0 && data[i] == '\'' && data[i-1] != '\\' {
// Return quoted string
return i + 1, data[:i+1], nil
}
continue
}
switch data[i] {
case '.', '[', ']': // delimiters
if i == 0 {
// Return the delimiter.
return 1, data[:1], nil
} else {
// Return identifier leading up to the delimiter.
// The next call to split will return the delimiter.
return i, data[:i], nil
}
}
}
if atEOF {
// Return the string.
return len(data), data, nil
}
}
return 0, nil, nil
})
var tok string
for scanner.Scan() {
tok = scanner.Text()
switch tok {
case "[":
if !scanner.Scan() {
return nil, fmt.Errorf("unexpected end of JSON path")
}
tok = scanner.Text()
if !strings.HasPrefix(tok, "'") || !strings.HasSuffix(tok, "'") {
return nil, fmt.Errorf("expected single quoted string but got %s", tok)
}
unescaped, err := unescapeSingleQuote(tok[1 : len(tok)-1])
if err != nil {
return nil, fmt.Errorf("invalid string literal: %v", err)
}
if err := appendToPath(unescaped); err != nil {
return nil, err
}
if !scanner.Scan() {
return nil, fmt.Errorf("unexpected end of JSON path")
}
tok = scanner.Text()
if tok != "]" {
return nil, fmt.Errorf("expected ] but got %s", tok)
}
case ".":
if !scanner.Scan() {
return nil, fmt.Errorf("unexpected end of JSON path")
}
tok = scanner.Text()
if err := appendToPath(tok); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("expected [ or . but got: %s", tok)
}
}
return validFieldPath, nil
}
var unescapeMatcher = regexp.MustCompile(`\\.`)
func unescapeSingleQuote(s string) (string, error) {
var err error
unescaped := unescapeMatcher.ReplaceAllStringFunc(s, func(matchStr string) string {
directive := matchStr[1]
switch directive {
case 'a':
return "\a"
case 'b':
return "\b"
case 'f':
return "\f"
case 'n':
return "\n"
case 'r':
return "\r"
case 't':
return "\t"
case 'v':
return "\v"
case '\'':
return "'"
case '\\':
return "\\"
default:
err = fmt.Errorf("invalid escape char %s", matchStr)
return ""
}
})
return unescaped, err
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment