Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Range Rule Implementation #24

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 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 @@ -22,7 +22,10 @@ public enum Operator {
INTERSECTS,
STARTS_WITH,
ENDS_WITH,
DIVISIBLE_BY
DIVISIBLE_BY,
BETWEEN,
IS_BEFORE,
IS_AFTER

// Operators not supported yet
// BETWEEN,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this

Expand Down
238 changes: 238 additions & 0 deletions src/main/java/com/adobe/abp/regola/rules/RangeRule.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
/*
* Copyright 2025 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License
*/

package com.adobe.abp.regola.rules;

import com.adobe.abp.regola.facts.FactsResolver;
import com.adobe.abp.regola.results.Result;
import com.adobe.abp.regola.results.RuleResult;
import com.adobe.abp.regola.results.ValuesRuleResult;
import com.adobe.abp.regola.utils.futures.FutureUtils;
import jdk.jfr.Experimental;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;

/**
* Experimental rule for evaluating facts against a range of values.
* Supports basic range operations for comparable types.
*
* The rule supports the following operators:
* <ul>
* <li>{@link Operator#BETWEEN}</li>
* <li>{@link Operator#IN}</li>
* <li>{@link Operator#IS_BEFORE}</li>
* <li>{@link Operator#IS_AFTER}</li>
* </ul>
*
* @param <T> type of the fact (must extend Comparable)
*/
@Experimental
public class RangeRule<T extends Comparable<T>> extends OperatorBasedRule {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for an update to the README as this is experimental for now, but do have unit tests please.


private final Executor executor;
private T min;
private T max;
private boolean minExclusive;
private boolean maxExclusive;

public RangeRule() {
this(null);
}

public RangeRule(Executor executor) {
super(RuleType.RANGE.getName());
this.executor = executor;
}

public T getMin() {
return min;
}

public RangeRule<T> setMin(T min) {
this.min = min;
return this;
}

public T getMax() {
return max;
}

public RangeRule<T> setMax(T max) {
this.max = max;
return this;
}


public boolean isMinExclusive() {
return minExclusive;
}

public RangeRule<T> setMinExclusive(boolean minExclusive) {
this.minExclusive = minExclusive;
return this;
}

public boolean isMaxExclusive() {
return maxExclusive;
}

public RangeRule<T> setMaxExclusive(boolean maxExclusive) {
this.maxExclusive = maxExclusive;
return this;
}

private Collection<T> generateExpectedValues() {
Collection<T> expectedValues = new ArrayList<>();

switch (getOperator()) {
case BETWEEN:
case IN:
expectedValues.add(min);
expectedValues.add(max);
break;
case IS_BEFORE:
expectedValues.add(min);
break;
case IS_AFTER:
expectedValues.add(max);
break;
}

return expectedValues;
}

@Override
public EvaluationResult evaluate(FactsResolver factsResolver) {
return new EvaluationResult() {
private Result result = Result.MAYBE;
private T evaluatedFact;
private Collection<T> evaluatedFacts;
private String message;
private Throwable cause;
private final Collection<T> expectedValues = generateExpectedValues();

@Override
public RuleResult snapshot() {
return ValuesRuleResult.<T>builder().with(r -> {
r.type = getType();
r.key = getKey();
r.operator = getOperator();
r.description = getDescription();
r.result = result;
r.actualValue = evaluatedFact;
r.actualValues = evaluatedFacts;
r.expectedValues = expectedValues;
if (!expectedValues.isEmpty()) {
r.expectedValue = expectedValues.iterator().next();
}
r.message = message;
r.cause = cause;
r.ignored = isIgnore();
}).build();
}

@Override
public CompletableFuture<Result> status() {
return resolveFact(factsResolver, getKey())
.thenCompose(fact -> FutureUtils.supplyAsync(() -> handleSuccess(fact), executor))
.whenComplete((result, throwable) -> Optional.ofNullable(getAction())
.ifPresent(action -> action.onCompletion(result, throwable, snapshot())))
.exceptionally(this::handleFailure);
}

private Result handleSuccess(T fact) {
if (fact == null) {
result = Result.INVALID;
return result;
}

evaluatedFact = fact;
result = evaluateRange(fact);
return result;
}

private Result handleFailure(Throwable throwable) {
result = Result.FAILED;
message = throwable.getMessage();
cause = throwable;
return result;
}
};
}

private Result evaluateRange(T fact) {
switch (getOperator()) {
case BETWEEN:
case IN:
return evaluateBetween(fact);
case IS_BEFORE:
return evaluateIsBefore(fact);
case IS_AFTER:
return evaluateIsAfter(fact);
default:
return Result.OPERATION_NOT_SUPPORTED;
}
}

private Result evaluateBetween(T fact) {
boolean afterMin = minExclusive ?
fact.compareTo(min) > 0 : fact.compareTo(min) >= 0;
boolean beforeMax = maxExclusive ?
fact.compareTo(max) < 0 : fact.compareTo(max) <= 0;

return afterMin && beforeMax ? Result.VALID : Result.INVALID;
}

private Result evaluateIsBefore(T fact) {
int compareMin = fact.compareTo(min);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should consider the exclusive param for min here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is still valid.

return compareMin < 0 ? Result.VALID : Result.INVALID;
}

private Result evaluateIsAfter(T fact) {
int compareMax = fact.compareTo(max);
return compareMax > 0 ? Result.VALID : Result.INVALID;
}

@SuppressWarnings("unchecked")
private CompletableFuture<T> resolveFact(FactsResolver factsResolver, String key) {
return factsResolver.resolveFact(key)
.thenApply(f -> {
if (f == null) {
return null;
}

try {
return (T) f;
} catch (ClassCastException e) {
throw new IllegalArgumentException("Fact must match type of the rule's range", e);
}
});
}

@Override
public String toString() {
return "RangeRule{" +
"type='" + getType() + '\'' +
", description='" + getDescription() + '\'' +
", key='" + getKey() + '\'' +
", operator=" + getOperator() +
", min=" + min +
", max=" + max +
", minExclusive=" + minExclusive +
", maxExclusive=" + maxExclusive +
", ignore=" + isIgnore() +
"}";
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New line at end of file missing

3 changes: 2 additions & 1 deletion src/main/java/com/adobe/abp/regola/rules/RuleType.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ public enum RuleType {
NUMBER("NUMBER"),
SET("SET"),
DATE("DATE"),
NULL("NULL");
NULL("NULL"),
RANGE("RANGE");

private final String name;

Expand Down