diff --git a/build.gradle b/build.gradle index f332823..142a7b9 100644 --- a/build.gradle +++ b/build.gradle @@ -108,6 +108,7 @@ subprojects { testImplementation 'org.testcontainers:junit-jupiter' testImplementation group: 'com.redis', name: 'testcontainers-redis', version: testcontainersRedisVersion testImplementation group: 'com.redis', name: 'testcontainers-redis-enterprise', version: testcontainersRedisVersion + testImplementation 'org.mockito:mockito-core:5.11.0' } test { diff --git a/core/lettucemod-query/src/main/java/com/redis/search/query/filter/CompositeCondition.java b/core/lettucemod-query/src/main/java/com/redis/search/query/filter/CompositeCondition.java index 650d76a..a2b2b15 100644 --- a/core/lettucemod-query/src/main/java/com/redis/search/query/filter/CompositeCondition.java +++ b/core/lettucemod-query/src/main/java/com/redis/search/query/filter/CompositeCondition.java @@ -2,9 +2,9 @@ public class CompositeCondition implements Condition { - private final Condition left; - private final Condition right; - private final CharSequence delimiter; + protected final Condition left; + protected final Condition right; + protected final CharSequence delimiter; public CompositeCondition(CharSequence delimiter, Condition left, Condition right) { this.delimiter = delimiter; diff --git a/core/lettucemod-query/src/main/java/com/redis/search/query/filter/Condition.java b/core/lettucemod-query/src/main/java/com/redis/search/query/filter/Condition.java index 88f151e..be7ce0d 100644 --- a/core/lettucemod-query/src/main/java/com/redis/search/query/filter/Condition.java +++ b/core/lettucemod-query/src/main/java/com/redis/search/query/filter/Condition.java @@ -1,23 +1,29 @@ package com.redis.search.query.filter; +import java.util.List; + public interface Condition { String getQuery(); default Condition and(Condition condition) { - return new And(this, condition); + return new And(this, condition); } default Condition or(Condition condition) { - return new Or(this, condition); + return new Or(this, condition); + } + + static Condition orList(final Condition... conditions) { + return new OrList(conditions); } default Condition not() { - return new Not(this); + return new Not(this); } default Condition optional() { - return new Optional(this); + return new Optional(this); } } diff --git a/core/lettucemod-query/src/main/java/com/redis/search/query/filter/Or.java b/core/lettucemod-query/src/main/java/com/redis/search/query/filter/Or.java index b2c61b3..8273572 100644 --- a/core/lettucemod-query/src/main/java/com/redis/search/query/filter/Or.java +++ b/core/lettucemod-query/src/main/java/com/redis/search/query/filter/Or.java @@ -5,7 +5,7 @@ public class Or extends CompositeCondition { public static final String DELIMITER = "|"; public Or(Condition left, Condition right) { - super(DELIMITER, left, right); + super(DELIMITER, left, right); } -} +} \ No newline at end of file diff --git a/core/lettucemod-query/src/main/java/com/redis/search/query/filter/OrList.java b/core/lettucemod-query/src/main/java/com/redis/search/query/filter/OrList.java new file mode 100644 index 0000000..432686c --- /dev/null +++ b/core/lettucemod-query/src/main/java/com/redis/search/query/filter/OrList.java @@ -0,0 +1,75 @@ +package com.redis.search.query.filter; + +import java.util.Arrays; +import java.util.List; +import java.util.StringJoiner; + +/** + * Represents a logical OR condition composed of multiple {@link Condition} elements. + *

+ * This class generates a query string where each condition is wrapped in parentheses + * and separated by the OR operator ('|'), suitable for use in Redis Search queries. + *

+ * Example output: + *

{@code
+ *     ( (query1)|(query2)|(query3) )
+ * }
+ */ +public class OrList implements Condition { + + /** + * The delimiter used to join conditions in an OR clause. + */ + public static final String OR_LIST_CONDITION_FORMAT = "|"; + + private final List conditions; + + /** + * Constructs an {@code OrList} with the given conditions. + * + * @param conditions One or more {@link Condition} objects to be ORed together. + */ + public OrList(Condition... conditions) { + this.conditions = Arrays.asList(conditions); + } + + /** + * Builds and returns the query string representing a logical OR condition. + *

+ * Logic: + *

+ * + * @return A formatted query string representing the logical OR of valid conditions, + * or an empty string if none exist. + */ + @Override + public String getQuery() { + if (conditions == null || conditions.isEmpty()) { + return ""; + } + + StringJoiner joiner = new StringJoiner(OR_LIST_CONDITION_FORMAT); + int validConditionCounter = 0; + for (Condition condition : conditions) { + if (condition == null) { + continue; + } + + String query = condition.getQuery(); + if (query != null && !query.isEmpty()) { + joiner.add("(" + query + ")"); + validConditionCounter++; + } + } + if (joiner.length() == 0 || validConditionCounter < 2) { + return joiner.toString(); + } + + return "(" + joiner.toString() + ")"; + } +} diff --git a/core/lettucemod-query/src/test/java/com/redis/query/TestOrList.java b/core/lettucemod-query/src/test/java/com/redis/query/TestOrList.java new file mode 100644 index 0000000..f8c6b30 --- /dev/null +++ b/core/lettucemod-query/src/test/java/com/redis/query/TestOrList.java @@ -0,0 +1,77 @@ +package com.redis.query; + +import com.redis.search.query.filter.Condition; +import com.redis.search.query.filter.OrList; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class TestOrList { + Condition cond1; + Condition cond2; + Condition cond3; + + @BeforeEach + public void setup() { + cond1 = Mockito.mock(Condition.class); + cond2 = Mockito.mock(Condition.class); + cond3 = Mockito.mock(Condition.class); + } + + @Test + public void testOrCondition_whenConditionsArePresent_thenReturnQuery() { + Mockito.when(cond1.getQuery()).thenReturn("@category:{italian}"); + Mockito.when(cond2.getQuery()).thenReturn("@price:[1000 2000]"); + Mockito.when(cond3.getQuery()).thenReturn("@rating:[4.0 5.0]"); + + OrList orListCondition = new OrList(cond1, cond2, cond3); + String orListQuery = orListCondition.getQuery(); + + Assertions.assertEquals("((@category:{italian})|(@price:[1000 2000])|(@rating:[4.0 5.0]))", orListQuery); + } + + @Test + public void testOrCondition_whenConditionsAreEmpty_thenReturnEmptyQuery() { + Mockito.when(cond1.getQuery()).thenReturn(""); + Mockito.when(cond2.getQuery()).thenReturn(""); + Mockito.when(cond3.getQuery()).thenReturn(""); + + OrList orListCondition = new OrList(cond1, cond2, cond3); + String orListQuery = orListCondition.getQuery(); + + Assertions.assertEquals("", orListQuery); + } + + @Test + public void testOrCondition_whenConditionsAreNull_thenReturnEmptyQuery() { + Mockito.when(cond1.getQuery()).thenReturn(null); + Mockito.when(cond2.getQuery()).thenReturn(null); + Mockito.when(cond3.getQuery()).thenReturn(null); + + OrList orListCondition = new OrList(cond1, cond2, cond3); + String orListQuery = orListCondition.getQuery(); + + Assertions.assertEquals("", orListQuery); + } + + @Test + public void testOrCondition_whenSingleConditionIsPresent_thenReturnQuery() { + Mockito.when(cond1.getQuery()).thenReturn("@category:{italian}"); + + OrList orListCondition = new OrList(cond1); + String orListQuery = orListCondition.getQuery(); + + Assertions.assertEquals("(@category:{italian})", orListQuery); + } + + @Test + public void testOrCondition_whenSingleConditionIsPresentAlongWithNullCondition_thenReturnQuery() { + Mockito.when(cond1.getQuery()).thenReturn("@category:{italian}"); + + OrList orListCondition = new OrList(cond1, null); + String orListQuery = orListCondition.getQuery(); + + Assertions.assertEquals("(@category:{italian})", orListQuery); + } +}