...

Source file src/sigs.k8s.io/structured-merge-diff/v4/typed/helpers.go

Documentation: sigs.k8s.io/structured-merge-diff/v4/typed

     1  /*
     2  Copyright 2018 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 typed
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"strings"
    23  
    24  	"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
    25  	"sigs.k8s.io/structured-merge-diff/v4/schema"
    26  	"sigs.k8s.io/structured-merge-diff/v4/value"
    27  )
    28  
    29  // ValidationError reports an error about a particular field
    30  type ValidationError struct {
    31  	Path         string
    32  	ErrorMessage string
    33  }
    34  
    35  // Error returns a human readable error message.
    36  func (ve ValidationError) Error() string {
    37  	if len(ve.Path) == 0 {
    38  		return ve.ErrorMessage
    39  	}
    40  	return fmt.Sprintf("%s: %v", ve.Path, ve.ErrorMessage)
    41  }
    42  
    43  // ValidationErrors accumulates multiple validation error messages.
    44  type ValidationErrors []ValidationError
    45  
    46  // Error returns a human readable error message reporting each error in the
    47  // list.
    48  func (errs ValidationErrors) Error() string {
    49  	if len(errs) == 1 {
    50  		return errs[0].Error()
    51  	}
    52  	messages := []string{"errors:"}
    53  	for _, e := range errs {
    54  		messages = append(messages, "  "+e.Error())
    55  	}
    56  	return strings.Join(messages, "\n")
    57  }
    58  
    59  // Set the given path to all the validation errors.
    60  func (errs ValidationErrors) WithPath(p string) ValidationErrors {
    61  	for i := range errs {
    62  		errs[i].Path = p
    63  	}
    64  	return errs
    65  }
    66  
    67  // WithPrefix prefixes all errors path with the given pathelement. This
    68  // is useful when unwinding the stack on errors.
    69  func (errs ValidationErrors) WithPrefix(prefix string) ValidationErrors {
    70  	for i := range errs {
    71  		errs[i].Path = prefix + errs[i].Path
    72  	}
    73  	return errs
    74  }
    75  
    76  // WithLazyPrefix prefixes all errors path with the given pathelement.
    77  // This is useful when unwinding the stack on errors. Prefix is
    78  // computed lazily only if there is an error.
    79  func (errs ValidationErrors) WithLazyPrefix(fn func() string) ValidationErrors {
    80  	if len(errs) == 0 {
    81  		return errs
    82  	}
    83  	prefix := ""
    84  	if fn != nil {
    85  		prefix = fn()
    86  	}
    87  	for i := range errs {
    88  		errs[i].Path = prefix + errs[i].Path
    89  	}
    90  	return errs
    91  }
    92  
    93  func errorf(format string, args ...interface{}) ValidationErrors {
    94  	return ValidationErrors{{
    95  		ErrorMessage: fmt.Sprintf(format, args...),
    96  	}}
    97  }
    98  
    99  type atomHandler interface {
   100  	doScalar(*schema.Scalar) ValidationErrors
   101  	doList(*schema.List) ValidationErrors
   102  	doMap(*schema.Map) ValidationErrors
   103  }
   104  
   105  func resolveSchema(s *schema.Schema, tr schema.TypeRef, v value.Value, ah atomHandler) ValidationErrors {
   106  	a, ok := s.Resolve(tr)
   107  	if !ok {
   108  		typeName := "inlined type"
   109  		if tr.NamedType != nil {
   110  			typeName = *tr.NamedType
   111  		}
   112  		return errorf("schema error: no type found matching: %v", typeName)
   113  	}
   114  
   115  	a = deduceAtom(a, v)
   116  	return handleAtom(a, tr, ah)
   117  }
   118  
   119  // deduceAtom determines which of the possible types in atom 'atom' applies to value 'val'.
   120  // If val is of a type allowed by atom, return a copy of atom with all other types set to nil.
   121  // if val is nil, or is not of a type allowed by atom, just return the original atom,
   122  // and validation will fail at a later stage. (with a more useful error)
   123  func deduceAtom(atom schema.Atom, val value.Value) schema.Atom {
   124  	switch {
   125  	case val == nil:
   126  	case val.IsFloat(), val.IsInt(), val.IsString(), val.IsBool():
   127  		if atom.Scalar != nil {
   128  			return schema.Atom{Scalar: atom.Scalar}
   129  		}
   130  	case val.IsList():
   131  		if atom.List != nil {
   132  			return schema.Atom{List: atom.List}
   133  		}
   134  	case val.IsMap():
   135  		if atom.Map != nil {
   136  			return schema.Atom{Map: atom.Map}
   137  		}
   138  	}
   139  	return atom
   140  }
   141  
   142  func handleAtom(a schema.Atom, tr schema.TypeRef, ah atomHandler) ValidationErrors {
   143  	switch {
   144  	case a.Map != nil:
   145  		return ah.doMap(a.Map)
   146  	case a.Scalar != nil:
   147  		return ah.doScalar(a.Scalar)
   148  	case a.List != nil:
   149  		return ah.doList(a.List)
   150  	}
   151  
   152  	name := "inlined"
   153  	if tr.NamedType != nil {
   154  		name = "named type: " + *tr.NamedType
   155  	}
   156  
   157  	return errorf("schema error: invalid atom: %v", name)
   158  }
   159  
   160  // Returns the list, or an error. Reminder: nil is a valid list and might be returned.
   161  func listValue(a value.Allocator, val value.Value) (value.List, error) {
   162  	if val.IsNull() {
   163  		// Null is a valid list.
   164  		return nil, nil
   165  	}
   166  	if !val.IsList() {
   167  		return nil, fmt.Errorf("expected list, got %v", val)
   168  	}
   169  	return val.AsListUsing(a), nil
   170  }
   171  
   172  // Returns the map, or an error. Reminder: nil is a valid map and might be returned.
   173  func mapValue(a value.Allocator, val value.Value) (value.Map, error) {
   174  	if val == nil {
   175  		return nil, fmt.Errorf("expected map, got nil")
   176  	}
   177  	if val.IsNull() {
   178  		// Null is a valid map.
   179  		return nil, nil
   180  	}
   181  	if !val.IsMap() {
   182  		return nil, fmt.Errorf("expected map, got %v", val)
   183  	}
   184  	return val.AsMapUsing(a), nil
   185  }
   186  
   187  func getAssociativeKeyDefault(s *schema.Schema, list *schema.List, fieldName string) (interface{}, error) {
   188  	atom, ok := s.Resolve(list.ElementType)
   189  	if !ok {
   190  		return nil, errors.New("invalid elementType for list")
   191  	}
   192  	if atom.Map == nil {
   193  		return nil, errors.New("associative list may not have non-map types")
   194  	}
   195  	// If the field is not found, we can assume there is no default.
   196  	field, _ := atom.Map.FindField(fieldName)
   197  	return field.Default, nil
   198  }
   199  
   200  func keyedAssociativeListItemToPathElement(a value.Allocator, s *schema.Schema, list *schema.List, child value.Value) (fieldpath.PathElement, error) {
   201  	pe := fieldpath.PathElement{}
   202  	if child.IsNull() {
   203  		// null entries are illegal.
   204  		return pe, errors.New("associative list with keys may not have a null element")
   205  	}
   206  	if !child.IsMap() {
   207  		return pe, errors.New("associative list with keys may not have non-map elements")
   208  	}
   209  	keyMap := value.FieldList{}
   210  	m := child.AsMapUsing(a)
   211  	defer a.Free(m)
   212  	for _, fieldName := range list.Keys {
   213  		if val, ok := m.Get(fieldName); ok {
   214  			keyMap = append(keyMap, value.Field{Name: fieldName, Value: val})
   215  		} else if def, err := getAssociativeKeyDefault(s, list, fieldName); err != nil {
   216  			return pe, fmt.Errorf("couldn't find default value for %v: %v", fieldName, err)
   217  		} else if def != nil {
   218  			keyMap = append(keyMap, value.Field{Name: fieldName, Value: value.NewValueInterface(def)})
   219  		} else {
   220  			return pe, fmt.Errorf("associative list with keys has an element that omits key field %q (and doesn't have default value)", fieldName)
   221  		}
   222  	}
   223  	keyMap.Sort()
   224  	pe.Key = &keyMap
   225  	return pe, nil
   226  }
   227  
   228  func setItemToPathElement(child value.Value) (fieldpath.PathElement, error) {
   229  	pe := fieldpath.PathElement{}
   230  	switch {
   231  	case child.IsMap():
   232  		// TODO: atomic maps should be acceptable.
   233  		return pe, errors.New("associative list without keys has an element that's a map type")
   234  	case child.IsList():
   235  		// Should we support a set of lists? For the moment
   236  		// let's say we don't.
   237  		// TODO: atomic lists should be acceptable.
   238  		return pe, errors.New("not supported: associative list with lists as elements")
   239  	case child.IsNull():
   240  		return pe, errors.New("associative list without keys has an element that's an explicit null")
   241  	default:
   242  		// We are a set type.
   243  		pe.Value = &child
   244  		return pe, nil
   245  	}
   246  }
   247  
   248  func listItemToPathElement(a value.Allocator, s *schema.Schema, list *schema.List, child value.Value) (fieldpath.PathElement, error) {
   249  	if list.ElementRelationship != schema.Associative {
   250  		return fieldpath.PathElement{}, errors.New("invalid indexing of non-associative list")
   251  	}
   252  
   253  	if len(list.Keys) > 0 {
   254  		return keyedAssociativeListItemToPathElement(a, s, list, child)
   255  	}
   256  
   257  	// If there's no keys, then we must be a set of primitives.
   258  	return setItemToPathElement(child)
   259  }
   260  

View as plain text