File: field_config.go

package info (click to toggle)
golang-github-hashicorp-go-bexpr 0.1.2-3
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 356 kB
  • sloc: makefile: 50
file content (352 lines) | stat: -rw-r--r-- 10,778 bytes parent folder | download | duplicates (2)
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
package bexpr

import (
	"fmt"
	"reflect"
	"strings"
)

// Function type for usage with a SelectorConfiguration
type FieldValueCoercionFn func(value string) (interface{}, error)

// Strongly typed name of a field
type FieldName string

// Used to represent an arbitrary field name
const FieldNameAny FieldName = ""

type FieldPath []FieldName

func (path FieldPath) String() string {
	var parts []string

	for _, part := range path {
		if part == FieldNameAny {
			parts = append(parts, "<any>")
		} else {
			parts = append(parts, string(part))
		}
	}

	return strings.Join(parts, ".")
}

// The FieldConfiguration struct represents how boolean expression
// validation and preparation should work for the given field. A field
// in this case is a single element of a selector.
//
// Example: foo.bar.baz has 3 fields separate by '.' characters.
type FieldConfiguration struct {
	// Name to use when looking up fields within a struct. This is useful when
	// the name(s) you want to expose to users writing the expressions does not
	// exactly match the Field name of the structure. If this is empty then the
	// user provided name will be used
	StructFieldName string

	// Nested field configurations
	SubFields FieldConfigurations

	// Function to run on the raw string value present in the expression
	// syntax to coerce into whatever form the MatchExpressionEvaluator wants
	// The coercion happens only once and will then be passed as the `value`
	// parameter to all EvaluateMatch invocations on the MatchExpressionEvaluator.
	CoerceFn FieldValueCoercionFn

	// List of MatchOperators supported for this field. This configuration
	// is used to pre-validate an expressions fields before execution.
	SupportedOperations []MatchOperator
}

// Represents all the valid fields and their corresponding configuration
type FieldConfigurations map[FieldName]*FieldConfiguration

func generateFieldConfigurationInterface(rtype reflect.Type) (FieldConfigurations, bool) {
	// Handle those types that implement our interface
	if rtype.Implements(reflect.TypeOf((*MatchExpressionEvaluator)(nil)).Elem()) {
		// TODO (mkeeler) Do we need to new a value just to call the function? Potentially we can
		// lookup the func and invoke it with a nil pointer?
		value := reflect.New(rtype)
		// have to take the Elem() of the new value because New gives us a ptr to the type that
		// we checked if it implements the interface
		configs := value.Elem().Interface().(MatchExpressionEvaluator).FieldConfigurations()
		return configs, true
	}

	return nil, false
}

func generateFieldConfigurationInternal(rtype reflect.Type) (*FieldConfiguration, error) {
	if fields, ok := generateFieldConfigurationInterface(rtype); ok {
		return &FieldConfiguration{
			SubFields: fields,
		}, nil
	}

	// must be done after checking for interface implementing
	rtype = derefType(rtype)

	// Handle primitive types
	if coerceFn, ok := primitiveCoercionFns[rtype.Kind()]; ok {
		ops := []MatchOperator{MatchEqual, MatchNotEqual}

		if rtype.Kind() == reflect.String {
			ops = append(ops, MatchIn, MatchNotIn, MatchMatches, MatchNotMatches)
		}

		return &FieldConfiguration{
			CoerceFn:            coerceFn,
			SupportedOperations: ops,
		}, nil
	}

	// Handle compound types
	switch rtype.Kind() {
	case reflect.Map:
		return generateMapFieldConfiguration(derefType(rtype.Key()), rtype.Elem())
	case reflect.Array, reflect.Slice:
		return generateSliceFieldConfiguration(rtype.Elem())
	case reflect.Struct:
		subfields, err := generateStructFieldConfigurations(rtype)
		if err != nil {
			return nil, err
		}

		return &FieldConfiguration{
			SubFields: subfields,
		}, nil

	default: // unsupported types are just not filterable
		return nil, nil
	}
}

func generateSliceFieldConfiguration(elemType reflect.Type) (*FieldConfiguration, error) {
	if coerceFn, ok := primitiveCoercionFns[elemType.Kind()]; ok {
		// slices of primitives have somewhat different supported operations
		return &FieldConfiguration{
			CoerceFn:            coerceFn,
			SupportedOperations: []MatchOperator{MatchIn, MatchNotIn, MatchIsEmpty, MatchIsNotEmpty},
		}, nil
	}

	subfield, err := generateFieldConfigurationInternal(elemType)
	if err != nil {
		return nil, err
	}

	cfg := &FieldConfiguration{
		SupportedOperations: []MatchOperator{MatchIsEmpty, MatchIsNotEmpty},
	}

	if subfield != nil && len(subfield.SubFields) > 0 {
		cfg.SubFields = subfield.SubFields
	}

	return cfg, nil
}

func generateMapFieldConfiguration(keyType, valueType reflect.Type) (*FieldConfiguration, error) {
	switch keyType.Kind() {
	case reflect.String:
		subfield, err := generateFieldConfigurationInternal(valueType)
		if err != nil {
			return nil, err
		}

		cfg := &FieldConfiguration{
			CoerceFn:            CoerceString,
			SupportedOperations: []MatchOperator{MatchIsEmpty, MatchIsNotEmpty, MatchIn, MatchNotIn},
		}

		if subfield != nil {
			cfg.SubFields = FieldConfigurations{
				FieldNameAny: subfield,
			}
		}

		return cfg, nil

	default:
		// For maps with non-string keys we can really only do emptiness checks
		// and cannot index into them at all
		return &FieldConfiguration{
			SupportedOperations: []MatchOperator{MatchIsEmpty, MatchIsNotEmpty},
		}, nil
	}
}

