From 64619d22acf83d63aeeb6c326d61cad7738b7006 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 5 Jul 2025 06:05:45 +0000 Subject: [PATCH 1/3] Initial plan From 15d224c539647452cf11709315773cba05cd0a80 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 5 Jul 2025 06:28:29 +0000 Subject: [PATCH 2/3] Implement DecisionGraph with nodes and edges for decision table components Co-authored-by: nergal-perm <4876699+nergal-perm@users.noreply.github.com> --- .../ru/ewc/decisions/core/BasicGraphEdge.java | 111 +++++++++++++ .../ru/ewc/decisions/core/ConditionNode.java | 94 +++++++++++ .../ru/ewc/decisions/core/CoordinateNode.java | 86 ++++++++++ .../ru/ewc/decisions/core/DecisionGraph.java | 85 ++++++++++ .../ewc/decisions/core/DecisionTableNode.java | 93 +++++++++++ .../java/ru/ewc/decisions/core/GraphEdge.java | 65 ++++++++ .../java/ru/ewc/decisions/core/GraphNode.java | 58 +++++++ .../decisions/core/InMemoryDecisionGraph.java | 99 ++++++++++++ .../java/ru/ewc/decisions/core/LhsOfEdge.java | 89 ++++++++++ .../ru/ewc/decisions/core/OperatorOfEdge.java | 89 ++++++++++ .../ru/ewc/decisions/core/PartOfEdge.java | 89 ++++++++++ .../java/ru/ewc/decisions/core/RhsOfEdge.java | 89 ++++++++++ .../ewc/decisions/core/RuleFragmentNode.java | 87 ++++++++++ .../java/ru/ewc/decisions/core/RuleNode.java | 93 +++++++++++ .../decisions/core/BasicGraphEdgeTest.java | 121 ++++++++++++++ .../decisions/core/CoordinateNodeTest.java | 73 +++++++++ .../core/DecisionGraphIntegrationTest.java | 143 ++++++++++++++++ .../core/InMemoryDecisionGraphTest.java | 152 ++++++++++++++++++ 18 files changed, 1716 insertions(+) create mode 100644 src/main/java/ru/ewc/decisions/core/BasicGraphEdge.java create mode 100644 src/main/java/ru/ewc/decisions/core/ConditionNode.java create mode 100644 src/main/java/ru/ewc/decisions/core/CoordinateNode.java create mode 100644 src/main/java/ru/ewc/decisions/core/DecisionGraph.java create mode 100644 src/main/java/ru/ewc/decisions/core/DecisionTableNode.java create mode 100644 src/main/java/ru/ewc/decisions/core/GraphEdge.java create mode 100644 src/main/java/ru/ewc/decisions/core/GraphNode.java create mode 100644 src/main/java/ru/ewc/decisions/core/InMemoryDecisionGraph.java create mode 100644 src/main/java/ru/ewc/decisions/core/LhsOfEdge.java create mode 100644 src/main/java/ru/ewc/decisions/core/OperatorOfEdge.java create mode 100644 src/main/java/ru/ewc/decisions/core/PartOfEdge.java create mode 100644 src/main/java/ru/ewc/decisions/core/RhsOfEdge.java create mode 100644 src/main/java/ru/ewc/decisions/core/RuleFragmentNode.java create mode 100644 src/main/java/ru/ewc/decisions/core/RuleNode.java create mode 100644 src/test/java/ru/ewc/decisions/core/BasicGraphEdgeTest.java create mode 100644 src/test/java/ru/ewc/decisions/core/CoordinateNodeTest.java create mode 100644 src/test/java/ru/ewc/decisions/core/DecisionGraphIntegrationTest.java create mode 100644 src/test/java/ru/ewc/decisions/core/InMemoryDecisionGraphTest.java diff --git a/src/main/java/ru/ewc/decisions/core/BasicGraphEdge.java b/src/main/java/ru/ewc/decisions/core/BasicGraphEdge.java new file mode 100644 index 0000000..e8e3963 --- /dev/null +++ b/src/main/java/ru/ewc/decisions/core/BasicGraphEdge.java @@ -0,0 +1,111 @@ +/* + * MIT License + * + * Copyright (c) 2024-2025 Eugene Terekhov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package ru.ewc.decisions.core; + +import java.util.Objects; + +/** + * I am a concrete edge in the DecisionGraph that represents a basic relationship. + * I am immutable and connect two nodes with a specific relationship type. + * + * @since 0.9.2 + */ +public final class BasicGraphEdge implements GraphEdge { + /** + * The source node of this edge. + */ + private final GraphNode source; + + /** + * The target node of this edge. + */ + private final GraphNode target; + + /** + * The type of relationship this edge represents. + */ + private final String relationshipType; + + /** + * Constructor. + * + * @param source The source node of this edge. + * @param target The target node of this edge. + * @param relationshipType The type of relationship this edge represents. + */ + public BasicGraphEdge(final GraphNode source, final GraphNode target, final String relationshipType) { + this.source = Objects.requireNonNull(source, "Source node cannot be null"); + this.target = Objects.requireNonNull(target, "Target node cannot be null"); + this.relationshipType = Objects.requireNonNull(relationshipType, "Relationship type cannot be null"); + } + + @Override + public GraphNode source() { + return this.source; + } + + @Override + public GraphNode target() { + return this.target; + } + + @Override + public String relationshipType() { + return this.relationshipType; + } + + @Override + public String id() { + return String.format("%s--%s-->%s", this.source.id(), this.relationshipType, this.target.id()); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final BasicGraphEdge that = (BasicGraphEdge) obj; + return Objects.equals(this.source, that.source) && + Objects.equals(this.target, that.target) && + Objects.equals(this.relationshipType, that.relationshipType); + } + + @Override + public int hashCode() { + return Objects.hash(this.source, this.target, this.relationshipType); + } + + @Override + public String toString() { + return "BasicGraphEdge{" + + "source=" + this.source.id() + + ", target=" + this.target.id() + + ", relationshipType='" + this.relationshipType + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/ru/ewc/decisions/core/ConditionNode.java b/src/main/java/ru/ewc/decisions/core/ConditionNode.java new file mode 100644 index 0000000..4332de3 --- /dev/null +++ b/src/main/java/ru/ewc/decisions/core/ConditionNode.java @@ -0,0 +1,94 @@ +/* + * MIT License + * + * Copyright (c) 2024-2025 Eugene Terekhov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package ru.ewc.decisions.core; + +import java.util.Objects; +import ru.ewc.decisions.conditions.Condition; + +/** + * I am a concrete node in the DecisionGraph that represents a Condition. + * I am immutable and provide a unique identifier based on the condition's properties. + * + * @since 0.9.2 + */ +public final class ConditionNode implements GraphNode { + /** + * The condition this node represents. + */ + private final Condition condition; + + /** + * The unique identifier for this condition. + */ + private final String identifier; + + /** + * Constructor. + * + * @param condition The condition this node represents. + * @param identifier A unique identifier for this condition. + */ + public ConditionNode(final Condition condition, final String identifier) { + this.condition = Objects.requireNonNull(condition, "Condition cannot be null"); + this.identifier = Objects.requireNonNull(identifier, "Identifier cannot be null"); + } + + @Override + public String id() { + return "condition:" + this.identifier; + } + + @Override + public String type() { + return "condition"; + } + + @Override + public Object component() { + return this.condition; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final ConditionNode that = (ConditionNode) obj; + return Objects.equals(this.identifier, that.identifier); + } + + @Override + public int hashCode() { + return Objects.hash(this.identifier); + } + + @Override + public String toString() { + return "ConditionNode{" + "identifier='" + this.identifier + '\'' + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/ru/ewc/decisions/core/CoordinateNode.java b/src/main/java/ru/ewc/decisions/core/CoordinateNode.java new file mode 100644 index 0000000..9409c4c --- /dev/null +++ b/src/main/java/ru/ewc/decisions/core/CoordinateNode.java @@ -0,0 +1,86 @@ +/* + * MIT License + * + * Copyright (c) 2024-2025 Eugene Terekhov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package ru.ewc.decisions.core; + +import java.util.Objects; + +/** + * I am a concrete node in the DecisionGraph that represents a Coordinate. + * I am immutable and provide a unique identifier based on the coordinate's string representation. + * + * @since 0.9.2 + */ +public final class CoordinateNode implements GraphNode { + /** + * The coordinate this node represents. + */ + private final Coordinate coordinate; + + /** + * Constructor. + * + * @param coordinate The coordinate this node represents. + */ + public CoordinateNode(final Coordinate coordinate) { + this.coordinate = Objects.requireNonNull(coordinate, "Coordinate cannot be null"); + } + + @Override + public String id() { + return "coordinate:" + this.coordinate.asString(); + } + + @Override + public String type() { + return "coordinate"; + } + + @Override + public Object component() { + return this.coordinate; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final CoordinateNode that = (CoordinateNode) obj; + return Objects.equals(this.coordinate, that.coordinate); + } + + @Override + public int hashCode() { + return Objects.hash(this.coordinate); + } + + @Override + public String toString() { + return "CoordinateNode{" + "coordinate=" + this.coordinate.asString() + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/ru/ewc/decisions/core/DecisionGraph.java b/src/main/java/ru/ewc/decisions/core/DecisionGraph.java new file mode 100644 index 0000000..dbf8f81 --- /dev/null +++ b/src/main/java/ru/ewc/decisions/core/DecisionGraph.java @@ -0,0 +1,85 @@ +/* + * MIT License + * + * Copyright (c) 2024-2025 Eugene Terekhov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package ru.ewc.decisions.core; + +import java.util.Set; + +/** + * I am an immutable graph representation of decision tables and their components. + * My nodes are decision table components like Coordinates, Conditions, Rules, and DecisionTables. + * My edges represent relationships like "part_of", "lhs_of", "rhs_of", and "operator_of". + * I am thread-safe and built once from source files, then cached for reuse. + * + * @since 0.9.2 + */ +public interface DecisionGraph { + /** + * Returns all nodes in this graph. + * + * @return An immutable set of all nodes in the graph. + */ + Set nodes(); + + /** + * Returns all edges in this graph. + * + * @return An immutable set of all edges in the graph. + */ + Set edges(); + + /** + * Returns all nodes of a specific type. + * + * @param nodeType The class of nodes to return. + * @param The type of nodes to return. + * @return An immutable set of nodes of the specified type. + */ + Set nodesOfType(Class nodeType); + + /** + * Returns all edges of a specific type. + * + * @param edgeType The class of edges to return. + * @param The type of edges to return. + * @return An immutable set of edges of the specified type. + */ + Set edgesOfType(Class edgeType); + + /** + * Returns all edges that have the specified node as their source. + * + * @param node The source node. + * @return An immutable set of edges originating from the specified node. + */ + Set edgesFrom(GraphNode node); + + /** + * Returns all edges that have the specified node as their target. + * + * @param node The target node. + * @return An immutable set of edges targeting the specified node. + */ + Set edgesTo(GraphNode node); +} \ No newline at end of file diff --git a/src/main/java/ru/ewc/decisions/core/DecisionTableNode.java b/src/main/java/ru/ewc/decisions/core/DecisionTableNode.java new file mode 100644 index 0000000..44c7c47 --- /dev/null +++ b/src/main/java/ru/ewc/decisions/core/DecisionTableNode.java @@ -0,0 +1,93 @@ +/* + * MIT License + * + * Copyright (c) 2024-2025 Eugene Terekhov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package ru.ewc.decisions.core; + +import java.util.Objects; + +/** + * I am a concrete node in the DecisionGraph that represents a DecisionTable. + * I am immutable and provide a unique identifier based on the table's name. + * + * @since 0.9.2 + */ +public final class DecisionTableNode implements GraphNode { + /** + * The decision table this node represents. + */ + private final DecisionTable table; + + /** + * The unique identifier for this table. + */ + private final String identifier; + + /** + * Constructor. + * + * @param table The decision table this node represents. + * @param identifier A unique identifier for this table. + */ + public DecisionTableNode(final DecisionTable table, final String identifier) { + this.table = Objects.requireNonNull(table, "DecisionTable cannot be null"); + this.identifier = Objects.requireNonNull(identifier, "Identifier cannot be null"); + } + + @Override + public String id() { + return "table:" + this.identifier; + } + + @Override + public String type() { + return "table"; + } + + @Override + public Object component() { + return this.table; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final DecisionTableNode that = (DecisionTableNode) obj; + return Objects.equals(this.identifier, that.identifier); + } + + @Override + public int hashCode() { + return Objects.hash(this.identifier); + } + + @Override + public String toString() { + return "DecisionTableNode{" + "identifier='" + this.identifier + '\'' + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/ru/ewc/decisions/core/GraphEdge.java b/src/main/java/ru/ewc/decisions/core/GraphEdge.java new file mode 100644 index 0000000..7a44c59 --- /dev/null +++ b/src/main/java/ru/ewc/decisions/core/GraphEdge.java @@ -0,0 +1,65 @@ +/* + * MIT License + * + * Copyright (c) 2024-2025 Eugene Terekhov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package ru.ewc.decisions.core; + +/** + * I am an edge in the DecisionGraph. + * I represent relationships between decision table components like + * "part_of", "lhs_of", "rhs_of", and "operator_of". + * I am immutable and connect two nodes with a specific relationship type. + * + * @since 0.9.2 + */ +public interface GraphEdge { + /** + * Returns the source node of this edge. + * + * @return The node where this edge originates. + */ + GraphNode source(); + + /** + * Returns the target node of this edge. + * + * @return The node where this edge points to. + */ + GraphNode target(); + + /** + * Returns the type of relationship this edge represents. + * Common types include "part_of", "lhs_of", "rhs_of", "operator_of". + * + * @return A string representing the relationship type. + */ + String relationshipType(); + + /** + * Returns a unique identifier for this edge. + * This identifier is used for equality and hashing. + * + * @return A unique string identifier for this edge. + */ + String id(); +} \ No newline at end of file diff --git a/src/main/java/ru/ewc/decisions/core/GraphNode.java b/src/main/java/ru/ewc/decisions/core/GraphNode.java new file mode 100644 index 0000000..1fb6821 --- /dev/null +++ b/src/main/java/ru/ewc/decisions/core/GraphNode.java @@ -0,0 +1,58 @@ +/* + * MIT License + * + * Copyright (c) 2024-2025 Eugene Terekhov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package ru.ewc.decisions.core; + +/** + * I am a node in the DecisionGraph. + * I represent components of decision tables like Coordinates, Conditions, Rules, and DecisionTables. + * I am immutable and provide a unique identifier for my position in the graph. + * + * @since 0.9.2 + */ +public interface GraphNode { + /** + * Returns a unique identifier for this node. + * This identifier is used for equality and hashing. + * + * @return A unique string identifier for this node. + */ + String id(); + + /** + * Returns the type of this node. + * This is used for filtering and categorizing nodes. + * + * @return A string representing the type of this node. + */ + String type(); + + /** + * Returns the underlying component that this node represents. + * This could be a Coordinate, Condition, Rule, or DecisionTable. + * + * @return The underlying component. + */ + Object component(); +} \ No newline at end of file diff --git a/src/main/java/ru/ewc/decisions/core/InMemoryDecisionGraph.java b/src/main/java/ru/ewc/decisions/core/InMemoryDecisionGraph.java new file mode 100644 index 0000000..de23805 --- /dev/null +++ b/src/main/java/ru/ewc/decisions/core/InMemoryDecisionGraph.java @@ -0,0 +1,99 @@ +/* + * MIT License + * + * Copyright (c) 2024-2025 Eugene Terekhov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package ru.ewc.decisions.core; + +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * I am an immutable implementation of DecisionGraph. + * I contain a fixed set of nodes and edges that represent the decision table structure. + * I am thread-safe and built once from source files, then cached for reuse. + * + * @since 0.9.2 + */ +public final class InMemoryDecisionGraph implements DecisionGraph { + /** + * The immutable set of all nodes in this graph. + */ + private final Set nodes; + + /** + * The immutable set of all edges in this graph. + */ + private final Set edges; + + /** + * Constructor. + * + * @param nodes The nodes that make up this graph. + * @param edges The edges that connect the nodes. + */ + public InMemoryDecisionGraph(final Set nodes, final Set edges) { + this.nodes = Objects.requireNonNull(nodes, "Nodes cannot be null"); + this.edges = Objects.requireNonNull(edges, "Edges cannot be null"); + } + + @Override + public Set nodes() { + return Set.copyOf(this.nodes); + } + + @Override + public Set edges() { + return Set.copyOf(this.edges); + } + + @Override + public Set nodesOfType(final Class nodeType) { + return this.nodes.stream() + .filter(nodeType::isInstance) + .map(nodeType::cast) + .collect(Collectors.toSet()); + } + + @Override + public Set edgesOfType(final Class edgeType) { + return this.edges.stream() + .filter(edgeType::isInstance) + .map(edgeType::cast) + .collect(Collectors.toSet()); + } + + @Override + public Set edgesFrom(final GraphNode node) { + return this.edges.stream() + .filter(edge -> edge.source().equals(node)) + .collect(Collectors.toSet()); + } + + @Override + public Set edgesTo(final GraphNode node) { + return this.edges.stream() + .filter(edge -> edge.target().equals(node)) + .collect(Collectors.toSet()); + } +} \ No newline at end of file diff --git a/src/main/java/ru/ewc/decisions/core/LhsOfEdge.java b/src/main/java/ru/ewc/decisions/core/LhsOfEdge.java new file mode 100644 index 0000000..31fd6a8 --- /dev/null +++ b/src/main/java/ru/ewc/decisions/core/LhsOfEdge.java @@ -0,0 +1,89 @@ +/* + * MIT License + * + * Copyright (c) 2024-2025 Eugene Terekhov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package ru.ewc.decisions.core; + +/** + * I am a concrete edge in the DecisionGraph that represents a "lhs_of" relationship. + * This relationship indicates that the source node is the left-hand side of the target node. + * For example, a Coordinate is the left-hand side of a Condition. + * + * @since 0.9.2 + */ +public final class LhsOfEdge implements GraphEdge { + /** + * The relationship type for "lhs_of" edges. + */ + public static final String RELATIONSHIP_TYPE = "lhs_of"; + + /** + * The underlying edge implementation. + */ + private final BasicGraphEdge edge; + + /** + * Constructor. + * + * @param source The source node that is the left-hand side of the target. + * @param target The target node that has the source as its left-hand side. + */ + public LhsOfEdge(final GraphNode source, final GraphNode target) { + this.edge = new BasicGraphEdge(source, target, RELATIONSHIP_TYPE); + } + + @Override + public GraphNode source() { + return this.edge.source(); + } + + @Override + public GraphNode target() { + return this.edge.target(); + } + + @Override + public String relationshipType() { + return this.edge.relationshipType(); + } + + @Override + public String id() { + return this.edge.id(); + } + + @Override + public boolean equals(final Object obj) { + return this.edge.equals(obj); + } + + @Override + public int hashCode() { + return this.edge.hashCode(); + } + + @Override + public String toString() { + return this.edge.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/ewc/decisions/core/OperatorOfEdge.java b/src/main/java/ru/ewc/decisions/core/OperatorOfEdge.java new file mode 100644 index 0000000..0b7b65f --- /dev/null +++ b/src/main/java/ru/ewc/decisions/core/OperatorOfEdge.java @@ -0,0 +1,89 @@ +/* + * MIT License + * + * Copyright (c) 2024-2025 Eugene Terekhov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package ru.ewc.decisions.core; + +/** + * I am a concrete edge in the DecisionGraph that represents an "operator_of" relationship. + * This relationship indicates that the source node is the operator of the target node. + * For example, a specific operator (like "equals", "greater_than") is the operator of a Condition. + * + * @since 0.9.2 + */ +public final class OperatorOfEdge implements GraphEdge { + /** + * The relationship type for "operator_of" edges. + */ + public static final String RELATIONSHIP_TYPE = "operator_of"; + + /** + * The underlying edge implementation. + */ + private final BasicGraphEdge edge; + + /** + * Constructor. + * + * @param source The source node that is the operator of the target. + * @param target The target node that has the source as its operator. + */ + public OperatorOfEdge(final GraphNode source, final GraphNode target) { + this.edge = new BasicGraphEdge(source, target, RELATIONSHIP_TYPE); + } + + @Override + public GraphNode source() { + return this.edge.source(); + } + + @Override + public GraphNode target() { + return this.edge.target(); + } + + @Override + public String relationshipType() { + return this.edge.relationshipType(); + } + + @Override + public String id() { + return this.edge.id(); + } + + @Override + public boolean equals(final Object obj) { + return this.edge.equals(obj); + } + + @Override + public int hashCode() { + return this.edge.hashCode(); + } + + @Override + public String toString() { + return this.edge.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/ewc/decisions/core/PartOfEdge.java b/src/main/java/ru/ewc/decisions/core/PartOfEdge.java new file mode 100644 index 0000000..cd25071 --- /dev/null +++ b/src/main/java/ru/ewc/decisions/core/PartOfEdge.java @@ -0,0 +1,89 @@ +/* + * MIT License + * + * Copyright (c) 2024-2025 Eugene Terekhov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package ru.ewc.decisions.core; + +/** + * I am a concrete edge in the DecisionGraph that represents a "part_of" relationship. + * This relationship indicates that the source node is a part of the target node. + * For example, a Rule is part of a DecisionTable, or a Fragment is part of a Rule. + * + * @since 0.9.2 + */ +public final class PartOfEdge implements GraphEdge { + /** + * The relationship type for "part_of" edges. + */ + public static final String RELATIONSHIP_TYPE = "part_of"; + + /** + * The underlying edge implementation. + */ + private final BasicGraphEdge edge; + + /** + * Constructor. + * + * @param source The source node that is part of the target. + * @param target The target node that contains the source. + */ + public PartOfEdge(final GraphNode source, final GraphNode target) { + this.edge = new BasicGraphEdge(source, target, RELATIONSHIP_TYPE); + } + + @Override + public GraphNode source() { + return this.edge.source(); + } + + @Override + public GraphNode target() { + return this.edge.target(); + } + + @Override + public String relationshipType() { + return this.edge.relationshipType(); + } + + @Override + public String id() { + return this.edge.id(); + } + + @Override + public boolean equals(final Object obj) { + return this.edge.equals(obj); + } + + @Override + public int hashCode() { + return this.edge.hashCode(); + } + + @Override + public String toString() { + return this.edge.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/ewc/decisions/core/RhsOfEdge.java b/src/main/java/ru/ewc/decisions/core/RhsOfEdge.java new file mode 100644 index 0000000..6f037fe --- /dev/null +++ b/src/main/java/ru/ewc/decisions/core/RhsOfEdge.java @@ -0,0 +1,89 @@ +/* + * MIT License + * + * Copyright (c) 2024-2025 Eugene Terekhov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package ru.ewc.decisions.core; + +/** + * I am a concrete edge in the DecisionGraph that represents a "rhs_of" relationship. + * This relationship indicates that the source node is the right-hand side of the target node. + * For example, a Coordinate is the right-hand side of a Condition. + * + * @since 0.9.2 + */ +public final class RhsOfEdge implements GraphEdge { + /** + * The relationship type for "rhs_of" edges. + */ + public static final String RELATIONSHIP_TYPE = "rhs_of"; + + /** + * The underlying edge implementation. + */ + private final BasicGraphEdge edge; + + /** + * Constructor. + * + * @param source The source node that is the right-hand side of the target. + * @param target The target node that has the source as its right-hand side. + */ + public RhsOfEdge(final GraphNode source, final GraphNode target) { + this.edge = new BasicGraphEdge(source, target, RELATIONSHIP_TYPE); + } + + @Override + public GraphNode source() { + return this.edge.source(); + } + + @Override + public GraphNode target() { + return this.edge.target(); + } + + @Override + public String relationshipType() { + return this.edge.relationshipType(); + } + + @Override + public String id() { + return this.edge.id(); + } + + @Override + public boolean equals(final Object obj) { + return this.edge.equals(obj); + } + + @Override + public int hashCode() { + return this.edge.hashCode(); + } + + @Override + public String toString() { + return this.edge.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/ewc/decisions/core/RuleFragmentNode.java b/src/main/java/ru/ewc/decisions/core/RuleFragmentNode.java new file mode 100644 index 0000000..439c5f8 --- /dev/null +++ b/src/main/java/ru/ewc/decisions/core/RuleFragmentNode.java @@ -0,0 +1,87 @@ +/* + * MIT License + * + * Copyright (c) 2024-2025 Eugene Terekhov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package ru.ewc.decisions.core; + +import java.util.Objects; +import ru.ewc.decisions.api.RuleFragment; + +/** + * I am a concrete node in the DecisionGraph that represents a RuleFragment. + * I am immutable and provide a unique identifier based on the fragment's properties. + * + * @since 0.9.2 + */ +public final class RuleFragmentNode implements GraphNode { + /** + * The rule fragment this node represents. + */ + private final RuleFragment fragment; + + /** + * Constructor. + * + * @param fragment The rule fragment this node represents. + */ + public RuleFragmentNode(final RuleFragment fragment) { + this.fragment = Objects.requireNonNull(fragment, "RuleFragment cannot be null"); + } + + @Override + public String id() { + return "fragment:" + this.fragment.type() + ":" + this.fragment.left() + ":" + this.fragment.right(); + } + + @Override + public String type() { + return "fragment"; + } + + @Override + public Object component() { + return this.fragment; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final RuleFragmentNode that = (RuleFragmentNode) obj; + return Objects.equals(this.fragment, that.fragment); + } + + @Override + public int hashCode() { + return Objects.hash(this.fragment); + } + + @Override + public String toString() { + return "RuleFragmentNode{" + "fragment=" + this.fragment + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/ru/ewc/decisions/core/RuleNode.java b/src/main/java/ru/ewc/decisions/core/RuleNode.java new file mode 100644 index 0000000..1171657 --- /dev/null +++ b/src/main/java/ru/ewc/decisions/core/RuleNode.java @@ -0,0 +1,93 @@ +/* + * MIT License + * + * Copyright (c) 2024-2025 Eugene Terekhov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package ru.ewc.decisions.core; + +import java.util.Objects; + +/** + * I am a concrete node in the DecisionGraph that represents a Rule. + * I am immutable and provide a unique identifier based on the rule's name. + * + * @since 0.9.2 + */ +public final class RuleNode implements GraphNode { + /** + * The rule this node represents. + */ + private final Rule rule; + + /** + * The unique identifier for this rule. + */ + private final String identifier; + + /** + * Constructor. + * + * @param rule The rule this node represents. + * @param identifier A unique identifier for this rule. + */ + public RuleNode(final Rule rule, final String identifier) { + this.rule = Objects.requireNonNull(rule, "Rule cannot be null"); + this.identifier = Objects.requireNonNull(identifier, "Identifier cannot be null"); + } + + @Override + public String id() { + return "rule:" + this.identifier; + } + + @Override + public String type() { + return "rule"; + } + + @Override + public Object component() { + return this.rule; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final RuleNode that = (RuleNode) obj; + return Objects.equals(this.identifier, that.identifier); + } + + @Override + public int hashCode() { + return Objects.hash(this.identifier); + } + + @Override + public String toString() { + return "RuleNode{" + "identifier='" + this.identifier + '\'' + '}'; + } +} \ No newline at end of file diff --git a/src/test/java/ru/ewc/decisions/core/BasicGraphEdgeTest.java b/src/test/java/ru/ewc/decisions/core/BasicGraphEdgeTest.java new file mode 100644 index 0000000..8fa0069 --- /dev/null +++ b/src/test/java/ru/ewc/decisions/core/BasicGraphEdgeTest.java @@ -0,0 +1,121 @@ +/* + * MIT License + * + * Copyright (c) 2024-2025 Eugene Terekhov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package ru.ewc.decisions.core; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link BasicGraphEdge}. + * + * @since 0.9.2 + */ +final class BasicGraphEdgeTest { + @Test + void shouldCreateEdgeWithSourceTargetAndRelationship() { + final CoordinateNode source = new CoordinateNode(Coordinate.from("test::source")); + final CoordinateNode target = new CoordinateNode(Coordinate.from("test::target")); + final BasicGraphEdge edge = new BasicGraphEdge(source, target, "part_of"); + + Assertions.assertThat(edge.source()).isEqualTo(source); + Assertions.assertThat(edge.target()).isEqualTo(target); + Assertions.assertThat(edge.relationshipType()).isEqualTo("part_of"); + Assertions.assertThat(edge.id()).isEqualTo("coordinate:test::source--part_of-->coordinate:test::target"); + } + + @Test + void shouldNotAllowNullSource() { + final CoordinateNode target = new CoordinateNode(Coordinate.from("test::target")); + Assertions.assertThatThrownBy(() -> new BasicGraphEdge(null, target, "part_of")) + .isInstanceOf(NullPointerException.class) + .hasMessage("Source node cannot be null"); + } + + @Test + void shouldNotAllowNullTarget() { + final CoordinateNode source = new CoordinateNode(Coordinate.from("test::source")); + Assertions.assertThatThrownBy(() -> new BasicGraphEdge(source, null, "part_of")) + .isInstanceOf(NullPointerException.class) + .hasMessage("Target node cannot be null"); + } + + @Test + void shouldNotAllowNullRelationshipType() { + final CoordinateNode source = new CoordinateNode(Coordinate.from("test::source")); + final CoordinateNode target = new CoordinateNode(Coordinate.from("test::target")); + Assertions.assertThatThrownBy(() -> new BasicGraphEdge(source, target, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("Relationship type cannot be null"); + } + + @Test + void shouldBeEqualWhenAllPropertiesAreEqual() { + final CoordinateNode source1 = new CoordinateNode(Coordinate.from("test::source")); + final CoordinateNode target1 = new CoordinateNode(Coordinate.from("test::target")); + final CoordinateNode source2 = new CoordinateNode(Coordinate.from("test::source")); + final CoordinateNode target2 = new CoordinateNode(Coordinate.from("test::target")); + + final BasicGraphEdge edge1 = new BasicGraphEdge(source1, target1, "part_of"); + final BasicGraphEdge edge2 = new BasicGraphEdge(source2, target2, "part_of"); + + Assertions.assertThat(edge1).isEqualTo(edge2); + Assertions.assertThat(edge1.hashCode()).isEqualTo(edge2.hashCode()); + } + + @Test + void shouldNotBeEqualWhenSourceIsDifferent() { + final CoordinateNode source1 = new CoordinateNode(Coordinate.from("test::source1")); + final CoordinateNode source2 = new CoordinateNode(Coordinate.from("test::source2")); + final CoordinateNode target = new CoordinateNode(Coordinate.from("test::target")); + + final BasicGraphEdge edge1 = new BasicGraphEdge(source1, target, "part_of"); + final BasicGraphEdge edge2 = new BasicGraphEdge(source2, target, "part_of"); + + Assertions.assertThat(edge1).isNotEqualTo(edge2); + } + + @Test + void shouldNotBeEqualWhenTargetIsDifferent() { + final CoordinateNode source = new CoordinateNode(Coordinate.from("test::source")); + final CoordinateNode target1 = new CoordinateNode(Coordinate.from("test::target1")); + final CoordinateNode target2 = new CoordinateNode(Coordinate.from("test::target2")); + + final BasicGraphEdge edge1 = new BasicGraphEdge(source, target1, "part_of"); + final BasicGraphEdge edge2 = new BasicGraphEdge(source, target2, "part_of"); + + Assertions.assertThat(edge1).isNotEqualTo(edge2); + } + + @Test + void shouldNotBeEqualWhenRelationshipTypeIsDifferent() { + final CoordinateNode source = new CoordinateNode(Coordinate.from("test::source")); + final CoordinateNode target = new CoordinateNode(Coordinate.from("test::target")); + + final BasicGraphEdge edge1 = new BasicGraphEdge(source, target, "part_of"); + final BasicGraphEdge edge2 = new BasicGraphEdge(source, target, "lhs_of"); + + Assertions.assertThat(edge1).isNotEqualTo(edge2); + } +} \ No newline at end of file diff --git a/src/test/java/ru/ewc/decisions/core/CoordinateNodeTest.java b/src/test/java/ru/ewc/decisions/core/CoordinateNodeTest.java new file mode 100644 index 0000000..a7646da --- /dev/null +++ b/src/test/java/ru/ewc/decisions/core/CoordinateNodeTest.java @@ -0,0 +1,73 @@ +/* + * MIT License + * + * Copyright (c) 2024-2025 Eugene Terekhov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package ru.ewc.decisions.core; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link CoordinateNode}. + * + * @since 0.9.2 + */ +final class CoordinateNodeTest { + @Test + void shouldCreateNodeWithCoordinate() { + final Coordinate coordinate = Coordinate.from("test::value"); + final CoordinateNode node = new CoordinateNode(coordinate); + + Assertions.assertThat(node.component()).isEqualTo(coordinate); + Assertions.assertThat(node.type()).isEqualTo("coordinate"); + Assertions.assertThat(node.id()).isEqualTo("coordinate:test::value"); + } + + @Test + void shouldNotAllowNullCoordinate() { + Assertions.assertThatThrownBy(() -> new CoordinateNode(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("Coordinate cannot be null"); + } + + @Test + void shouldBeEqualWhenCoordinatesAreEqual() { + final Coordinate coordinate1 = Coordinate.from("test::value"); + final Coordinate coordinate2 = Coordinate.from("test::value"); + final CoordinateNode node1 = new CoordinateNode(coordinate1); + final CoordinateNode node2 = new CoordinateNode(coordinate2); + + Assertions.assertThat(node1).isEqualTo(node2); + Assertions.assertThat(node1.hashCode()).isEqualTo(node2.hashCode()); + } + + @Test + void shouldNotBeEqualWhenCoordinatesAreDifferent() { + final Coordinate coordinate1 = Coordinate.from("test::value1"); + final Coordinate coordinate2 = Coordinate.from("test::value2"); + final CoordinateNode node1 = new CoordinateNode(coordinate1); + final CoordinateNode node2 = new CoordinateNode(coordinate2); + + Assertions.assertThat(node1).isNotEqualTo(node2); + } +} \ No newline at end of file diff --git a/src/test/java/ru/ewc/decisions/core/DecisionGraphIntegrationTest.java b/src/test/java/ru/ewc/decisions/core/DecisionGraphIntegrationTest.java new file mode 100644 index 0000000..6f08ca2 --- /dev/null +++ b/src/test/java/ru/ewc/decisions/core/DecisionGraphIntegrationTest.java @@ -0,0 +1,143 @@ +/* + * MIT License + * + * Copyright (c) 2024-2025 Eugene Terekhov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package ru.ewc.decisions.core; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.ewc.decisions.api.RuleFragment; +import ru.ewc.decisions.conditions.EqualsCondition; + +import java.util.List; +import java.util.Set; + +/** + * Integration tests for DecisionGraph showing various node types and relationships. + * + * @since 0.9.2 + */ +final class DecisionGraphIntegrationTest { + @Test + void shouldCreateComplexGraph() { + // Create some coordinates + final Coordinate coord1 = Coordinate.from("customer::age"); + final Coordinate coord2 = Coordinate.from("21"); + final CoordinateNode coordNode1 = new CoordinateNode(coord1); + final CoordinateNode coordNode2 = new CoordinateNode(coord2); + + // Create a condition + final EqualsCondition condition = new EqualsCondition(coord1, coord2); + final ConditionNode conditionNode = new ConditionNode(condition, "age_check"); + + // Create a rule fragment + final RuleFragment fragment = new RuleFragment("CND", "customer::age", "21"); + final RuleFragmentNode fragmentNode = new RuleFragmentNode(fragment); + + // Create a rule + final Rule rule = Rule.elseRule("test_rule"); + final RuleNode ruleNode = new RuleNode(rule, "test_rule"); + + // Create a decision table + final DecisionTable table = new DecisionTable(List.of(rule), rule, "test_table"); + final DecisionTableNode tableNode = new DecisionTableNode(table, "test_table"); + + // Create edges showing relationships + final LhsOfEdge lhsEdge = new LhsOfEdge(coordNode1, conditionNode); + final RhsOfEdge rhsEdge = new RhsOfEdge(coordNode2, conditionNode); + final PartOfEdge partOfEdge1 = new PartOfEdge(fragmentNode, ruleNode); + final PartOfEdge partOfEdge2 = new PartOfEdge(ruleNode, tableNode); + + // Create the graph + final Set nodes = Set.of(coordNode1, coordNode2, conditionNode, fragmentNode, ruleNode, tableNode); + final Set edges = Set.of(lhsEdge, rhsEdge, partOfEdge1, partOfEdge2); + final InMemoryDecisionGraph graph = new InMemoryDecisionGraph(nodes, edges); + + // Test node filtering + final Set coordinateNodes = graph.nodesOfType(CoordinateNode.class); + Assertions.assertThat(coordinateNodes).hasSize(2); + + final Set conditionNodes = graph.nodesOfType(ConditionNode.class); + Assertions.assertThat(conditionNodes).hasSize(1); + + final Set fragmentNodes = graph.nodesOfType(RuleFragmentNode.class); + Assertions.assertThat(fragmentNodes).hasSize(1); + + final Set ruleNodes = graph.nodesOfType(RuleNode.class); + Assertions.assertThat(ruleNodes).hasSize(1); + + final Set tableNodes = graph.nodesOfType(DecisionTableNode.class); + Assertions.assertThat(tableNodes).hasSize(1); + + // Test edge filtering + final Set lhsEdges = graph.edgesOfType(LhsOfEdge.class); + Assertions.assertThat(lhsEdges).hasSize(1); + + final Set rhsEdges = graph.edgesOfType(RhsOfEdge.class); + Assertions.assertThat(rhsEdges).hasSize(1); + + final Set partOfEdges = graph.edgesOfType(PartOfEdge.class); + Assertions.assertThat(partOfEdges).hasSize(2); + + // Test relationship traversal + final Set edgesFromCoord1 = graph.edgesFrom(coordNode1); + Assertions.assertThat(edgesFromCoord1).hasSize(1); + Assertions.assertThat(edgesFromCoord1.iterator().next().relationshipType()).isEqualTo("lhs_of"); + + final Set edgesToCondition = graph.edgesTo(conditionNode); + Assertions.assertThat(edgesToCondition).hasSize(2); + + final Set edgesFromRule = graph.edgesFrom(ruleNode); + Assertions.assertThat(edgesFromRule).hasSize(1); + Assertions.assertThat(edgesFromRule.iterator().next().relationshipType()).isEqualTo("part_of"); + + // Test that the graph is complete + Assertions.assertThat(graph.nodes()).hasSize(6); + Assertions.assertThat(graph.edges()).hasSize(4); + } + + @Test + void shouldCreateGraphWithSpecificRelationshipTypes() { + final CoordinateNode coord1 = new CoordinateNode(Coordinate.from("test::value1")); + final CoordinateNode coord2 = new CoordinateNode(Coordinate.from("test::value2")); + final ConditionNode condition = new ConditionNode(new EqualsCondition((Coordinate) coord1.component(), (Coordinate) coord2.component()), "test_condition"); + + final PartOfEdge partOfEdge = new PartOfEdge(coord1, condition); + final LhsOfEdge lhsEdge = new LhsOfEdge(coord1, condition); + final RhsOfEdge rhsEdge = new RhsOfEdge(coord2, condition); + + final Set nodes = Set.of(coord1, coord2, condition); + final Set edges = Set.of(partOfEdge, lhsEdge, rhsEdge); + final InMemoryDecisionGraph graph = new InMemoryDecisionGraph(nodes, edges); + + // Verify relationship types + final Set allEdges = graph.edges(); + Assertions.assertThat(allEdges).hasSize(3); + + final Set relationshipTypes = allEdges.stream() + .map(GraphEdge::relationshipType) + .collect(java.util.stream.Collectors.toSet()); + + Assertions.assertThat(relationshipTypes).containsExactlyInAnyOrder("part_of", "lhs_of", "rhs_of"); + } +} \ No newline at end of file diff --git a/src/test/java/ru/ewc/decisions/core/InMemoryDecisionGraphTest.java b/src/test/java/ru/ewc/decisions/core/InMemoryDecisionGraphTest.java new file mode 100644 index 0000000..7f087a7 --- /dev/null +++ b/src/test/java/ru/ewc/decisions/core/InMemoryDecisionGraphTest.java @@ -0,0 +1,152 @@ +/* + * MIT License + * + * Copyright (c) 2024-2025 Eugene Terekhov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package ru.ewc.decisions.core; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +/** + * Tests for {@link InMemoryDecisionGraph}. + * + * @since 0.9.2 + */ +final class InMemoryDecisionGraphTest { + @Test + void shouldCreateEmptyGraph() { + final InMemoryDecisionGraph graph = new InMemoryDecisionGraph(Set.of(), Set.of()); + + Assertions.assertThat(graph.nodes()).isEmpty(); + Assertions.assertThat(graph.edges()).isEmpty(); + } + + @Test + void shouldCreateGraphWithNodes() { + final CoordinateNode node1 = new CoordinateNode(Coordinate.from("test::value1")); + final CoordinateNode node2 = new CoordinateNode(Coordinate.from("test::value2")); + final InMemoryDecisionGraph graph = new InMemoryDecisionGraph(Set.of(node1, node2), Set.of()); + + Assertions.assertThat(graph.nodes()).hasSize(2); + Assertions.assertThat(graph.nodes()).contains(node1, node2); + Assertions.assertThat(graph.edges()).isEmpty(); + } + + @Test + void shouldCreateGraphWithNodesAndEdges() { + final CoordinateNode node1 = new CoordinateNode(Coordinate.from("test::value1")); + final CoordinateNode node2 = new CoordinateNode(Coordinate.from("test::value2")); + final BasicGraphEdge edge = new BasicGraphEdge(node1, node2, "part_of"); + final InMemoryDecisionGraph graph = new InMemoryDecisionGraph(Set.of(node1, node2), Set.of(edge)); + + Assertions.assertThat(graph.nodes()).hasSize(2); + Assertions.assertThat(graph.edges()).hasSize(1); + Assertions.assertThat(graph.edges()).contains(edge); + } + + @Test + void shouldFilterNodesByType() { + final CoordinateNode node1 = new CoordinateNode(Coordinate.from("test::value1")); + final CoordinateNode node2 = new CoordinateNode(Coordinate.from("test::value2")); + final InMemoryDecisionGraph graph = new InMemoryDecisionGraph(Set.of(node1, node2), Set.of()); + + final Set coordinateNodes = graph.nodesOfType(CoordinateNode.class); + Assertions.assertThat(coordinateNodes).hasSize(2); + Assertions.assertThat(coordinateNodes).contains(node1, node2); + } + + @Test + void shouldFilterEdgesByType() { + final CoordinateNode node1 = new CoordinateNode(Coordinate.from("test::value1")); + final CoordinateNode node2 = new CoordinateNode(Coordinate.from("test::value2")); + final BasicGraphEdge edge = new BasicGraphEdge(node1, node2, "part_of"); + final InMemoryDecisionGraph graph = new InMemoryDecisionGraph(Set.of(node1, node2), Set.of(edge)); + + final Set basicEdges = graph.edgesOfType(BasicGraphEdge.class); + Assertions.assertThat(basicEdges).hasSize(1); + Assertions.assertThat(basicEdges).contains(edge); + } + + @Test + void shouldFindEdgesFromNode() { + final CoordinateNode node1 = new CoordinateNode(Coordinate.from("test::value1")); + final CoordinateNode node2 = new CoordinateNode(Coordinate.from("test::value2")); + final BasicGraphEdge edge = new BasicGraphEdge(node1, node2, "part_of"); + final InMemoryDecisionGraph graph = new InMemoryDecisionGraph(Set.of(node1, node2), Set.of(edge)); + + final Set edgesFromNode1 = graph.edgesFrom(node1); + Assertions.assertThat(edgesFromNode1).hasSize(1); + Assertions.assertThat(edgesFromNode1).contains(edge); + + final Set edgesFromNode2 = graph.edgesFrom(node2); + Assertions.assertThat(edgesFromNode2).isEmpty(); + } + + @Test + void shouldFindEdgesToNode() { + final CoordinateNode node1 = new CoordinateNode(Coordinate.from("test::value1")); + final CoordinateNode node2 = new CoordinateNode(Coordinate.from("test::value2")); + final BasicGraphEdge edge = new BasicGraphEdge(node1, node2, "part_of"); + final InMemoryDecisionGraph graph = new InMemoryDecisionGraph(Set.of(node1, node2), Set.of(edge)); + + final Set edgesToNode1 = graph.edgesTo(node1); + Assertions.assertThat(edgesToNode1).isEmpty(); + + final Set edgesToNode2 = graph.edgesTo(node2); + Assertions.assertThat(edgesToNode2).hasSize(1); + Assertions.assertThat(edgesToNode2).contains(edge); + } + + @Test + void shouldNotAllowNullNodes() { + Assertions.assertThatThrownBy(() -> new InMemoryDecisionGraph(null, Set.of())) + .isInstanceOf(NullPointerException.class) + .hasMessage("Nodes cannot be null"); + } + + @Test + void shouldNotAllowNullEdges() { + Assertions.assertThatThrownBy(() -> new InMemoryDecisionGraph(Set.of(), null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("Edges cannot be null"); + } + + @Test + void shouldReturnImmutableCollections() { + final CoordinateNode node1 = new CoordinateNode(Coordinate.from("test::value1")); + final CoordinateNode node2 = new CoordinateNode(Coordinate.from("test::value2")); + final BasicGraphEdge edge = new BasicGraphEdge(node1, node2, "part_of"); + final InMemoryDecisionGraph graph = new InMemoryDecisionGraph(Set.of(node1, node2), Set.of(edge)); + + final Set nodes = graph.nodes(); + final Set edges = graph.edges(); + + Assertions.assertThatThrownBy(() -> nodes.add(new CoordinateNode(Coordinate.from("test::value3")))) + .isInstanceOf(UnsupportedOperationException.class); + + Assertions.assertThatThrownBy(() -> edges.add(new BasicGraphEdge(node1, node2, "test"))) + .isInstanceOf(UnsupportedOperationException.class); + } +} \ No newline at end of file From 730b213fd5d7a11729f70656fa1fff3d07e8ab3a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 5 Jul 2025 06:30:52 +0000 Subject: [PATCH 3/3] Add DecisionGraphBuilder utility and comprehensive documentation Co-authored-by: nergal-perm <4876699+nergal-perm@users.noreply.github.com> --- .../decisions/core/DecisionGraphBuilder.java | 153 ++++++++++++++++ src/main/java/ru/ewc/decisions/core/README.md | 119 +++++++++++++ .../core/DecisionGraphBuilderTest.java | 164 ++++++++++++++++++ 3 files changed, 436 insertions(+) create mode 100644 src/main/java/ru/ewc/decisions/core/DecisionGraphBuilder.java create mode 100644 src/main/java/ru/ewc/decisions/core/README.md create mode 100644 src/test/java/ru/ewc/decisions/core/DecisionGraphBuilderTest.java diff --git a/src/main/java/ru/ewc/decisions/core/DecisionGraphBuilder.java b/src/main/java/ru/ewc/decisions/core/DecisionGraphBuilder.java new file mode 100644 index 0000000..f399b12 --- /dev/null +++ b/src/main/java/ru/ewc/decisions/core/DecisionGraphBuilder.java @@ -0,0 +1,153 @@ +/* + * MIT License + * + * Copyright (c) 2024-2025 Eugene Terekhov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package ru.ewc.decisions.core; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * I am a builder for creating DecisionGraph instances. + * I provide convenient methods to construct graphs from decision table components. + * I ensure that all nodes and edges are properly connected and valid. + * + * @since 0.9.2 + */ +public final class DecisionGraphBuilder { + /** + * The set of nodes being built. + */ + private final Set nodes; + + /** + * The set of edges being built. + */ + private final Set edges; + + /** + * Constructor. + */ + public DecisionGraphBuilder() { + this.nodes = new HashSet<>(); + this.edges = new HashSet<>(); + } + + /** + * Adds a node to the graph. + * + * @param node The node to add. + * @return This builder for method chaining. + */ + public DecisionGraphBuilder addNode(final GraphNode node) { + this.nodes.add(Objects.requireNonNull(node, "Node cannot be null")); + return this; + } + + /** + * Adds an edge to the graph. + * The source and target nodes of the edge will be automatically added to the graph. + * + * @param edge The edge to add. + * @return This builder for method chaining. + */ + public DecisionGraphBuilder addEdge(final GraphEdge edge) { + Objects.requireNonNull(edge, "Edge cannot be null"); + this.edges.add(edge); + this.nodes.add(edge.source()); + this.nodes.add(edge.target()); + return this; + } + + /** + * Adds a "part_of" relationship between two nodes. + * + * @param source The source node that is part of the target. + * @param target The target node that contains the source. + * @return This builder for method chaining. + */ + public DecisionGraphBuilder addPartOfRelationship(final GraphNode source, final GraphNode target) { + return this.addEdge(new PartOfEdge(source, target)); + } + + /** + * Adds a "lhs_of" relationship between two nodes. + * + * @param source The source node that is the left-hand side of the target. + * @param target The target node that has the source as its left-hand side. + * @return This builder for method chaining. + */ + public DecisionGraphBuilder addLhsOfRelationship(final GraphNode source, final GraphNode target) { + return this.addEdge(new LhsOfEdge(source, target)); + } + + /** + * Adds a "rhs_of" relationship between two nodes. + * + * @param source The source node that is the right-hand side of the target. + * @param target The target node that has the source as its right-hand side. + * @return This builder for method chaining. + */ + public DecisionGraphBuilder addRhsOfRelationship(final GraphNode source, final GraphNode target) { + return this.addEdge(new RhsOfEdge(source, target)); + } + + /** + * Adds an "operator_of" relationship between two nodes. + * + * @param source The source node that is the operator of the target. + * @param target The target node that has the source as its operator. + * @return This builder for method chaining. + */ + public DecisionGraphBuilder addOperatorOfRelationship(final GraphNode source, final GraphNode target) { + return this.addEdge(new OperatorOfEdge(source, target)); + } + + /** + * Builds the DecisionGraph from the accumulated nodes and edges. + * + * @return A new immutable DecisionGraph instance. + */ + public DecisionGraph build() { + return new InMemoryDecisionGraph(Set.copyOf(this.nodes), Set.copyOf(this.edges)); + } + + /** + * Returns the current number of nodes in the builder. + * + * @return The number of nodes. + */ + public int nodeCount() { + return this.nodes.size(); + } + + /** + * Returns the current number of edges in the builder. + * + * @return The number of edges. + */ + public int edgeCount() { + return this.edges.size(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/ewc/decisions/core/README.md b/src/main/java/ru/ewc/decisions/core/README.md new file mode 100644 index 0000000..0ae5dc5 --- /dev/null +++ b/src/main/java/ru/ewc/decisions/core/README.md @@ -0,0 +1,119 @@ +# DecisionGraph Implementation + +This directory contains the implementation of an immutable, in-memory representation of decision tables and their components as a graph structure. + +## Overview + +The DecisionGraph provides a thread-safe, immutable graph representation where: +- **Nodes** represent decision table components: Coordinates, Conditions, Rules, DecisionTables, and RuleFragments +- **Edges** represent relationships: "part_of", "lhs_of", "rhs_of", and "operator_of" + +This implementation follows the architectural decision (ADR-0003) to separate static logic structure from per-session execution state. + +## Core Components + +### Interfaces + +- `DecisionGraph` - Main interface for the graph structure +- `GraphNode` - Interface for all graph nodes +- `GraphEdge` - Interface for all graph edges + +### Implementations + +- `InMemoryDecisionGraph` - Concrete implementation of DecisionGraph +- `BasicGraphEdge` - Basic edge implementation + +### Node Types + +- `CoordinateNode` - Represents a Coordinate component +- `ConditionNode` - Represents a Condition component +- `RuleNode` - Represents a Rule component +- `DecisionTableNode` - Represents a DecisionTable component +- `RuleFragmentNode` - Represents a RuleFragment component + +### Edge Types + +- `PartOfEdge` - Represents "part_of" relationships (e.g., Rule is part of DecisionTable) +- `LhsOfEdge` - Represents "lhs_of" relationships (e.g., Coordinate is left-hand side of Condition) +- `RhsOfEdge` - Represents "rhs_of" relationships (e.g., Coordinate is right-hand side of Condition) +- `OperatorOfEdge` - Represents "operator_of" relationships (e.g., operator is part of Condition) + +### Utilities + +- `DecisionGraphBuilder` - Builder pattern for constructing graphs + +## Usage Examples + +### Basic Graph Creation + +```java +// Create nodes +CoordinateNode coord1 = new CoordinateNode(Coordinate.from("customer::age")); +CoordinateNode coord2 = new CoordinateNode(Coordinate.from("21")); +ConditionNode condition = new ConditionNode(someCondition, "age_check"); + +// Create edges +LhsOfEdge lhsEdge = new LhsOfEdge(coord1, condition); +RhsOfEdge rhsEdge = new RhsOfEdge(coord2, condition); + +// Build graph +DecisionGraph graph = new InMemoryDecisionGraph( + Set.of(coord1, coord2, condition), + Set.of(lhsEdge, rhsEdge) +); +``` + +### Using the Builder + +```java +DecisionGraph graph = new DecisionGraphBuilder() + .addLhsOfRelationship(coord1, condition) + .addRhsOfRelationship(coord2, condition) + .addPartOfRelationship(condition, rule) + .build(); +``` + +### Querying the Graph + +```java +// Get all coordinate nodes +Set coordinates = graph.nodesOfType(CoordinateNode.class); + +// Get all part_of edges +Set partOfEdges = graph.edgesOfType(PartOfEdge.class); + +// Find edges from a specific node +Set outgoingEdges = graph.edgesFrom(someNode); + +// Find edges to a specific node +Set incomingEdges = graph.edgesTo(someNode); +``` + +## Key Features + +### Immutability +All graph components are immutable after creation. The graph structure cannot be modified once built. + +### Thread Safety +The graph can be safely accessed from multiple threads concurrently without synchronization. + +### Type Safety +Nodes and edges can be filtered by their concrete types using the `nodesOfType()` and `edgesOfType()` methods. + +### Relationship Modeling +The graph explicitly models the relationships between decision table components, making dependencies clear and traversable. + +## Design Principles + +1. **Separation of Concerns** - Static structure is separated from execution state +2. **Immutability** - All components are immutable for thread safety +3. **Type Safety** - Strong typing prevents runtime errors +4. **Builder Pattern** - Convenient construction while maintaining immutability +5. **Explicit Relationships** - All component relationships are explicitly modeled + +## Performance Considerations + +- Graph construction is a one-time cost intended to be done during initialization +- Graph traversal operations are O(n) where n is the number of nodes/edges +- Memory usage is proportional to the number of decision table components +- No dynamic allocation during graph traversal operations \ No newline at end of file diff --git a/src/test/java/ru/ewc/decisions/core/DecisionGraphBuilderTest.java b/src/test/java/ru/ewc/decisions/core/DecisionGraphBuilderTest.java new file mode 100644 index 0000000..fc54e8c --- /dev/null +++ b/src/test/java/ru/ewc/decisions/core/DecisionGraphBuilderTest.java @@ -0,0 +1,164 @@ +/* + * MIT License + * + * Copyright (c) 2024-2025 Eugene Terekhov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package ru.ewc.decisions.core; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.ewc.decisions.conditions.EqualsCondition; + +/** + * Tests for {@link DecisionGraphBuilder}. + * + * @since 0.9.2 + */ +final class DecisionGraphBuilderTest { + @Test + void shouldCreateEmptyGraph() { + final DecisionGraphBuilder builder = new DecisionGraphBuilder(); + final DecisionGraph graph = builder.build(); + + Assertions.assertThat(graph.nodes()).isEmpty(); + Assertions.assertThat(graph.edges()).isEmpty(); + Assertions.assertThat(builder.nodeCount()).isZero(); + Assertions.assertThat(builder.edgeCount()).isZero(); + } + + @Test + void shouldAddNodesAndBuildGraph() { + final CoordinateNode node1 = new CoordinateNode(Coordinate.from("test::value1")); + final CoordinateNode node2 = new CoordinateNode(Coordinate.from("test::value2")); + + final DecisionGraphBuilder builder = new DecisionGraphBuilder(); + final DecisionGraph graph = builder + .addNode(node1) + .addNode(node2) + .build(); + + Assertions.assertThat(graph.nodes()).hasSize(2); + Assertions.assertThat(graph.nodes()).contains(node1, node2); + Assertions.assertThat(graph.edges()).isEmpty(); + Assertions.assertThat(builder.nodeCount()).isEqualTo(2); + Assertions.assertThat(builder.edgeCount()).isZero(); + } + + @Test + void shouldAddEdgesAndAutomaticallyIncludeNodes() { + final CoordinateNode node1 = new CoordinateNode(Coordinate.from("test::value1")); + final CoordinateNode node2 = new CoordinateNode(Coordinate.from("test::value2")); + final BasicGraphEdge edge = new BasicGraphEdge(node1, node2, "test_relationship"); + + final DecisionGraphBuilder builder = new DecisionGraphBuilder(); + final DecisionGraph graph = builder + .addEdge(edge) + .build(); + + Assertions.assertThat(graph.nodes()).hasSize(2); + Assertions.assertThat(graph.nodes()).contains(node1, node2); + Assertions.assertThat(graph.edges()).hasSize(1); + Assertions.assertThat(graph.edges()).contains(edge); + Assertions.assertThat(builder.nodeCount()).isEqualTo(2); + Assertions.assertThat(builder.edgeCount()).isEqualTo(1); + } + + @Test + void shouldAddSpecificRelationshipTypes() { + final CoordinateNode coord1 = new CoordinateNode(Coordinate.from("test::value1")); + final CoordinateNode coord2 = new CoordinateNode(Coordinate.from("test::value2")); + final ConditionNode condition = new ConditionNode( + new EqualsCondition((Coordinate) coord1.component(), (Coordinate) coord2.component()), + "test_condition" + ); + + final DecisionGraphBuilder builder = new DecisionGraphBuilder(); + final DecisionGraph graph = builder + .addNode(condition) + .addLhsOfRelationship(coord1, condition) + .addRhsOfRelationship(coord2, condition) + .build(); + + Assertions.assertThat(graph.nodes()).hasSize(3); + Assertions.assertThat(graph.edges()).hasSize(2); + + final var lhsEdges = graph.edgesOfType(LhsOfEdge.class); + final var rhsEdges = graph.edgesOfType(RhsOfEdge.class); + + Assertions.assertThat(lhsEdges).hasSize(1); + Assertions.assertThat(rhsEdges).hasSize(1); + + Assertions.assertThat(lhsEdges.iterator().next().source()).isEqualTo(coord1); + Assertions.assertThat(lhsEdges.iterator().next().target()).isEqualTo(condition); + Assertions.assertThat(rhsEdges.iterator().next().source()).isEqualTo(coord2); + Assertions.assertThat(rhsEdges.iterator().next().target()).isEqualTo(condition); + } + + @Test + void shouldCreateComplexGraphWithAllRelationshipTypes() { + final CoordinateNode coord1 = new CoordinateNode(Coordinate.from("customer::age")); + final CoordinateNode coord2 = new CoordinateNode(Coordinate.from("21")); + final ConditionNode condition = new ConditionNode( + new EqualsCondition((Coordinate) coord1.component(), (Coordinate) coord2.component()), + "age_check" + ); + final RuleNode rule = new RuleNode(Rule.elseRule("test_rule"), "test_rule"); + + final DecisionGraphBuilder builder = new DecisionGraphBuilder(); + final DecisionGraph graph = builder + .addLhsOfRelationship(coord1, condition) + .addRhsOfRelationship(coord2, condition) + .addPartOfRelationship(condition, rule) + .build(); + + Assertions.assertThat(graph.nodes()).hasSize(4); + Assertions.assertThat(graph.edges()).hasSize(3); + + // Verify specific relationships + Assertions.assertThat(graph.edgesOfType(LhsOfEdge.class)).hasSize(1); + Assertions.assertThat(graph.edgesOfType(RhsOfEdge.class)).hasSize(1); + Assertions.assertThat(graph.edgesOfType(PartOfEdge.class)).hasSize(1); + + // Verify graph connectivity + Assertions.assertThat(graph.edgesFrom(coord1)).hasSize(1); + Assertions.assertThat(graph.edgesFrom(coord2)).hasSize(1); + Assertions.assertThat(graph.edgesFrom(condition)).hasSize(1); + Assertions.assertThat(graph.edgesTo(condition)).hasSize(2); + Assertions.assertThat(graph.edgesTo(rule)).hasSize(1); + } + + @Test + void shouldNotAllowNullNode() { + final DecisionGraphBuilder builder = new DecisionGraphBuilder(); + Assertions.assertThatThrownBy(() -> builder.addNode(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("Node cannot be null"); + } + + @Test + void shouldNotAllowNullEdge() { + final DecisionGraphBuilder builder = new DecisionGraphBuilder(); + Assertions.assertThatThrownBy(() -> builder.addEdge(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("Edge cannot be null"); + } +} \ No newline at end of file