diff --git a/example/example-client/src/main/java/com/jacobmountain/StarWarsClient.java b/example/example-client/src/main/java/com/jacobmountain/StarWarsClient.java index ac4e135..85bd259 100644 --- a/example/example-client/src/main/java/com/jacobmountain/StarWarsClient.java +++ b/example/example-client/src/main/java/com/jacobmountain/StarWarsClient.java @@ -16,16 +16,17 @@ ) public interface StarWarsClient { + @GraphQLFragment(type = "Character", name = "Hero") @GraphQLQuery(value = "hero", name = "HeroByEpisode") com.jacobmountain.dto.Character getHero(Episode episode, int first, String after, LengthUnit unit); - @GraphQLQuery(value = "hero", name = "HeroSummary") + @GraphQLQuery(value = "hero", name = "HeroSummary", maxDepth = 6) com.jacobmountain.dto.Character getHero(@GraphQLArgument("hero") String id); @GraphQLQuery(value = "hero", select = { @GraphQLField("id"), @GraphQLField("name") - }, maxDepth = 1) + }) com.jacobmountain.dto.Character getHeroSummary(String id); @GraphQLQuery("hero") diff --git a/graphql-java-client-annotations/src/main/java/com/jacobmountain/graphql/client/annotations/GraphQLField.java b/graphql-java-client-annotations/src/main/java/com/jacobmountain/graphql/client/annotations/GraphQLField.java index 91be534..eb41a3b 100644 --- a/graphql-java-client-annotations/src/main/java/com/jacobmountain/graphql/client/annotations/GraphQLField.java +++ b/graphql-java-client-annotations/src/main/java/com/jacobmountain/graphql/client/annotations/GraphQLField.java @@ -1,5 +1,11 @@ package com.jacobmountain.graphql.client.annotations; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({}) +@Retention(RetentionPolicy.SOURCE) public @interface GraphQLField { String value(); diff --git a/graphql-java-client-annotations/src/main/java/com/jacobmountain/graphql/client/annotations/GraphQLFragment.java b/graphql-java-client-annotations/src/main/java/com/jacobmountain/graphql/client/annotations/GraphQLFragment.java new file mode 100644 index 0000000..301defb --- /dev/null +++ b/graphql-java-client-annotations/src/main/java/com/jacobmountain/graphql/client/annotations/GraphQLFragment.java @@ -0,0 +1,16 @@ +package com.jacobmountain.graphql.client.annotations; + +import java.lang.annotation.*; + +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.SOURCE) +@Repeatable(GraphQLFragments.class) +public @interface GraphQLFragment { + + String name() default ""; + + String type(); + + GraphQLField[] select() default {}; + +} diff --git a/graphql-java-client-annotations/src/main/java/com/jacobmountain/graphql/client/annotations/GraphQLFragments.java b/graphql-java-client-annotations/src/main/java/com/jacobmountain/graphql/client/annotations/GraphQLFragments.java new file mode 100644 index 0000000..6ba3062 --- /dev/null +++ b/graphql-java-client-annotations/src/main/java/com/jacobmountain/graphql/client/annotations/GraphQLFragments.java @@ -0,0 +1,11 @@ +package com.jacobmountain.graphql.client.annotations; + +import java.lang.annotation.*; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.SOURCE) +public @interface GraphQLFragments { + + GraphQLFragment[] value(); + +} diff --git a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/ClientGenerator.java b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/ClientGenerator.java index 9b32e42..1be9e70 100644 --- a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/ClientGenerator.java +++ b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/ClientGenerator.java @@ -91,7 +91,7 @@ public void generate(Element element, String suffix) { // for each method on the interface, generate its implementation element.getEnclosedElements() .stream() - .map(this::generateImpl) + .map(el -> generateImpl(el, details)) .forEach(builder::addMethod); writeToFile(builder.build()); @@ -115,7 +115,7 @@ private MethodSpec generateConstructor(List variab * @param method the method of the @GraphQLClient annotated interface * @return a method spec to add to the implementation */ - private MethodSpec generateImpl(Element method) { + private MethodSpec generateImpl(Element method, ClientDetails client) { log.info(""); MethodDetails details = method.accept(new MethodDetailsVisitor(schema), typeMapper); log.info("{}", details); @@ -125,9 +125,9 @@ private MethodSpec generateImpl(Element method) { .addModifiers(Modifier.PUBLIC) .addParameters(details.getParameterSpec()); - this.arguments.assemble(details).forEach(builder::addStatement); - this.query.assemble(details).forEach(builder::addStatement); - this.returnResults.assemble(details).forEach(builder::addStatement); + this.arguments.assemble(client, details).forEach(builder::addStatement); + this.query.assemble(client, details).forEach(builder::addStatement); + this.returnResults.assemble(client, details).forEach(builder::addStatement); return builder.build(); } diff --git a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/GraphQLClientProcessor.java b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/GraphQLClientProcessor.java index 7076142..90d8899 100644 --- a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/GraphQLClientProcessor.java +++ b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/GraphQLClientProcessor.java @@ -23,7 +23,6 @@ import java.nio.file.Paths; import java.util.Arrays; import java.util.Set; -import java.util.function.Consumer; @Slf4j @AutoService(Processor.class) @@ -57,18 +56,16 @@ public boolean process(Set annotations, RoundEnvironment return elements.stream() .map(el -> (TypeElement) el) .map(Input::new) - .peek(this.generateJavaDataClasses()) + .peek(this::generateJavaDataClasses) .peek(this::generateClientImplementation) .count() > 0; } - private Consumer generateJavaDataClasses() { - return input -> { - log.info("Generating java classes from GraphQL schema"); - DTOGenerator dtoGenerator = new DTOGenerator(input.getDtoPackage(), new FileWriter(this.filer), input.getTypeMapper()); - dtoGenerator.generate(input.getSchema().types().values()); - dtoGenerator.generateArgumentDTOs(input.element); - }; + private void generateJavaDataClasses(Input input) { + log.info("Generating java classes from GraphQL schema"); + DTOGenerator dtoGenerator = new DTOGenerator(input.getDtoPackage(), new FileWriter(this.filer), input.getTypeMapper()); + dtoGenerator.generate(input.getSchema().types().values()); + dtoGenerator.generateArgumentDTOs(input.element); } private void generateClientImplementation(Input client) { diff --git a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/AbstractQueryStage.java b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/AbstractQueryStage.java index a7e308b..d8d11b7 100644 --- a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/AbstractQueryStage.java +++ b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/AbstractQueryStage.java @@ -1,8 +1,10 @@ package com.jacobmountain.graphql.client.modules; import com.jacobmountain.graphql.client.TypeMapper; +import com.jacobmountain.graphql.client.annotations.GraphQLFragment; import com.jacobmountain.graphql.client.dto.Response; import com.jacobmountain.graphql.client.query.QueryGenerator; +import com.jacobmountain.graphql.client.query.selectors.Fragment; import com.jacobmountain.graphql.client.utils.Schema; import com.jacobmountain.graphql.client.visitor.GraphQLFieldSelection; import com.jacobmountain.graphql.client.visitor.MethodDetails; @@ -10,6 +12,7 @@ import com.squareup.javapoet.*; import graphql.language.ObjectTypeDefinition; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; @@ -53,12 +56,11 @@ protected String getMethod(MethodDetails details) { return method; } - protected CodeBlock generateQueryCode(String request, MethodDetails details) { + protected CodeBlock generateQueryCode(String request, ClientDetails client, MethodDetails details) { Set params = details.getParameters() .stream() .map(Parameter::getField) .collect(Collectors.toSet()); - queryGenerator.query(); QueryGenerator.QueryBuilder builder; if (details.isQuery()) { builder = queryGenerator.query(); @@ -69,6 +71,7 @@ protected CodeBlock generateQueryCode(String request, MethodDetails details) { } else { throw new RuntimeException(""); } + String query = builder .select( details.getSelection() @@ -77,10 +80,22 @@ protected CodeBlock generateQueryCode(String request, MethodDetails details) { .collect(Collectors.toList()) ) .maxDepth(details.getMaxDepth()) + .fragments(getFragments(client, details)) .build(request, details.getField(), params); return CodeBlock.of( "(\"$L\", $L)", query, details.hasParameters() ? "args" : "null" ); } + private List getFragments(ClientDetails client, MethodDetails details) { + List fragments = new ArrayList<>(); + for (GraphQLFragment f : client.getFragments()) { + fragments.add(new Fragment(f)); + } + for (GraphQLFragment f : details.getFragments()) { + fragments.add(new Fragment(f)); + } + return fragments; + } + } \ No newline at end of file diff --git a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/AbstractStage.java b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/AbstractStage.java index ce2c607..c9ab140 100644 --- a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/AbstractStage.java +++ b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/AbstractStage.java @@ -28,7 +28,7 @@ public List getTypeArguments() { return Collections.emptyList(); } - public List assemble(MethodDetails details) { + public List assemble(ClientDetails client, MethodDetails method) { return Collections.emptyList(); } diff --git a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/ArgumentAssemblyStage.java b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/ArgumentAssemblyStage.java index d59c22d..7122ca2 100644 --- a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/ArgumentAssemblyStage.java +++ b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/ArgumentAssemblyStage.java @@ -23,15 +23,15 @@ public ArgumentAssemblyStage(String dtoPackageName) { } @Override - public List assemble(MethodDetails details) { - List parameters = details.getParameters(); + public List assemble(ClientDetails client, MethodDetails method) { + List parameters = method.getParameters(); if (parameters.isEmpty()) { return Collections.emptyList(); } List ret = new ArrayList<>(); - TypeName type = ClassName.get(dtoPackageName, details.getArgumentClassname()); + TypeName type = ClassName.get(dtoPackageName, method.getArgumentClassname()); ret.add(CodeBlock.of("$T args = new $T()", type, type)); - details.getParameters() + method.getParameters() .stream() .map(this::setArgumentField) .forEach(ret::add); diff --git a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/BlockingQueryStage.java b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/BlockingQueryStage.java index 1532c61..e696991 100644 --- a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/BlockingQueryStage.java +++ b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/BlockingQueryStage.java @@ -49,12 +49,12 @@ public List getTypeArguments() { } @Override - public List assemble(MethodDetails details) { - ObjectTypeDefinition query = getTypeDefinition(details); + public List assemble(ClientDetails client, MethodDetails method) { + ObjectTypeDefinition query = getTypeDefinition(method); return Collections.singletonList( CodeBlock.builder() .add("$T thing = ", ParameterizedTypeName.get(ClassName.get(Response.class), typeMapper.getType(query.getName()), TypeVariableName.get("Error"))) - .add("fetcher.$L", getMethod(details)).add(generateQueryCode(details.getRequestName(), details)) + .add("fetcher.$L", getMethod(method)).add(generateQueryCode(method.getRequestName(), client, method)) .build() ); } diff --git a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/ClientDetails.java b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/ClientDetails.java index 6872440..2d76024 100644 --- a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/ClientDetails.java +++ b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/ClientDetails.java @@ -1,6 +1,10 @@ package com.jacobmountain.graphql.client.modules; +import com.jacobmountain.graphql.client.annotations.GraphQLFragment; import lombok.Builder; +import lombok.Getter; + +import java.util.List; @Builder public class ClientDetails { @@ -9,6 +13,9 @@ public class ClientDetails { private final boolean requiresFetcher; + @Getter + private final List fragments; + public boolean requiresSubscriber() { return requiresFetcher; } diff --git a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/OptionalReturnStage.java b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/OptionalReturnStage.java index 7be6c9b..469c890 100644 --- a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/OptionalReturnStage.java +++ b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/OptionalReturnStage.java @@ -19,17 +19,17 @@ public OptionalReturnStage(Schema schema, TypeMapper typeMapper) { } @Override - public List assemble(MethodDetails details) { - ObjectTypeDefinition typeDefinition = getTypeDefinition(details); + public List assemble(ClientDetails client, MethodDetails method) { + ObjectTypeDefinition typeDefinition = getTypeDefinition(method); List ret = new ArrayList<>( Arrays.asList( CodeBlock.of("return $T.ofNullable(thing)", Optional.class), CodeBlock.of("map($T::getData)", ClassName.get(Response.class)), - CodeBlock.of("map($T::$L)", typeMapper.getType(typeDefinition.getName()), StringUtils.camelCase("get", details.getField())) + CodeBlock.of("map($T::$L)", typeMapper.getType(typeDefinition.getName()), StringUtils.camelCase("get", method.getField())) ) ); - if (!returnsOptional(details)) { + if (!returnsOptional(method)) { ret.add(CodeBlock.of("orElse(null)")); } return Collections.singletonList(CodeBlock.join(ret, "\n\t.")); diff --git a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/ReactiveQueryStage.java b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/ReactiveQueryStage.java index 6dcbfe8..fdf9a7b 100644 --- a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/ReactiveQueryStage.java +++ b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/ReactiveQueryStage.java @@ -58,12 +58,12 @@ public List getMemberVariables(ClientDetails details) { } @Override - public List assemble(MethodDetails details) { - String member = details.isSubscription() ? "subscriber" : "fetcher"; + public List assemble(ClientDetails client, MethodDetails method) { + String member = method.isSubscription() ? "subscriber" : "fetcher"; return Collections.singletonList( CodeBlock.builder() - .add("$T thing = ", ParameterizedTypeName.get(ClassName.get(Publisher.class), getReturnTypeName(details))) - .add("$L.$L", member, getMethod(details)).add(generateQueryCode(details.getRequestName(), details)) + .add("$T thing = ", ParameterizedTypeName.get(ClassName.get(Publisher.class), getReturnTypeName(method))) + .add("$L.$L", member, getMethod(method)).add(generateQueryCode(method.getRequestName(), client, method)) .build() ); } diff --git a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/ReactiveReturnStage.java b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/ReactiveReturnStage.java index fb20c65..0050ae6 100644 --- a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/ReactiveReturnStage.java +++ b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/modules/ReactiveReturnStage.java @@ -22,21 +22,21 @@ public ReactiveReturnStage(Schema schema, TypeMapper typeMapper) { } @Override - public List assemble(MethodDetails details) { - ObjectTypeDefinition typeDefinition = getTypeDefinition(details); + public List assemble(ClientDetails client, MethodDetails method) { + ObjectTypeDefinition typeDefinition = getTypeDefinition(method); List ret = new ArrayList<>( Arrays.asList( - CodeBlock.of("return $T.from(thing)", details.isSubscription() ? Flux.class : Mono.class), + CodeBlock.of("return $T.from(thing)", method.isSubscription() ? Flux.class : Mono.class), CodeBlock.of("map($T::getData)", ClassName.get(Response.class)), - CodeBlock.of("map($T::$L)", typeMapper.getType(typeDefinition.getName()), StringUtils.camelCase("get", details.getField())) + CodeBlock.of("map($T::$L)", typeMapper.getType(typeDefinition.getName()), StringUtils.camelCase("get", method.getField())) ) ); - if (!returnsPublisher(details)) { + if (!returnsPublisher(method)) { ret.add(CodeBlock.of("blockOptional()")); - if (!returnsOptional(details)) { + if (!returnsOptional(method)) { ret.add(CodeBlock.of("orElse(null)")); } - } else if (returnsClass(details, Flux.class) && !details.isSubscription()) { + } else if (returnsClass(method, Flux.class) && !method.isSubscription()) { ret.add(CodeBlock.of("flatMapIterable($T.identity())", Function.class)); } return Collections.singletonList(CodeBlock.join(ret, "\n\t.")); diff --git a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/FieldFilter.java b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/FieldFilter.java deleted file mode 100644 index bc8310c..0000000 --- a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/FieldFilter.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.jacobmountain.graphql.client.query; - -@FunctionalInterface -public interface FieldFilter { - - boolean shouldAddField(QueryContext context); - -} diff --git a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/QueryContext.java b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/QueryContext.java index eb0a280..e10c698 100644 --- a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/QueryContext.java +++ b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/QueryContext.java @@ -1,6 +1,7 @@ package com.jacobmountain.graphql.client.query; import graphql.language.FieldDefinition; +import graphql.language.Type; import lombok.AllArgsConstructor; import lombok.Value; @@ -13,18 +14,20 @@ public class QueryContext { QueryContext parent; + Type type; + int depth; FieldDefinition fieldDefinition; Set params; - QueryContext increment() { - return new QueryContext(this, depth + 1, fieldDefinition, params); + public QueryContext increment() { + return new QueryContext(this, type, depth + 1, fieldDefinition, params); } - QueryContext withType(FieldDefinition fieldDefinition) { - return new QueryContext(parent, depth, fieldDefinition, params); + public QueryContext withType(FieldDefinition fieldDefinition) { + return new QueryContext(parent, this.fieldDefinition.getType(), depth, fieldDefinition, params); } } diff --git a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/QueryGenerator.java b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/QueryGenerator.java index 5f9ad1e..706bb01 100644 --- a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/QueryGenerator.java +++ b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/QueryGenerator.java @@ -1,14 +1,14 @@ package com.jacobmountain.graphql.client.query; import com.jacobmountain.graphql.client.exceptions.FieldNotFoundException; -import com.jacobmountain.graphql.client.query.filters.AllNonNullArgsFieldFilter; -import com.jacobmountain.graphql.client.query.filters.FieldDuplicationFilter; -import com.jacobmountain.graphql.client.query.filters.MaxDepthFieldFilter; -import com.jacobmountain.graphql.client.query.filters.SelectionFieldFilter; +import com.jacobmountain.graphql.client.query.filters.*; +import com.jacobmountain.graphql.client.query.selectors.DefaultFieldSelector; +import com.jacobmountain.graphql.client.query.selectors.DelegatingFieldSelector; +import com.jacobmountain.graphql.client.query.selectors.Fragment; +import com.jacobmountain.graphql.client.query.selectors.InlineFragmentRenderer; import com.jacobmountain.graphql.client.utils.Schema; import com.jacobmountain.graphql.client.utils.StringUtils; import com.jacobmountain.graphql.client.visitor.GraphQLFieldSelection; -import graphql.com.google.common.collect.Streams; import graphql.language.*; import lombok.extern.slf4j.Slf4j; @@ -36,12 +36,14 @@ public QueryBuilder subscription() { return new QueryBuilder("subscription"); } - private String doGenerateQuery(String request, String field, String type, Set params, List filters) { + private String doGenerateQuery(String request, String field, String type, List fragments, Set params, List filters) { FieldDefinition definition = schema.findField(field).orElseThrow(FieldNotFoundException.create(field)); Set args = new HashSet<>(); - String inner = generateQueryRec(field, new QueryContext(null, 0, definition, params), args, filters).orElseThrow(RuntimeException::new); + final QueryContext root = new QueryContext(null, definition.getType(), 0, definition, params); + String inner = generateFieldSelection(field, root, args, filters) + .orElseThrow(RuntimeException::new); String collect = String.join(", ", args); @@ -59,10 +61,10 @@ private String generateQueryName(String request, String type, String field) { return type + " " + request; } - private Optional generateQueryRec(String alias, - QueryContext context, - Set argumentCollector, - List filters) { + public Optional generateFieldSelection(String alias, + QueryContext context, + Set argumentCollector, + List filters) { String type = Schema.unwrap(context.getFieldDefinition().getType()); TypeDefinition typeDefinition = schema.getTypeDefinition(type).orElse(null); @@ -75,37 +77,13 @@ private Optional generateQueryRec(String alias, return Optional.of(alias + args); } - List children = Streams.concat( - schema.getChildren(typeDefinition) - .map(definition -> generateQueryRec( - definition.getName(), - context.withType(definition).increment(), - argumentCollector, - filters - )) - .filter(Optional::isPresent) - .map(Optional::get), - schema.getTypesImplementing(typeDefinition) - .map(interfac -> generateQueryRec( - interfac, - context.withType(new FieldDefinition(interfac, new TypeName(interfac))), - argumentCollector, - filters - )) - .filter(Optional::isPresent) - .map(Optional::get) - .map(query -> "... on " + query) - ).collect(Collectors.toList()); - - if (children.isEmpty()) { - return Optional.empty(); - } - return Optional.of( - alias + args + " { " + - String.join(" ", children) + - " __typename" + - " }" - ); + return new DelegatingFieldSelector( + new DefaultFieldSelector(schema, this), + new InlineFragmentRenderer(schema, this) + ) + .selectFields(typeDefinition, context, argumentCollector, filters) + .map(children -> alias + args + " " + children) + .findFirst(); } public class QueryBuilder { @@ -114,6 +92,8 @@ public class QueryBuilder { private final List filters = new ArrayList<>(); + private List fragments = new ArrayList<>(); + QueryBuilder(String type) { this.type = type; } @@ -128,12 +108,16 @@ public QueryBuilder select(List selections) { return this; } + public QueryBuilder fragments(List fragments) { + this.fragments = fragments; + return this; + } + public String build(String request, String field, Set params) { this.filters.add(new AllNonNullArgsFieldFilter()); this.filters.add(new FieldDuplicationFilter()); - return doGenerateQuery(request, field, type, params, filters); + return doGenerateQuery(request, field, type, fragments, params, filters); } - } private String generateFieldArgs(FieldDefinition field, Set params, Set argsCollector) { diff --git a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/filters/AllNonNullArgsFieldFilter.java b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/filters/AllNonNullArgsFieldFilter.java index 6d6d8d9..3cbe6db 100644 --- a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/filters/AllNonNullArgsFieldFilter.java +++ b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/filters/AllNonNullArgsFieldFilter.java @@ -1,6 +1,5 @@ package com.jacobmountain.graphql.client.query.filters; -import com.jacobmountain.graphql.client.query.FieldFilter; import com.jacobmountain.graphql.client.query.QueryContext; import graphql.language.NonNullType; diff --git a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/filters/FieldDuplicationFilter.java b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/filters/FieldDuplicationFilter.java index eebf165..21b12eb 100644 --- a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/filters/FieldDuplicationFilter.java +++ b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/filters/FieldDuplicationFilter.java @@ -1,11 +1,7 @@ package com.jacobmountain.graphql.client.query.filters; -import com.jacobmountain.graphql.client.query.FieldFilter; import com.jacobmountain.graphql.client.query.QueryContext; import com.jacobmountain.graphql.client.utils.Schema; -import graphql.language.ListType; -import graphql.language.NonNullType; -import graphql.language.Type; import lombok.extern.slf4j.Slf4j; import java.util.*; @@ -29,7 +25,7 @@ private String generatePath(QueryContext context) { path.add(0, parent.getFieldDefinition().getName()); parent = parent.getParent(); } - path.set(path.size() - 1, Schema.unwrap(context.getFieldDefinition().getType())); + path.set(path.size() - 1, Schema.unwrap(context.getType())); return String.join(".", path); } diff --git a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/filters/FieldFilter.java b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/filters/FieldFilter.java new file mode 100644 index 0000000..dd79501 --- /dev/null +++ b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/filters/FieldFilter.java @@ -0,0 +1,10 @@ +package com.jacobmountain.graphql.client.query.filters; + +import com.jacobmountain.graphql.client.query.QueryContext; + +@FunctionalInterface +public interface FieldFilter { + + boolean shouldAddField(QueryContext context); + +} diff --git a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/filters/MaxDepthFieldFilter.java b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/filters/MaxDepthFieldFilter.java index fea1586..3e73178 100644 --- a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/filters/MaxDepthFieldFilter.java +++ b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/filters/MaxDepthFieldFilter.java @@ -1,6 +1,5 @@ package com.jacobmountain.graphql.client.query.filters; -import com.jacobmountain.graphql.client.query.FieldFilter; import com.jacobmountain.graphql.client.query.QueryContext; import lombok.RequiredArgsConstructor; diff --git a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/filters/SelectionFieldFilter.java b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/filters/SelectionFieldFilter.java index 6fe89fb..0b9d5ae 100644 --- a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/filters/SelectionFieldFilter.java +++ b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/filters/SelectionFieldFilter.java @@ -1,6 +1,5 @@ package com.jacobmountain.graphql.client.query.filters; -import com.jacobmountain.graphql.client.query.FieldFilter; import com.jacobmountain.graphql.client.query.QueryContext; import com.jacobmountain.graphql.client.visitor.GraphQLFieldSelection; import lombok.RequiredArgsConstructor; diff --git a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/selectors/DefaultFieldSelector.java b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/selectors/DefaultFieldSelector.java new file mode 100644 index 0000000..18ad5e0 --- /dev/null +++ b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/selectors/DefaultFieldSelector.java @@ -0,0 +1,41 @@ +package com.jacobmountain.graphql.client.query.selectors; + +import com.jacobmountain.graphql.client.query.filters.FieldFilter; +import com.jacobmountain.graphql.client.query.QueryContext; +import com.jacobmountain.graphql.client.query.QueryGenerator; +import com.jacobmountain.graphql.client.utils.OptionalUtils; +import com.jacobmountain.graphql.client.utils.Schema; +import graphql.language.FieldDefinition; +import graphql.language.TypeDefinition; +import lombok.RequiredArgsConstructor; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +@RequiredArgsConstructor +public class DefaultFieldSelector implements FieldSelector { + + private final Schema schema; + + private final QueryGenerator queryGenerator; + + @Override + public Stream selectFields(TypeDefinition typeDefinition, QueryContext context, Set argumentCollector, List filters) { + return schema.getChildren(typeDefinition) + .filter(this::filter) + .map(child -> queryGenerator.generateFieldSelection( + child.getName(), + context.withType(child).increment(), + argumentCollector, + filters + )) + .flatMap(OptionalUtils::toStream); + } + + protected boolean filter(FieldDefinition fieldDefinition) { + return true; + } + +} diff --git a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/selectors/DelegatingFieldSelector.java b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/selectors/DelegatingFieldSelector.java new file mode 100644 index 0000000..aea4bc0 --- /dev/null +++ b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/selectors/DelegatingFieldSelector.java @@ -0,0 +1,35 @@ +package com.jacobmountain.graphql.client.query.selectors; + +import com.jacobmountain.graphql.client.query.QueryContext; +import com.jacobmountain.graphql.client.query.filters.FieldFilter; +import graphql.language.TypeDefinition; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +public class DelegatingFieldSelector implements FieldSelector { + + private final List selectors; + + public DelegatingFieldSelector(FieldSelector... selectors) { + this.selectors = Arrays.asList(selectors); + } + + @Override + public Stream selectFields(TypeDefinition typeDefinition, + QueryContext context, + Set argumentCollector, + List filters) { + return selectors.stream() + .flatMap(selector -> selector.selectFields(typeDefinition, context, argumentCollector, filters)) + .reduce((a, b) -> String.join(" ", a, b)) + .map(children -> "{ " + + children + + " __typename" + + " }") + .map(Stream::of) + .orElseGet(Stream::empty); + } +} diff --git a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/selectors/FieldSelector.java b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/selectors/FieldSelector.java new file mode 100644 index 0000000..2206138 --- /dev/null +++ b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/selectors/FieldSelector.java @@ -0,0 +1,18 @@ +package com.jacobmountain.graphql.client.query.selectors; + +import com.jacobmountain.graphql.client.query.filters.FieldFilter; +import com.jacobmountain.graphql.client.query.QueryContext; +import graphql.language.TypeDefinition; + +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +public interface FieldSelector { + + Stream selectFields(TypeDefinition typeDefinition, + QueryContext context, + Set argumentCollector, + List filters); + +} diff --git a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/selectors/Fragment.java b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/selectors/Fragment.java new file mode 100644 index 0000000..930b1cf --- /dev/null +++ b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/selectors/Fragment.java @@ -0,0 +1,33 @@ +package com.jacobmountain.graphql.client.query.selectors; + +import com.jacobmountain.graphql.client.annotations.GraphQLFragment; +import com.jacobmountain.graphql.client.utils.StringUtils; +import com.jacobmountain.graphql.client.visitor.GraphQLFieldSelection; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Data +@AllArgsConstructor +public class Fragment { + + private String type; + + private String name; + + private Set selection; + + public Fragment(GraphQLFragment annotation) { + this.type = annotation.type(); + this.name = Optional.of(annotation.name()) + .filter(StringUtils::hasLength) + .orElseGet(() -> StringUtils.camelCase(annotation.type())); + this.selection = Stream.of(annotation.select()) + .map(GraphQLFieldSelection::new) + .collect(Collectors.toSet()); + } +} diff --git a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/selectors/FragmentRenderer.java b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/selectors/FragmentRenderer.java new file mode 100644 index 0000000..f40a8e8 --- /dev/null +++ b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/selectors/FragmentRenderer.java @@ -0,0 +1,65 @@ +package com.jacobmountain.graphql.client.query.selectors; + +import com.jacobmountain.graphql.client.query.QueryContext; +import com.jacobmountain.graphql.client.query.QueryGenerator; +import com.jacobmountain.graphql.client.query.filters.FieldFilter; +import com.jacobmountain.graphql.client.utils.Schema; +import graphql.language.TypeDefinition; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Slf4j +public class FragmentRenderer implements FieldSelector { + + private final Schema schema; + + private final QueryGenerator queryGenerator; + + private final Map fragments; + + private final Map generated = new HashMap<>(); + + public FragmentRenderer(Schema schema, QueryGenerator queryGenerator, List fragments) { + this.schema = schema; + this.queryGenerator = queryGenerator; + this.fragments = fragments.stream() + .collect(Collectors.toMap(Fragment::getType, Function.identity(), FragmentRenderer::duplicateFragments)); + } + + private static Fragment duplicateFragments(Fragment fragment, Fragment fragment2) { + if (!fragment.equals(fragment2)) { + log.warn("Duplicate fragments defined {} {}", fragment.getName(), fragment2.getName()); + } + return fragment; + } + + @Override + public Stream selectFields(TypeDefinition typeDefinition, QueryContext context, Set argumentCollector, List filters) { + log.info("\nGenerating fragment"); + final String type = Schema.unwrap(context.getFieldDefinition().getType()); + final Fragment fragment = fragments.get(type); + if (fragment == null) { + return Stream.empty(); + } + final Optional children = new DelegatingFieldSelector( + new DefaultFieldSelector(schema, queryGenerator), + new InlineFragmentRenderer(schema, queryGenerator) + ) + .selectFields(schema.getTypeDefinition(fragment.getType()).orElseThrow(RuntimeException::new), context, argumentCollector, filters) + .findFirst(); + if (children.isPresent()) { + generated.computeIfAbsent(fragment.getType(), a -> "fragment " + fragment.getName() + " on " + fragment.getName() + " " + children.get()); + return Stream.of("..." + fragment.getName()); + } + return Stream.empty(); + } + + public String render() { + return String.join(" ", generated.values()); + } + +} diff --git a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/selectors/InlineFragmentRenderer.java b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/selectors/InlineFragmentRenderer.java new file mode 100644 index 0000000..5440d41 --- /dev/null +++ b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/query/selectors/InlineFragmentRenderer.java @@ -0,0 +1,37 @@ +package com.jacobmountain.graphql.client.query.selectors; + +import com.jacobmountain.graphql.client.query.filters.FieldFilter; +import com.jacobmountain.graphql.client.query.QueryContext; +import com.jacobmountain.graphql.client.query.QueryGenerator; +import com.jacobmountain.graphql.client.utils.Schema; +import graphql.language.FieldDefinition; +import graphql.language.TypeDefinition; +import graphql.language.TypeName; +import lombok.RequiredArgsConstructor; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +@RequiredArgsConstructor +public class InlineFragmentRenderer implements FieldSelector { + + private final Schema schema; + + private final QueryGenerator queryGenerator; + + @Override + public Stream selectFields(TypeDefinition typeDefinition, QueryContext context, Set argumentCollector, List filters) { + return schema.getTypesImplementing(typeDefinition) + .map(interfac -> queryGenerator.generateFieldSelection( + interfac, + context.withType(new FieldDefinition(interfac, new TypeName(interfac))), + argumentCollector, + filters + )) + .filter(Optional::isPresent) + .map(Optional::get) + .map(query -> "... on " + query); + } +} diff --git a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/utils/OptionalUtils.java b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/utils/OptionalUtils.java index e39ed5e..ceb14db 100644 --- a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/utils/OptionalUtils.java +++ b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/utils/OptionalUtils.java @@ -5,6 +5,7 @@ import java.util.Arrays; import java.util.Optional; import java.util.function.Supplier; +import java.util.stream.Stream; @UtilityClass public class OptionalUtils { @@ -21,4 +22,9 @@ public Optional first(Optional first, Supplier>... later) return first(head, Arrays.copyOfRange(later, 1, later.length)); } + public Stream toStream(Optional optional) { + return optional.map(Stream::of) + .orElseGet(Stream::empty); + } + } diff --git a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/visitor/ClientDetailsVisitor.java b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/visitor/ClientDetailsVisitor.java index d3f6a0a..baf0727 100644 --- a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/visitor/ClientDetailsVisitor.java +++ b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/visitor/ClientDetailsVisitor.java @@ -1,11 +1,16 @@ package com.jacobmountain.graphql.client.visitor; +import com.jacobmountain.graphql.client.annotations.GraphQLFragment; +import com.jacobmountain.graphql.client.annotations.GraphQLMutation; import com.jacobmountain.graphql.client.annotations.GraphQLQuery; import com.jacobmountain.graphql.client.annotations.GraphQLSubscription; import com.jacobmountain.graphql.client.modules.ClientDetails; +import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import javax.lang.model.util.ElementKindVisitor8; +import java.lang.annotation.Annotation; +import java.util.Arrays; public class ClientDetailsVisitor extends ElementKindVisitor8 { @@ -14,19 +19,24 @@ public ClientDetails visitType(TypeElement type, Void unused) { return ClientDetails.builder() .requiresSubscriber(requiresSubscriber(type)) .requiresFetcher(requiresFetcher(type)) + .fragments(Arrays.asList(type.getAnnotationsByType(GraphQLFragment.class))) .build(); } private boolean requiresSubscriber(TypeElement element) { return element.getEnclosedElements() .stream() - .anyMatch(it -> it.getAnnotation(GraphQLSubscription.class) != null); + .anyMatch(it -> hasAnnotation(it, GraphQLSubscription.class)); } private boolean requiresFetcher(TypeElement element) { return element.getEnclosedElements() .stream() - .anyMatch(it -> it.getAnnotation(GraphQLQuery.class) != null); + .anyMatch(it -> hasAnnotation(it, GraphQLQuery.class) || hasAnnotation(it, GraphQLMutation.class)); + } + + private boolean hasAnnotation(Element el, Class annotation) { + return el.getAnnotation(annotation) != null; } } diff --git a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/visitor/MethodDetails.java b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/visitor/MethodDetails.java index 1a1c2ff..91c6b4f 100644 --- a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/visitor/MethodDetails.java +++ b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/visitor/MethodDetails.java @@ -1,6 +1,7 @@ package com.jacobmountain.graphql.client.visitor; import com.jacobmountain.graphql.client.annotations.GraphQLField; +import com.jacobmountain.graphql.client.annotations.GraphQLFragment; import com.jacobmountain.graphql.client.utils.StringUtils; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.ParameterSpec; @@ -45,6 +46,8 @@ public class MethodDetails { @Getter private final int maxDepth; + @Getter + private final List fragments; public boolean hasParameters() { return !parameters.isEmpty(); diff --git a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/visitor/MethodDetailsVisitor.java b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/visitor/MethodDetailsVisitor.java index 64b284c..16120fc 100644 --- a/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/visitor/MethodDetailsVisitor.java +++ b/graphql-java-client-processor/src/main/java/com/jacobmountain/graphql/client/visitor/MethodDetailsVisitor.java @@ -1,10 +1,7 @@ package com.jacobmountain.graphql.client.visitor; import com.jacobmountain.graphql.client.TypeMapper; -import com.jacobmountain.graphql.client.annotations.GraphQLArgument; -import com.jacobmountain.graphql.client.annotations.GraphQLMutation; -import com.jacobmountain.graphql.client.annotations.GraphQLQuery; -import com.jacobmountain.graphql.client.annotations.GraphQLSubscription; +import com.jacobmountain.graphql.client.annotations.*; import com.jacobmountain.graphql.client.exceptions.MissingAnnotationException; import com.jacobmountain.graphql.client.utils.OptionalUtils; import com.jacobmountain.graphql.client.utils.Schema; @@ -19,10 +16,13 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.util.ElementKindVisitor8; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; +import static java.util.Collections.singletonList; + @Slf4j public class MethodDetailsVisitor extends ElementKindVisitor8 { @@ -53,6 +53,7 @@ private Optional getQueryDetails(ExecutableElement e, TypeMapper .maxDepth(annotation.maxDepth()) .parameters(getParameters(e, typeMapper, annotation.value())) .selection(Arrays.asList(annotation.select())) + .fragments(getFragments(e)) .build()); } @@ -68,6 +69,7 @@ private Optional getMutationDetails(ExecutableElement e, TypeMapp .maxDepth(annotation.maxDepth()) .parameters(getParameters(e, typeMapper, annotation.value())) .selection(Arrays.asList(annotation.select())) + .fragments(getFragments(e)) .build()); } @@ -83,10 +85,16 @@ private Optional getSubscriptionDetails(ExecutableElement e, Type .maxDepth(annotation.maxDepth()) .parameters(getParameters(e, typeMapper, annotation.value())) .selection(Arrays.asList(annotation.select())) + .fragments(getFragments(e)) .build() ); } + private List getFragments(ExecutableElement e) { + final GraphQLFragment[] fragments = e.getAnnotationsByType(GraphQLFragment.class); + return Arrays.asList(fragments); + } + private List getParameters(ExecutableElement e, TypeMapper typeMapper, String root) { return e.getParameters() .stream() diff --git a/graphql-java-client-processor/src/test/groovy/com/jacobmountain/graphql/client/query/QueryGeneratorSpec.groovy b/graphql-java-client-processor/src/test/groovy/com/jacobmountain/graphql/client/query/QueryGeneratorSpec.groovy index 7d33b0b..19fd297 100644 --- a/graphql-java-client-processor/src/test/groovy/com/jacobmountain/graphql/client/query/QueryGeneratorSpec.groovy +++ b/graphql-java-client-processor/src/test/groovy/com/jacobmountain/graphql/client/query/QueryGeneratorSpec.groovy @@ -156,7 +156,7 @@ class QueryGeneratorSpec extends Specification { } """) when: - def result = generator.query().maxDepth(3).build(null, "friend", [] as Set) + def result = generator.query().maxDepth(4).build(null, "friend", [] as Set) then: assertQueriesAreEqual(""" @@ -170,6 +170,11 @@ class QueryGeneratorSpec extends Specification { friends { id name + friends { + id + name + __typename + } __typename } __typename diff --git a/graphql-java-client-processor/src/test/groovy/com/jacobmountain/query/QueryGeneratorSpec.groovy b/graphql-java-client-processor/src/test/groovy/com/jacobmountain/query/QueryGeneratorSpec.groovy new file mode 100644 index 0000000..b088d42 --- /dev/null +++ b/graphql-java-client-processor/src/test/groovy/com/jacobmountain/query/QueryGeneratorSpec.groovy @@ -0,0 +1,89 @@ +package com.jacobmountain.query + +import com.jacobmountain.graphql.client.query.QueryGenerator +import com.jacobmountain.graphql.client.query.selectors.Fragment +import com.jacobmountain.graphql.client.utils.Schema +import spock.lang.Specification + +class QueryGeneratorSpec extends Specification { + + static QueryGenerator givenQuery(String query, String types) { + new QueryGenerator(new Schema(""" + schema { + query: Query + } + type Query { + $query + } + $types + """)) + } + + def "I can generate a query with a fragment"() { + given: + def generator = givenQuery("hero: Hero",""" + type Hero { + id: String + name: String + } + """) + + when: + def query = generator.query() + .fragments([new Fragment("Hero", "HeroSummary", [] as Set)]) + .build(null, "hero", [] as Set) + + then: + query == "query Hero { hero { ...HeroSummary __typename } } fragment HeroSummary on HeroSummary { id name __typename }" + } + + def "I can generate a complex query with a fragment"() { + given: + def generator = givenQuery("hero: Hero",""" + type Hero { + id: String + name: String + ships: [Starship!]! + } + type Starship { + id: String! + name: String! + length(unit: LengthUnit = METER): Float + coordinates: [[Float!]!] + } + enum LengthUnit { + METER + FOOT + } + """) + + when: + def query = generator.query() + .fragments([new Fragment("Hero", "HeroSummary", [] as Set)]) + .build(null, "hero", [] as Set) + + then: + query == "query Hero { hero { ...HeroSummary __typename } } fragment HeroSummary on HeroSummary { id name ships { id name length coordinates __typename } __typename }" + } + + def "I can generate a recursive query with a fragment"() { + given: + def generator = givenQuery("hero: Hero",""" + type Hero { + id: String + name: String + friends: [Hero!]! + } + """) + + when: + def query = generator.query() + .fragments([new Fragment("Hero", "HeroSummary", [] as Set)]) + .maxDepth(3) + .build(null, "hero", [] as Set) + + then: + query == "query Hero { hero { ...HeroSummary __typename } } fragment HeroSummary on HeroSummary { id name friends { id name } __typename }" + } + +} diff --git a/graphql-java-client-processor/src/test/groovy/com/jacobmountain/query/filters/MaxDepthFieldFilterSpec.groovy b/graphql-java-client-processor/src/test/groovy/com/jacobmountain/query/filters/MaxDepthFieldFilterSpec.groovy index 39344d5..9c00097 100644 --- a/graphql-java-client-processor/src/test/groovy/com/jacobmountain/query/filters/MaxDepthFieldFilterSpec.groovy +++ b/graphql-java-client-processor/src/test/groovy/com/jacobmountain/query/filters/MaxDepthFieldFilterSpec.groovy @@ -1,6 +1,6 @@ package com.jacobmountain.query.filters -import com.jacobmountain.graphql.client.query.FieldFilter +import com.jacobmountain.graphql.client.query.filters.FieldFilter import com.jacobmountain.graphql.client.query.QueryContext import com.jacobmountain.graphql.client.query.filters.MaxDepthFieldFilter import spock.lang.Specification diff --git a/graphql-java-client-processor/src/test/groovy/com/jacobmountain/query/filters/SelectionFieldFilterSpec.groovy b/graphql-java-client-processor/src/test/groovy/com/jacobmountain/query/filters/SelectionFieldFilterSpec.groovy index e0ae282..f9b94e6 100644 --- a/graphql-java-client-processor/src/test/groovy/com/jacobmountain/query/filters/SelectionFieldFilterSpec.groovy +++ b/graphql-java-client-processor/src/test/groovy/com/jacobmountain/query/filters/SelectionFieldFilterSpec.groovy @@ -1,6 +1,6 @@ package com.jacobmountain.query.filters -import com.jacobmountain.graphql.client.query.FieldFilter +import com.jacobmountain.graphql.client.query.filters.FieldFilter import com.jacobmountain.graphql.client.query.QueryContext import com.jacobmountain.graphql.client.query.filters.SelectionFieldFilter import com.jacobmountain.graphql.client.utils.Schema