-
Notifications
You must be signed in to change notification settings - Fork 8
/
filter.go
222 lines (180 loc) · 5.54 KB
/
filter.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
// Package filter implements a helper for Objects supporting filtered List operations.
package filter
import (
"fmt"
"net/url"
"reflect"
"strings"
"go.anx.io/go-anxcloud/pkg/api/types"
"go.anx.io/go-anxcloud/pkg/apis/common"
)
// NewHelper creates a new instance of a filter.Helper from the given object.
//
// The Helper will be set up for the fields in the given object (which commonly is a generic client Object, but
// has to be a struct or pointer to one) that have a `anxcloud:"filterable"` tag, retrieving their values,
// allowing easy access and building the filter query out of them automatically.
//
// The tag has an optional second field, `anxcloud:"filterable,foo", allowing to rename the field in the query
// to "foo". If no name is given, the name given in the encoding/json tag is used, if that is not given either,
// the name of the field is used.
//
// References to generic client Objects are resolved to their identifier, making the filter not set when the
// identifier of the referenced Object is empty.
func NewHelper(o interface{}) (Helper, error) {
helper := filterHelper{
values: make(map[string]interface{}),
fields: make(map[string]bool),
}
err := helper.parseObject(o)
if err != nil {
return nil, err
}
return helper, nil
}
type filterHelper struct {
values map[string]interface{}
fields map[string]bool
}
// Get returns the value and if it was set for a given named field.
func (f filterHelper) Get(field string) (interface{}, bool, error) {
if _, ok := f.fields[field]; !ok {
return nil, false, fmt.Errorf("%w: field %q is not configured as filterable", ErrUnknownField, field)
}
v, ok := f.values[field]
return v, ok, nil
}
// BuildQuery returns the query parameters to set for filtering.
func (f filterHelper) BuildQuery() url.Values {
values := make(url.Values)
for field, value := range f.values {
// we also support numbers and stuff - this should work with all we have left
values.Set(field, fmt.Sprintf("%v", value))
}
return values
}
func parseFilterableTag(field reflect.StructField) (isFilterable bool, filterName string, option string) {
filterName = field.Name
tag, ok := field.Tag.Lookup("anxcloud")
if !ok {
return false, "", ""
}
tagParts := strings.Split(tag, ",")
if len(tagParts) == 0 || tagParts[0] != "filterable" {
return false, "", ""
}
isFilterable = true
if len(tagParts) >= 2 && tagParts[1] != "" {
filterName = tagParts[1]
} else {
if jsonTag, ok := field.Tag.Lookup("json"); ok {
parts := strings.Split(jsonTag, ",")
if parts[0] != "" || len(parts) > 1 {
filterName = parts[0]
}
}
}
if len(tagParts) >= 3 {
option = tagParts[2]
}
return isFilterable, filterName, option
}
func parseOptionSingle(fieldValue reflect.Value) (reflect.Value, error) {
fieldType := fieldValue.Type()
fieldKind := fieldType.Kind()
if fieldKind == reflect.Slice || fieldKind == reflect.Array {
if fieldValue.Len() == 1 {
return fieldValue.Index(0), nil
} else if fieldValue.Len() == 0 {
return reflect.New(fieldType.Elem()).Elem(), nil
} else {
return reflect.Value{}, fmt.Errorf("%w: only a single value can be filtered", types.ErrInvalidFilter)
}
} else {
return reflect.Value{}, fmt.Errorf("%w: option 'single' can only be used on array or slice attributes", types.ErrInvalidFilter)
}
}
func extractFilterValue(fieldValue reflect.Value) (reflect.Value, error) {
fieldType := fieldValue.Type()
fieldKind := fieldType.Kind()
if fieldKind == reflect.Ptr {
fieldType = fieldType.Elem()
fieldKind = fieldType.Kind()
if fieldValue.IsNil() || fieldValue.IsZero() {
return reflect.Zero(fieldType), nil
}
fieldValue = fieldValue.Elem()
}
if fieldKind == reflect.Struct {
if fieldValue.Addr().Type() == reflect.TypeOf((*common.PartialResource)(nil)) {
fieldValue = fieldValue.FieldByName("Identifier")
} else if object, ok := fieldValue.Addr().Interface().(types.Object); ok {
identifier, err := types.GetObjectIdentifier(object, false)
if err != nil {
return reflect.Value{}, fmt.Errorf("Object referenced: %w", err)
}
fieldValue = reflect.ValueOf(identifier)
}
}
return fieldValue, nil
}
func (f *filterHelper) parseField(fieldValue reflect.Value, field reflect.StructField) error {
filterable, filterName, option := parseFilterableTag(field)
if !filterable {
return nil
}
f.fields[filterName] = true
if option == "single" {
val, err := parseOptionSingle(fieldValue)
if err != nil {
return fmt.Errorf("field %q: %w", filterName, err)
}
fieldValue = val
}
fieldValue, err := extractFilterValue(fieldValue)
if err != nil {
return err
}
if isSupportedPrimitive(fieldValue.Type().Kind()) && !fieldValue.IsZero() {
f.values[filterName] = fieldValue.Interface()
}
return nil
}
func (f *filterHelper) parseObject(v interface{}) error {
val := reflect.ValueOf(v)
valType := val.Type()
if valType.Kind() == reflect.Ptr {
val = val.Elem()
valType = val.Type()
}
if valType.Kind() != reflect.Struct {
return fmt.Errorf("%w: filter.Helper only works with structs or pointers to them", types.ErrTypeNotSupported)
}
numFields := val.NumField()
for i := 0; i < numFields; i++ {
if err := f.parseField(val.Field(i), valType.Field(i)); err != nil {
return err
}
}
return nil
}
func isSupportedPrimitive(k reflect.Kind) bool {
switch k {
case reflect.Bool,
reflect.Int,
reflect.Int8,
reflect.Int16,
reflect.Int32,
reflect.Int64,
reflect.Uint,
reflect.Uint8,
reflect.Uint16,
reflect.Uint32,
reflect.Uint64,
reflect.Float32,
reflect.Float64,
reflect.String:
return true
default:
return false
}
}