github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/constraints/validation.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package constraints
     5  
     6  import (
     7  	"fmt"
     8  	"math"
     9  	"reflect"
    10  
    11  	"github.com/juju/utils/set"
    12  )
    13  
    14  // Validator defines operations on constraints attributes which are
    15  // used to ensure a constraints value is valid, as well as being able
    16  // to handle overridden attributes.
    17  type Validator interface {
    18  
    19  	// RegisterConflicts is used to define cross-constraint override behaviour.
    20  	// The red and blue attribute lists contain attribute names which conflict
    21  	// with those in the other list.
    22  	// When two constraints conflict:
    23  	//  it is an error to set both constraints in the same constraints Value.
    24  	//  when a constraints Value overrides another which specifies a conflicting
    25  	//   attribute, the attribute in the overridden Value is cleared.
    26  	RegisterConflicts(reds, blues []string)
    27  
    28  	// RegisterUnsupported records attributes which are not supported by a constraints Value.
    29  	RegisterUnsupported(unsupported []string)
    30  
    31  	// RegisterVocabulary records allowed values for the specified constraint attribute.
    32  	// allowedValues is expected to be a slice/array but is declared as interface{} so
    33  	// that vocabs of different types can be passed in.
    34  	RegisterVocabulary(attributeName string, allowedValues interface{})
    35  
    36  	// Validate returns an error if the given constraints are not valid, and also
    37  	// any unsupported attributes.
    38  	Validate(cons Value) ([]string, error)
    39  
    40  	// Merge merges cons into consFallback, with any conflicting attributes from cons
    41  	// overriding those from consFallback.
    42  	Merge(consFallback, cons Value) (Value, error)
    43  
    44  	// UpdateVocabulary merges new attribute values with existing values.
    45  	// This method does not overwrite or delete values, i.e.
    46  	//     if existing values are {a, b}
    47  	//     and new values are {c, d},
    48  	//     then the merge result would be {a, b, c, d}.
    49  	UpdateVocabulary(attributeName string, newValues interface{})
    50  }
    51  
    52  // NewValidator returns a new constraints Validator instance.
    53  func NewValidator() Validator {
    54  	return &validator{
    55  		conflicts: make(map[string]set.Strings),
    56  		vocab:     make(map[string][]interface{}),
    57  	}
    58  }
    59  
    60  type validator struct {
    61  	unsupported set.Strings
    62  	conflicts   map[string]set.Strings
    63  	vocab       map[string][]interface{}
    64  }
    65  
    66  // RegisterConflicts is defined on Validator.
    67  func (v *validator) RegisterConflicts(reds, blues []string) {
    68  	for _, red := range reds {
    69  		v.conflicts[red] = set.NewStrings(blues...)
    70  	}
    71  	for _, blue := range blues {
    72  		v.conflicts[blue] = set.NewStrings(reds...)
    73  	}
    74  }
    75  
    76  // RegisterUnsupported is defined on Validator.
    77  func (v *validator) RegisterUnsupported(unsupported []string) {
    78  	v.unsupported = set.NewStrings(unsupported...)
    79  }
    80  
    81  // RegisterVocabulary is defined on Validator.
    82  func (v *validator) RegisterVocabulary(attributeName string, allowedValues interface{}) {
    83  	v.vocab[attributeName] = convertToSlice(allowedValues)
    84  }
    85  
    86  var checkIsCollection = func(coll interface{}) {
    87  	k := reflect.TypeOf(coll).Kind()
    88  	if k != reflect.Slice && k != reflect.Array {
    89  		panic(fmt.Errorf("invalid vocab: %v of type %T is not a slice", coll, coll))
    90  	}
    91  }
    92  
    93  var convertToSlice = func(coll interface{}) []interface{} {
    94  	checkIsCollection(coll)
    95  	var slice []interface{}
    96  	val := reflect.ValueOf(coll)
    97  	for i := 0; i < val.Len(); i++ {
    98  		slice = append(slice, val.Index(i).Interface())
    99  	}
   100  	return slice
   101  }
   102  
   103  // UpdateVocabulary is defined on Validator.
   104  func (v *validator) UpdateVocabulary(attributeName string, allowedValues interface{}) {
   105  	// If this attribute is not registered, delegate to RegisterVocabulary()
   106  	currentValues, ok := v.vocab[attributeName]
   107  	if !ok {
   108  		v.RegisterVocabulary(attributeName, allowedValues)
   109  	}
   110  
   111  	unique := map[interface{}]bool{}
   112  	writeUnique := func(all []interface{}) {
   113  		for _, one := range all {
   114  			unique[one] = true
   115  		}
   116  	}
   117  
   118  	// merge existing values with new, ensuring uniqueness
   119  	writeUnique(currentValues)
   120  	newValues := convertToSlice(allowedValues)
   121  	writeUnique(newValues)
   122  
   123  	var merged []interface{}
   124  	for one, _ := range unique {
   125  		merged = append(merged, one)
   126  	}
   127  	v.vocab[attributeName] = merged
   128  }
   129  
   130  // checkConflicts returns an error if the constraints Value contains conflicting attributes.
   131  func (v *validator) checkConflicts(cons Value) error {
   132  	attrValues := cons.attributesWithValues()
   133  	attrSet := make(set.Strings)
   134  	for attrTag := range attrValues {
   135  		attrSet.Add(attrTag)
   136  	}
   137  	for _, attrTag := range attrSet.SortedValues() {
   138  		conflicts, ok := v.conflicts[attrTag]
   139  		if !ok {
   140  			continue
   141  		}
   142  		for _, conflict := range conflicts.SortedValues() {
   143  			if attrSet.Contains(conflict) {
   144  				return fmt.Errorf("ambiguous constraints: %q overlaps with %q", attrTag, conflict)
   145  			}
   146  		}
   147  	}
   148  	return nil
   149  }
   150  
   151  // checkUnsupported returns any unsupported attributes.
   152  func (v *validator) checkUnsupported(cons Value) []string {
   153  	return cons.hasAny(v.unsupported.Values()...)
   154  }
   155  
   156  // checkValidValues returns an error if the constraints value contains an
   157  // attribute value which is not allowed by the vocab which may have been
   158  // registered for it.
   159  func (v *validator) checkValidValues(cons Value) error {
   160  	for attrTag, attrValue := range cons.attributesWithValues() {
   161  		k := reflect.TypeOf(attrValue).Kind()
   162  		if k == reflect.Slice || k == reflect.Array {
   163  			// For slices we check that all values are valid.
   164  			val := reflect.ValueOf(attrValue)
   165  			for i := 0; i < val.Len(); i++ {
   166  				if err := v.checkInVocab(attrTag, val.Index(i).Interface()); err != nil {
   167  					return err
   168  				}
   169  			}
   170  		} else {
   171  			if err := v.checkInVocab(attrTag, attrValue); err != nil {
   172  				return err
   173  			}
   174  		}
   175  	}
   176  	return nil
   177  }
   178  
   179  // checkInVocab returns an error if the attribute value is not allowed by the
   180  // vocab which may have been registered for it.
   181  func (v *validator) checkInVocab(attributeName string, attributeValue interface{}) error {
   182  	validValues, ok := v.vocab[attributeName]
   183  	if !ok {
   184  		return nil
   185  	}
   186  	for _, validValue := range validValues {
   187  		if coerce(validValue) == coerce(attributeValue) {
   188  			return nil
   189  		}
   190  	}
   191  	return fmt.Errorf(
   192  		"invalid constraint value: %v=%v\nvalid values are: %v", attributeName, attributeValue, validValues)
   193  }
   194  
   195  // coerce returns v in a format that allows constraint values to be easily compared.
   196  // Its main purpose is to cast all numeric values to int64 or float64.
   197  func coerce(v interface{}) interface{} {
   198  	if v != nil {
   199  		switch vv := reflect.TypeOf(v); vv.Kind() {
   200  		case reflect.String:
   201  			return v
   202  		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
   203  			return int64(reflect.ValueOf(v).Int())
   204  		case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
   205  			uval := reflect.ValueOf(v).Uint()
   206  			// Just double check the value is in range.
   207  			if uval > math.MaxInt64 {
   208  				panic(fmt.Errorf("constraint value %v is too large", uval))
   209  			}
   210  			return int64(uval)
   211  		case reflect.Float32, reflect.Float64:
   212  			return float64(reflect.ValueOf(v).Float())
   213  		}
   214  	}
   215  	return v
   216  }
   217  
   218  // withFallbacks returns a copy of v with nil values taken from vFallback.
   219  func withFallbacks(v Value, vFallback Value) Value {
   220  	result := vFallback
   221  	for _, fieldName := range fieldNames {
   222  		resultVal := reflect.ValueOf(&result).Elem().FieldByName(fieldName)
   223  		val := reflect.ValueOf(&v).Elem().FieldByName(fieldName)
   224  		if !val.IsNil() {
   225  			resultVal.Set(val)
   226  		}
   227  	}
   228  	return result
   229  }
   230  
   231  // Validate is defined on Validator.
   232  func (v *validator) Validate(cons Value) ([]string, error) {
   233  	unsupported := v.checkUnsupported(cons)
   234  	if err := v.checkConflicts(cons); err != nil {
   235  		return unsupported, err
   236  	}
   237  	if err := v.checkValidValues(cons); err != nil {
   238  		return unsupported, err
   239  	}
   240  	return unsupported, nil
   241  }
   242  
   243  // Merge is defined on Validator.
   244  func (v *validator) Merge(consFallback, cons Value) (Value, error) {
   245  	// First ensure both constraints are valid. We don't care if there
   246  	// are constraint attributes that are unsupported.
   247  	if _, err := v.Validate(consFallback); err != nil {
   248  		return Value{}, err
   249  	}
   250  	if _, err := v.Validate(cons); err != nil {
   251  		return Value{}, err
   252  	}
   253  	// Gather any attributes from consFallback which conflict with those on cons.
   254  	attrValues := cons.attributesWithValues()
   255  	var fallbackConflicts []string
   256  	for attrTag := range attrValues {
   257  		fallbackConflicts = append(fallbackConflicts, v.conflicts[attrTag].Values()...)
   258  	}
   259  	// Null out the conflicting consFallback attribute values because
   260  	// cons takes priority. We can't error here because we
   261  	// know that aConflicts contains valid attr names.
   262  	consFallbackMinusConflicts, _ := consFallback.without(fallbackConflicts...)
   263  	// The result is cons with fallbacks coming from any
   264  	// non conflicting consFallback attributes.
   265  	return withFallbacks(cons, consFallbackMinusConflicts), nil
   266  }