Skip to content

Commit 7799ed2

Browse files
committed
Handle conflicting aliases / arguments when combining response projections #985
1 parent b140a8e commit 7799ed2

File tree

3 files changed

+185
-28
lines changed

3 files changed

+185
-28
lines changed

src/main/java/com/kobylynskyi/graphql/codegen/model/graphql/GraphQLResponseProjection.java

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
import java.util.LinkedHashMap;
44
import java.util.List;
55
import java.util.Map;
6+
import java.util.Objects;
67
import java.util.StringJoiner;
78

89
/**
910
* The implementation class should contain the fields of the particular type that should be returned to the client.
1011
*/
1112
public abstract class GraphQLResponseProjection {
1213

13-
protected final Map<String, GraphQLResponseField> fields = new LinkedHashMap<>();
14+
protected final Map<Pair<String, String>, GraphQLResponseField> fields = new LinkedHashMap<>();
1415

1516
public GraphQLResponseProjection() {
1617
}
@@ -37,26 +38,33 @@ public GraphQLResponseProjection(List<? extends GraphQLResponseProjection> proje
3738
public abstract GraphQLResponseProjection deepCopy$();
3839

3940
protected void add$(GraphQLResponseField responseField) {
40-
GraphQLResponseField existingResponseField = fields.get(responseField.getName());
41-
if (existingResponseField != null) {
42-
if (responseField.getAlias() != null) {
43-
existingResponseField.alias(responseField.getAlias());
44-
}
45-
if (responseField.getParameters() != null) {
46-
existingResponseField.parameters(responseField.getParameters().deepCopy());
47-
}
48-
if (responseField.getProjection() != null) {
49-
GraphQLResponseProjection projectionCopy = responseField.getProjection().deepCopy$();
50-
if (existingResponseField.getProjection() != null) {
51-
for (GraphQLResponseField field : projectionCopy.fields.values()) {
52-
existingResponseField.getProjection().add$(field);
53-
}
54-
} else {
55-
existingResponseField.projection(projectionCopy);
41+
Pair<String, String> nameAndAlias = new Pair<>(responseField.getName(), responseField.getAlias());
42+
GraphQLResponseField existingResponseField = fields.get(nameAndAlias);
43+
if (existingResponseField == null) {
44+
fields.put(nameAndAlias, responseField.deepCopy());
45+
return;
46+
}
47+
48+
if (!Objects.equals(responseField.getParameters(), existingResponseField.getParameters())) {
49+
throw new IllegalArgumentException(
50+
String.format("Field '%s' has an argument conflict", existingResponseField.getName()));
51+
}
52+
53+
if (responseField.getAlias() != null) {
54+
existingResponseField.alias(responseField.getAlias());
55+
}
56+
if (responseField.getParameters() != null) {
57+
existingResponseField.parameters(responseField.getParameters().deepCopy());
58+
}
59+
if (responseField.getProjection() != null) {
60+
GraphQLResponseProjection projectionCopy = responseField.getProjection().deepCopy$();
61+
if (existingResponseField.getProjection() != null) {
62+
for (GraphQLResponseField field : projectionCopy.fields.values()) {
63+
existingResponseField.getProjection().add$(field);
5664
}
65+
} else {
66+
existingResponseField.projection(projectionCopy);
5767
}
58-
} else {
59-
fields.put(responseField.getName(), responseField.deepCopy());
6068
}
6169
}
6270

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.kobylynskyi.graphql.codegen.model.graphql;
2+
3+
public class Pair<K, V> {
4+
5+
private final K key;
6+
private final V value;
7+
8+
public K getKey() {
9+
return key;
10+
}
11+
12+
public V getValue() {
13+
return value;
14+
}
15+
16+
public Pair(K key, V value) {
17+
this.key = key;
18+
this.value = value;
19+
}
20+
21+
@Override
22+
public String toString() {
23+
return key + "=" + value;
24+
}
25+
26+
@Override
27+
public int hashCode() {
28+
return key.hashCode() * 13 + (value == null ? 0 : value.hashCode());
29+
}
30+
31+
@Override
32+
public boolean equals(Object o) {
33+
if (this == o) return true;
34+
if (o instanceof Pair) {
35+
Pair pair = (Pair) o;
36+
if (key != null ? !key.equals(pair.key) : pair.key != null) {
37+
return false;
38+
}
39+
return value != null ? value.equals(pair.value) : pair.value == null;
40+
}
41+
return false;
42+
}
43+
}

src/test/java/com/kobylynskyi/graphql/codegen/model/graphql/GraphQLResponseProjectionTest.java

Lines changed: 115 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,44 @@
55
import com.kobylynskyi.graphql.codegen.model.graphql.data.EventResponseProjection;
66
import org.junit.jupiter.api.Test;
77

8-
import java.util.Arrays;
8+
import java.util.ArrayList;
9+
import java.util.List;
910

11+
import static java.util.Arrays.asList;
12+
import static java.util.Collections.singletonList;
1013
import static org.junit.jupiter.api.Assertions.assertEquals;
1114
import static org.junit.jupiter.api.Assertions.assertTrue;
15+
import static org.junit.jupiter.api.Assertions.fail;
1216

1317
class GraphQLResponseProjectionTest {
1418

1519
@Test
1620
public void deepCopy_empty() {
1721
EventResponseProjection original = new EventResponseProjection();
18-
1922
EventResponseProjection deepCopy = original.deepCopy$();
20-
2123
assertTrue(deepCopy.fields.isEmpty());
2224
}
2325

26+
@Test
27+
public void deepCopy_null() {
28+
assertTrue(new EventResponseProjection((EventResponseProjection) null).fields.isEmpty());
29+
}
30+
31+
@Test
32+
public void deepCopy_null_list() {
33+
assertTrue(new EventResponseProjection((List<EventResponseProjection>) null).fields.isEmpty());
34+
}
35+
36+
@Test
37+
public void deepCopy_empty_list() {
38+
assertTrue(new EventResponseProjection(new ArrayList<>()).fields.isEmpty());
39+
}
40+
41+
@Test
42+
public void deepCopy_list_with_null_element() {
43+
assertTrue(new EventResponseProjection(new ArrayList<>(singletonList(null))).fields.isEmpty());
44+
}
45+
2446
@Test
2547
public void deepCopy() {
2648
EventResponseProjection original = new EventResponseProjection();
@@ -40,6 +62,8 @@ public void deepCopy() {
4062
assertEquals("{ id state : status properties { intVal stringVal child (first: 1, last: 2) { booleanVal } } }",
4163
deepCopy.toString());
4264

65+
// check that original and deepcopy are not modified
66+
4367
original.active();
4468
deepCopy.rating();
4569

@@ -54,29 +78,111 @@ public void join() {
5478
EventResponseProjection projection1 = new EventResponseProjection();
5579
projection1.id();
5680
projection1.status("state");
81+
projection1.rating();
5782
projection1.properties(new EventPropertyResponseProjection()
5883
.intVal()
5984
.stringVal()
60-
.child(new EventPropertyChildParametrizedInput(1, 2),
85+
.child("child12", new EventPropertyChildParametrizedInput(1, 2),
6186
new EventPropertyResponseProjection()
6287
.booleanVal()));
6388

6489
EventResponseProjection projection2 = new EventResponseProjection();
6590
projection2.id("uid");
91+
projection2.status();
6692
projection2.active();
6793
projection2.properties(new EventPropertyResponseProjection()
6894
.floatVal()
69-
.child(new EventPropertyChildParametrizedInput(3, 4),
95+
.child("child34", new EventPropertyChildParametrizedInput(3, 4),
7096
new EventPropertyResponseProjection()
7197
.intVal()));
7298

73-
EventResponseProjection projection12 = new EventResponseProjection(Arrays.asList(projection1, projection2));
74-
assertEquals("{ uid : id state : status properties { intVal stringVal child (first: 3, last: 4) { booleanVal intVal } floatVal } active }",
99+
EventResponseProjection projection12 = new EventResponseProjection(asList(projection1, projection2));
100+
assertEquals("{ id state : status rating properties { intVal stringVal child12 : child (first: 1, last: 2) { booleanVal } floatVal child34 : child (first: 3, last: 4) { intVal } } uid : id status active }",
75101
projection12.toString());
76102
}
77103

78104
@Test
79-
public void join_safe() {
105+
public void join_same_aliases_different_inputs() {
106+
EventResponseProjection projection1 = new EventResponseProjection()
107+
.properties(new EventPropertyResponseProjection()
108+
.child(new EventPropertyChildParametrizedInput(1, 2),
109+
new EventPropertyResponseProjection().intVal()));
110+
111+
EventResponseProjection projection2 = new EventResponseProjection()
112+
.properties(new EventPropertyResponseProjection()
113+
.child(new EventPropertyChildParametrizedInput(3, 4),
114+
new EventPropertyResponseProjection().intVal()));
115+
116+
try {
117+
new EventResponseProjection(asList(projection1, projection2));
118+
fail();
119+
} catch (IllegalArgumentException e) {
120+
assertEquals("Field 'child' has an argument conflict", e.getMessage());
121+
}
122+
}
123+
124+
@Test
125+
public void join_same_aliases_different_inputs2() {
126+
EventResponseProjection projection1 = new EventResponseProjection()
127+
.properties(new EventPropertyResponseProjection()
128+
.child("children", new EventPropertyChildParametrizedInput(1, 2),
129+
new EventPropertyResponseProjection().intVal()));
130+
131+
EventResponseProjection projection2 = new EventResponseProjection()
132+
.properties(new EventPropertyResponseProjection()
133+
.child("children", new EventPropertyChildParametrizedInput(3, 4),
134+
new EventPropertyResponseProjection().intVal()));
135+
136+
try {
137+
new EventResponseProjection(asList(projection1, projection2));
138+
fail();
139+
} catch (IllegalArgumentException e) {
140+
assertEquals("Field 'child' has an argument conflict", e.getMessage());
141+
}
142+
}
143+
144+
@Test
145+
public void join_same_aliases_different_inputs3() {
146+
EventResponseProjection projection1 = new EventResponseProjection()
147+
.properties(new EventPropertyResponseProjection()
148+
.child("children", new EventPropertyChildParametrizedInput(1, 2),
149+
null));
150+
151+
EventResponseProjection projection2 = new EventResponseProjection()
152+
.properties(new EventPropertyResponseProjection()
153+
.child("children", new EventPropertyChildParametrizedInput(3, 4),
154+
new EventPropertyResponseProjection().intVal()));
155+
156+
try {
157+
new EventResponseProjection(asList(projection1, projection2));
158+
fail();
159+
} catch (IllegalArgumentException e) {
160+
assertEquals("Field 'child' has an argument conflict", e.getMessage());
161+
}
162+
}
163+
164+
@Test
165+
public void join_same_aliases_different_inputs4() {
166+
EventResponseProjection projection1 = new EventResponseProjection()
167+
.properties(new EventPropertyResponseProjection()
168+
.child("children", new EventPropertyChildParametrizedInput(1, 2),
169+
new EventPropertyResponseProjection().intVal()));
170+
171+
EventResponseProjection projection2 = new EventResponseProjection()
172+
.properties(new EventPropertyResponseProjection()
173+
.child("children", new EventPropertyChildParametrizedInput(3, 4),
174+
null));
175+
176+
try {
177+
new EventResponseProjection(asList(projection1, projection2));
178+
fail();
179+
} catch (IllegalArgumentException e) {
180+
assertEquals("Field 'child' has an argument conflict", e.getMessage());
181+
}
182+
}
183+
184+
@Test
185+
public void join_null_values() {
80186
EventResponseProjection projection1 = new EventResponseProjection();
81187
projection1.id(null);
82188
projection1.properties(null);
@@ -90,7 +196,7 @@ public void join_safe() {
90196
.child(null, null, new EventPropertyResponseProjection()
91197
.child(null)));
92198

93-
EventResponseProjection projection123 = new EventResponseProjection(Arrays.asList(projection1, projection2, projection3));
199+
EventResponseProjection projection123 = new EventResponseProjection(asList(projection1, projection2, projection3));
94200
assertEquals("{ id properties { child { child } } }",
95201
projection123.toString());
96202
}

0 commit comments

Comments
 (0)