-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathjsonpath.go
More file actions
135 lines (119 loc) · 3.32 KB
/
jsonpath.go
File metadata and controls
135 lines (119 loc) · 3.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
package gin
import (
"fmt"
"strings"
"github.com/ohler55/ojg/jp"
)
type JSONPathError struct {
Path string
Message string
}
func (e *JSONPathError) Error() string {
return fmt.Sprintf("invalid JSONPath %q: %s", e.Path, e.Message)
}
// ValidateJSONPath validates a JSONPath expression and ensures it only uses
// features supported by the GIN index (dot notation, wildcards).
// Unsupported: array indices [0], filters [?()], recursive descent .., scripts
func ValidateJSONPath(path string) error {
if path == "" {
return &JSONPathError{Path: path, Message: "empty path"}
}
if strings.HasPrefix(path, internalRepresentationPathPrefix) {
return &JSONPathError{Path: path, Message: "reserved internal representation path namespace"}
}
if path[0] != '$' {
return &JSONPathError{Path: path, Message: "path must start with '$'"}
}
expr, err := jp.ParseString(path)
if err != nil {
return &JSONPathError{Path: path, Message: err.Error()}
}
if len(expr) == 0 {
return &JSONPathError{Path: path, Message: "empty expression"}
}
// First fragment must be Root ($)
if _, ok := expr[0].(jp.Root); !ok {
return &JSONPathError{Path: path, Message: "path must start with '$'"}
}
for _, frag := range expr {
switch f := frag.(type) {
case jp.Root:
// $ - OK
case jp.Child:
// .field - OK
case jp.Wildcard:
// [*] - OK
case jp.Bracket:
// ['field'] bracket notation - OK (equivalent to .field)
case jp.Nth:
// [0], [1] - NOT supported
return &JSONPathError{
Path: path,
Message: fmt.Sprintf("array index [%d] not supported, use [*] for array elements", f),
}
case *jp.Slice:
// [0:5], [::2] - NOT supported
return &JSONPathError{
Path: path,
Message: "slice notation not supported",
}
case *jp.Filter:
// [?(@.price < 10)] - NOT supported
return &JSONPathError{
Path: path,
Message: "filter expressions not supported",
}
case jp.Descent:
// .. recursive descent - NOT supported
return &JSONPathError{
Path: path,
Message: "recursive descent (..) not supported",
}
case jp.Union:
// [name1, name2] - NOT supported
return &JSONPathError{
Path: path,
Message: "union notation not supported",
}
default:
return &JSONPathError{
Path: path,
Message: fmt.Sprintf("unsupported path fragment type: %T", frag),
}
}
}
return nil
}
func MustValidateJSONPath(path string) string {
if err := ValidateJSONPath(path); err != nil {
panic(err)
}
return path
}
func IsValidJSONPath(path string) bool {
return ValidateJSONPath(path) == nil
}
// ParseJSONPath parses and validates a JSONPath, returning the parsed expression.
func ParseJSONPath(path string) (jp.Expr, error) {
if err := ValidateJSONPath(path); err != nil {
return nil, err
}
return jp.ParseString(path)
}
func canonicalizeSupportedPath(path string) (string, error) {
if err := ValidateJSONPath(path); err != nil {
return "", err
}
return NormalizePath(path), nil
}
// NormalizePath converts a JSONPath to a canonical dot-notation form without
// validating that the path uses only GIN-supported JSONPath features.
// Callers handling untrusted input should use ValidateJSONPath or
// canonicalizeSupportedPath first.
func NormalizePath(path string) string {
expr, err := jp.ParseString(path)
if err != nil {
return path
}
return expr.String()
}