Skip to content

Commit 28cfef5

Browse files
committed
feat: MergerReverser
1 parent 0cb2a7a commit 28cfef5

7 files changed

Lines changed: 231 additions & 49 deletions

File tree

src/main/java/blue/language/Blue.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ public Node resolve(Node node, Limits limits) {
6565
return merger.resolve(node, effectiveLimits);
6666
}
6767

68+
public Node reverse(Node node) {
69+
return new MergeReverser().reverse(node);
70+
}
71+
72+
public Node reverse(Object object) {
73+
return reverse(objectToNode(object));
74+
}
75+
6876
public void extend(Node node, Limits limits) {
6977
Limits effectiveLimits = combineWithGlobalLimits(limits);
7078
new NodeExtender(nodeProvider).extend(node, effectiveLimits);

src/main/java/blue/language/model/Node.java

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -262,43 +262,6 @@ public Integer getAsInteger(String path) {
262262
}
263263
}
264264

265-
public Node clone2() {
266-
try {
267-
Node cloned = (Node) super.clone();
268-
if (this.type != null) {
269-
cloned.type = this.type.clone();
270-
}
271-
if (this.itemType != null) {
272-
cloned.itemType = this.itemType.clone();
273-
}
274-
if (this.keyType != null) {
275-
cloned.keyType = this.keyType.clone();
276-
}
277-
if (this.valueType != null) {
278-
cloned.valueType = this.valueType.clone();
279-
}
280-
if (this.items != null) {
281-
cloned.items = this.items.stream()
282-
.map(Node::clone)
283-
.collect(Collectors.toCollection(ArrayList::new));
284-
}
285-
if (this.properties != null) {
286-
cloned.properties = this.properties.entrySet().stream()
287-
.collect(Collectors.toMap(
288-
Map.Entry::getKey,
289-
entry -> entry.getValue().clone()
290-
));
291-
}
292-
if (this.blue != null) {
293-
cloned.blue = this.blue.clone();
294-
}
295-
cloned.inlineValue = this.inlineValue;
296-
return cloned;
297-
} catch (CloneNotSupportedException e) {
298-
throw new AssertionError("BasicNode must be cloneable", e);
299-
}
300-
}
301-
302265
@Override
303266
public Node clone() {
304267
try {
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package blue.language.utils;
2+
3+
import blue.language.model.Node;
4+
5+
import java.util.*;
6+
import java.util.function.BiConsumer;
7+
import java.util.function.Function;
8+
9+
import static blue.language.utils.Nodes.NodeField.*;
10+
import static blue.language.utils.Nodes.hasFieldsAndMayHaveFields;
11+
12+
public class MergeReverser {
13+
14+
public Node reverse(Node mergedNode) {
15+
Node minimalNode = new Node();
16+
reverseNode(minimalNode, mergedNode, null);
17+
return minimalNode;
18+
}
19+
20+
private void reverseNode(Node minimal, Node merged, Node fromType) {
21+
22+
if (merged.getBlueId() != null && fromType != null && merged.getBlueId().equals(fromType.getBlueId())) {
23+
return;
24+
}
25+
26+
if (merged.getValue() != null && (fromType == null || fromType.getValue() == null)) {
27+
minimal.value(merged.getValue());
28+
}
29+
30+
setTypeIfDifferent(merged, fromType, minimal, Node::getType, Node::type);
31+
setTypeIfDifferent(merged, fromType, minimal, Node::getItemType, Node::itemType);
32+
setTypeIfDifferent(merged, fromType, minimal, Node::getKeyType, Node::keyType);
33+
setTypeIfDifferent(merged, fromType, minimal, Node::getValueType, Node::valueType);
34+
35+
if (merged.getName() != null && (fromType == null || !merged.getName().equals(fromType.getName()))) {
36+
minimal.name(merged.getName());
37+
}
38+
if (merged.getDescription() != null && (fromType == null || !merged.getDescription().equals(fromType.getDescription()))) {
39+
minimal.description(merged.getDescription());
40+
}
41+
42+
if (merged.getBlueId() != null && (fromType == null || !merged.getBlueId().equals(fromType.getBlueId()))) {
43+
minimal.blueId(merged.getBlueId());
44+
}
45+
46+
if (merged.getItems() != null) {
47+
int start = 0;
48+
List<Node> minimalItems = new ArrayList<>();
49+
if (fromType != null && fromType.getItems() != null) {
50+
String itemsBlueId = BlueIdCalculator.calculateBlueId(fromType.getItems());
51+
minimalItems.add(new Node().blueId(itemsBlueId));
52+
start = fromType.getItems().size();
53+
}
54+
if (merged.getItems().size() > start) {
55+
for (int i = start; i < merged.getItems().size(); i++) {
56+
Node item = merged.getItems().get(i);
57+
Node minimalItem = new Node();
58+
reverseNode(minimalItem, item, null);
59+
minimalItems.add(minimalItem);
60+
}
61+
minimal.items(minimalItems);
62+
}
63+
}
64+
65+
if (merged.getProperties() != null) {
66+
Map<String, Node> minimalProperties = new HashMap<>();
67+
for (Map.Entry<String, Node> entry : merged.getProperties().entrySet()) {
68+
Node minimalProperty = new Node();
69+
Node typeProperty = (merged.getType() != null && merged.getType().getProperties() != null) ?
70+
merged.getType().getProperties().get(entry.getKey()) : null;
71+
reverseNode(minimalProperty, entry.getValue(), typeProperty);
72+
if (!Nodes.isEmptyNode(minimalProperty)) {
73+
minimalProperties.put(entry.getKey(), minimalProperty);
74+
}
75+
}
76+
if (!minimalProperties.isEmpty()) {
77+
minimal.properties(minimalProperties);
78+
}
79+
}
80+
81+
}
82+
83+
private void setTypeIfDifferent(Node merged, Node fromType, Node minimal,
84+
Function<Node, Node> typeGetter,
85+
BiConsumer<Node, Node> typeSetter) {
86+
Node mergedType = typeGetter.apply(merged);
87+
if (mergedType != null && (fromType == null || typeGetter.apply(fromType) == null ||
88+
!typeGetter.apply(fromType).getBlueId().equals(mergedType.getBlueId()))) {
89+
Node typeNode = new Node().blueId(mergedType.getBlueId());
90+
typeSetter.accept(minimal, typeNode);
91+
}
92+
}
93+
}

src/main/java/blue/language/utils/NodePathAccessor.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ private static Object getRecursive(Node node, String[] segments, int index, Func
3636
}
3737

3838
if (index == segments.length) {
39-
return node.getValue() != null ? node.getValue() : node;
39+
return node != null && node.getValue() != null ? node.getValue() : node;
4040
}
4141

4242
String segment = segments[index];
@@ -54,6 +54,12 @@ private static Node getNodeForSegment(Node node, String segment, Function<Node,
5454
return new Node().value(node.getDescription());
5555
case "type":
5656
return node.getType();
57+
case "itemType":
58+
return node.getItemType();
59+
case "keyType":
60+
return node.getKeyType();
61+
case "valueType":
62+
return node.getValueType();
5763
case "value":
5864
return new Node().value(node.getValue());
5965
case "blueId":

src/main/java/blue/language/utils/Nodes.java

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,38 @@
44

55
import java.math.BigDecimal;
66
import java.math.BigInteger;
7+
import java.util.EnumSet;
8+
import java.util.Set;
79

810
import static blue.language.utils.Properties.*;
911

1012
public class Nodes {
1113

14+
public enum NodeField {
15+
NAME,
16+
DESCRIPTION,
17+
TYPE,
18+
BLUE_ID,
19+
KEY_TYPE,
20+
VALUE_TYPE,
21+
ITEM_TYPE,
22+
VALUE,
23+
PROPERTIES,
24+
BLUE,
25+
ITEMS,
26+
CONSTRAINTS
27+
}
28+
1229
public static boolean isEmptyNode(Node node) {
13-
return node.getName() == null && node.getType() == null && node.getValue() == null && node.getDescription() == null &&
14-
node.getProperties() == null && node.getBlue() == null && node.getItems() == null && node.getConstraints() == null &&
15-
node.getKeyType() == null && node.getValueType() == null && node.getItemType() == null && node.getBlueId() == null;
30+
return hasFieldsAndMayHaveFields(node, EnumSet.noneOf(NodeField.class), EnumSet.noneOf(NodeField.class));
31+
}
32+
33+
public static boolean hasBlueIdOnly(Node node) {
34+
return hasFieldsAndMayHaveFields(node, EnumSet.of(NodeField.BLUE_ID), EnumSet.noneOf(NodeField.class));
1635
}
1736

1837
public static boolean hasItemsOnly(Node node) {
19-
return node.getName() == null && node.getType() == null && node.getValue() == null && node.getDescription() == null &&
20-
node.getProperties() == null;
38+
return hasFieldsAndMayHaveFields(node, EnumSet.of(NodeField.ITEMS), EnumSet.noneOf(NodeField.class));
2139
}
2240

2341
public static Node textNode(String text) {
@@ -36,4 +54,41 @@ public static Node booleanNode(Boolean booleanValue) {
3654
return new Node().type(new Node().blueId(BOOLEAN_TYPE_BLUE_ID)).value(booleanValue);
3755
}
3856

57+
public static boolean hasFieldsAndMayHaveFields(Node node, Set<NodeField> mustHaveFields, Set<NodeField> mayHaveFields) {
58+
for (NodeField field : NodeField.values()) {
59+
boolean fieldIsPresent = !isNull(getFieldValue(node, field));
60+
61+
if (mustHaveFields.contains(field)) {
62+
if (!fieldIsPresent) return false;
63+
} else if (mayHaveFields.contains(field)) {
64+
// This field may or may not be present, so we don't need to check
65+
} else {
66+
if (fieldIsPresent) return false;
67+
}
68+
}
69+
return true;
70+
}
71+
72+
private static Object getFieldValue(Node node, NodeField field) {
73+
switch (field) {
74+
case NAME: return node.getName();
75+
case TYPE: return node.getType();
76+
case VALUE: return node.getValue();
77+
case DESCRIPTION: return node.getDescription();
78+
case PROPERTIES: return node.getProperties();
79+
case BLUE: return node.getBlue();
80+
case ITEMS: return node.getItems();
81+
case CONSTRAINTS: return node.getConstraints();
82+
case KEY_TYPE: return node.getKeyType();
83+
case VALUE_TYPE: return node.getValueType();
84+
case ITEM_TYPE: return node.getItemType();
85+
case BLUE_ID: return node.getBlueId();
86+
default: throw new IllegalArgumentException("Unknown field: " + field);
87+
}
88+
}
89+
90+
private static boolean isNull(Object value) {
91+
return value == null;
92+
}
93+
3994
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package blue.language;
2+
3+
import blue.language.utils.MergeReverser;
4+
import blue.language.model.Node;
5+
import blue.language.provider.BasicNodeProvider;
6+
import blue.language.utils.Properties;
7+
import org.junit.jupiter.api.Test;
8+
9+
import static org.junit.jupiter.api.Assertions.*;
10+
11+
public class MergeReverserTest {
12+
13+
@Test
14+
public void testBasic1() throws Exception {
15+
16+
BasicNodeProvider nodeProvider = new BasicNodeProvider();
17+
18+
String a = "name: A\n" +
19+
"description: Xyz\n" +
20+
"x: 1\n" +
21+
"y:\n" +
22+
" type: Integer\n" +
23+
"z:\n" +
24+
" type: List";
25+
nodeProvider.addSingleDocs(a);
26+
27+
String b = "name: B\n" +
28+
"type:\n" +
29+
" blueId: " + nodeProvider.getBlueIdByName("A") + "\n" +
30+
"x: 1\n" +
31+
"y: 2\n" +
32+
"z:\n" +
33+
" type: List\n" +
34+
" itemType: Text\n" +
35+
" items:\n" +
36+
" - A\n" +
37+
" - B";
38+
nodeProvider.addSingleDocs(b);
39+
40+
Node bNode = nodeProvider.getNodeByName("B");
41+
42+
Blue blue = new Blue(nodeProvider);
43+
Node resolved = blue.resolve(bNode);
44+
45+
MergeReverser reverser = new MergeReverser();
46+
Node reversed = reverser.reverse(resolved);
47+
48+
assertFalse(reversed.getProperties().containsKey("x"));
49+
assertEquals(2, reversed.getAsInteger("/y/value"));
50+
assertNull(reversed.get("/z/type"));
51+
assertEquals(Properties.TEXT_TYPE_BLUE_ID, reversed.getAsText("/z/itemType/blueId"));
52+
}
53+
54+
}

src/test/java/blue/language/utils/limits/PathLimitsTest.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,9 @@ public void testTwoLevelWildcard() {
162162

163163
@Test
164164
public void testConstraintsAndBlueId() throws Exception {
165+
BasicNodeProvider nodeProvider = new BasicNodeProvider();
166+
Blue blue = new Blue(nodeProvider);
167+
165168
String a = "name: A\n" +
166169
"x:\n" +
167170
" description: aa\n" +
@@ -170,26 +173,26 @@ public void testConstraintsAndBlueId() throws Exception {
170173
"y:\n" +
171174
" constraints:\n" +
172175
" maxLength: 4";
173-
Node aNode = YAML_MAPPER.readValue(a, Node.class);
176+
Node aNode = blue.yamlToNode(a);
177+
nodeProvider.addSingleNodes(aNode);
174178

175179
String b = "name: B\n" +
176180
"type:\n" +
177181
" blueId: " + calculateBlueId(aNode) + "\n" +
178182
"x:\n" +
179183
" blueId: some-blue-id\n" +
180184
"y: abcd";
181-
Node bNode = YAML_MAPPER.readValue(b, Node.class);
185+
Node bNode = blue.yamlToNode(b);
186+
nodeProvider.addSingleNodes(bNode);
182187

183188
String bInst = "name: B Inst\n" +
184189
"type:\n" +
185190
" blueId: " + calculateBlueId(bNode) + "\n" +
186191
"x:\n" +
187192
" blueId: some-blue-id\n" +
188193
"y: abcd";
189-
Node bInstNode = YAML_MAPPER.readValue(bInst, Node.class);
190-
191-
BasicNodeProvider nodeProvider = new BasicNodeProvider(aNode, bNode, bInstNode);
192-
Blue blue = new Blue(nodeProvider);
194+
Node bInstNode = blue.yamlToNode(bInst);
195+
nodeProvider.addSingleNodes(bInstNode);
193196

194197
String typeBlueId = calculateBlueId(bNode);
195198
Set<String> ignoredProperties = new HashSet<>(Collections.singletonList("x"));

0 commit comments

Comments
 (0)