1/*
2Copyright 2018 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package typed
18
19import (
20	"sync"
21
22	"sigs.k8s.io/structured-merge-diff/v3/fieldpath"
23	"sigs.k8s.io/structured-merge-diff/v3/schema"
24	"sigs.k8s.io/structured-merge-diff/v3/value"
25)
26
27var vPool = sync.Pool{
28	New: func() interface{} { return &validatingObjectWalker{} },
29}
30
31func (tv TypedValue) walker() *validatingObjectWalker {
32	v := vPool.Get().(*validatingObjectWalker)
33	v.value = tv.value
34	v.schema = tv.schema
35	v.typeRef = tv.typeRef
36	if v.allocator == nil {
37		v.allocator = value.NewFreelistAllocator()
38	}
39	return v
40}
41
42func (v *validatingObjectWalker) finished() {
43	v.schema = nil
44	v.typeRef = schema.TypeRef{}
45	vPool.Put(v)
46}
47
48type validatingObjectWalker struct {
49	value   value.Value
50	schema  *schema.Schema
51	typeRef schema.TypeRef
52
53	// Allocate only as many walkers as needed for the depth by storing them here.
54	spareWalkers *[]*validatingObjectWalker
55	allocator    value.Allocator
56}
57
58func (v *validatingObjectWalker) prepareDescent(tr schema.TypeRef) *validatingObjectWalker {
59	if v.spareWalkers == nil {
60		// first descent.
61		v.spareWalkers = &[]*validatingObjectWalker{}
62	}
63	var v2 *validatingObjectWalker
64	if n := len(*v.spareWalkers); n > 0 {
65		v2, *v.spareWalkers = (*v.spareWalkers)[n-1], (*v.spareWalkers)[:n-1]
66	} else {
67		v2 = &validatingObjectWalker{}
68	}
69	*v2 = *v
70	v2.typeRef = tr
71	return v2
72}
73
74func (v *validatingObjectWalker) finishDescent(v2 *validatingObjectWalker) {
75	// if the descent caused a realloc, ensure that we reuse the buffer
76	// for the next sibling.
77	*v.spareWalkers = append(*v.spareWalkers, v2)
78}
79
80func (v *validatingObjectWalker) validate(prefixFn func() string) ValidationErrors {
81	return resolveSchema(v.schema, v.typeRef, v.value, v).WithLazyPrefix(prefixFn)
82}
83
84func validateScalar(t *schema.Scalar, v value.Value, prefix string) (errs ValidationErrors) {
85	if v == nil {
86		return nil
87	}
88	if v.IsNull() {
89		return nil
90	}
91	switch *t {
92	case schema.Numeric:
93		if !v.IsFloat() && !v.IsInt() {
94			// TODO: should the schema separate int and float?
95			return errorf("%vexpected numeric (int or float), got %T", prefix, v)
96		}
97	case schema.String:
98		if !v.IsString() {
99			return errorf("%vexpected string, got %#v", prefix, v)
100		}
101	case schema.Boolean:
102		if !v.IsBool() {
103			return errorf("%vexpected boolean, got %v", prefix, v)
104		}
105	}
106	return nil
107}
108
109func (v *validatingObjectWalker) doScalar(t *schema.Scalar) ValidationErrors {
110	if errs := validateScalar(t, v.value, ""); len(errs) > 0 {
111		return errs
112	}
113	return nil
114}
115
116func (v *validatingObjectWalker) visitListItems(t *schema.List, list value.List) (errs ValidationErrors) {
117	observedKeys := fieldpath.MakePathElementSet(list.Length())
118	for i := 0; i < list.Length(); i++ {
119		child := list.AtUsing(v.allocator, i)
120		defer v.allocator.Free(child)
121		var pe fieldpath.PathElement
122		if t.ElementRelationship != schema.Associative {
123			pe.Index = &i
124		} else {
125			var err error
126			pe, err = listItemToPathElement(v.allocator, t, i, child)
127			if err != nil {
128				errs = append(errs, errorf("element %v: %v", i, err.Error())...)
129				// If we can't construct the path element, we can't
130				// even report errors deeper in the schema, so bail on
131				// this element.
132				return
133			}
134			if observedKeys.Has(pe) {
135				errs = append(errs, errorf("duplicate entries for key %v", pe.String())...)
136			}
137			observedKeys.Insert(pe)
138		}
139		v2 := v.prepareDescent(t.ElementType)
140		v2.value = child
141		errs = append(errs, v2.validate(pe.String)...)
142		v.finishDescent(v2)
143	}
144	return errs
145}
146
147func (v *validatingObjectWalker) doList(t *schema.List) (errs ValidationErrors) {
148	list, err := listValue(v.allocator, v.value)
149	if err != nil {
150		return errorf(err.Error())
151	}
152
153	if list == nil {
154		return nil
155	}
156
157	defer v.allocator.Free(list)
158	errs = v.visitListItems(t, list)
159
160	return errs
161}
162
163func (v *validatingObjectWalker) visitMapItems(t *schema.Map, m value.Map) (errs ValidationErrors) {
164	m.IterateUsing(v.allocator, func(key string, val value.Value) bool {
165		pe := fieldpath.PathElement{FieldName: &key}
166		tr := t.ElementType
167		if sf, ok := t.FindField(key); ok {
168			tr = sf.Type
169		} else if (t.ElementType == schema.TypeRef{}) {
170			errs = append(errs, errorf("field not declared in schema").WithPrefix(pe.String())...)
171			return false
172		}
173		v2 := v.prepareDescent(tr)
174		v2.value = val
175		// Giving pe.String as a parameter actually increases the allocations.
176		errs = append(errs, v2.validate(func() string { return pe.String() })...)
177		v.finishDescent(v2)
178		return true
179	})
180	return errs
181}
182
183func (v *validatingObjectWalker) doMap(t *schema.Map) (errs ValidationErrors) {
184	m, err := mapValue(v.allocator, v.value)
185	if err != nil {
186		return errorf(err.Error())
187	}
188	if m == nil {
189		return nil
190	}
191	defer v.allocator.Free(m)
192	errs = v.visitMapItems(t, m)
193
194	return errs
195}
196