forked from MisterMX/crossbuilder
-
Notifications
You must be signed in to change notification settings - Fork 0
/
fieldpath.go
149 lines (134 loc) · 3.89 KB
/
fieldpath.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
package build
import (
"reflect"
"strings"
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
"github.com/pkg/errors"
)
const (
errEmptyPath = "the given path is empty"
errParseFieldPath = "cannot parse fieldpath"
errFmtNotStruct = "expected struct type, but got %s"
errFmtNotArrayOrSlice = "expected array or slice type but got %s"
errFmtFieldNotFound = "no field with JSON key '%s'"
errGetStructField = "cannot get field"
errMapTypeNotSupported = "static path validation is not supported for maps"
errFmtInvalidFieldPath = "invalid field path '%s'"
)
// ValidateFieldPath checks if the JSON path exists for the given object.
func ValidateFieldPath(obj interface{}, path string, knownPaths []fieldpath.Segments) error {
segments, err := fieldpath.Parse(path)
if err != nil {
return errors.Wrap(err, errParseFieldPath)
}
if len(segments) == 0 {
return errors.New(errEmptyPath)
}
if isKnownPath(segments, knownPaths) {
return nil // path is a registered path
}
return errors.Wrap(validatePath(obj, segments), path)
}
func validatePath(obj interface{}, segments fieldpath.Segments) error {
current := reflect.TypeOf(obj)
for _, segment := range segments {
if current.Kind() == reflect.Ptr {
current = current.Elem()
}
switch segment.Type {
case fieldpath.SegmentField:
var err error
current, err = getObjectField(current, segment.Field)
if err != nil {
return errors.Wrap(err, errGetStructField)
}
case fieldpath.SegmentIndex:
if current.Kind() != reflect.Array && current.Kind() != reflect.Slice {
return errors.Errorf(errFmtNotArrayOrSlice, current.Kind())
}
current = current.Elem()
}
}
return nil // Path exists
}
func isKnownPath(path fieldpath.Segments, knownPaths []fieldpath.Segments) bool {
for _, known := range knownPaths {
if len(path) != len(known) {
continue
}
for i, knownSeg := range known {
if path[i] != knownSeg {
continue
}
}
return true
}
return false
}
func parseFieldPaths(paths []string) ([]fieldpath.Segments, error) {
parsed := make([]fieldpath.Segments, len(paths))
for i, p := range paths {
seg, err := fieldpath.Parse(p)
if err != nil {
return nil, errors.Wrapf(err, errFmtInvalidFieldPath, p)
}
parsed[i] = seg
}
return parsed, nil
}
func getObjectField(obj reflect.Type, jsonKey string) (reflect.Type, error) {
if obj.Kind() == reflect.Map {
return nil, errors.New(errMapTypeNotSupported)
}
if obj.Kind() != reflect.Struct {
return nil, errors.Errorf(errFmtNotStruct, obj.Kind())
}
for i := 0; i < obj.NumField(); i++ {
field := obj.Field(i)
tag := field.Tag.Get("json")
name, options := parseTag(tag)
if options.Contains("inline") {
res, err := getObjectField(field.Type, jsonKey)
if err == nil && res != nil {
return res, nil
}
} else if name == jsonKey {
return field.Type, nil
}
}
return nil, errors.Errorf(errFmtFieldNotFound, jsonKey)
}
// The following code is extracted from
// https://cs.opensource.google/go/go/+/release-branch.go1.17:src/encoding/json/tags.go
// tagOptions is the string following a comma in a struct field's "json"
// tag, or the empty string. It does not include the leading comma.
type tagOptions string
// parseTag splits a struct field's json tag into its name and
// comma-separated options.
func parseTag(tag string) (string, tagOptions) {
if idx := strings.Index(tag, ","); idx != -1 {
return tag[:idx], tagOptions(tag[idx+1:])
}
return tag, tagOptions("")
}
// Contains reports whether a comma-separated list of options
// contains a particular substr flag. substr must be surrounded by a
// string boundary or commas.
func (o tagOptions) Contains(optionName string) bool {
if len(o) == 0 {
return false
}
s := string(o)
for s != "" {
var next string
i := strings.Index(s, ",")
if i >= 0 {
s, next = s[:i], s[i+1:]
}
if s == optionName {
return true
}
s = next
}
return false
}