-
Notifications
You must be signed in to change notification settings - Fork 1
/
path.go
496 lines (421 loc) · 11.4 KB
/
path.go
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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
package validation
import (
"errors"
"fmt"
"math"
"strconv"
"strings"
"unicode"
)
// PropertyPathElement is a part of the [PropertyPath].
type PropertyPathElement interface {
// IsIndex can be used to determine whether an element is a string (property name) or
// an index array.
IsIndex() bool
fmt.Stringer
}
// PropertyName holds up property name value under [PropertyPath].
type PropertyName string
// IsIndex on [PropertyName] always returns false.
func (p PropertyName) IsIndex() bool {
return false
}
// String returns property name as is.
func (p PropertyName) String() string {
return string(p)
}
// ArrayIndex holds up array index value under [PropertyPath].
type ArrayIndex int
// IsIndex on [ArrayIndex] always returns true.
func (a ArrayIndex) IsIndex() bool {
return true
}
// String returns array index values converted into a string.
func (a ArrayIndex) String() string {
return strconv.Itoa(int(a))
}
// PropertyPath is generated by the validator and indicates how it reached the invalid value
// from the root element. Property path is denoted by dots, while array access
// is denoted by square brackets. For example, "book.keywords[0]" means that the violation
// occurred on the first element of array "keywords" in the "book" object.
//
// Internally [PropertyPath] is a linked list. You can create a new path using [PropertyPath.WithProperty]
// or [PropertyPath.WithIndex] methods. [PropertyPath] should always be used as a pointer value.
// Nil value is a valid value that means that the property path is empty.
type PropertyPath struct {
parent *PropertyPath
value PropertyPathElement
}
// NewPropertyPath creates a [PropertyPath] from the list of elements. If the list is empty nil will be returned.
// Nil value is a valid value that means that the property path is empty.
func NewPropertyPath(elements ...PropertyPathElement) *PropertyPath {
var path *PropertyPath
return path.With(elements...)
}
// With returns new [PropertyPath] with appended elements to the end of the list.
func (path *PropertyPath) With(elements ...PropertyPathElement) *PropertyPath {
current := path
for _, element := range elements {
current = &PropertyPath{parent: current, value: element}
}
return current
}
// WithProperty returns new [PropertyPath] with appended [PropertyName] to the end of the list.
func (path *PropertyPath) WithProperty(name string) *PropertyPath {
return &PropertyPath{
parent: path,
value: PropertyName(name),
}
}
// WithIndex returns new [PropertyPath] with appended [ArrayIndex] to the end of the list.
func (path *PropertyPath) WithIndex(index int) *PropertyPath {
return &PropertyPath{
parent: path,
value: ArrayIndex(index),
}
}
// Elements returns property path as a slice of [PropertyPathElement].
// It returns nil if property path is nil (empty).
func (path *PropertyPath) Elements() []PropertyPathElement {
if path == nil || path.value == nil {
return nil
}
length := path.Len()
elements := make([]PropertyPathElement, length)
i := length - 1
element := path
for element != nil {
elements[i] = element.value
element = element.parent
i--
}
return elements
}
// Len returns count of property path elements.
func (path *PropertyPath) Len() int {
length := 0
element := path
for element != nil {
length++
element = element.parent
}
return length
}
// String is used to format property path to a string.
func (path *PropertyPath) String() string {
elements := path.Elements()
count := 0
for _, element := range elements {
if s, ok := element.(PropertyName); ok {
count += len(s)
} else {
count += 2
}
}
s := strings.Builder{}
s.Grow(count)
for i, element := range elements {
name := element.String()
if element.IsIndex() {
s.WriteString("[" + name + "]")
} else if isIdentifier(name) {
if i > 0 {
s.WriteString(".")
}
s.WriteString(name)
} else {
s.WriteString("['")
writePropertyName(&s, name)
s.WriteString("']")
}
}
return s.String()
}
// MarshalText will marshal property path value to a string.
func (path *PropertyPath) MarshalText() (text []byte, err error) {
return []byte(path.String()), nil
}
// UnmarshalText unmarshal string representation of property path into [PropertyPath].
func (path *PropertyPath) UnmarshalText(text []byte) error {
parser := pathParser{}
p, err := parser.Parse(string(text))
if p == nil || err != nil {
return err
}
*path = *p
return nil
}
func isIdentifier(s string) bool {
if len(s) == 0 {
return false
}
for i, c := range s {
if i == 0 && !isFirstIdentifierChar(c) {
return false
}
if i > 0 && !isIdentifierChar(c) {
return false
}
}
return true
}
func isFirstIdentifierChar(c rune) bool {
return unicode.IsLetter(c) || c == '$' || c == '_'
}
func isIdentifierChar(c rune) bool {
return unicode.IsLetter(c) || unicode.IsDigit(c) || c == '$' || c == '_'
}
func writePropertyName(s *strings.Builder, name string) {
for _, c := range name {
if c == '\'' || c == '\\' {
s.WriteRune('\\')
}
s.WriteRune(c)
}
}
type parsingState byte
const (
initialState parsingState = iota
beginIdentifierState
identifierState
beginIndexState
indexState
bracketedNameState
endBracketedNameState
closeBracketState
)
type pathParser struct {
buffer strings.Builder
state parsingState
isEscape bool
index int
pathIndex int
path *PropertyPath
}
func (parser *pathParser) Parse(encodedPath string) (*PropertyPath, error) {
if len(encodedPath) == 0 {
return nil, nil
}
for i, c := range encodedPath {
parser.index = i
if err := parser.handleNext(c); err != nil {
return nil, err
}
}
return parser.finish()
}
func (parser *pathParser) handleNext(c rune) error {
var err error
switch c {
case '[':
err = parser.handleOpenBracket(c)
case ']':
err = parser.handleCloseBracket(c)
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
err = parser.handleDigit(c)
case '\'':
err = parser.handleQuote(c)
case '\\':
err = parser.handleEscape(c)
case '.':
err = parser.handlePoint(c)
default:
err = parser.handleOther(c)
}
return err
}
func (parser *pathParser) handleOpenBracket(c rune) error {
switch parser.state {
case beginIdentifierState, beginIndexState, indexState, endBracketedNameState:
return parser.newCharError(c, "unexpected char")
case identifierState:
if parser.buffer.Len() > 0 {
parser.addProperty()
}
parser.state = beginIndexState
case initialState, closeBracketState:
parser.state = beginIndexState
case bracketedNameState:
parser.buffer.WriteRune(c)
}
return nil
}
func (parser *pathParser) handleCloseBracket(c rune) error {
switch parser.state {
case indexState:
err := parser.addIndex()
if err != nil {
return err
}
parser.state = closeBracketState
case bracketedNameState:
parser.buffer.WriteRune(c)
case endBracketedNameState:
parser.addProperty()
parser.state = closeBracketState
default:
return parser.newCharError(c, "unexpected close bracket")
}
return nil
}
func (parser *pathParser) handleDigit(c rune) error {
switch parser.state {
case beginIndexState, indexState:
parser.state = indexState
case bracketedNameState, identifierState:
case initialState, beginIdentifierState:
return parser.newCharError(c, "unexpected identifier character")
default:
return parser.newCharError(c, "invalid array index")
}
parser.buffer.WriteRune(c)
return nil
}
func (parser *pathParser) handlePoint(c rune) error {
switch parser.state {
case beginIdentifierState, identifierState:
if parser.buffer.Len() == 0 {
return parser.newCharError(c, "unexpected point")
}
parser.addProperty()
parser.state = beginIdentifierState
case bracketedNameState:
parser.buffer.WriteRune(c)
case closeBracketState:
parser.state = beginIdentifierState
default:
return parser.newCharError(c, "unexpected point")
}
return nil
}
func (parser *pathParser) handleQuote(c rune) error {
if parser.isEscape {
parser.buffer.WriteRune(c)
parser.isEscape = false
return nil
}
switch parser.state {
case beginIndexState:
parser.state = bracketedNameState
case bracketedNameState:
parser.state = endBracketedNameState
default:
return parser.newCharError(c, "unexpected quote")
}
return nil
}
func (parser *pathParser) handleEscape(c rune) error {
if parser.state != bracketedNameState {
return parser.newCharError(c, "unexpected backslash")
}
if parser.isEscape {
parser.buffer.WriteRune(c)
parser.isEscape = false
} else {
parser.isEscape = true
}
return nil
}
func (parser *pathParser) handleOther(c rune) error {
switch parser.state {
case beginIndexState, indexState:
return parser.newCharError(c, "unexpected array index character")
case initialState, beginIdentifierState, identifierState:
if !isFirstIdentifierChar(c) {
return parser.newCharError(c, "unexpected identifier char")
}
parser.state = identifierState
case closeBracketState, endBracketedNameState:
return parser.newCharError(c, "unexpected char")
}
parser.buffer.WriteRune(c)
return nil
}
func (parser *pathParser) addProperty() {
parser.path = parser.path.WithProperty(parser.buffer.String())
parser.pathIndex++
parser.buffer.Reset()
}
func (parser *pathParser) addIndex() error {
s := parser.buffer.String()
u, err := strconv.ParseUint(s, 10, 0)
if err != nil {
if errors.Is(err, strconv.ErrRange) {
return parser.newProcessingError("value out of range: " + s)
}
return parser.newProcessingError("invalid array index: " + s)
}
if u > math.MaxInt {
return parser.newProcessingError("value out of range: " + s)
}
parser.path = parser.path.WithIndex(int(u))
parser.pathIndex++
parser.buffer.Reset()
return nil
}
func (parser *pathParser) finish() (*PropertyPath, error) {
switch parser.state {
case beginIdentifierState, identifierState:
if parser.buffer.Len() == 0 {
return nil, parser.newError("incomplete property name")
}
parser.path = parser.path.WithProperty(parser.buffer.String())
case beginIndexState, indexState:
return nil, parser.newError("incomplete array index")
case bracketedNameState, endBracketedNameState:
return nil, parser.newError("incomplete bracketed property name")
case closeBracketState:
default:
return nil, parser.newError("unexpected parsing state")
}
return parser.path, nil
}
func (parser *pathParser) newError(message string) *pathParsingError {
return &pathParsingError{
pathIndex: parser.pathIndex,
message: message,
}
}
func (parser *pathParser) newCharError(char rune, message string) *pathParsingCharError {
return &pathParsingCharError{
index: parser.index,
pathIndex: parser.pathIndex,
char: char,
message: message,
}
}
func (parser *pathParser) newProcessingError(message string) *pathParsingProcessingError {
return &pathParsingProcessingError{
pathIndex: parser.pathIndex,
message: message,
}
}
type pathParsingError struct {
pathIndex int
message string
}
func (err *pathParsingError) Error() string {
return fmt.Sprintf("parsing path element #%d: %s", err.pathIndex, err.message)
}
type pathParsingCharError struct {
index int
pathIndex int
char rune
message string
}
func (err *pathParsingCharError) Error() string {
return fmt.Sprintf(
"parsing path element #%d at char #%d %q: %s",
err.pathIndex,
err.index,
err.char,
err.message,
)
}
type pathParsingProcessingError struct {
pathIndex int
message string
}
func (err *pathParsingProcessingError) Error() string {
return fmt.Sprintf("parsing path element #%d: %s", err.pathIndex, err.message)
}