Skip to content

Commit 256b50c

Browse files
committed
Ability to combine multiple response projections #985 (#1031)
1 parent f60d4b7 commit 256b50c

File tree

57 files changed

+1253
-224
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+1253
-224
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@
55
*/
66
public interface GraphQLParametrizedInput {
77

8+
GraphQLParametrizedInput deepCopy();
9+
810
}

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,23 @@ public boolean equals(Object obj) {
8686
public int hashCode() {
8787
return Objects.hash(name, alias, parameters, projection);
8888
}
89+
90+
/**
91+
* Returns a clone of the instance, having a deep copy of the parameters and projection.
92+
*
93+
* @return a clone (deep copy)
94+
*/
95+
public GraphQLResponseField deepCopy() {
96+
GraphQLResponseField deepCopy = new GraphQLResponseField(this.name);
97+
if (this.alias != null) {
98+
deepCopy.alias = this.alias;
99+
}
100+
if (this.parameters != null) {
101+
deepCopy.parameters = this.parameters.deepCopy();
102+
}
103+
if (this.projection != null) {
104+
deepCopy.projection = this.projection.deepCopy$();
105+
}
106+
return deepCopy;
107+
}
89108
}
Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,90 @@
11
package com.kobylynskyi.graphql.codegen.model.graphql;
22

3-
import java.util.ArrayList;
3+
import java.util.LinkedHashMap;
44
import java.util.List;
5+
import java.util.Map;
6+
import java.util.Objects;
57
import java.util.StringJoiner;
68

79
/**
8-
* The implementation class should basically contain the fields of the particular type which
9-
* should be returned back to the client.
10+
* 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 List<GraphQLResponseField> fields = new ArrayList<>();
14+
/**
15+
* Contains all response projection fields, where:
16+
* key - is the name+alias pair (where alias is nullable)
17+
* value - is GraphQLResponseField which represents the response projection field
18+
*/
19+
protected final Map<Pair<String, String>, GraphQLResponseField> fields = new LinkedHashMap<>();
20+
21+
protected GraphQLResponseProjection() {
22+
}
23+
24+
protected GraphQLResponseProjection(GraphQLResponseProjection projection) {
25+
if (projection == null) {
26+
return;
27+
}
28+
projection.fields.values().forEach(this::add$);
29+
}
30+
31+
protected GraphQLResponseProjection(List<? extends GraphQLResponseProjection> projections) {
32+
if (projections == null) {
33+
return;
34+
}
35+
for (GraphQLResponseProjection projection : projections) {
36+
if (projection == null) {
37+
continue;
38+
}
39+
projection.fields.values().forEach(this::add$);
40+
}
41+
}
42+
43+
@SuppressWarnings({"checkstyle:MethodName", "java:S100"})
44+
public abstract GraphQLResponseProjection deepCopy$();
45+
46+
@SuppressWarnings({"checkstyle:MethodName", "java:S100", "java:S3824"})
47+
protected void add$(GraphQLResponseField responseField) {
48+
Pair<String, String> nameAndAlias = new Pair<>(responseField.getName(), responseField.getAlias());
49+
GraphQLResponseField existingResponseField = fields.get(nameAndAlias);
50+
if (existingResponseField == null) {
51+
fields.put(nameAndAlias, responseField.deepCopy());
52+
return;
53+
}
54+
55+
if (!Objects.equals(responseField.getParameters(), existingResponseField.getParameters())) {
56+
throw new IllegalArgumentException(
57+
String.format("Field '%s' has an argument conflict", existingResponseField.getName()));
58+
}
59+
60+
if (responseField.getAlias() != null) {
61+
existingResponseField.alias(responseField.getAlias());
62+
}
63+
if (responseField.getParameters() != null) {
64+
existingResponseField.parameters(responseField.getParameters().deepCopy());
65+
}
66+
if (responseField.getProjection() != null) {
67+
GraphQLResponseProjection projectionCopy = responseField.getProjection().deepCopy$();
68+
if (existingResponseField.getProjection() != null) {
69+
for (GraphQLResponseField field : projectionCopy.fields.values()) {
70+
existingResponseField.getProjection().add$(field);
71+
}
72+
} else {
73+
existingResponseField.projection(projectionCopy);
74+
}
75+
}
76+
}
1477

