Skip to content
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
2 changes: 1 addition & 1 deletion backend-core-business-impl/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>com.flowingcode.backend-core</groupId>
<artifactId>backend-core</artifactId>
<version>1.1.1-SNAPSHOT</version>
<version>1.2.0-SNAPSHOT</version>
</parent>

<artifactId>backend-core-business-impl</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion backend-core-business-spring-impl/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>com.flowingcode.backend-core</groupId>
<artifactId>backend-core</artifactId>
<version>1.1.1-SNAPSHOT</version>
<version>1.2.0-SNAPSHOT</version>
</parent>

<artifactId>backend-core-business-spring-impl</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion backend-core-business/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>com.flowingcode.backend-core</groupId>
<artifactId>backend-core</artifactId>
<version>1.1.1-SNAPSHOT</version>
<version>1.2.0-SNAPSHOT</version>
</parent>

<artifactId>backend-core-business</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion backend-core-data-impl/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>com.flowingcode.backend-core</groupId>
<artifactId>backend-core</artifactId>
<version>1.1.1-SNAPSHOT</version>
<version>1.2.0-SNAPSHOT</version>
</parent>

<artifactId>backend-core-data-impl</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* #%L
* Commons Backend - Data Access Layer Implementations
* %%
* Copyright (C) 2020 - 2021 Flowing Code
* Copyright (C) 2020 - 2026 Flowing Code
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -31,6 +31,7 @@
import com.flowingcode.backendcore.model.constraints.AttributeLikeConstraint;
import com.flowingcode.backendcore.model.constraints.AttributeNullConstraint;
import com.flowingcode.backendcore.model.constraints.AttributeRelationalConstraint;
import com.flowingcode.backendcore.model.constraints.DisjunctionConstraint;
import com.flowingcode.backendcore.model.constraints.NegatedConstraint;
import com.flowingcode.backendcore.model.constraints.RelationalConstraint;

Expand All @@ -44,6 +45,11 @@
import lombok.NonNull;
import lombok.RequiredArgsConstructor;

/**
* JPA/Criteria implementation of {@link ConstraintTransformer}.
*
* <p><b>Instances are not thread-safe.</b> A new instance must be created for each query.
*/
@RequiredArgsConstructor
public class ConstraintTransformerJpaImpl extends ConstraintTransformer<Predicate> {

Expand Down Expand Up @@ -80,10 +86,12 @@ private From<?,?> join(From<?,?> root, String[] path) {
return from;
}

private JoinType currentJoinType = JoinType.INNER;

@SuppressWarnings("rawtypes")
private From<?,?> join(From<?,?> source, String attributeName) {
Optional<Join> existingJoin = source.getJoins().stream().filter(join->join.getAttribute().getName().equals(attributeName)).map(join->(Join)join).findFirst();
return existingJoin.orElseGet(()->source.join(attributeName, JoinType.INNER));
return existingJoin.orElseGet(()->source.join(attributeName, currentJoinType));
}
Comment on lines 92 to 95
Copy link

@coderabbitai coderabbitai bot Mar 17, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

impl_file="$(fd ConstraintTransformerJpaImpl.java | head -n1)"
test_file="$(fd JpaDaoSupportTest.java | head -n1)"

echo "== join(...) implementation around Line 92 =="
sed -n '88,102p' "$impl_file"

echo
echo "== occurrences of getJoinType() in transformer (expecting join-type-aware reuse) =="
rg -n 'getJoinType\(\)' "$impl_file" || true

echo
echo "== OR-related tests currently present =="
rg -n 'testFilterWithOrConstraint|testFilterWithOrConstraintPartialMatch|\\.or\\(' "$test_file" -A3 -B2

Repository: FlowingCode/backend-core

Length of output: 1805


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the implementation file
impl_file="backend-core-data-impl/src/main/java/com/flowingcode/backendcore/dao/jpa/ConstraintTransformerJpaImpl.java"

if [ -f "$impl_file" ]; then
    echo "== join(...) implementation around Line 92 =="
    sed -n '88,102p' "$impl_file"
    
    echo
    echo "== All getJoinType() calls in transformer =="
    rg -n 'getJoinType\(\)' "$impl_file" -B2 -A2 || true
    
