Skip to content

Commit

Permalink
Add STARTS_WITH/ENDS_WITH operators to StringRule (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
sic2 authored Nov 14, 2024
1 parent 2c4ea24 commit 89d55d5
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 23 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,12 @@ Comparisons are case-sensitive.
| "bar" | GREATER_THAN | "car" | VALID |
| "bar" | GREATER_THAN_EQUAL | "bar" | VALID |
| "bar" | GREATER_THAN | "are" | INVALID |
| "bar" | STARTS_WITH | "bar" | VALID |
| "bar" | STARTS_WITH | "barfoo" | VALID |
| "bar" | STARTS_WITH | "foobar" | INVALID |
| "bar" | ENDS_WITH | "bar" | VALID |
| "bar" | ENDS_WITH | "foobar" | VALID |
| "bar" | ENDS_WITH | "barfoo" | INVALID |
| "bar" | CONTAINS | ["are", "bar", "baz"] | VALID |
| "bar" | CONTAINS | ["are", "baz"] | INVALID |
| any string | supported operator | null | INVALID |
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/com/adobe/abp/regola/rules/Operator.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ public enum Operator {
LESS_THAN_EQUAL,
IN,
CONTAINS,
INTERSECTS
INTERSECTS,
STARTS_WITH,
ENDS_WITH

// Operators not supported yet
// BETWEEN,
Expand Down
25 changes: 16 additions & 9 deletions src/main/java/com/adobe/abp/regola/rules/SingleValueRule.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,6 @@

public abstract class SingleValueRule<T> extends OperatorBasedRule {

protected final Set<Operator> SINGLE_FACT_OPERATORS = Set.of(Operator.EQUALS,
Operator.GREATER_THAN,
Operator.GREATER_THAN_EQUAL,
Operator.LESS_THAN,
Operator.LESS_THAN_EQUAL);
protected final Set<Operator> SET_FACT_OPERATORS = Set.of(Operator.CONTAINS);

private T value;
private final Executor executor;
private final Set<Class<?>> factTypes;
Expand Down Expand Up @@ -81,7 +74,7 @@ public RuleResult snapshot() {

@Override
public CompletableFuture<Result> status() {
if (SINGLE_FACT_OPERATORS.contains(getOperator())) {
if (getSingleFactOperators().contains(getOperator())) {
return resolveFact(factsResolver, getKey())
.thenApply(fact -> {
result = check(fact);
Expand All @@ -91,7 +84,7 @@ public CompletableFuture<Result> status() {
.whenComplete((result, throwable) -> Optional.ofNullable(getAction())
.ifPresent(action -> action.onCompletion(result, throwable, snapshot())))
.exceptionally(this::handleFailure);
} else if (SET_FACT_OPERATORS.contains(getOperator())) {
} else if (getSetFactOperators().contains(getOperator())) {
return resolveSetFact(factsResolver, getKey())
.thenApply(factsSet -> {
result = check(factsSet);
Expand Down Expand Up @@ -120,6 +113,20 @@ private Result handleFailure(Throwable throwable) {
};
}

protected Set<Operator> getSingleFactOperators() {
return Set.of(
Operator.EQUALS,
Operator.GREATER_THAN,
Operator.GREATER_THAN_EQUAL,
Operator.LESS_THAN,
Operator.LESS_THAN_EQUAL
);
}

protected Set<Operator> getSetFactOperators() {
return Set.of(Operator.CONTAINS);
}

/**
* Check if the provided fact satisfies the rule condition.
*
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/com/adobe/abp/regola/rules/StringRule.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,25 @@ Result check(String fact) {
return checkFact(fact, (f, value) -> StringUtils.compare(f, value) < 0);
case LESS_THAN_EQUAL:
return checkFact(fact, (f, value) -> StringUtils.compare(f, value) <= 0);
case STARTS_WITH:
return checkFact(fact, StringUtils::startsWith);
case ENDS_WITH:
return checkFact(fact, StringUtils::endsWith);
default:
return Result.OPERATION_NOT_SUPPORTED;
}
}

@Override
protected Set<Operator> getSingleFactOperators() {
return Set.of(
Operator.EQUALS,
Operator.GREATER_THAN,
Operator.GREATER_THAN_EQUAL,
Operator.LESS_THAN,
Operator.LESS_THAN_EQUAL,
Operator.STARTS_WITH,
Operator.ENDS_WITH
);
}
}
157 changes: 144 additions & 13 deletions src/test/java/com/adobe/abp/regola/rules/StringRuleTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
import com.adobe.abp.regola.facts.FactsResolver;
import com.adobe.abp.regola.results.Result;
import com.adobe.abp.regola.results.ValuesRuleResult;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.BeforeEach;
Expand All @@ -24,13 +30,6 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
Expand Down Expand Up @@ -368,6 +367,138 @@ void factIsNotValidOnNullRuleValue() {
}
}

@Nested
@DisplayName("starts-with should")
class StartsWith {

private ValuesRuleResult.RuleResultBuilder<String> ruleResultBuilder;

@BeforeEach
void setup() {
rule.setOperator(Operator.STARTS_WITH);
rule.setValue("fo");

ruleResultBuilder = ValuesRuleResult.<String>builder().with(r -> {
r.type = RuleType.STRING.getName();
r.operator = Operator.STARTS_WITH;
r.key = RULE_KEY;
r.expectedValue = "fo";
});
}

@Test
@DisplayName("evaluate as valid if fact matches")
void factIsValid() {
when(resolver.resolveFact(RULE_KEY))
.thenReturn(CompletableFuture.supplyAsync(() -> "foo"));

RuleTestUtils.evaluateAndTest(rule, resolver, ruleResultBuilder, Result.VALID, "foo", "fo");
}

@Test
@DisplayName("evaluate as invalid if fact does not match")
void factIsNotValid() {
when(resolver.resolveFact(RULE_KEY))
.thenReturn(CompletableFuture.supplyAsync(() -> "fao"));

RuleTestUtils.evaluateAndTest(rule, resolver, ruleResultBuilder, Result.INVALID, "fao", "fo");
}

@Test
@DisplayName("evaluate as valid if fact matches (same value)")
void factIsNotValidWithSameValue() {
when(resolver.resolveFact(RULE_KEY))
.thenReturn(CompletableFuture.supplyAsync(() -> "fo"));

RuleTestUtils.evaluateAndTest(rule, resolver, ruleResultBuilder, Result.VALID, "fo", "fo");
}

@Test
@DisplayName("evaluate as valid if fact matches (case sensitive)")
void factIsInvalidWhenCaseDiffers() {
when(resolver.resolveFact(RULE_KEY))
.thenReturn(CompletableFuture.supplyAsync(() -> "FOO"));

RuleTestUtils.evaluateAndTest(rule, resolver, ruleResultBuilder, Result.INVALID, "FOO", "fo");
}

@Test
@DisplayName("evaluate as invalid if fact exists and rule has a null value")
void factIsNotValidOnNullRuleValue() {
rule.setValue(null);
when(resolver.resolveFact(RULE_KEY))
.thenReturn(CompletableFuture.supplyAsync(() -> "some-value"));

RuleTestUtils.evaluateAndTestWithNullValue(rule, resolver, ruleResultBuilder, Result.INVALID, "some-value");
}
}

@Nested
@DisplayName("ends-with should")
class EndsWith {

private ValuesRuleResult.RuleResultBuilder<String> ruleResultBuilder;

@BeforeEach
void setup() {
rule.setOperator(Operator.ENDS_WITH);
rule.setValue("of");

ruleResultBuilder = ValuesRuleResult.<String>builder().with(r -> {
r.type = RuleType.STRING.getName();
r.operator = Operator.ENDS_WITH;
r.key = RULE_KEY;
r.expectedValue = "of";
});
}

@Test
@DisplayName("evaluate as valid if fact matches")
void factIsValid() {
when(resolver.resolveFact(RULE_KEY))
.thenReturn(CompletableFuture.supplyAsync(() -> "foof"));

RuleTestUtils.evaluateAndTest(rule, resolver, ruleResultBuilder, Result.VALID, "foof", "of");
}

@Test
@DisplayName("evaluate as invalid if fact does not match")
void factIsNotValid() {
when(resolver.resolveFact(RULE_KEY))
.thenReturn(CompletableFuture.supplyAsync(() -> "foaf"));

RuleTestUtils.evaluateAndTest(rule, resolver, ruleResultBuilder, Result.INVALID, "foaf", "of");
}

@Test
@DisplayName("evaluate as valid if fact matches (same value)")
void factIsNotValidWithSameValue() {
when(resolver.resolveFact(RULE_KEY))
.thenReturn(CompletableFuture.supplyAsync(() -> "of"));

RuleTestUtils.evaluateAndTest(rule, resolver, ruleResultBuilder, Result.VALID, "of", "of");
}

@Test
@DisplayName("evaluate as valid if fact matches (case sensitive)")
void factIsInvalidWhenCaseDiffers() {
when(resolver.resolveFact(RULE_KEY))
.thenReturn(CompletableFuture.supplyAsync(() -> "FOOF"));

RuleTestUtils.evaluateAndTest(rule, resolver, ruleResultBuilder, Result.INVALID, "FOOF", "of");
}

@Test
@DisplayName("evaluate as invalid if fact exists and rule has a null value")
void factIsNotValidOnNullRuleValue() {
rule.setValue(null);
when(resolver.resolveFact(RULE_KEY))
.thenReturn(CompletableFuture.supplyAsync(() -> "some-value"));

RuleTestUtils.evaluateAndTestWithNullValue(rule, resolver, ruleResultBuilder, Result.INVALID, "some-value");
}
}

@Nested
@DisplayName("contains should")
class Contains {
Expand Down Expand Up @@ -460,7 +591,7 @@ void setup(Operator operator) {

@ParameterizedTest
@EnumSource(value = Operator.class,
names = {"EQUALS", "GREATER_THAN", "GREATER_THAN_EQUAL", "LESS_THAN", "LESS_THAN_EQUAL", "CONTAINS"},
names = {"EQUALS", "GREATER_THAN", "GREATER_THAN_EQUAL", "LESS_THAN", "LESS_THAN_EQUAL", "CONTAINS", "STARTS_WITH", "ENDS_WITH"},
mode = EnumSource.Mode.EXCLUDE)
@DisplayName("evaluate as not supported")
void factIsNotValidOnEmptyFact(Operator operator) {
Expand All @@ -474,7 +605,7 @@ void factIsNotValidOnEmptyFact(Operator operator) {

@ParameterizedTest
@EnumSource(value = Operator.class,
names = {"EQUALS", "GREATER_THAN", "GREATER_THAN_EQUAL", "LESS_THAN", "LESS_THAN_EQUAL", "CONTAINS"},
names = {"EQUALS", "GREATER_THAN", "GREATER_THAN_EQUAL", "LESS_THAN", "LESS_THAN_EQUAL", "CONTAINS", "STARTS_WITH", "ENDS_WITH"},
mode = EnumSource.Mode.EXCLUDE)
@DisplayName("evaluate as not supported even if both fact and rule value are null")
void factAndRuleValueAreNull(Operator operator) {
Expand Down Expand Up @@ -520,7 +651,7 @@ void descriptionInResult() {

@ParameterizedTest
@EnumSource(value = Operator.class,
names = {"EQUALS", "GREATER_THAN", "GREATER_THAN_EQUAL", "LESS_THAN", "LESS_THAN_EQUAL", "CONTAINS"})
names = {"EQUALS", "GREATER_THAN", "GREATER_THAN_EQUAL", "LESS_THAN", "LESS_THAN_EQUAL", "CONTAINS", "STARTS_WITH", "ENDS_WITH"})
@DisplayName("evaluate as invalid if fact is null")
void factIsNotValidOnNullFact(Operator operator) {
setup(operator);
Expand All @@ -533,7 +664,7 @@ void factIsNotValidOnNullFact(Operator operator) {

@ParameterizedTest
@EnumSource(value = Operator.class,
names = {"EQUALS", "GREATER_THAN", "GREATER_THAN_EQUAL", "LESS_THAN", "LESS_THAN_EQUAL", "CONTAINS"})
names = {"EQUALS", "GREATER_THAN", "GREATER_THAN_EQUAL", "LESS_THAN", "LESS_THAN_EQUAL", "CONTAINS", "STARTS_WITH", "ENDS_WITH"})
@DisplayName("evaluate as invalid if rule has an empty value and fact is null")
void emptyValueButFactIsNull(Operator operator) {
setup(operator);
Expand All @@ -547,7 +678,7 @@ void emptyValueButFactIsNull(Operator operator) {

@ParameterizedTest
@EnumSource(value = Operator.class,
names = {"EQUALS", "GREATER_THAN", "GREATER_THAN_EQUAL", "LESS_THAN", "LESS_THAN_EQUAL", "CONTAINS"})
names = {"EQUALS", "GREATER_THAN", "GREATER_THAN_EQUAL", "LESS_THAN", "LESS_THAN_EQUAL", "CONTAINS", "STARTS_WITH", "ENDS_WITH"})
@DisplayName("evaluate as invalid if fact and value are both null")
void nullFactIsInvalidIfRuleValueIsNull(Operator operator) {
setup(operator);
Expand Down Expand Up @@ -617,7 +748,7 @@ void setup(Operator operator) {

@ParameterizedTest
@EnumSource(value = Operator.class,
names = {"EQUALS", "GREATER_THAN", "GREATER_THAN_EQUAL", "LESS_THAN", "LESS_THAN_EQUAL", "CONTAINS"},
names = {"EQUALS", "GREATER_THAN", "GREATER_THAN_EQUAL", "LESS_THAN", "LESS_THAN_EQUAL", "CONTAINS", "STARTS_WITH", "ENDS_WITH"},
mode = EnumSource.Mode.EXCLUDE)
@DisplayName("evaluate as not supported")
void factIsNotValidOnEmptyFact(Operator operator) {
Expand Down

0 comments on commit 89d55d5

Please sign in to comment.