...

Source file src/github.com/growthbook/growthbook-golang/conditions.go

Documentation: github.com/growthbook/growthbook-golang

     1  package growthbook
     2  
     3  import (
     4  	"encoding/json"
     5  	"reflect"
     6  	"regexp"
     7  	"strings"
     8  )
     9  
    10  // Condition represents conditions used to target features/experiments
    11  // to specific users.
    12  type Condition interface {
    13  	Eval(attrs Attributes) bool
    14  }
    15  
    16  // Concrete condition representing ORing together a list of
    17  // conditions.
    18  type orCondition struct {
    19  	conds []Condition
    20  }
    21  
    22  // Concrete condition representing NORing together a list of
    23  // conditions.
    24  type norCondition struct {
    25  	conds []Condition
    26  }
    27  
    28  // Concrete condition representing ANDing together a list of
    29  // conditions.
    30  type andCondition struct {
    31  	conds []Condition
    32  }
    33  
    34  // Concrete condition representing the complement of another
    35  // condition.
    36  type notCondition struct {
    37  	cond Condition
    38  }
    39  
    40  // Concrete condition representing the base condition case of a set of
    41  // keys and values or subsidiary conditions.
    42  type baseCondition struct {
    43  	// This is represented in this dynamically typed form to make lax
    44  	// error handling easier.
    45  	values map[string]interface{}
    46  }
    47  
    48  // Evaluate ORed list of conditions.
    49  func (cond orCondition) Eval(attrs Attributes) bool {
    50  	if len(cond.conds) == 0 {
    51  		return true
    52  	}
    53  	for i := range cond.conds {
    54  		if cond.conds[i].Eval(attrs) {
    55  			return true
    56  		}
    57  	}
    58  	return false
    59  }
    60  
    61  // Evaluate NORed list of conditions.
    62  func (cond norCondition) Eval(attrs Attributes) bool {
    63  	or := orCondition{cond.conds}
    64  	return !or.Eval(attrs)
    65  }
    66  
    67  // Evaluate ANDed list of conditions.
    68  func (cond andCondition) Eval(attrs Attributes) bool {
    69  	for i := range cond.conds {
    70  		if !cond.conds[i].Eval(attrs) {
    71  			return false
    72  		}
    73  	}
    74  	return true
    75  }
    76  
    77  // Evaluate complemented condition.
    78  func (cond notCondition) Eval(attrs Attributes) bool {
    79  	return !cond.cond.Eval(attrs)
    80  }
    81  
    82  // Evaluate base Condition case by iterating over keys and performing
    83  // evaluation for each one (either a simple comparison, or an operator
    84  // evaluation).
    85  func (cond baseCondition) Eval(attrs Attributes) bool {
    86  	for k, v := range cond.values {
    87  		if !evalConditionValue(v, getPath(attrs, k)) {
    88  			return false
    89  		}
    90  	}
    91  	return true
    92  }
    93  
    94  // ParseCondition creates a Condition value from raw JSON input.
    95  func ParseCondition(data []byte) Condition {
    96  	topLevel := make(map[string]interface{})
    97  	err := json.Unmarshal(data, &topLevel)
    98  	if err != nil {
    99  		logError("Failed parsing JSON input", "Condition")
   100  		return nil
   101  	}
   102  
   103  	return BuildCondition(topLevel)
   104  }
   105  
   106  // BuildCondition creates a Condition value from a JSON object
   107  // represented as a Go map.
   108  func BuildCondition(cond map[string]interface{}) Condition {
   109  	if or, ok := cond["$or"]; ok {
   110  		conds := buildSeq(or)
   111  		if conds == nil {
   112  			return nil
   113  		}
   114  		return orCondition{conds}
   115  	}
   116  
   117  	if nor, ok := cond["$nor"]; ok {
   118  		conds := buildSeq(nor)
   119  		if conds == nil {
   120  			return nil
   121  		}
   122  		return norCondition{conds}
   123  	}
   124  
   125  	if and, ok := cond["$and"]; ok {
   126  		conds := buildSeq(and)
   127  		if conds == nil {
   128  			return nil
   129  		}
   130  		return andCondition{conds}
   131  	}
   132  
   133  	if not, ok := cond["$not"]; ok {
   134  		subcond, ok := not.(map[string]interface{})
   135  		if !ok {
   136  			logError("Invalid $not in JSON condition data")
   137  			return nil
   138  		}
   139  		cond := BuildCondition(subcond)
   140  		if cond == nil {
   141  			return nil
   142  		}
   143  		return notCondition{cond}
   144  	}
   145  
   146  	return baseCondition{cond}
   147  }
   148  
   149  //-- PRIVATE FUNCTIONS START HERE ----------------------------------------------
   150  
   151  // Extract sub-elements of an attribute object using dot-separated
   152  // paths.
   153  func getPath(attrs Attributes, path string) interface{} {
   154  	parts := strings.Split(path, ".")
   155  	var current interface{}
   156  	for i, p := range parts {
   157  		if i == 0 {
   158  			current = attrs[p]
   159  		} else {
   160  			m, ok := current.(map[string]interface{})
   161  			if !ok {
   162  				return nil
   163  			}
   164  			current = m[p]
   165  		}
   166  	}
   167  	return current
   168  }
   169  
   170  // Process a sequence of JSON values into an array of Conditions.
   171  func buildSeq(seq interface{}) []Condition {
   172  	// The input should be a JSON array.
   173  	conds, ok := seq.([]interface{})
   174  	if !ok {
   175  		logError("Something wrong in condition sequence")
   176  		return nil
   177  	}
   178  
   179  	retval := make([]Condition, len(conds))
   180  	for i := range conds {
   181  		// Each condition in the sequence should be a JSON object.
   182  		condmap, ok := conds[i].(map[string]interface{})
   183  		if !ok {
   184  			logError("Something wrong in condition sequence element")
   185  			return nil
   186  		}
   187  		cond := BuildCondition(condmap)
   188  		if cond == nil {
   189  			return nil
   190  		}
   191  		retval[i] = cond
   192  	}
   193  	return retval
   194  }
   195  
   196  // Evaluate one element of a base condition. If the condition value is
   197  // a JSON object and each key in it is an operator name (e.g. "$eq",
   198  // "$gt", "$elemMatch", etc.), then evaluate as an operator condition.
   199  // Otherwise, just directly compare the condition value with the
   200  // attribute value.
   201  func evalConditionValue(condVal interface{}, attrVal interface{}) bool {
   202  	condmap, ok := condVal.(map[string]interface{})
   203  	if ok && isOperatorObject(condmap) {
   204  		for k, v := range condmap {
   205  			if !evalOperatorCondition(k, attrVal, v) {
   206  				return false
   207  			}
   208  		}
   209  		return true
   210  	}
   211  
   212  	return jsEqual(condVal, attrVal)
   213  }
   214  
   215  // An operator object is a JSON object all of whose keys start with a
   216  // "$" character, representing comparison operators.
   217  func isOperatorObject(obj map[string]interface{}) bool {
   218  	for k := range obj {
   219  		if !strings.HasPrefix(k, "$") {
   220  			return false
   221  		}
   222  	}
   223  	return true
   224  }
   225  
   226  // Evaluate operator conditions. The first parameter here is the
   227  // operator name.
   228  func evalOperatorCondition(key string, attrVal interface{}, condVal interface{}) bool {
   229  	switch key {
   230  	case "$veq", "$vne", "$vgt", "$vgte", "$vlt", "$vlte":
   231  		attrstring, attrok := attrVal.(string)
   232  		condstring, reok := condVal.(string)
   233  		if !reok || !attrok {
   234  			return false
   235  		}
   236  		return versionCompare(key, attrstring, condstring)
   237  
   238  	case "$eq":
   239  		return jsEqual(attrVal, condVal)
   240  
   241  	case "$ne":
   242  		return !jsEqual(attrVal, condVal)
   243  
   244  	case "$lt", "$lte", "$gt", "$gte":
   245  		return compare(key, attrVal, condVal)
   246  
   247  	case "$regex":
   248  		restring, reok := condVal.(string)
   249  		attrstring, attrok := attrVal.(string)
   250  		if !reok || !attrok {
   251  			return false
   252  		}
   253  		re, err := regexp.Compile(restring)
   254  		if err != nil {
   255  			return false
   256  		}
   257  		return re.MatchString(attrstring)
   258  
   259  	case "$in":
   260  		vals, ok := condVal.([]interface{})
   261  		if !ok {
   262  			return false
   263  		}
   264  		return elementIn(attrVal, vals)
   265  
   266  	case "$nin":
   267  		vals, ok := condVal.([]interface{})
   268  		if !ok {
   269  			return false
   270  		}
   271  		return !elementIn(attrVal, vals)
   272  
   273  	case "$elemMatch":
   274  		return elemMatch(attrVal, condVal)
   275  
   276  	case "$size":
   277  		if getType(attrVal) != "array" {
   278  			return false
   279  		}
   280  		return evalConditionValue(condVal, float64(len(attrVal.([]interface{}))))
   281  
   282  	case "$all":
   283  		return evalAll(condVal, attrVal)
   284  
   285  	case "$exists":
   286  		return existsCheck(condVal, attrVal)
   287  
   288  	case "$type":
   289  		return getType(attrVal) == condVal.(string)
   290  
   291  	case "$not":
   292  		return !evalConditionValue(condVal, attrVal)
   293  
   294  	default:
   295  		return false
   296  	}
   297  }
   298  
   299  // Get JSON type name for Go representation of JSON objects.
   300  func getType(v interface{}) string {
   301  	if v == nil {
   302  		return "null"
   303  	}
   304  	switch v.(type) {
   305  	case string:
   306  		return "string"
   307  	case float64:
   308  		return "number"
   309  	case bool:
   310  		return "boolean"
   311  	case []interface{}:
   312  		return "array"
   313  	case map[string]interface{}:
   314  		return "object"
   315  	default:
   316  		return "unknown"
   317  	}
   318  }
   319  
   320  // Perform version string comparisons.
   321  
   322  func versionCompare(comp string, v1 string, v2 string) bool {
   323  	v1 = paddedVersionString(v1)
   324  	v2 = paddedVersionString(v2)
   325  	switch comp {
   326  	case "$veq":
   327  		return v1 == v2
   328  	case "$vne":
   329  		return v1 != v2
   330  	case "$vgt":
   331  		return v1 > v2
   332  	case "$vgte":
   333  		return v1 >= v2
   334  	case "$vlt":
   335  		return v1 < v2
   336  	case "$vlte":
   337  		return v1 <= v2
   338  	}
   339  	return false
   340  }
   341  
   342  // Perform numeric or string ordering comparisons on polymorphic JSON
   343  // values.
   344  func compare(comp string, x interface{}, y interface{}) bool {
   345  	switch x.(type) {
   346  	case float64:
   347  		xn := x.(float64)
   348  		yn, ok := y.(float64)
   349  		if !ok {
   350  			logWarn("Types don't match in condition comparison operation")
   351  			return false
   352  		}
   353  		switch comp {
   354  		case "$lt":
   355  			return xn < yn
   356  		case "$lte":
   357  			return xn <= yn
   358  		case "$gt":
   359  			return xn > yn
   360  		case "$gte":
   361  			return xn >= yn
   362  		}
   363  
   364  	case string:
   365  		xs := x.(string)
   366  		ys, ok := y.(string)
   367  		if !ok {
   368  			logWarn("Types don't match in condition comparison operation")
   369  			return false
   370  		}
   371  		switch comp {
   372  		case "$lt":
   373  			return xs < ys
   374  		case "$lte":
   375  			return xs <= ys
   376  		case "$gt":
   377  			return xs > ys
   378  		case "$gte":
   379  			return xs >= ys
   380  		}
   381  	}
   382  	return false
   383  }
   384  
   385  // Check for membership of a JSON value in a JSON array or
   386  // intersection of two arrays.
   387  
   388  func elementIn(v interface{}, array []interface{}) bool {
   389  	otherArray, ok := v.([]interface{})
   390  	if ok {
   391  		// Both arguments are arrays, so look for intersection.
   392  		return commonElement(array, otherArray)
   393  	}
   394  
   395  	// One single value, one array, so do membership test.
   396  	for _, val := range array {
   397  		if jsEqual(v, val) {
   398  			return true
   399  		}
   400  	}
   401  	return false
   402  }
   403  
   404  // Check for common element in two arrays.
   405  
   406  func commonElement(a1 []interface{}, a2 []interface{}) bool {
   407  	for _, el1 := range a1 {
   408  		for _, el2 := range a2 {
   409  			if reflect.DeepEqual(el1, el2) {
   410  				return true
   411  			}
   412  		}
   413  	}
   414  	return false
   415  }
   416  
   417  // Perform "element matching" operation.
   418  func elemMatch(attrVal interface{}, condVal interface{}) bool {
   419  	// Check that the attribute and condition values are of the
   420  	// appropriate types (an array and an object respectively).
   421  	attrs, ok := attrVal.([]interface{})
   422  	if !ok {
   423  		return false
   424  	}
   425  	condmap, ok := condVal.(map[string]interface{})
   426  	if !ok {
   427  		return false
   428  	}
   429  
   430  	// Decide on the type of check to perform on the attribute values.
   431  	check := func(v interface{}) bool { return evalConditionValue(condVal, v) }
   432  	if !isOperatorObject(condmap) {
   433  		cond := BuildCondition(condmap)
   434  		if cond == nil {
   435  			return false
   436  		}
   437  
   438  		check = func(v interface{}) bool {
   439  			vmap, ok := v.(map[string]interface{})
   440  			if !ok {
   441  				return false
   442  			}
   443  			as := Attributes(vmap)
   444  			return cond.Eval(as)
   445  		}
   446  	}
   447  
   448  	// Check attribute array values.
   449  	for _, a := range attrs {
   450  		if check(a) {
   451  			return true
   452  		}
   453  	}
   454  	return false
   455  }
   456  
   457  // Perform "exists" operation.
   458  func existsCheck(condVal interface{}, attrVal interface{}) bool {
   459  	cond, ok := condVal.(bool)
   460  	if !ok {
   461  		return false
   462  	}
   463  	if !cond {
   464  		return attrVal == nil
   465  	}
   466  	return attrVal != nil
   467  }
   468  
   469  // Perform "all" operation.
   470  func evalAll(condVal interface{}, attrVal interface{}) bool {
   471  	conds, okc := condVal.([]interface{})
   472  	attrs, oka := attrVal.([]interface{})
   473  	if !okc || !oka {
   474  		return false
   475  	}
   476  	for _, c := range conds {
   477  		passed := false
   478  		for _, a := range attrs {
   479  			if evalConditionValue(c, a) {
   480  				passed = true
   481  				break
   482  			}
   483  		}
   484  		if !passed {
   485  			return false
   486  		}
   487  	}
   488  	return true
   489  }
   490  
   491  // Equality on values derived from JSON data, following JavaScript
   492  // number comparison rules. This compares arrays/slices (derived from
   493  // JSON arrays), string-keyed maps (derived from JSON objects) and
   494  // atomic values, treating all numbers as floating point, so that "2"
   495  // as an integer compares equal to "2.0", for example. This gets
   496  // around the problem where the Go JSON package decodes all numbers as
   497  // float64, but users may want to use integer values for attributes
   498  // within their Go code, and we would like them to compare equal,
   499  // since that's what happens in the JS SDK.
   500  
   501  func jsEqual(a interface{}, b interface{}) bool {
   502  	if a == nil {
   503  		return b == nil
   504  	}
   505  	if b == nil {
   506  		return false
   507  	}
   508  	switch reflect.TypeOf(a).Kind() {
   509  	case reflect.Array, reflect.Slice:
   510  		aa, aok := a.([]interface{})
   511  		ba, bok := b.([]interface{})
   512  		if !aok || !bok {
   513  			return false
   514  		}
   515  		if len(aa) != len(ba) {
   516  			return false
   517  		}
   518  		for i, av := range aa {
   519  			if !jsEqual(av, ba[i]) {
   520  				return false
   521  			}
   522  		}
   523  		return true
   524  
   525  	case reflect.Map:
   526  		am, aok := a.(map[string]interface{})
   527  		bm, bok := b.(map[string]interface{})
   528  		if !aok || !bok {
   529  			return false
   530  		}
   531  		if len(am) != len(bm) {
   532  			return false
   533  		}
   534  		for k, av := range am {
   535  			bv, ok := bm[k]
   536  			if !ok {
   537  				return false
   538  			}
   539  			if !jsEqual(av, bv) {
   540  				return false
   541  			}
   542  		}
   543  		return true
   544  
   545  	default:
   546  		return reflect.DeepEqual(normalizeNumber(a), normalizeNumber(b))
   547  	}
   548  }
   549  
   550  func normalizeNumber(a interface{}) interface{} {
   551  	v := reflect.ValueOf(a)
   552  	if v.CanFloat() {
   553  		return v.Float()
   554  	}
   555  	if v.CanInt() {
   556  		return float64(v.Int())
   557  	}
   558  	if v.CanUint() {
   559  		return float64(v.Uint())
   560  	}
   561  	return a
   562  }
   563  

View as plain text