    echo
    echo "== transformDisjunctionConstraint method context (lines 179-186) =="
    sed -n '175,190p' "$impl_file"
else
    echo "File not found at: $impl_file"
    # Try to locate it
    find . -name "ConstraintTransformerJpaImpl.java" -type f 2>/dev/null || echo "No ConstraintTransformerJpaImpl.java found"
fi

Repository: FlowingCode/backend-core

Length of output: 1388


Make join reuse join-type-aware to preserve OR semantics.

The join() method (lines 92-95) reuses any existing join by attribute name only. When transformDisjunctionConstraint() switches currentJoinType to LEFT (line 183), a previous INNER join with the same attribute name will be incorrectly reused, causing the OR evaluation to apply INNER join semantics (dropping nullable-association rows) before OR logic.

The suggested patch correctly adds a join-type check to ensure reused joins match both the attribute name and the current join type.

Suggested patch
 `@SuppressWarnings`("rawtypes")
 private From<?,?> join(From<?,?> source, String attributeName) {
-    Optional<Join> existingJoin = source.getJoins().stream().filter(join->join.getAttribute().getName().equals(attributeName)).map(join->(Join)join).findFirst();
-    return existingJoin.orElseGet(()->source.join(attributeName, currentJoinType));
+    Optional<Join> existingJoin = source.getJoins().stream()
+            .map(join -> (Join) join)
+            .filter(join -> join.getAttribute().getName().equals(attributeName))
+            .filter(join -> join.getJoinType() == currentJoinType)
+            .findFirst();
+    return existingJoin.orElseGet(() -> source.join(attributeName, currentJoinType));
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend-core-data-impl/src/main/java/com/flowingcode/backendcore/dao/jpa/ConstraintTransformerJpaImpl.java`
around lines 92 - 95, The join() method currently reuses existing joins based
only on attribute name, which can reuse an INNER join when currentJoinType is
LEFT and break OR semantics in transformDisjunctionConstraint; update
join(From<?,?> source, String attributeName) to check both
join.getAttribute().getName().equals(attributeName) and that the existing Join's
join type equals currentJoinType (compare via join.getJoinType() or equivalent),
and only reuse when both match—otherwise call source.join(attributeName,
currentJoinType) to create a join with the correct type.

Copy link
Member Author

Choose a reason for hiding this comment

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

@coderabbitai The finding is technically correct — the filter on line 93 does not check join type — but it has no observable effect on query results. When a prior non-disjunction constraint has already created an INNER JOIN on an association, rows with a null foreign key are eliminated by that join before any WHERE predicate is evaluated, including the OR predicate from the disjunction. Conversely, when the disjunction runs first and creates a LEFT JOIN, any subsequent non-disjunction predicate on that association (e.g. city.population > 0) evaluates to NULL for no-city rows, which SQL treats as UNKNOWN and filters out — the same rows excluded by an INNER JOIN. In both orderings the final result set is identical. The fix would not change which rows are returned.

Copy link

Choose a reason for hiding this comment

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

@javier-godoy, that's a well-reasoned explanation — thank you for the detailed walkthrough. You're right on both orderings:

  • INNER JOIN pre-exists: null-FK rows are already absent from the result set before the OR predicate is even applied, so reusing the INNER join inside transformDisjunctionConstraint produces no different outcome.
  • LEFT JOIN pre-exists (disjunction ran first): any subsequent equality/comparison predicate on the joined table's columns will evaluate to UNKNOWN for the null-padded rows (SQL three-valued logic), causing them to be silently excluded — effectively the same result as an INNER join would produce.

I'll retract the suggestion. The code is functionally correct as written; the join-type mismatch has no observable effect on query results.

ᓚᘏᗢ


private static Class<?> boxed(Class<?> type) {
Expand Down Expand Up @@ -166,4 +174,18 @@ protected Predicate transformNullConstraint(AttributeNullConstraint c) {
protected Predicate transformILikeConstraint(AttributeILikeConstraint c) {
return criteriaBuilder.like(criteriaBuilder.lower(getExpression(c, String.class)), c.getPattern().toLowerCase());
}

@Override
protected Predicate transformDisjunctionConstraint(DisjunctionConstraint c) {
JoinType saved = currentJoinType;
currentJoinType = JoinType.LEFT;
try {
Predicate[] predicates = c.getConstraints().stream()
.map(this::apply)
.toArray(Predicate[]::new);
return criteriaBuilder.or(predicates);
} finally {
currentJoinType = saved;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* #%L
* Commons Backend - Data Access Layer Implementations
* %%
* Copyright (C) 2020 - 2021 Flowing Code
* Copyright (C) 2020 - 2026 Flowing Code
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -147,6 +147,27 @@ void testFilterByState() {
assertEquals(5, count);
}

@Test
void testFilterWithOrConstraint() {
// OR of both city ids must match all 10 persons that have a city assigned
PersonFilter pf = new PersonFilter();
pf.addConstraint(
ConstraintBuilder.of("city", "id").equal(cities.get(0).getId())
.or(ConstraintBuilder.of("city", "id").equal(cities.get(1).getId())));
assertEquals(10, dao.count(pf));
}

@Test
void testFilterWithOrConstraintPartialMatch() {
// city.id branch matches 5 persons; id branch matches persistedPerson (who has no city).
// LEFT JOIN on city must keep persistedPerson in the result set so the OR can match them.
PersonFilter pf = new PersonFilter();
pf.addConstraint(
ConstraintBuilder.of("city", "id").equal(cities.get(0).getId())
.or(ConstraintBuilder.of("id").equal(persistedPerson.getId())));
assertEquals(6, dao.count(pf));
}

@Test
@Disabled
void testDelete() {
Expand Down
2 changes: 1 addition & 1 deletion backend-core-data/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>com.flowingcode.backend-core</groupId>
<artifactId>backend-core</artifactId>
<version>1.1.1-SNAPSHOT</version>
<version>1.2.0-SNAPSHOT</version>
</parent>

<artifactId>backend-core-data</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion backend-core-model/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>com.flowingcode.backend-core</groupId>
<artifactId>backend-core</artifactId>
<version>1.1.1-SNAPSHOT</version>
<version>1.2.0-SNAPSHOT</version>
</parent>

<artifactId>backend-core-model</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* #%L
* Commons Backend - Model
* %%
* Copyright (C) 2020 - 2021 Flowing Code
* Copyright (C) 2020 - 2026 Flowing Code
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -19,12 +19,32 @@
*/
package com.flowingcode.backendcore.model;

import com.flowingcode.backendcore.model.constraints.DisjunctionConstraint;
import com.flowingcode.backendcore.model.constraints.NegatedConstraint;

public interface Constraint {

default Constraint not() {
return new NegatedConstraint(this);
}


/**
* Returns a constraint that is satisfied when this constraint or any of the given constraints is
* satisfied (logical OR).
*
* @param first the first additional constraint
* @param rest optional additional constraints
* @return a {@link DisjunctionConstraint} combining this and the given constraints
*/
default Constraint or(Constraint first, Constraint... rest) {
return DisjunctionConstraint.of(this, prepend(first, rest));
}

private static Constraint[] prepend(Constraint first, Constraint[] rest) {
Constraint[] result = new Constraint[1 + rest.length];
result[0] = first;
System.arraycopy(rest, 0, result, 1, rest.length);
return result;
Comment on lines +39 to +47
Copy link

@coderabbitai coderabbitai bot Mar 17, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle explicit null varargs defensively in or(...).

Line 43 assumes rest is non-null. A call like or(x, (Constraint[]) null) throws an NPE in prepend(...) before constraint-level null validation runs.

Suggested fix
 import com.flowingcode.backendcore.model.constraints.DisjunctionConstraint;
 import com.flowingcode.backendcore.model.constraints.NegatedConstraint;
+import java.util.Objects;
@@
   default Constraint or(Constraint first, Constraint... rest) {
-    return DisjunctionConstraint.of(this, prepend(first, rest));
+    return DisjunctionConstraint.of(this, prepend(first, Objects.requireNonNull(rest, "rest must not be null")));
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
default Constraint or(Constraint first, Constraint... rest) {
return DisjunctionConstraint.of(this, prepend(first, rest));
}
private static Constraint[] prepend(Constraint first, Constraint[] rest) {
Constraint[] result = new Constraint[1 + rest.length];
result[0] = first;
System.arraycopy(rest, 0, result, 1, rest.length);
return result;
default Constraint or(Constraint first, Constraint... rest) {
return DisjunctionConstraint.of(this, prepend(first, Objects.requireNonNull(rest, "rest must not be null")));
}
private static Constraint[] prepend(Constraint first, Constraint[] rest) {
Constraint[] result = new Constraint[1 + rest.length];
result[0] = first;
System.arraycopy(rest, 0, result, 1, rest.length);
return result;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend-core-model/src/main/java/com/flowingcode/backendcore/model/Constraint.java`
around lines 39 - 47, The or(Constraint first, Constraint... rest) path assumes
rest is non-null and will NPE in prepend(...) for calls like or(x,
(Constraint[]) null); modify prepend(Constraint first, Constraint[] rest) (or
add a null-guard in or()) to treat a null rest as an empty array before creating
the result array and calling System.arraycopy — e.g., normalize rest to an empty
Constraint[] when null so the array creation and copy logic work safely and
subsequent constraint-level null validations can run.

Copy link
Member Author

Choose a reason for hiding this comment

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

@coderabbitai please check if the comment has been addressed.

Copy link

Choose a reason for hiding this comment

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

@javier-godoy, based on the current file content, the comment has not been addressed yet. The or(Constraint first, Constraint... rest) method still passes rest directly to prepend(...) without any null-guard:

default Constraint or(Constraint first, Constraint... rest) {
    return DisjunctionConstraint.of(this, prepend(first, rest));
}

A call like or(x, (Constraint[]) null) will still throw an NPE inside prepend(...) at rest.length before any constraint-level validation runs. The suggested fix (Objects.requireNonNull(rest, "rest must not be null")) has not been applied.

Copy link
Member Author

Choose a reason for hiding this comment

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

@coderabbitai Note that DisjunctionConstraint.of already checks whether the varargs is null

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd "DisjunctionConstraint.java" --exec cat {}

Length of output: 1759


@javier-godoy, you're right that DisjunctionConstraint.of does include a null check (Objects.requireNonNull(rest, "constraints must not be null")). However, that check is never reached in this code path when rest is null. The call chain is:

  1. or(first, null) → calls prepend(first, null)
  2. In prepend: new Constraint[1 + rest.length]NPE here (no message), before DisjunctionConstraint.of is ever invoked.

So the null guard in DisjunctionConstraint.of doesn't cover this case — prepend fails first. The original concern remains: a caller passing or(x, (Constraint[]) null) gets an NPE with no helpful message, whereas the guard in DisjunctionConstraint.of would give "constraints must not be null".

That said, explicitly casting null to Constraint[] is a very unusual call pattern, and both paths still fail fast with an NPE. If you consider this acceptable behavior, I can close the comment.

Copy link
Member Author

Choose a reason for hiding this comment

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

@coderabbitai I consider this acceptable behavior.

Copy link

Choose a reason for hiding this comment

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

@javier-godoy, understood — acknowledged and noted. The existing fast-fail NPE behavior for or(x, (Constraint[]) null) is acceptable as-is.

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* #%L
* Commons Backend - Model
* %%
* Copyright (C) 2020 - 2021 Flowing Code
* Copyright (C) 2020 - 2026 Flowing Code
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -28,6 +28,7 @@
import com.flowingcode.backendcore.model.constraints.AttributeLikeConstraint;
import com.flowingcode.backendcore.model.constraints.AttributeNullConstraint;
import com.flowingcode.backendcore.model.constraints.AttributeRelationalConstraint;
import com.flowingcode.backendcore.model.constraints.DisjunctionConstraint;
import com.flowingcode.backendcore.model.constraints.NegatedConstraint;

/**
Expand Down Expand Up @@ -80,6 +81,10 @@
return transformILikeConstraint((AttributeILikeConstraint) c);
}

if (c instanceof DisjunctionConstraint) {

Check warning on line 84 in backend-core-model/src/main/java/com/flowingcode/backendcore/model/ConstraintTransformer.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this instanceof check and cast with 'instanceof DisjunctionConstraint disjunctionconstraint'

See more on https://sonarcloud.io/project/issues?id=FlowingCode_backend-core&issues=AZz3-zsR2UZ2OVrkOAs-&open=AZz3-zsR2UZ2OVrkOAs-&pullRequest=108
return transformDisjunctionConstraint((DisjunctionConstraint) c);
}

return null;
}

Expand Down Expand Up @@ -125,4 +130,10 @@
protected T transformILikeConstraint(AttributeILikeConstraint c) {
return null;
}

/** Return an implementation-specific representation of a {@code DisjunctionConstraint} constraint.
* @return an implementation-specific representation of the constraint, or {@code null} if it cannot be transformed.*/
protected T transformDisjunctionConstraint(DisjunctionConstraint c) {

Check warning on line 136 in backend-core-model/src/main/java/com/flowingcode/backendcore/model/ConstraintTransformer.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unused method parameter "c".

See more on https://sonarcloud.io/project/issues?id=FlowingCode_backend-core&issues=AZz3-zsR2UZ2OVrkOAs_&open=AZz3-zsR2UZ2OVrkOAs_&pullRequest=108
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*-
* #%L
* Commons Backend - Model
* %%
* Copyright (C) 2020 - 2026 Flowing Code
* %%
* Licensed 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 CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package com.flowingcode.backendcore.model.constraints;

import com.flowingcode.backendcore.model.Constraint;
import java.util.List;
import java.util.Objects;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;

/** A constraint that is satisfied when any of its member constraints is satisfied (logical OR). */
@Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public final class DisjunctionConstraint implements Constraint {

@NonNull List<Constraint> constraints;

public static DisjunctionConstraint of(Constraint first, Constraint... rest) {
List<Constraint> list = new java.util.ArrayList<>();
Objects.requireNonNull(rest, "constraints must not be null");
add(list, Objects.requireNonNull(first, "constraint must not be null"));
for (Constraint c : rest) {
add(list, Objects.requireNonNull(c, "constraint must not be null"));
}
return new DisjunctionConstraint(List.copyOf(list));
}

private static void add(List<Constraint> list, Constraint c) {
if (c instanceof DisjunctionConstraint) {

Check warning on line 50 in backend-core-model/src/main/java/com/flowingcode/backendcore/model/constraints/DisjunctionConstraint.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this instanceof check and cast with 'instanceof DisjunctionConstraint disjunctionconstraint'

See more on https://sonarcloud.io/project/issues?id=FlowingCode_backend-core&issues=AZz78jGqKMkdTNmI-HaT&open=AZz78jGqKMkdTNmI-HaT&pullRequest=108
list.addAll(((DisjunctionConstraint) c).constraints);
} else {
list.add(c);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*-
* #%L
* Commons Backend - Model
* %%
* Copyright (C) 2020 - 2026 Flowing Code
* %%
* Licensed 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 CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package com.flowingcode.backendcore.model.constraints;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;

import com.flowingcode.backendcore.model.Constraint;
import com.flowingcode.backendcore.model.ConstraintBuilder;
import java.util.List;
import org.junit.jupiter.api.Test;

class DisjunctionConstraintTest {

private static final Constraint A = ConstraintBuilder.of("a").equal(1);
private static final Constraint B = ConstraintBuilder.of("b").equal(2);
private static final Constraint C = ConstraintBuilder.of("c").equal(3);

@Test
void testOfProducesCorrectMembers() {
DisjunctionConstraint d = DisjunctionConstraint.of(A, B, C);
assertEquals(List.of(A, B, C), d.getConstraints());
}

@Test
void testChainedOrFlattens() {
// a.or(b).or(c) must produce OR(a, b, c), not OR(OR(a, b), c)
Constraint chained = A.or(B).or(C);
DisjunctionConstraint d = (DisjunctionConstraint) chained;
assertEquals(3, d.getConstraints().size());
assertSame(A, d.getConstraints().get(0));
assertSame(B, d.getConstraints().get(1));
assertSame(C, d.getConstraints().get(2));
}

@Test
void testOrWithExistingDisjunctionInRestFlattens() {
// DisjunctionConstraint passed as a rest element is also flattened
DisjunctionConstraint ab = DisjunctionConstraint.of(A, B);
DisjunctionConstraint d = DisjunctionConstraint.of(C, ab);
assertEquals(List.of(C, A, B), d.getConstraints());
}

@Test
void testNullFirstThrows() {
assertThrows(NullPointerException.class, () -> DisjunctionConstraint.of(null, B));
}

@Test
void testNullRestArrayThrows() {
assertThrows(NullPointerException.class, () -> DisjunctionConstraint.of(A, (Constraint[]) null));
}

@Test
void testNullElementInRestThrows() {
assertThrows(NullPointerException.class, () -> DisjunctionConstraint.of(A, B, null));
}

}
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<groupId>com.flowingcode.backend-core</groupId>
<artifactId>backend-core</artifactId>
<packaging>pom</packaging>
<version>1.1.1-SNAPSHOT</version>
<version>1.2.0-SNAPSHOT</version>
<name>Backend Core</name>
<description>Common utilities for backend enterprise application development</description>
<url>https://www.flowingcode.com/en/open-source/</url>
Expand Down
Loading