1578
@Override
1679
public String toString() {
1780
if (fields.isEmpty()) {
1881
return "";
1982
}
2083
StringJoiner joiner = new StringJoiner(" ", "{ ", " }");
21-
fields.forEach(field -> joiner.add(field.toString()));
84+
for (GraphQLResponseField value : fields.values()) {
85+
joiner.add(value.toString());
86+
}
2287
return joiner.toString();
2388
}
89+
2490
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.kobylynskyi.graphql.codegen.model.graphql;
2+
3+
import java.util.Objects;
4+
5+
/**
6+
* Class that represents a key-value pair.
7+
*
8+
* @param <K> key
9+
* @param <V> value
10+
*/
11+
public class Pair<K, V> {
12+
13+
private final K key;
14+
private final V value;
15+
16+
public K getKey() {
17+
return key;
18+
}
19+
20+
public V getValue() {
21+
return value;
22+
}
23+
24+
public Pair(K key, V value) {
25+
this.key = key;
26+
this.value = value;
27+
}
28+
29+
@Override
30+
public String toString() {
31+
return key + "=" + value;
32+
}
33+
34+
@Override
35+
public int hashCode() {
36+
return key.hashCode() * 13 + (value == null ? 0 : value.hashCode());
37+
}
38+
39+
@Override
40+
public boolean equals(Object o) {
41+
if (this == o) {
42+
return true;
43+
}
44+
if (o instanceof Pair) {
45+
Pair<?, ?> pair = (Pair<?, ?>) o;
46+
return Objects.equals(key, pair.key) && Objects.equals(value, pair.value);
47+
}
48+
return false;
49+
}
50+
}

