Skip to content

Commit

Permalink
Ability to combine multiple response projections #985 (#1031)
Browse files Browse the repository at this point in the history
  • Loading branch information
kobylynskyi authored Feb 22, 2023
1 parent b6dfc63 commit e06d69b
Show file tree
Hide file tree
Showing 57 changed files with 1,253 additions and 224 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@
*/
public interface GraphQLParametrizedInput {

GraphQLParametrizedInput deepCopy();

}
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,23 @@ public boolean equals(Object obj) {
public int hashCode() {
return Objects.hash(name, alias, parameters, projection);
}

/**
* Returns a clone of the instance, having a deep copy of the parameters and projection.
*
* @return a clone (deep copy)
*/
public GraphQLResponseField deepCopy() {
GraphQLResponseField deepCopy = new GraphQLResponseField(this.name);
if (this.alias != null) {
deepCopy.alias = this.alias;
}
if (this.parameters != null) {
deepCopy.parameters = this.parameters.deepCopy();
}
if (this.projection != null) {
deepCopy.projection = this.projection.deepCopy$();
}
return deepCopy;
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,90 @@
package com.kobylynskyi.graphql.codegen.model.graphql;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.StringJoiner;

/**
* The implementation class should basically contain the fields of the particular type which
* should be returned back to the client.
* The implementation class should contain the fields of the particular type that should be returned to the client.
*/
public abstract class GraphQLResponseProjection {

protected final List<GraphQLResponseField> fields = new ArrayList<>();
/**
* Contains all response projection fields, where:
* key - is the name+alias pair (where alias is nullable)
* value - is GraphQLResponseField which represents the response projection field
*/
protected final Map<Pair<String, String>, GraphQLResponseField> fields = new LinkedHashMap<>();

protected GraphQLResponseProjection() {
}

protected GraphQLResponseProjection(GraphQLResponseProjection projection) {
if (projection == null) {
return;
}
projection.fields.values().forEach(this::add$);
}

protected GraphQLResponseProjection(List<? extends GraphQLResponseProjection> projections) {
if (projections == null) {
return;
}
for (GraphQLResponseProjection projection : projections) {
if (projection == null) {
continue;
}
projection.fields.values().forEach(this::add$);
}
}

@SuppressWarnings({"checkstyle:MethodName", "java:S100"})
public abstract GraphQLResponseProjection deepCopy$();

@SuppressWarnings({"checkstyle:MethodName", "java:S100", "java:S3824"})
protected void add$(GraphQLResponseField responseField) {
Pair<String, String> nameAndAlias = new Pair<>(responseField.getName(), responseField.getAlias());
GraphQLResponseField existingResponseField = fields.get(nameAndAlias);
if (existingResponseField == null) {
fields.put(nameAndAlias, responseField.deepCopy());
return;
}

if (!Objects.equals(responseField.getParameters(), existingResponseField.getParameters())) {
throw new IllegalArgumentException(
String.format("Field '%s' has an argument conflict", existingResponseField.getName()));
}

if (responseField.getAlias() != null) {
existingResponseField.alias(responseField.getAlias());
}
if (responseField.getParameters() != null) {
existingResponseField.parameters(responseField.getParameters().deepCopy());
}
if (responseField.getProjection() != null) {
GraphQLResponseProjection projectionCopy = responseField.getProjection().deepCopy$();
if (existingResponseField.getProjection() != null) {
for (GraphQLResponseField field : projectionCopy.fields.values()) {
existingResponseField.getProjection().add$(field);
}
} else {
existingResponseField.projection(projectionCopy);
}
}
}

@Override
public String toString() {
if (fields.isEmpty()) {
return "";
}
StringJoiner joiner = new StringJoiner(" ", "{ ", " }");
fields.forEach(field -> joiner.add(field.toString()));
for (GraphQLResponseField value : fields.values()) {
joiner.add(value.toString());
}
return joiner.toString();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.kobylynskyi.graphql.codegen.model.graphql;

import java.util.Objects;

/**
* Class that represents a key-value pair.
*
* @param <K> key
* @param <V> value
*/
public class Pair<K, V> {

private final K key;
private final V value;

public K getKey() {
return key;
}

public V getValue() {
return value;
}

public Pair(K key, V value) {
this.key = key;
this.value = value;
}

@Override
public String toString() {
return key + "=" + value;
}

@Override
public int hashCode() {
return key.hashCode() * 13 + (value == null ? 0 : value.hashCode());
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o instanceof Pair) {
Pair<?, ?> pair = (Pair<?, ?>) o;
return Objects.equals(key, pair.key) && Objects.equals(value, pair.value);
}
return false;
}
}
11 changes: 11 additions & 0 deletions src/main/resources/templates/java-lang/parametrized_input.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,17 @@ public class ${className} implements GraphQLParametrizedInput {

</#list>
</#if>
@Override
public ${className} deepCopy() {
${className} parametrizedInput = new ${className}();
<#if fields?has_content>
<#list fields as field>
parametrizedInput.${field.name}(this.${field.name});
</#list>
</#if>
return parametrizedInput;
}

<#if equalsAndHashCode>
@Override
public boolean equals(Object obj) {
Expand Down
18 changes: 16 additions & 2 deletions src/main/resources/templates/java-lang/response_projection.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLResponseProjection;
import java.util.HashMap;
import java.util.Map;
</#if>
import java.util.List;
<#if equalsAndHashCode>
import java.util.Objects;
</#if>
Expand Down Expand Up @@ -36,6 +37,14 @@ public class ${className} extends GraphQLResponseProjection {

public ${className}() {
}

public ${className}(${className} projection) {
super(projection);
}

public ${className}(List<${className}> projections) {
super(projections);
}
<#if fields?has_content && generateAllMethodInProjection>

public ${className} all$() {
Expand Down Expand Up @@ -76,7 +85,7 @@ public class ${className} extends GraphQLResponseProjection {
}

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

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

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

</#if>
</#list>
</#if>
@Override
public ${className} deepCopy$() {
return new ${className}(this);
}

<#if equalsAndHashCode>
@Override
public boolean equals(Object obj) {
Expand Down
13 changes: 13 additions & 0 deletions src/main/resources/templates/kotlin-lang/parametrized_input.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ data class ${className}(
</#if>
) : GraphQLParametrizedInput {

override fun deepCopy(): ${className} {
<#if fields?has_content>
return ${className}(
<#list fields as field>
this.${field.name}<#if field_has_next>,</#if>
</#list>
)
<#else>
return ${className}()
</#if>

}

override fun toString(): String {
val joiner = StringJoiner(", ", "( ", " )")
<#list fields as field>
Expand Down
14 changes: 11 additions & 3 deletions src/main/resources/templates/kotlin-lang/response_projection.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ import java.util.Objects
<#list annotations as annotation>
@${annotation}
</#list>
open class ${className} : GraphQLResponseProjection() {
open class ${className} : GraphQLResponseProjection {

constructor(): super()

constructor(projection: ${className}): super(projection)

constructor(projections: List<${className}>): super(projections)

<#if fields?has_content && generateAllMethodInProjection>
private val projectionDepthOnFields: MutableMap<String, Int> by lazy { mutableMapOf<String, Int>() }
Expand Down Expand Up @@ -63,21 +69,23 @@ open class ${className} : GraphQLResponseProjection() {
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>)

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

<#if field.parametrizedInputClassName?has_content>
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>)

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

</#if>
</#list>
</#if>
override fun `deepCopy$`(): ${className} = ${className}(this)

<#if equalsAndHashCode>
override fun equals(other: Any?): Boolean {
if (this === other) {
Expand Down
12 changes: 12 additions & 0 deletions src/main/resources/templates/scala-lang/parametrized_input.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ case class ${className}(
</#if>
) extends GraphQLParametrizedInput {

override def deepCopy(): ${className} = {
<#if fields?has_content>
${className}(
<#list fields as field>
this.${field.name}<#if field_has_next>,</#if>
</#list>
)
<#else>
${className}()
</#if>
}

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.-->
<#if fields?has_content>
scala.Seq(<#list fields as field><#assign getMethod = ".get"><#assign asJava = ".asJava">
Expand Down
31 changes: 28 additions & 3 deletions src/main/resources/templates/scala-lang/response_projection.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import java.util.Objects
<#if fields?has_content && generateAllMethodInProjection>
import scala.collection.mutable.HashMap
</#if>
import scala.collection.JavaConverters._

<#if javaDoc?has_content>
/**
Expand All @@ -27,7 +28,29 @@ import scala.collection.mutable.HashMap
<#list annotations as annotation>
@${annotation}
</#list>
class ${className} extends GraphQLResponseProjection {
class ${className}() extends GraphQLResponseProjection() {

def this(projection: ${className}) = {
this()
if (projection != null) {
for (field <- projection.fields.values.asScala) {
add$(field)
}
}
}

def this(projections: scala.Seq[${className}]) = {
this()
if (projections != null) {
for (projection <- projections) {
if (projection != null) {
for (field <- projection.fields.values.asScala) {
add$(field)
}
}
}
}
}

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

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

Expand All @@ -78,13 +101,15 @@ class ${className} extends GraphQLResponseProjection {
}

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

</#if>
</#list>
</#if>
override def deepCopy$(): ${className} = new ${className}(this)

<#if equalsAndHashCode>
override def equals(obj: Any): Boolean = {
if (this == obj) {
Expand Down
Empty file.
Loading

0 comments on commit e06d69b

Please sign in to comment.