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