Skip to content

Commit 0b0d0c6

Browse files
authored
Merge pull request #136 from jpbetz/value-reflector
Add reflector based implementation of value interface
2 parents 67a7b8c + 925b736 commit 0b0d0c6

15 files changed

+1540
-5
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.idea

typed/merge.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -328,8 +328,7 @@ func (w *mergingWalker) visitMapItems(t *schema.Map, lhs, rhs value.Map) (errs V
328328
if rhs != nil {
329329
rhs.Iterate(func(key string, val value.Value) bool {
330330
if lhs != nil {
331-
if v, ok := lhs.Get(key); ok {
332-
v.Recycle()
331+
if lhs.Has(key) {
333332
return true
334333
}
335334
}

typed/parser.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,29 @@ func (p ParseableType) FromYAML(object YAMLObject) (*TypedValue, error) {
102102
return AsTyped(value.NewValueInterface(v), p.Schema, p.TypeRef)
103103
}
104104

105-
// FromUnstructured converts a go interface to a TypedValue. It will return an
105+
// FromUnstructured converts a go "interface{}" type, typically an
106+
// unstructured object in Kubernetes world, to a TypedValue. It returns an
106107
// error if the resulting object fails schema validation.
108+
// The provided interface{} must be one of: map[string]interface{},
109+
// map[interface{}]interface{}, []interface{}, int types, float types,
110+
// string or boolean. Nested interface{} must also be one of these types.
107111
func (p ParseableType) FromUnstructured(in interface{}) (*TypedValue, error) {
108112
return AsTyped(value.NewValueInterface(in), p.Schema, p.TypeRef)
109113
}
110114

115+
// FromStructured converts a go "interface{}" type, typically an structured object in
116+
// Kubernetes, to a TypedValue. It will return an error if the resulting object fails
117+
// schema validation. The provided "interface{}" value must be a pointer so that the
118+
// value can be modified via reflection. The provided "interface{}" may contain structs
119+
// and types that are converted to Values by the jsonMarshaler interface.
120+
func (p ParseableType) FromStructured(in interface{}) (*TypedValue, error) {
121+
v, err := value.NewValueReflect(in)
122+
if err != nil {
123+
return nil, fmt.Errorf("error creating struct value reflector: %v", err)
124+
}
125+
return AsTyped(v, p.Schema, p.TypeRef)
126+
}
127+
111128
// DeducedParseableType is a ParseableType that deduces the type from
112129
// the content of the object.
113130
var DeducedParseableType ParseableType = createOrDie(YAMLObject(`types:

typed/union.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ func (u *union) Normalize(old, new, out value.Map) error {
243243
return fmt.Errorf("multiple fields set without discriminator change: %v", ns)
244244
}
245245

246-
// Update discriminiator if it needs to be deduced.
246+
// Set discriminiator if it needs to be deduced.
247247
if u.deduceInvalidDiscriminator && len(ns) == 1 {
248248
u.d.Set(out, u.dn.toDiscriminated(*ns.One()))
249249
}

typed/validate_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,3 +288,98 @@ func TestSchemaSchema(t *testing.T) {
288288
t.Fatalf("failed to create schemaschema: %v", err)
289289
}
290290
}
291+
292+
func BenchmarkValidateStructured(b *testing.B) {
293+
type Primitives struct {
294+
s string
295+
i int64
296+
f float64
297+
b bool
298+
}
299+
300+
primitive1 := Primitives{s: "string1"}
301+
primitive2 := Primitives{i: 100}
302+
primitive3 := Primitives{f: 3.14}
303+
primitive4 := Primitives{b: true}
304+
305+
type Example struct {
306+
listOfPrimitives []Primitives
307+
mapOfPrimitives map[string]Primitives
308+
mapOfLists map[string][]Primitives
309+
}
310+
311+
tests := []struct {
312+
name string
313+
rootTypeName string
314+
schema typed.YAMLObject
315+
object interface{}
316+
}{
317+
{
318+
name: "struct",
319+
rootTypeName: "example",
320+
schema: `types:
321+
- name: example
322+
map:
323+
fields:
324+
- name: listOfPrimitives
325+
type:
326+
list:
327+
elementType:
328+
namedType: primitives
329+
- name: mapOfPrimitives
330+
type:
331+
map:
332+
elementType:
333+
namedType: primitives
334+
- name: mapOfLists
335+
type:
336+
map:
337+
elementType:
338+
list:
339+
elementType:
340+
namedType: primitives
341+
- name: primitives
342+
map:
343+
fields:
344+
- name: s
345+
type:
346+
scalar: string
347+
- name: i
348+
type:
349+
scalar: numeric
350+
- name: f
351+
type:
352+
scalar: numeric
353+
- name: b
354+
type:
355+
scalar: boolean
356+
`,
357+
object: &Example{
358+
listOfPrimitives: []Primitives{primitive1, primitive2, primitive3, primitive4},
359+
mapOfPrimitives: map[string]Primitives{"1": primitive1, "2": primitive2, "3": primitive3, "4": primitive4},
360+
mapOfLists: map[string][]Primitives{
361+
"1": {primitive1, primitive2, primitive3, primitive4},
362+
"2": {primitive1, primitive2, primitive3, primitive4},
363+
},
364+
},
365+
},
366+
}
367+
368+
for _, test := range tests {
369+
b.Run(test.name, func(b *testing.B) {
370+
parser, err := typed.NewParser(test.schema)
371+
if err != nil {
372+
b.Fatalf("failed to create schema: %v", err)
373+
}
374+
pt := parser.Type(test.rootTypeName)
375+
376+
b.ReportAllocs()
377+
for n := 0; n < b.N; n++ {
378+
tv, err := pt.FromStructured(test.object)
379+
if err != nil {
380+
b.Errorf("failed to parse/validate yaml: %v\n%v", err, tv)
381+
}
382+
}
383+
})
384+
}
385+
}

value/fields.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ type Field struct {
2727
Value Value
2828
}
2929

30-
// FieldList is a list of key-value pairs. Each field is expected to
30+
// FieldList is a list of key-value pairs. Each field is expectUpdated to
3131
// have a different name.
3232
type FieldList []Field
3333

value/jsontagutil.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
Copyright 2019 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package value
18+
19+
import (
20+
"fmt"
21+
"reflect"
22+
"strings"
23+
)
24+
25+
// TODO: This implements the same functionality as https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go#L236
26+
// but is based on the highly efficient approach from https://golang.org/src/encoding/json/encode.go
27+
28+
func lookupJsonTags(f reflect.StructField) (name string, omit bool, inline bool, omitempty bool) {
29+
tag := f.Tag.Get("json")
30+
if tag == "-" {
31+
return "", true, false, false
32+
}
33+
name, opts := parseTag(tag)
34+
if name == "" {
35+
name = f.Name
36+
}
37+
return name, false, opts.Contains("inline"), opts.Contains("omitempty")
38+
}
39+
40+
func isZero(v reflect.Value) bool {
41+
switch v.Kind() {
42+
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
43+
return v.Len() == 0
44+
case reflect.Bool:
45+
return !v.Bool()
46+
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
47+
return v.Int() == 0
48+
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
49+
return v.Uint() == 0
50+
case reflect.Float32, reflect.Float64:
51+
return v.Float() == 0
52+
case reflect.Interface, reflect.Ptr:
53+
return v.IsNil()
54+
case reflect.Chan, reflect.Func:
55+
panic(fmt.Sprintf("unsupported type: %v", v.Type()))
56+
}
57+
return false
58+
}
59+
60+
type tagOptions string
61+
62+
// parseTag splits a struct field's json tag into its name and
63+
// comma-separated options.
64+
func parseTag(tag string) (string, tagOptions) {
65+
if idx := strings.Index(tag, ","); idx != -1 {
66+
return tag[:idx], tagOptions(tag[idx+1:])
67+
}
68+
return tag, tagOptions("")
69+
}
70+
71+
// Contains reports whether a comma-separated list of options
72+
// contains a particular substr flag. substr must be surrounded by a
73+
// string boundary or commas.
74+
func (o tagOptions) Contains(optionName string) bool {
75+
if len(o) == 0 {
76+
return false
77+
}
78+
s := string(o)
79+
for s != "" {
80+
var next string
81+
i := strings.Index(s, ",")
82+
if i >= 0 {
83+
s, next = s[:i], s[i+1:]
84+
}
85+
if s == optionName {
86+
return true
87+
}
88+
s = next
89+
}
90+
return false
91+
}

value/listreflect.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
Copyright 2019 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package value
18+
19+
import "reflect"
20+
21+
type listReflect struct {
22+
Value reflect.Value
23+
}
24+
25+
func (r listReflect) Length() int {
26+
val := r.Value
27+
return val.Len()
28+
}
29+
30+
func (r listReflect) At(i int) Value {
31+
val := r.Value
32+
return mustWrapValueReflect(val.Index(i))
33+
}
34+
35+
func (r listReflect) Unstructured() interface{} {
36+
l := r.Length()
37+
result := make([]interface{}, l)
38+
for i := 0; i < l; i++ {
39+
result[i] = r.At(i).Unstructured()
40+
}
41+
return result
42+
}

value/map.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ type Map interface {
2727
Set(key string, val Value)
2828
// Get returns the value for the given key, if present, or (nil, false) otherwise.
2929
Get(key string) (Value, bool)
30+
// Has returns true if the key is present, or false otherwise.
31+
Has(key string) bool
3032
// Delete removes the key from the map.
3133
Delete(key string)
3234
// Equals compares the two maps, and return true if they are the same, false otherwise.

0 commit comments

Comments
 (0)