src/main/resources/templates/java-lang/parametrized_input.ftl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,17 @@ public class ${className} implements GraphQLParametrizedInput {
7070

7171
</#list>
7272
</#if>
73+
@Override
74+
public ${className} deepCopy() {
75+
${className} parametrizedInput = new ${className}();
76+
<#if fields?has_content>
77+
<#list fields as field>
78+
parametrizedInput.${field.name}(this.${field.name});
79+
</#list>
80+
</#if>
81+
return parametrizedInput;
82+
}
83+
7384
<#if equalsAndHashCode>
7485
@Override
7586
public boolean equals(Object obj) {

src/main/resources/templates/java-lang/response_projection.ftl

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLResponseProjection;
88
import java.util.HashMap;
99
import java.util.Map;
1010
</#if>
11+
import java.util.List;
1112
<#if equalsAndHashCode>
1213
import java.util.Objects;
1314
</#if>
@@ -36,6 +37,14 @@ public class ${className} extends GraphQLResponseProjection {
3637

3738
public ${className}() {
3839
}
40+
41+
public ${className}(${className} projection) {
42+
super(projection);
43+
}
44+
45+
public ${className}(List<${className}> projections) {
46+
super(projections);
47+
}
3948
<#if fields?has_content && generateAllMethodInProjection>
4049

4150
public ${className} all$() {
@@ -76,7 +85,7 @@ public class ${className} extends GraphQLResponseProjection {
7685
}
7786

7887
public ${className} ${field.methodName}(String alias<#if field.type?has_content>, ${field.type} subProjection</#if>) {
79-
fields.add(new GraphQLResponseField("${field.name}").alias(alias)<#if field.type?has_content>.projection(subProjection)</#if>);
88+
add$(new GraphQLResponseField("${field.name}").alias(alias)<#if field.type?has_content>.projection(subProjection)</#if>);
8089
return this;
8190
}
8291

@@ -86,13 +95,18 @@ public class ${className} extends GraphQLResponseProjection {
8695
}
8796

8897
public ${className} ${field.methodName}(String alias, ${field.parametrizedInputClassName} input<#if field.type?has_content>, ${field.type} subProjection</#if>) {
89-
fields.add(new GraphQLResponseField("${field.name}").alias(alias).parameters(input)<#if field.type?has_content>.projection(subProjection)</#if>);
98+
add$(new GraphQLResponseField("${field.name}").alias(alias).parameters(input)<#if field.type?has_content>.projection(subProjection)</#if>);
9099
return this;
91100
}
92101

93102
</#if>
94103
</#list>
95104
</#if>
105+
@Override
106+
public ${className} deepCopy$() {
107+
return new ${className}(this);
108+
}
109+
96110
<#if equalsAndHashCode>
97111
@Override
98112
public boolean equals(Object obj) {

src/main/resources/templates/kotlin-lang/parametrized_input.ftl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,19 @@ data class ${className}(
3838
</#if>
3939
) : GraphQLParametrizedInput {
4040

41+
override fun deepCopy(): ${className} {
42+
<#if fields?has_content>
43+
return ${className}(
44+
<#list fields as field>
45+
this.${field.name}<#if field_has_next>,</#if>
46+
</#list>
47+
)
48+
<#else>
49+
return ${className}()
50+
</#if>
51+
52+
}
53+
4154
override fun toString(): String {
4255
val joiner = StringJoiner(", ", "( ", " )")
4356
<#list fields as field>

src/main/resources/templates/kotlin-lang/response_projection.ftl

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,13 @@ import java.util.Objects
2424
<#list annotations as annotation>
2525
@${annotation}
2626
</#list>
27-
open class ${className} : GraphQLResponseProjection() {
27+
open class ${className} : GraphQLResponseProjection {
28+
29+
constructor(): super()
30+
31+
constructor(projection: ${className}): super(projection)
32+
33+
constructor(projections: List<${className}>): super(projections)
2834

2935
<#if fields?has_content && generateAllMethodInProjection>
3036
private val projectionDepthOnFields: MutableMap<String, Int> by lazy { mutableMapOf<String, Int>() }
@@ -63,21 +69,23 @@ open class ${className} : GraphQLResponseProjection() {
6369
fun ${field.methodName}(<#if field.type?has_content>subProjection: ${field.type}</#if>): ${className} = ${field.methodName}(<#if field.parametrizedInputClassName?has_content></#if>null<#if field.type?has_content>, subProjection</#if>)
6470

6571
fun ${field.methodName}(alias: String?<#if field.type?has_content>, subProjection: ${field.type}</#if>): ${className} {
66-
fields.add(GraphQLResponseField("${field.name}").alias(alias)<#if field.type?has_content>.projection(subProjection)</#if>)
72+
`add$`(GraphQLResponseField("${field.name}").alias(alias)<#if field.type?has_content>.projection(subProjection)</#if>)
6773
return this
6874
}
6975

7076
<#if field.parametrizedInputClassName?has_content>
7177
fun ${field.methodName}(input: ${field.parametrizedInputClassName}<#if field.type?has_content>, subProjection: ${field.type}</#if>): ${className} = ${field.methodName}(null, input<#if field.type?has_content>, subProjection</#if>)
7278

7379
fun ${field.methodName}(alias: String?, input: ${field.parametrizedInputClassName}<#if field.type?has_content>, subProjection: ${field.type}</#if>): ${className} {
74-
fields.add(GraphQLResponseField("${field.name}").alias(alias).parameters(input)<#if field.type?has_content>.projection(subProjection)</#if>)
80+
`add$`(GraphQLResponseField("${field.name}").alias(alias).parameters(input)<#if field.type?has_content>.projection(subProjection)</#if>)
7581
return this
7682
}
7783

7884
</#if>
7985
</#list>
8086
</#if>
87+
override fun `deepCopy$`(): ${className} = ${className}(this)
88+
8189
<#if equalsAndHashCode>
8290
override fun equals(other: Any?): Boolean {
8391
if (this === other) {

src/main/resources/templates/scala-lang/parametrized_input.ftl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,18 @@ case class ${className}(
5454
</#if>
5555
) extends GraphQLParametrizedInput {
5656

57+
override def deepCopy(): ${className} = {
58+
<#if fields?has_content>
59+
${className}(
60+
<#list fields as field>
61+
this.${field.name}<#if field_has_next>,</#if>
62+
</#list>
63+
)
64+
<#else>
65+
${className}()
66+
</#if>
67+
}
68+
5769
override def toString(): String = {<#--There is no Option[Seq[T]], Format is not supported in the generated code, so it is very difficult to write template for this format.-->
5870
<#if fields?has_content>
5971
scala.Seq(<#list fields as field><#assign getMethod = ".get"><#assign asJava = ".asJava">

src/main/resources/templates/scala-lang/response_projection.ftl

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import java.util.Objects
1010
<#if fields?has_content && generateAllMethodInProjection>
1111
import scala.collection.mutable.HashMap
1212
</#if>
13+
import scala.collection.JavaConverters._
1314

1415
<#if javaDoc?has_content>
1516
/**
@@ -27,7 +28,29 @@ import scala.collection.mutable.HashMap
2728
<#list annotations as annotation>
2829
@${annotation}
2930
</#list>
30-
class ${className} extends GraphQLResponseProjection {
31+
class ${className}() extends GraphQLResponseProjection() {
32+
33+
def this(projection: ${className}) = {
34+
this()
35+
if (projection != null) {
36+
for (field <- projection.fields.values.asScala) {
37+
add$(field)
38+
}
39+
}
40+
}
41+
42+
def this(projections: scala.Seq[${className}]) = {
43+
this()
44+
if (projections != null) {
45+
for (projection <- projections) {
46+
if (projection != null) {
47+
for (field <- projection.fields.values.asScala) {
48+
add$(field)
49+
}
50+
}
51+
}
52+
}
53+
}
3154

3255
<#if fields?has_content && generateAllMethodInProjection>
3356
private final lazy val projectionDepthOnFields = new HashMap[String, Int]
@@ -68,7 +91,7 @@ class ${className} extends GraphQLResponseProjection {
6891
}
6992

7093
def ${field.methodName}(alias: String<#if field.type?has_content>, subProjection: ${field.type}</#if>): ${className} = {
71-
fields.add(new GraphQLResponseField("${field.name}").alias(alias)<#if field.type?has_content>.projection(subProjection)</#if>)
94+
add$(new GraphQLResponseField("${field.name}").alias(alias)<#if field.type?has_content>.projection(subProjection)</#if>)
7295
this
7396
}
7497

@@ -78,13 +101,15 @@ class ${className} extends GraphQLResponseProjection {
78101
}
79102

80103
def ${field.methodName}(alias: String, input: ${field.parametrizedInputClassName} <#if field.type?has_content>, subProjection: ${field.type}</#if>): ${className} = {
81-
fields.add(new GraphQLResponseField("${field.name}").alias(alias).parameters(input)<#if field.type?has_content>.projection(subProjection)</#if>)
104+
add$(new GraphQLResponseField("${field.name}").alias(alias).parameters(input)<#if field.type?has_content>.projection(subProjection)</#if>)
82105
this
83106
}
84107

85108
</#if>
86109
</#list>
87110
</#if>
111+
override def deepCopy$(): ${className} = new ${className}(this)
112+
88113
<#if equalsAndHashCode>
89114
override def equals(obj: Any): Boolean = {
90115
if (this == obj) {

0 commit comments

Comments
 (0)