path.go   F
last analyzed

Size/Duplication

Total Lines 495
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
cc 110
eloc 305
dl 0
loc 495
ccs 21
cts 21
cp 1
crap 110
rs 2
c 0
b 0
f 0

35 Methods

Rating   Name   Duplication   Size   Complexity  
A validation.PropertyName.String 0 2 1
A validation.*pathParser.Parse 0 12 4
A validation.isFirstIdentifierChar 0 2 3
A validation.*pathParser.handleQuote 0 18 5
A validation.*pathParser.handleEscape 0 13 3
B validation.*PropertyPath.String 0 30 7
A validation.*pathParser.newProcessingError 0 4 1
B validation.*pathParser.handleNext 0 21 8
A validation.*PropertyPath.With 0 6 2
A validation.*pathParsingCharError.Error 0 7 1
A validation.*PropertyPath.Len 0 8 2
A validation.NewPropertyPath 0 4 1
A validation.*pathParser.addIndex 0 19 4
A validation.writePropertyName 0 6 4
A validation.*PropertyPath.WithProperty 0 4 1
A validation.*pathParser.handleDigit 0 13 5
A validation.*pathParsingProcessingError.Error 0 2 1
A validation.*pathParser.newError 0 4 1
B validation.*pathParser.finish 0 17 7
B validation.*pathParser.handlePoint 0 17 6
B validation.isIdentifier 0 14 7
A validation.ArrayIndex.IsIndex 0 2 1
A validation.*PropertyPath.Elements 0 17 4
B validation.*pathParser.handleOpenBracket 0 16 6
A validation.*PropertyPath.WithIndex 0 4 1
A validation.PropertyName.IsIndex 0 2 1
B validation.*pathParser.handleCloseBracket 0 18 6
A validation.*PropertyPath.MarshalText 0 2 1
A validation.*pathParser.handleOther 0 15 5
A validation.ArrayIndex.String 0 2 1
A validation.isIdentifierChar 0 2 4
A validation.*pathParsingError.Error 0 2 1
A validation.*pathParser.addProperty 0 4 1
A validation.*PropertyPath.UnmarshalText 0 10 3
A validation.*pathParser.newCharError 0 6 1
1
package validation
2
3
import (
4
	"errors"
5
	"fmt"
6
	"math"
7
	"strconv"
8
	"strings"
9
	"unicode"
10
)
11
12
// PropertyPathElement is a part of the [PropertyPath].
13
type PropertyPathElement interface {
14
	// IsIndex can be used to determine whether an element is a string (property name) or
15
	// an index array.
16
	IsIndex() bool
17
	fmt.Stringer
18
}
19
20
// PropertyName holds up property name value under [PropertyPath].
21
type PropertyName string
22 1
23
// IsIndex on [PropertyName] always returns false.
24
func (p PropertyName) IsIndex() bool {
25
	return false
26
}
27 1
28
// String returns property name as is.
29
func (p PropertyName) String() string {
30
	return string(p)
31
}
32
33
// ArrayIndex holds up array index value under [PropertyPath].
34
type ArrayIndex int
35 1
36
// IsIndex on [ArrayIndex] always returns true.
37
func (a ArrayIndex) IsIndex() bool {
38
	return true
39
}
40 1
41
// String returns array index values converted into a string.
42
func (a ArrayIndex) String() string {
43
	return strconv.Itoa(int(a))
44
}
45
46
// PropertyPath is generated by the validator and indicates how it reached the invalid value
47
// from the root element. Property path is denoted by dots, while array access
48
// is denoted by square brackets. For example, "book.keywords[0]" means that the violation
49
// occurred on the first element of array "keywords" in the "book" object.
50
//
51
// Internally [PropertyPath] is a linked list. You can create a new path using [PropertyPath.WithProperty]
52
// or [PropertyPath.WithIndex] methods. [PropertyPath] should always be used as a pointer value.
53
// Nil value is a valid value that means that the property path is empty.
54
type PropertyPath struct {
55
	parent *PropertyPath
56
	value  PropertyPathElement
57
}
58
59 1
// NewPropertyPath creates a [PropertyPath] from the list of elements. If the list is empty nil will be returned.
60 1
// Nil value is a valid value that means that the property path is empty.
61 1
func NewPropertyPath(elements ...PropertyPathElement) *PropertyPath {
62
	var path *PropertyPath
63 1
64
	return path.With(elements...)
65
}
66
67
// With returns new [PropertyPath] with appended elements to the end of the list.
68 1
func (path *PropertyPath) With(elements ...PropertyPathElement) *PropertyPath {
69
	current := path
70
	for _, element := range elements {
71
		current = &PropertyPath{parent: current, value: element}
72
	}
73
	return current
74
}
75
76 1
// WithProperty returns new [PropertyPath] with appended [PropertyName] to the end of the list.
77
func (path *PropertyPath) WithProperty(name string) *PropertyPath {
78
	return &PropertyPath{
79
		parent: path,
80
		value:  PropertyName(name),
81
	}
82
}
83
84 1
// WithIndex returns new [PropertyPath] with appended [ArrayIndex] to the end of the list.
85 1
func (path *PropertyPath) WithIndex(index int) *PropertyPath {
86 1
	return &PropertyPath{
87 1
		parent: path,
88 1
		value:  ArrayIndex(index),
89
	}
90 1
}
91 1
92 1
// Elements returns property path as a slice of [PropertyPathElement].
93
// It returns nil if property path is nil (empty).
94
func (path *PropertyPath) Elements() []PropertyPathElement {
95 1
	if path == nil || path.value == nil {
96
		return nil
97
	}
98 1
99
	length := path.Len()
100
	elements := make([]PropertyPathElement, length)
101
102
	i := length - 1
103 1
	element := path
104
	for element != nil {
105
		elements[i] = element.value
106
		element = element.parent
107
		i--
108
	}
109
110
	return elements
111
}
112
113
// Len returns count of property path elements.
114
func (path *PropertyPath) Len() int {
115
	length := 0
116
	element := path
117
	for element != nil {
118
		length++
119
		element = element.parent
120
	}
121
	return length
122
}
123
124
// String is used to format property path to a string.
125
func (path *PropertyPath) String() string {
126
	elements := path.Elements()
127
	count := 0
128
	for _, element := range elements {
129
		if s, ok := element.(PropertyName); ok {
130
			count += len(s)
131
		} else {
132
			count += 2
133
		}
134
	}
135
136
	s := strings.Builder{}
137
	s.Grow(count)
138
	for i, element := range elements {
139
		name := element.String()
140
		if element.IsIndex() {
141
			s.WriteString("[" + name + "]")
142
		} else if isIdentifier(name) {
143
			if i > 0 {
144
				s.WriteString(".")
145
			}
146
			s.WriteString(name)
147
		} else {
148
			s.WriteString("['")
149
			writePropertyName(&s, name)
150
			s.WriteString("']")
151
		}
152
	}
153
154
	return s.String()
155
}
156
157
// MarshalText will marshal property path value to a string.
158
func (path *PropertyPath) MarshalText() (text []byte, err error) {
159
	return []byte(path.String()), nil
160
}
161
162
// UnmarshalText unmarshal string representation of property path into [PropertyPath].
163
func (path *PropertyPath) UnmarshalText(text []byte) error {
164
	parser := pathParser{}
165
	p, err := parser.Parse(string(text))
166
	if p == nil || err != nil {
167
		return err
168
	}
169
170
	*path = *p
171
172
	return nil
173
}
174
175
func isIdentifier(s string) bool {
176
	if len(s) == 0 {
177
		return false
178
	}
179
	for i, c := range s {
180
		if i == 0 && !isFirstIdentifierChar(c) {
181
			return false
182
		}
183
		if i > 0 && !isIdentifierChar(c) {
184
			return false
185
		}
186
	}
187
188
	return true
189
}
190
191
func isFirstIdentifierChar(c rune) bool {
192
	return unicode.IsLetter(c) || c == '$' || c == '_'
193
}
194
195
func isIdentifierChar(c rune) bool {
196
	return unicode.IsLetter(c) || unicode.IsDigit(c) || c == '$' || c == '_'
197
}
198
199
func writePropertyName(s *strings.Builder, name string) {
200
	for _, c := range name {
201
		if c == '\'' || c == '\\' {
202
			s.WriteRune('\\')
203
		}
204
		s.WriteRune(c)
205
	}
206
}
207
208
type parsingState byte
209
210
const (
211
	initialState parsingState = iota
212
	beginIdentifierState
213
	identifierState
214
215
	beginIndexState
216
	indexState
217
218
	bracketedNameState
219
	endBracketedNameState
220
221
	closeBracketState
222
)
223
224
type pathParser struct {
225
	buffer    strings.Builder
226
	state     parsingState
227
	isEscape  bool
228
	index     int
229
	pathIndex int
230
	path      *PropertyPath
231
}
232
233
func (parser *pathParser) Parse(encodedPath string) (*PropertyPath, error) {
234
	if len(encodedPath) == 0 {
235
		return nil, nil
236
	}
237
	for i, c := range encodedPath {
238
		parser.index = i
239
		if err := parser.handleNext(c); err != nil {
240
			return nil, err
241
		}
242
	}
243
244
	return parser.finish()
245
}
246
247
func (parser *pathParser) handleNext(c rune) error {
248
	var err error
249
250
	switch c {
251
	case '[':
252
		err = parser.handleOpenBracket(c)
253
	case ']':
254
		err = parser.handleCloseBracket(c)
255
	case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
256
		err = parser.handleDigit(c)
257
	case '\'':
258
		err = parser.handleQuote(c)
259
	case '\\':
260
		err = parser.handleEscape(c)
261
	case '.':
262
		err = parser.handlePoint(c)
263
	default:
264
		err = parser.handleOther(c)
265
	}
266
267
	return err
268
}
269
270
func (parser *pathParser) handleOpenBracket(c rune) error {
271
	switch parser.state {
272
	case beginIdentifierState, beginIndexState, indexState, endBracketedNameState:
273
		return parser.newCharError(c, "unexpected char")
274
	case identifierState:
275
		if parser.buffer.Len() > 0 {
276
			parser.addProperty()
277
		}
278
		parser.state = beginIndexState
279
	case initialState, closeBracketState:
280
		parser.state = beginIndexState
281
	case bracketedNameState:
282
		parser.buffer.WriteRune(c)
283
	}
284
285
	return nil
286
}
287
288
func (parser *pathParser) handleCloseBracket(c rune) error {
289
	switch parser.state {
290
	case indexState:
291
		err := parser.addIndex()
292
		if err != nil {
293
			return err
294
		}
295
		parser.state = closeBracketState
296
	case bracketedNameState:
297
		parser.buffer.WriteRune(c)
298
	case endBracketedNameState:
299
		parser.addProperty()
300
		parser.state = closeBracketState
301
	default:
302
		return parser.newCharError(c, "unexpected close bracket")
303
	}
304
305
	return nil
306
}
307
308
func (parser *pathParser) handleDigit(c rune) error {
309
	switch parser.state {
310
	case beginIndexState, indexState:
311
		parser.state = indexState
312
	case bracketedNameState, identifierState:
313
	case initialState, beginIdentifierState:
314
		return parser.newCharError(c, "unexpected identifier character")
315
	default:
316
		return parser.newCharError(c, "invalid array index")
317
	}
318
	parser.buffer.WriteRune(c)
319
320
	return nil
321
}
322
323
func (parser *pathParser) handlePoint(c rune) error {
324
	switch parser.state {
325
	case beginIdentifierState, identifierState:
326
		if parser.buffer.Len() == 0 {
327
			return parser.newCharError(c, "unexpected point")
328
		}
329
		parser.addProperty()
330
		parser.state = beginIdentifierState
331
	case bracketedNameState:
332
		parser.buffer.WriteRune(c)
333
	case closeBracketState:
334
		parser.state = beginIdentifierState
335
	default:
336
		return parser.newCharError(c, "unexpected point")
337
	}
338
339
	return nil
340
}
341
342
func (parser *pathParser) handleQuote(c rune) error {
343
	if parser.isEscape {
344
		parser.buffer.WriteRune(c)
345
		parser.isEscape = false
346
347
		return nil
348
	}
349
350
	switch parser.state {
351
	case beginIndexState:
352
		parser.state = bracketedNameState
353
	case bracketedNameState:
354
		parser.state = endBracketedNameState
355
	default:
356
		return parser.newCharError(c, "unexpected quote")
357
	}
358
359
	return nil
360
}
361
362
func (parser *pathParser) handleEscape(c rune) error {
363
	if parser.state != bracketedNameState {
364
		return parser.newCharError(c, "unexpected backslash")
365
	}
366
367
	if parser.isEscape {
368
		parser.buffer.WriteRune(c)
369
		parser.isEscape = false
370
	} else {
371
		parser.isEscape = true
372
	}
373
374
	return nil
375
}
376
377
func (parser *pathParser) handleOther(c rune) error {
378
	switch parser.state {
379
	case beginIndexState, indexState:
380
		return parser.newCharError(c, "unexpected array index character")
381
	case initialState, beginIdentifierState, identifierState:
382
		if !isFirstIdentifierChar(c) {
383
			return parser.newCharError(c, "unexpected identifier char")
384
		}
385
		parser.state = identifierState
386
	case closeBracketState, endBracketedNameState:
387
		return parser.newCharError(c, "unexpected char")
388
	}
389
	parser.buffer.WriteRune(c)
390
391
	return nil
392
}
393
394
func (parser *pathParser) addProperty() {
395
	parser.path = parser.path.WithProperty(parser.buffer.String())
396
	parser.pathIndex++
397
	parser.buffer.Reset()
398
}
399
400
func (parser *pathParser) addIndex() error {
401
	s := parser.buffer.String()
402
403
	u, err := strconv.ParseUint(s, 10, 0)
404
	if err != nil {
405
		if errors.Is(err, strconv.ErrRange) {
406
			return parser.newProcessingError("value out of range: " + s)
407
		}
408
		return parser.newProcessingError("invalid array index: " + s)
409
	}
410
	if u > math.MaxInt {
411
		return parser.newProcessingError("value out of range: " + s)
412
	}
413
414
	parser.path = parser.path.WithIndex(int(u))
415
	parser.pathIndex++
416
	parser.buffer.Reset()
417
418
	return nil
419
}
420
421
func (parser *pathParser) finish() (*PropertyPath, error) {
422
	switch parser.state {
423
	case beginIdentifierState, identifierState:
424
		if parser.buffer.Len() == 0 {
425
			return nil, parser.newError("incomplete property name")
426
		}
427
		parser.path = parser.path.WithProperty(parser.buffer.String())
428
	case beginIndexState, indexState:
429
		return nil, parser.newError("incomplete array index")
430
	case bracketedNameState, endBracketedNameState:
431
		return nil, parser.newError("incomplete bracketed property name")
432
	case closeBracketState:
433
	default:
434
		return nil, parser.newError("unexpected parsing state")
435
	}
436
437
	return parser.path, nil
438
}
439
440
func (parser *pathParser) newError(message string) *pathParsingError {
441
	return &pathParsingError{
442
		pathIndex: parser.pathIndex,
443
		message:   message,
444
	}
445
}
446
447
func (parser *pathParser) newCharError(char rune, message string) *pathParsingCharError {
448
	return &pathParsingCharError{
449
		index:     parser.index,
450
		pathIndex: parser.pathIndex,
451
		char:      char,
452
		message:   message,
453
	}
454
}
455
456
func (parser *pathParser) newProcessingError(message string) *pathParsingProcessingError {
457
	return &pathParsingProcessingError{
458
		pathIndex: parser.pathIndex,
459
		message:   message,
460
	}
461
}
462
463
type pathParsingError struct {
464
	pathIndex int
465
	message   string
466
}
467
468
func (err *pathParsingError) Error() string {
469
	return fmt.Sprintf("parsing path element #%d: %s", err.pathIndex, err.message)
470
}
471
472
type pathParsingCharError struct {
473
	index     int
474
	pathIndex int
475
	char      rune
476
	message   string
477
}
478
479
func (err *pathParsingCharError) Error() string {
480
	return fmt.Sprintf(
481
		"parsing path element #%d at char #%d %q: %s",
482
		err.pathIndex,
483
		err.index,
484
		err.char,
485
		err.message,
486
	)
487
}
488
489
type pathParsingProcessingError struct {
490
	pathIndex int
491
	message   string
492
}
493
494
func (err *pathParsingProcessingError) Error() string {
495
	return fmt.Sprintf("parsing path element #%d: %s", err.pathIndex, err.message)
496
}
497