1/*
2Copyright 2017 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 validation
18
19import (
20	"reflect"
21	"sort"
22
23	"k8s.io/kube-openapi/pkg/util/proto"
24)
25
26type validationItem interface {
27	proto.SchemaVisitor
28
29	Errors() []error
30	Path() *proto.Path
31}
32
33type baseItem struct {
34	errors errors
35	path   proto.Path
36}
37
38// Errors returns the list of errors found for this item.
39func (item *baseItem) Errors() []error {
40	return item.errors.Errors()
41}
42
43// AddValidationError wraps the given error into a ValidationError and
44// attaches it to this item.
45func (item *baseItem) AddValidationError(err error) {
46	item.errors.AppendErrors(ValidationError{Path: item.path.String(), Err: err})
47}
48
49// AddError adds a regular (non-validation related) error to the list.
50func (item *baseItem) AddError(err error) {
51	item.errors.AppendErrors(err)
52}
53
54// CopyErrors adds a list of errors to this item. This is useful to copy
55// errors from subitems.
56func (item *baseItem) CopyErrors(errs []error) {
57	item.errors.AppendErrors(errs...)
58}
59
60// Path returns the path of this item, helps print useful errors.
61func (item *baseItem) Path() *proto.Path {
62	return &item.path
63}
64
65// mapItem represents a map entry in the yaml.
66type mapItem struct {
67	baseItem
68
69	Map map[string]interface{}
70}
71
72func (item *mapItem) sortedKeys() []string {
73	sortedKeys := []string{}
74	for key := range item.Map {
75		sortedKeys = append(sortedKeys, key)
76	}
77	sort.Strings(sortedKeys)
78	return sortedKeys
79}
80
81var _ validationItem = &mapItem{}
82
83func (item *mapItem) VisitPrimitive(schema *proto.Primitive) {
84	item.AddValidationError(InvalidTypeError{Path: schema.GetPath().String(), Expected: schema.Type, Actual: "map"})
85}
86
87func (item *mapItem) VisitArray(schema *proto.Array) {
88	item.AddValidationError(InvalidTypeError{Path: schema.GetPath().String(), Expected: "array", Actual: "map"})
89}
90
91func (item *mapItem) VisitMap(schema *proto.Map) {
92	for _, key := range item.sortedKeys() {
93		subItem, err := itemFactory(item.Path().FieldPath(key), item.Map[key])
94		if err != nil {
95			item.AddError(err)
96			continue
97		}
98		schema.SubType.Accept(subItem)
99		item.CopyErrors(subItem.Errors())
100	}
101}
102
103func (item *mapItem) VisitKind(schema *proto.Kind) {
104	// Verify each sub-field.
105	for _, key := range item.sortedKeys() {
106		if item.Map[key] == nil {
107			continue
108		}
109		subItem, err := itemFactory(item.Path().FieldPath(key), item.Map[key])
110		if err != nil {
111			item.AddError(err)
112			continue
113		}
114		if _, ok := schema.Fields[key]; !ok {
115			item.AddValidationError(UnknownFieldError{Path: schema.GetPath().String(), Field: key})
116			continue
117		}
118		schema.Fields[key].Accept(subItem)
119		item.CopyErrors(subItem.Errors())
120	}
121
122	// Verify that all required fields are present.
123	for _, required := range schema.RequiredFields {
124		if v, ok := item.Map[required]; !ok || v == nil {
125			item.AddValidationError(MissingRequiredFieldError{Path: schema.GetPath().String(), Field: required})
126		}
127	}
128}
129
130func (item *mapItem) VisitArbitrary(schema *proto.Arbitrary) {
131}
132
133func (item *mapItem) VisitReference(schema proto.Reference) {
134	// passthrough
135	schema.SubSchema().Accept(item)
136}
137
138// arrayItem represents a yaml array.
139type arrayItem struct {
140	baseItem
141
142	Array []interface{}
143}
144
145var _ validationItem = &arrayItem{}
146
147func (item *arrayItem) VisitPrimitive(schema *proto.Primitive) {
148	item.AddValidationError(InvalidTypeError{Path: schema.GetPath().String(), Expected: schema.Type, Actual: "array"})
149}
150
151func (item *arrayItem) VisitArray(schema *proto.Array) {
152	for i, v := range item.Array {
153		path := item.Path().ArrayPath(i)
154		if v == nil {
155			item.AddValidationError(InvalidObjectTypeError{Type: "nil", Path: path.String()})
156			continue
157		}
158		subItem, err := itemFactory(path, v)
159		if err != nil {
160			item.AddError(err)
161			continue
162		}
163		schema.SubType.Accept(subItem)
164		item.CopyErrors(subItem.Errors())
165	}
166}
167
168func (item *arrayItem) VisitMap(schema *proto.Map) {
169	item.AddValidationError(InvalidTypeError{Path: schema.GetPath().String(), Expected: "map", Actual: "array"})
170}
171
172func (item *arrayItem) VisitKind(schema *proto.Kind) {
173	item.AddValidationError(InvalidTypeError{Path: schema.GetPath().String(), Expected: "map", Actual: "array"})
174}
175
176func (item *arrayItem) VisitArbitrary(schema *proto.Arbitrary) {
177}
178
179func (item *arrayItem) VisitReference(schema proto.Reference) {
180	// passthrough
181	schema.SubSchema().Accept(item)
182}
183
184// primitiveItem represents a yaml value.
185type primitiveItem struct {
186	baseItem
187
188	Value interface{}
189	Kind  string
190}
191
192var _ validationItem = &primitiveItem{}
193
194func (item *primitiveItem) VisitPrimitive(schema *proto.Primitive) {
195	// Some types of primitives can match more than one (a number
196	// can be a string, but not the other way around). Return from
197	// the switch if we have a valid possible type conversion
198	// NOTE(apelisse): This logic is blindly copied from the
199	// existing swagger logic, and I'm not sure I agree with it.
200	switch schema.Type {
201	case proto.Boolean:
202		switch item.Kind {
203		case proto.Boolean:
204			return
205		}
206	case proto.Integer:
207		switch item.Kind {
208		case proto.Integer, proto.Number:
209			return
210		}
211	case proto.Number:
212		switch item.Kind {
213		case proto.Number:
214			return
215		}
216	case proto.String:
217		return
218	}
219	// TODO(wrong): this misses "null"
220
221	item.AddValidationError(InvalidTypeError{Path: schema.GetPath().String(), Expected: schema.Type, Actual: item.Kind})
222}
223
224func (item *primitiveItem) VisitArray(schema *proto.Array) {
225	item.AddValidationError(InvalidTypeError{Path: schema.GetPath().String(), Expected: "array", Actual: item.Kind})
226}
227
228func (item *primitiveItem) VisitMap(schema *proto.Map) {
229	item.AddValidationError(InvalidTypeError{Path: schema.GetPath().String(), Expected: "map", Actual: item.Kind})
230}
231
232func (item *primitiveItem) VisitKind(schema *proto.Kind) {
233	item.AddValidationError(InvalidTypeError{Path: schema.GetPath().String(), Expected: "map", Actual: item.Kind})
234}
235
236func (item *primitiveItem) VisitArbitrary(schema *proto.Arbitrary) {
237}
238
239func (item *primitiveItem) VisitReference(schema proto.Reference) {
240	// passthrough
241	schema.SubSchema().Accept(item)
242}
243
244// itemFactory creates the relevant item type/visitor based on the current yaml type.
245func itemFactory(path proto.Path, v interface{}) (validationItem, error) {
246	// We need to special case for no-type fields in yaml (e.g. empty item in list)
247	if v == nil {
248		return nil, InvalidObjectTypeError{Type: "nil", Path: path.String()}
249	}
250	kind := reflect.TypeOf(v).Kind()
251	switch kind {
252	case reflect.Bool:
253		return &primitiveItem{
254			baseItem: baseItem{path: path},
255			Value:    v,
256			Kind:     proto.Boolean,
257		}, nil
258	case reflect.Int,
259		reflect.Int8,
260		reflect.Int16,
261		reflect.Int32,
262		reflect.Int64,
263		reflect.Uint,
264		reflect.Uint8,
265		reflect.Uint16,
266		reflect.Uint32,
267		reflect.Uint64:
268		return &primitiveItem{
269			baseItem: baseItem{path: path},
270			Value:    v,
271			Kind:     proto.Integer,
272		}, nil
273	case reflect.Float32,
274		reflect.Float64:
275		return &primitiveItem{
276			baseItem: baseItem{path: path},
277			Value:    v,
278			Kind:     proto.Number,
279		}, nil
280	case reflect.String:
281		return &primitiveItem{
282			baseItem: baseItem{path: path},
283			Value:    v,
284			Kind:     proto.String,
285		}, nil
286	case reflect.Array,
287		reflect.Slice:
288		return &arrayItem{
289			baseItem: baseItem{path: path},
290			Array:    v.([]interface{}),
291		}, nil
292	case reflect.Map:
293		return &mapItem{
294			baseItem: baseItem{path: path},
295			Map:      v.(map[string]interface{}),
296		}, nil
297	}
298	return nil, InvalidObjectTypeError{Type: kind.String(), Path: path.String()}
299}
300