func generateStructFieldConfigurations(rtype reflect.Type) (FieldConfigurations, error) {
	fieldConfigs := make(FieldConfigurations)

	for i := 0; i < rtype.NumField(); i++ {
		field := rtype.Field(i)

		fieldTag := field.Tag.Get("bexpr")

		var fieldNames []string

		if field.PkgPath != "" {
			// we cant handle unexported fields using reflection
			continue
		}

		if fieldTag != "" {
			parts := strings.Split(fieldTag, ",")

			if len(parts) > 0 {
				if parts[0] == "-" {
					continue
				}

				fieldNames = parts
			} else {
				fieldNames = append(fieldNames, field.Name)
			}
		} else {
			fieldNames = append(fieldNames, field.Name)
		}

		cfg, err := generateFieldConfigurationInternal(field.Type)
		if err != nil {
			return nil, err
		}
		cfg.StructFieldName = field.Name

		// link the config to all the correct names
		for _, name := range fieldNames {
			fieldConfigs[FieldName(name)] = cfg
		}
	}

	return fieldConfigs, nil
}

// `generateFieldConfigurations` can be used to generate the `FieldConfigurations` map
// It supports generating configurations for either a `map[string]*` or a `struct` as the `topLevelType`
//
// Internally within the top level type the following is supported:
//
// Primitive Types:
//    strings
//    integers (all width types and signedness)
//    floats (32 and 64 bit)
//    bool
//
// Compound Types
//   `map[*]*`
//       - Supports emptiness checking. Does not support further selector nesting.
//   `map[string]*`
//       - Supports in/contains operations on the keys.
//   `map[string]<supported type>`
//       - Will have a single subfield with name `FieldNameAny` (wildcard) and the rest of
//         the field configuration will come from the `<supported type>`
//   `[]*`
//       - Supports emptiness checking only. Does not support further selector nesting.
//   `[]<supported primitive type>`
//       - Supports in/contains operations against the primitive values.
//   `[]<supported compund type>`
//       - Will have subfields with the configuration of whatever the supported
//         compound type is.
//       - Does not support indexing of individual values like a map does currently
//         and with the current evaluation logic slices of slices will mostly be
//         handled as if they were flattened. One thing that cannot be done is
//         to be able to perform emptiness/contains checking against the internal
//         slice.
//   structs
//       - No operations are supported on the struct itself
//       - Will have subfield configurations generated for the fields of the struct.
//       - A struct tag like `bexpr:"<name>"` allows changing the name that allows indexing
//         into the subfield.
//       - By default unexported fields of a struct are not selectable. If The struct tag is
//         present then this behavior is overridden.
//       - Exported fields can be made unselectable by adding a tag to the field like `bexpr:"-"`
func GenerateFieldConfigurations(topLevelType interface{}) (FieldConfigurations, error) {
	return generateFieldConfigurations(reflect.TypeOf(topLevelType))
}

func generateFieldConfigurations(rtype reflect.Type) (FieldConfigurations, error) {
	if fields, ok := generateFieldConfigurationInterface(rtype); ok {
		return fields, nil
	}

	// Do this after we check for interface implementation
	rtype = derefType(rtype)

	switch rtype.Kind() {
	case reflect.Struct:
		fields, err := generateStructFieldConfigurations(rtype)
		return fields, err
	case reflect.Map:
		if rtype.Key().Kind() != reflect.String {
			return nil, fmt.Errorf("Cannot generate FieldConfigurations for maps with keys that are not strings")
		}

		elemType := rtype.Elem()

		field, err := generateFieldConfigurationInternal(elemType)
		if err != nil {
			return nil, err
		}

		if field == nil {
			return nil, nil
		}

		return FieldConfigurations{
			FieldNameAny: field,
		}, nil
	}

	return nil, fmt.Errorf("Invalid top level type - can only use structs, map[string]* or an MatchExpressionEvaluator")
}

func (config *FieldConfiguration) stringInternal(builder *strings.Builder, level int, path string) {
	fmt.Fprintf(builder, "%sPath: %s, StructFieldName: %s, CoerceFn: %p, SupportedOperations: %v\n", strings.Repeat("   ", level), path, config.StructFieldName, config.CoerceFn, config.SupportedOperations)
	if len(config.SubFields) > 0 {
		config.SubFields.stringInternal(builder, level+1, path)
	}
}

func (config *FieldConfiguration) String() string {
	var builder strings.Builder
	config.stringInternal(&builder, 0, "")
	return builder.String()
}

func (configs FieldConfigurations) stringInternal(builder *strings.Builder, level int, path string) {
	for fieldName, cfg := range configs {
		newPath := string(fieldName)
		if level > 0 {
			newPath = fmt.Sprintf("%s.%s", path, fieldName)
		}
		cfg.stringInternal(builder, level, newPath)
	}
}

func (configs FieldConfigurations) String() string {
	var builder strings.Builder
	configs.stringInternal(&builder, 0, "")
	return builder.String()
}

type FieldConfigurationWalkFn func(path FieldPath, config *FieldConfiguration) bool

func (configs FieldConfigurations) walk(path FieldPath, walkFn FieldConfigurationWalkFn) bool {
	for fieldName, fieldConfig := range configs {
		newPath := append(path, fieldName)

		if !walkFn(newPath, fieldConfig) {
			return false
		}

		if !fieldConfig.SubFields.walk(newPath, walkFn) {
			return false
		}
	}

	return true
}

func (configs FieldConfigurations) Walk(walkFn FieldConfigurationWalkFn) bool {
	return configs.walk(nil, walkFn)
}