diff --git a/Rakefile b/Rakefile index bdb054b..a9b8b28 100644 --- a/Rakefile +++ b/Rakefile @@ -25,6 +25,12 @@ task :generate do deserialize_expr: ->(expr) { "LocalDateTime.parse(jsonAsString(#{expr}, key))" }, imports: ['java.time.LocalDateTime'], ) + ], + custom_annotations: [ + GraphQLJavaGen::Annotation.new( + 'Nullable', + imports: ['com.shopify.graphql.support.Nullable'] + ) { |field| !field.type.non_null? }, ] ).save('support/src/test/java/com/shopify/graphql/support/Generated.java') diff --git a/codegen/lib/graphql_java_gen.rb b/codegen/lib/graphql_java_gen.rb index 59c9e23..b814aff 100644 --- a/codegen/lib/graphql_java_gen.rb +++ b/codegen/lib/graphql_java_gen.rb @@ -253,10 +253,17 @@ def java_implements(type) "implements #{interfaces.to_a.join(', ')} " end - def java_annotations(field) - @annotations.map do |annotation| + def java_annotations(field, in_argument: false) + annotations = @annotations.map do |annotation| "@#{annotation.name}" if annotation.annotate?(field) - end.compact.join("\n") + end.compact + return "" unless annotations.any? + + if in_argument + annotations.join(" ") + " " + else + annotations.join("\n") + end end def type_names_set diff --git a/codegen/lib/graphql_java_gen/templates/APISchema.java.erb b/codegen/lib/graphql_java_gen/templates/APISchema.java.erb index 945bfac..5357f70 100644 --- a/codegen/lib/graphql_java_gen/templates/APISchema.java.erb +++ b/codegen/lib/graphql_java_gen/templates/APISchema.java.erb @@ -237,7 +237,7 @@ public class <%= schema_name %> { <% fields.each do |field| %> <%= java_doc(field) %> - <%= java_annotations(field) -%> + <%= java_annotations(field) %> public <%= java_output_type(field.type) %> get<%= field.classify_name %>() { return (<%= java_output_type(field.type) %>) get("<%= field.name %>"); } @@ -265,6 +265,7 @@ public class <%= schema_name %> { <% end %> <% type.optional_input_fields.each do |field| %> private <%= java_input_type(field.type) %> <%= escape_reserved_word(field.camelize_name) %>; + private boolean <%= field.camelize_name %>Seen = false; <% end %> <% unless type.required_input_fields.empty? %> @@ -276,22 +277,25 @@ public class <%= schema_name %> { <% end %> <% type.required_input_fields.each do |field| %> + <%= java_annotations(field) %> public <%= java_input_type(field.type) %> get<%= field.classify_name %>() { return <%= escape_reserved_word(field.camelize_name) %>; } - public <%= type.name %> set<%= field.classify_name %>(<%= java_input_type(field.type) %> <%= escape_reserved_word(field.camelize_name) %>) { + public <%= type.name %> set<%= field.classify_name %>(<%= java_annotations(field, in_argument: true) %><%= java_input_type(field.type) %> <%= escape_reserved_word(field.camelize_name) %>) { this.<%= escape_reserved_word(field.camelize_name) %> = <%= escape_reserved_word(field.camelize_name) %>; return this; } <% end %> <% type.optional_input_fields.each do |field| %> + <%= java_annotations(field) %> public <%= java_input_type(field.type) %> get<%= field.classify_name %>() { return <%= escape_reserved_word(field.camelize_name) %>; } - public <%= type.name %> set<%= field.classify_name %>(<%= java_input_type(field.type) %> <%= escape_reserved_word(field.camelize_name) %>) { + public <%= type.name %> set<%= field.classify_name %>(<%= java_annotations(field, in_argument: true) %><%= java_input_type(field.type) %> <%= escape_reserved_word(field.camelize_name) %>) { this.<%= escape_reserved_word(field.camelize_name) %> = <%= escape_reserved_word(field.camelize_name) %>; + this.<%= field.camelize_name %>Seen = true; return this; } <% end %> @@ -306,11 +310,15 @@ public class <%= schema_name %> { <%= generate_build_input_code(escape_reserved_word(field.camelize_name), field.type) %> <% end %> <% type.optional_input_fields.each do |field| %> - if (<%= escape_reserved_word(field.camelize_name) %> != null) { + if (this.<%= field.camelize_name %>Seen) { _queryBuilder.append(separator); separator = ","; _queryBuilder.append("<%= field.name %>:"); - <%= generate_build_input_code(escape_reserved_word(field.camelize_name), field.type) %> + if (<%= escape_reserved_word(field.camelize_name) %> != null) { + <%= generate_build_input_code(escape_reserved_word(field.camelize_name), field.type) %> + } else { + _queryBuilder.append("null"); + } } <% end %> _queryBuilder.append('}'); diff --git a/codegen/test/support/schema.rb b/codegen/test/support/schema.rb index 21d3e2c..2efd2c6 100644 --- a/codegen/test/support/schema.rb +++ b/codegen/test/support/schema.rb @@ -91,6 +91,7 @@ module Schema argument :value, !types.Int argument :ttl, TimeType argument :negate, types.Boolean, default_value: false + argument :api_client, types.String end MutationType = GraphQL::ObjectType.define do diff --git a/support/src/main/java/com/shopify/graphql/support/Nullable.java b/support/src/main/java/com/shopify/graphql/support/Nullable.java new file mode 100644 index 0000000..f1b7b04 --- /dev/null +++ b/support/src/main/java/com/shopify/graphql/support/Nullable.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.shopify.graphql.support; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PACKAGE; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Denotes that a parameter, field or method return value can be null. + *

+ * When decorating a method call parameter, this denotes that the parameter can + * legitimately be null and the method will gracefully deal with it. Typically + * used on optional parameters. + *

+ * When decorating a method, this denotes the method might legitimately return + * null. + *

+ * This is a marker annotation and it has no specific attributes. + */ +@Documented +@Retention(RUNTIME) +@Target({METHOD, PARAMETER, FIELD, ANNOTATION_TYPE, PACKAGE}) +public @interface Nullable { +} + diff --git a/support/src/test/java/com/shopify/graphql/support/AnnotationTest.java b/support/src/test/java/com/shopify/graphql/support/AnnotationTest.java new file mode 100644 index 0000000..1b77f6f --- /dev/null +++ b/support/src/test/java/com/shopify/graphql/support/AnnotationTest.java @@ -0,0 +1,28 @@ +package com.shopify.graphql.support; + +import org.junit.Test; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import static junit.framework.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.shopify.graphql.support.Generated; + +public class AnnotationTest { + @Test + public void testAppliesAnnotation() throws Exception { + Class obj = Generated.class; + boolean foundNullable = false; + for (Class klass: obj.getDeclaredClasses()) { + for (Method method : klass.getDeclaredMethods()) { + if (method.isAnnotationPresent(Nullable.class)) { + foundNullable = true; + break; + } + } + } + assertTrue("Should have found a class with @Nullable annotation in Generated.java", foundNullable); + } +} diff --git a/support/src/test/java/com/shopify/graphql/support/Generated.java b/support/src/test/java/com/shopify/graphql/support/Generated.java index 726b41b..3ae9cf7 100644 --- a/support/src/test/java/com/shopify/graphql/support/Generated.java +++ b/support/src/test/java/com/shopify/graphql/support/Generated.java @@ -15,6 +15,8 @@ import com.shopify.graphql.support.ID; +import com.shopify.graphql.support.Nullable; + import java.time.LocalDateTime; import java.io.Serializable; @@ -216,6 +218,7 @@ public UnknownEntry setKey(String arg) { return this; } + @Nullable public LocalDateTime getTtl() { return (LocalDateTime) get("ttl"); } @@ -398,6 +401,7 @@ public IntegerEntry setKey(String arg) { return this; } + @Nullable public LocalDateTime getTtl() { return (LocalDateTime) get("ttl"); } @@ -429,6 +433,9 @@ public boolean unwrapsToObject(String key) { } } + /** + * Types of values that can be stored in a key + */ public enum KeyType { INTEGER, @@ -676,6 +683,9 @@ public QueryRootQuery entries(int first, EntriesArgumentsDefinition argsDef, Ent return this; } + /** + * Get an entry of any type with the given key + */ public QueryRootQuery entry(String key, EntryQueryDefinition queryDef) { startField("entry"); @@ -691,6 +701,9 @@ public QueryRootQuery entry(String key, EntryQueryDefinition queryDef) { return this; } + /** + * Get an entry of any type with the given key as a union + */ public QueryRootQuery entryUnion(String key, EntryUnionQueryDefinition queryDef) { startField("entry_union"); @@ -706,6 +719,9 @@ public QueryRootQuery entryUnion(String key, EntryUnionQueryDefinition queryDef) return this; } + /** + * Get a integer value with the given key + */ public QueryRootQuery integer(String key) { startField("integer"); @@ -760,6 +776,9 @@ public QueryRootQuery keys(int first, KeysArgumentsDefinition argsDef) { return this; } + /** + * Get a string value with the given key + */ public QueryRootQuery string(String key) { startField("string"); @@ -936,6 +955,10 @@ public QueryRoot setEntries(List arg) { return this; } + /** + * Get an entry of any type with the given key + */ + @Nullable public Entry getEntry() { return (Entry) get("entry"); } @@ -945,6 +968,10 @@ public QueryRoot setEntry(Entry arg) { return this; } + /** + * Get an entry of any type with the given key as a union + */ + @Nullable public EntryUnion getEntryUnion() { return (EntryUnion) get("entry_union"); } @@ -954,6 +981,10 @@ public QueryRoot setEntryUnion(EntryUnion arg) { return this; } + /** + * Get a integer value with the given key + */ + @Nullable public Integer getInteger() { return (Integer) get("integer"); } @@ -972,6 +1003,10 @@ public QueryRoot setKeys(List arg) { return this; } + /** + * Get a string value with the given key + */ + @Nullable public String getString() { return (String) get("string"); } @@ -981,6 +1016,7 @@ public QueryRoot setString(String arg) { return this; } + @Nullable public LocalDateTime getTtl() { return (LocalDateTime) get("ttl"); } @@ -990,6 +1026,7 @@ public QueryRoot setTtl(LocalDateTime arg) { return this; } + @Nullable public KeyType getType() { return (KeyType) get("type"); } @@ -999,6 +1036,7 @@ public QueryRoot setType(KeyType arg) { return this; } + @Nullable public String getVersion() { return (String) get("version"); } @@ -1039,8 +1077,13 @@ public static class SetIntegerInput implements Serializable { private int value; private LocalDateTime ttl; + private boolean ttlSeen = false; private Boolean negate; + private boolean negateSeen = false; + + private String apiClient; + private boolean apiClientSeen = false; public SetIntegerInput(String key, int value) { this.key = key; @@ -1066,21 +1109,36 @@ public SetIntegerInput setValue(int value) { return this; } + @Nullable public LocalDateTime getTtl() { return ttl; } - public SetIntegerInput setTtl(LocalDateTime ttl) { + public SetIntegerInput setTtl(@Nullable LocalDateTime ttl) { this.ttl = ttl; + this.ttlSeen = true; return this; } + @Nullable public Boolean getNegate() { return negate; } - public SetIntegerInput setNegate(Boolean negate) { + public SetIntegerInput setNegate(@Nullable Boolean negate) { this.negate = negate; + this.negateSeen = true; + return this; + } + + @Nullable + public String getApiClient() { + return apiClient; + } + + public SetIntegerInput setApiClient(@Nullable String apiClient) { + this.apiClient = apiClient; + this.apiClientSeen = true; return this; } @@ -1098,18 +1156,37 @@ public void appendTo(StringBuilder _queryBuilder) { _queryBuilder.append("value:"); _queryBuilder.append(value); - if (ttl != null) { + if (this.ttlSeen) { _queryBuilder.append(separator); separator = ","; _queryBuilder.append("ttl:"); - Query.appendQuotedString(_queryBuilder, ttl.toString()); + if (ttl != null) { + Query.appendQuotedString(_queryBuilder, ttl.toString()); + } else { + _queryBuilder.append("null"); + } } - if (negate != null) { + if (this.negateSeen) { _queryBuilder.append(separator); separator = ","; _queryBuilder.append("negate:"); - _queryBuilder.append(negate); + if (negate != null) { + _queryBuilder.append(negate); + } else { + _queryBuilder.append("null"); + } + } + + if (this.apiClientSeen) { + _queryBuilder.append(separator); + separator = ","; + _queryBuilder.append("api_client:"); + if (apiClient != null) { + Query.appendQuotedString(_queryBuilder, apiClient.toString()); + } else { + _queryBuilder.append("null"); + } } _queryBuilder.append('}'); @@ -1200,6 +1277,7 @@ public StringEntry setKey(String arg) { return this; } + @Nullable public LocalDateTime getTtl() { return (LocalDateTime) get("ttl"); } diff --git a/support/src/test/java/com/shopify/graphql/support/IntegrationTest.java b/support/src/test/java/com/shopify/graphql/support/IntegrationTest.java index 3115cab..0ea10d7 100644 --- a/support/src/test/java/com/shopify/graphql/support/IntegrationTest.java +++ b/support/src/test/java/com/shopify/graphql/support/IntegrationTest.java @@ -156,4 +156,12 @@ public void testMutationResponse() throws Exception { Generated.Mutation data = Generated.MutationResponse.fromJson(json).getData(); assertEquals(true, data.getSetString().booleanValue()); } + + @Test + public void testOptionalFieldOnInput() throws Exception { + String queryString = Generated.mutation(mutation -> mutation + .setInteger(new Generated.SetIntegerInput("answer", 42).setTtl(null)) + ).toString(); + assertEquals("mutation{set_integer(input:{key:\"answer\",value:42,ttl:null})}", queryString); + } }