package evaluation import ( "strings" "time" "github.com/launchdarkly/go-sdk-common/v3/ldattr" "github.com/launchdarkly/go-sdk-common/v3/ldcontext" "github.com/launchdarkly/go-sdk-common/v3/ldvalue" "github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel" ) func (es *evaluationScope) clauseMatchesContext(clause *ldmodel.Clause, stack evaluationStack) (bool, error) { // Note that clause is passed by reference only for efficiency; we do not modify it // In the case of a segment match operator, we check if the user is in any of the segments, // and possibly negate if clause.Op == ldmodel.OperatorSegmentMatch { for _, value := range clause.Values { if value.Type() == ldvalue.StringType { if segment := es.owner.dataProvider.GetSegment(value.StringValue()); segment != nil { match, err := es.segmentContainsContext(segment, stack) if err != nil { return false, err } if match { return !clause.Negate, nil // match - true unless negated } } } } return clause.Negate, nil // non-match - false unless negated } return clauseMatchesContextNoSegments(clause, &es.context) } func clauseMatchesContextNoSegments(c *ldmodel.Clause, context *ldcontext.Context) (bool, error) { if !c.Attribute.IsDefined() { return false, emptyAttrRefError{} } if c.Attribute.Err() != nil { return false, badAttrRefError(c.Attribute.String()) } if c.Attribute.String() == ldattr.KindAttr { return maybeNegate(c.Negate, clauseMatchByKind(c, context)), nil } actualContext := context.IndividualContextByKind(c.ContextKind) if !actualContext.IsDefined() { return false, nil } uValue := actualContext.GetValueForRef(c.Attribute) if uValue.IsNull() { // if the user attribute is null/missing, it's an automatic non-match - regardless of c.Negate return false, nil } // If the user value is an array, see if the intersection is non-empty. If so, this clause matches if uValue.Type() == ldvalue.ArrayType { for i := 0; i < uValue.Count(); i++ { if matchAny(c, uValue.GetByIndex(i)) { return maybeNegate(c.Negate, true), nil } } return maybeNegate(c.Negate, false), nil } return maybeNegate(c.Negate, matchAny(c, uValue)), nil } func maybeNegate(negate, result bool) bool { if negate { return !result } return result } func matchAny( c *ldmodel.Clause, value ldvalue.Value, ) bool { if c.Op == ldmodel.OperatorIn { return ldmodel.EvaluatorAccessors.ClauseFindValue(c, value) } for i, v := range c.Values { if doOp(c, value, v, i) { return true } } return false } func doOp(c *ldmodel.Clause, ctxValue, clValue ldvalue.Value, index int) bool { switch c.Op { case ldmodel.OperatorEndsWith: return stringOperator(ctxValue, clValue, strings.HasSuffix) case ldmodel.OperatorStartsWith: return stringOperator(ctxValue, clValue, strings.HasPrefix) case ldmodel.OperatorMatches: return operatorMatchesFn(c, ctxValue, index) case ldmodel.OperatorContains: return stringOperator(ctxValue, clValue, strings.Contains) case ldmodel.OperatorLessThan: return numericOperator(ctxValue, clValue, func(a float64, b float64) bool { return a < b }) case ldmodel.OperatorLessThanOrEqual: return numericOperator(ctxValue, clValue, func(a float64, b float64) bool { return a <= b }) case ldmodel.OperatorGreaterThan: return numericOperator(ctxValue, clValue, func(a float64, b float64) bool { return a > b }) case ldmodel.OperatorGreaterThanOrEqual: return numericOperator(ctxValue, clValue, func(a float64, b float64) bool { return a >= b }) case ldmodel.OperatorBefore: return dateOperator(c, ctxValue, index, time.Time.Before) case ldmodel.OperatorAfter: return dateOperator(c, ctxValue, index, time.Time.After) case ldmodel.OperatorSemVerEqual: return semVerOperator(c, ctxValue, index, 0) case ldmodel.OperatorSemVerLessThan: return semVerOperator(c, ctxValue, index, -1) case ldmodel.OperatorSemVerGreaterThan: return semVerOperator(c, ctxValue, index, 1) } return false } func clauseMatchByKind(c *ldmodel.Clause, context *ldcontext.Context) bool { // If Attribute is "kind", then we treat Operator and Values as a match expression against a list // of all individual kinds in the context. That is, for a multi-kind context with kinds of "org" // and "user", it is a match if either of those strings is a match with Operator and Values. if context.Multiple() { for i := 0; i < context.IndividualContextCount(); i++ { if individualContext := context.IndividualContextByIndex(i); individualContext.IsDefined() { ctxValue := ldvalue.String(string(individualContext.Kind())) if matchAny(c, ctxValue) { return true } } } return false } ctxValue := ldvalue.String(string(context.Kind())) return matchAny(c, ctxValue) } func stringOperator( ctxValue, clValue ldvalue.Value, stringTestFn func(string, string) bool, ) bool { if ctxValue.IsString() && clValue.IsString() { return stringTestFn(ctxValue.StringValue(), clValue.StringValue()) } return false } func operatorMatchesFn(c *ldmodel.Clause, ctxValue ldvalue.Value, clValueIndex int) bool { if ctxValue.IsString() { r := ldmodel.EvaluatorAccessors.ClauseGetValueAsRegexp(c, clValueIndex) if r != nil { return r.MatchString(ctxValue.StringValue()) } } return false } func numericOperator(ctxValue, clValue ldvalue.Value, fn func(float64, float64) bool) bool { if ctxValue.IsNumber() && clValue.IsNumber() { return fn(ctxValue.Float64Value(), clValue.Float64Value()) } return false } func dateOperator( c *ldmodel.Clause, ctxValue ldvalue.Value, clValueIndex int, fn func(time.Time, time.Time) bool, ) bool { if clValueTime, ok := ldmodel.EvaluatorAccessors.ClauseGetValueAsTimestamp(c, clValueIndex); ok { if ctxValueTime, ok := ldmodel.TypeConversions.ValueToTimestamp(ctxValue); ok { return fn(ctxValueTime, clValueTime) } } return false } func semVerOperator( c *ldmodel.Clause, ctxValue ldvalue.Value, clValueIndex int, expectedComparisonResult int, ) bool { if clValueVer, ok := ldmodel.EvaluatorAccessors.ClauseGetValueAsSemanticVersion(c, clValueIndex); ok { if ctxValueVer, ok := ldmodel.TypeConversions.ValueToSemanticVersion(ctxValue); ok { return ctxValueVer.ComparePrecedence(clValueVer) == expectedComparisonResult } } return false }