-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathexpr.go
More file actions
216 lines (191 loc) · 5.63 KB
/
Copy pathexpr.go
File metadata and controls
216 lines (191 loc) · 5.63 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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
package gimpl
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"slices"
"strings"
"github.com/pedramktb/go-tagerr"
)
var (
ErrInvalidExpr = tagerr.ErrInvalidReq.Wrap(&tagerr.Err{
Err: errors.New("invalid expression"),
Tag: "invalid_expr",
})
)
// LogOp for logical operators
type LogOp string
const (
LogOpAnd LogOp = "and" // logical and
LogOpOr LogOp = "or" // logical or
)
func (o LogOp) isValid() bool { return slices.Contains([]LogOp{LogOpAnd, LogOpOr}, o) }
// QuantOp is quantifier operators (first-class array quantifiers)
type QuantOp string
const (
QuantOpAny QuantOp = "any" // there exists an element in array
QuantOpAll QuantOp = "all" // for all elements in array
)
// CondOp for conditional operators
type CondOp string
const (
CondOpRE CondOp = "re" // regular expression (string)
CondOpNIN CondOp = "nin" // not in list
CondOpIN CondOp = "in" // in list
CondOpEQ CondOp = "eq" // equality
CondOpNE CondOp = "ne" // not equal
CondOpGT CondOp = "gt" // greater than
CondOpLT CondOp = "lt" // less than
CondOpGTE CondOp = "gte" // greater than or equal to
CondOpLTE CondOp = "lte" // less than or equal to
)
func (o CondOp) isValid() bool {
return slices.Contains([]CondOp{
CondOpRE, CondOpIN, CondOpNIN, CondOpEQ, CondOpNE,
CondOpGT, CondOpLT, CondOpGTE, CondOpLTE,
}, o)
}
func (q QuantOp) isValid() bool { return slices.Contains([]QuantOp{QuantOpAny, QuantOpAll}, q) }
// Expr is the root expression wrapper
type Expr struct {
Sample Entity // Only used for unmarshalling, must be set before unmarshalling
Expr any
}
// LogExpr is a logical expression combining two sub-expressions.
type LogExpr struct {
Left Expr
Op LogOp
Right Expr
}
// QuantExpr is a quantifier expression that applies a nested expression relative to each element in an array path.
type QuantExpr struct {
Path string
Op QuantOp
Expr Expr
}
// CondExpr is a Condition expression (field comparison).
type CondExpr struct {
Path string
Op CondOp
Val any
}
// UnmarshalJSON unmarshal for top-level Expr. Delegates to decodeExpr with root entity context.
func (e *Expr) UnmarshalJSON(src []byte) error {
if e.Sample == nil {
return tagerr.ErrInternal.Wrap(ErrInvalidExpr.Wrap(errors.New("Expr.Sample must be set before unmarshalling")))
}
expr, err := decodeExpr(src, e.Sample)
if err != nil {
return ErrInvalidExpr.Wrap(err)
}
e.Expr = expr
return nil
}
// decodeExpr recursively decodes any expression variant using the provided root entity context (for field type inference).
func decodeExpr(src json.RawMessage, root any) (any, error) {
var probe struct {
Op string `json:"op"`
}
if err := json.Unmarshal(src, &probe); err != nil {
return nil, err
}
if LogOp(probe.Op).isValid() {
var tmp struct {
Left json.RawMessage `json:"left"`
Op LogOp `json:"op"`
Right json.RawMessage `json:"right"`
}
if err := json.Unmarshal(src, &tmp); err != nil {
return nil, err
}
left, err := decodeExpr(tmp.Left, root)
if err != nil {
return nil, err
}
right, err := decodeExpr(tmp.Right, root)
if err != nil {
return nil, err
}
return LogExpr{Left: Expr{Expr: left}, Op: tmp.Op, Right: Expr{Expr: right}}, nil
}
if CondOp(probe.Op).isValid() {
var tmp struct {
Path string `json:"path"`
Op CondOp `json:"op"`
Val json.RawMessage `json:"val"`
}
if err := json.Unmarshal(src, &tmp); err != nil {
return nil, err
}
val, err := resolveVal(root, tmp.Op, tmp.Path)
if err != nil {
return nil, fmt.Errorf("resolving condition path %q: %w", tmp.Path, err)
}
if err := json.Unmarshal(tmp.Val, val); err != nil {
return nil, fmt.Errorf("condition value for path %q: %w", tmp.Path, err)
}
return CondExpr{Path: tmp.Path, Op: tmp.Op, Val: reflect.ValueOf(val).Elem().Interface()}, nil
}
if QuantOp(probe.Op).isValid() {
var tmp struct {
Path string `json:"path"`
Op QuantOp `json:"op"`
Expr json.RawMessage `json:"expr"`
}
if err := json.Unmarshal(src, &tmp); err != nil {
return nil, err
}
elem, err := resolveArrayElem(root, tmp.Path)
if err != nil {
return nil, fmt.Errorf("quantifier path %q: %w", tmp.Path, err)
}
nested, err := decodeExpr(tmp.Expr, elem)
if err != nil {
return nil, err
}
return QuantExpr{Path: tmp.Path, Op: tmp.Op, Expr: Expr{Expr: nested}}, nil
}
return nil, fmt.Errorf("invalid operator %q", probe.Op)
}
func resolveVal(root any, op CondOp, path string) (any, error) {
if path != "" {
for field := range strings.SplitSeq(path, ".") {
ent, ok := root.(Entity)
if !ok {
return nil, fmt.Errorf("field %q in path %q is not an entity", field, path)
}
root = ent.FilterPtr(field)
if root == nil {
return nil, fmt.Errorf("field %q in path %q not found", field, path)
}
}
}
// For the IN and NIN operators, we need to unmarshal into *[]TYPE instead of *TYPE
if op == CondOpIN || op == CondOpNIN {
root = reflect.New(reflect.SliceOf(reflect.TypeOf(root).Elem())).Interface()
}
return root, nil
}
func resolveArrayElem(root any, path string) (any, error) {
last := path
for field := range strings.SplitSeq(path, ".") {
ent, ok := root.(Entity)
if !ok {
return nil, fmt.Errorf("field %q in path %q is not an entity", field, path)
}
root = ent.FilterPtr(field)
if root == nil {
return nil, fmt.Errorf("field %q in path %q not found", field, path)
}
last = field
}
t := reflect.TypeOf(root)
if t.Kind() == reflect.Pointer {
t = t.Elem()
}
if t.Kind() != reflect.Slice && t.Kind() != reflect.Array {
return nil, fmt.Errorf("field %q in path %q is not an array or slice", last, path)
}
return reflect.New(t.Elem()).Interface(), nil
}