Skip to content

Commit

Permalink
Introduce InputType for optional values with undefined state
Browse files Browse the repository at this point in the history
Introduce wrapper class InputType for optional values that can be in 3 states: some value, null and undefined.
Update generation of input object types to utilize new InputType wrapper and don't serialize optional value if it's undefined.

Closes #652
  • Loading branch information
sav007 committed Sep 14, 2017
1 parent ad12885 commit 8482d6c
Show file tree
Hide file tree
Showing 15 changed files with 411 additions and 272 deletions.
21 changes: 21 additions & 0 deletions apollo-api/src/main/java/com/apollographql/apollo/api/Input.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.apollographql.apollo.api;

import javax.annotation.Nullable;

public final class Input<V> {
public final V value;
public final boolean defined;

private Input(V value, boolean defined) {
this.value = value;
this.defined = defined;
}

public static <V> Input<V> fromNullable(@Nullable V value) {
return new Input<>(value, true);
}

public static <V> Input<V> absent() {
return new Input<>(null, false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,44 +21,47 @@ class BuilderTypeSpecBuilder(
.build()
}

private fun TypeSpec.Builder.addBuilderFields(): TypeSpec.Builder =
addFields(fields.map {
val fieldName = it.first
val fieldType = it.second
val defaultValue = fieldDefaultValues[fieldName]?.let {
(it as? Number)?.castTo(fieldType.withoutAnnotations()) ?: it
}
val initializerCode = defaultValue?.let {
if (fieldType.isEnum(typeDeclarations))
CodeBlock.of("\$T.\$L", fieldType.withoutAnnotations(), defaultValue)
else
CodeBlock.of("\$L", defaultValue)
} ?: CodeBlock.of("")
FieldSpec.builder(fieldType, fieldName)
.addModifiers(Modifier.PRIVATE)
.initializer(initializerCode)
.build()
})
private fun TypeSpec.Builder.addBuilderFields(): TypeSpec.Builder {
return addFields(fields.map {
val fieldName = it.first
val fieldType = it.second
val initializerCode = fieldDefaultValues[fieldName]
?.let { (it as? Number)?.castTo(fieldType.unwrapOptionalType(true)) ?: it }
?.let { defaultValue ->
if (fieldType.unwrapOptionalType(true).isEnum(typeDeclarations))
CodeBlock.of("\$T.\$L", fieldType.unwrapOptionalType(true), defaultValue)
else
CodeBlock.of("\$L", defaultValue)
}
?.let { fieldType.wrapOptionalValue(it) }
?: fieldType.defaultOptionalValue()
FieldSpec.builder(fieldType, fieldName)
.addModifiers(Modifier.PRIVATE)
.initializer(initializerCode)
.build()
})
}

private fun TypeSpec.Builder.addBuilderMethods(): TypeSpec.Builder =
addMethods(fields.map {
val fieldName = it.first
val fieldType = it.second
val javaDoc = fieldJavaDocs[fieldName]
MethodSpec.methodBuilder(fieldName)
.addModifiers(Modifier.PUBLIC)
.addParameter(ParameterSpec.builder(fieldType, fieldName).build())
.let {
if (!javaDoc.isNullOrBlank())
it.addJavadoc(CodeBlock.of("\$L\n", javaDoc))
else
it
}
.returns(builderClass)
.addStatement("this.\$L = \$L", fieldName, fieldName)
.addStatement("return this")
.build()
})
private fun TypeSpec.Builder.addBuilderMethods(): TypeSpec.Builder {
return addMethods(fields.map {
val fieldName = it.first
val fieldType = it.second
val javaDoc = fieldJavaDocs[fieldName]
MethodSpec.methodBuilder(fieldName)
.addModifiers(Modifier.PUBLIC)
.addParameter(ParameterSpec.builder(fieldType.unwrapOptionalType(), fieldName).build())
.let {
if (!javaDoc.isNullOrBlank())
it.addJavadoc(CodeBlock.of("\$L\n", javaDoc))
else
it
}
.returns(builderClass)
.addStatement("this.\$L = \$L", fieldName, fieldType.wrapOptionalValue(CodeBlock.of("\$L", fieldName)))
.addStatement("return this")
.build()
})
}

private fun TypeSpec.Builder.addBuilderBuildMethod(): TypeSpec.Builder {
val validationCodeBuilder = fields.filter {
Expand All @@ -83,24 +86,27 @@ class BuilderTypeSpecBuilder(
val CLASS_NAME: String = "Builder"
private val builderClass = ClassName.get("", CLASS_NAME)

fun builderFactoryMethod(): MethodSpec =
MethodSpec
.methodBuilder("builder")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(builderClass)
.addStatement("return new \$T()", builderClass)
.build()
fun builderFactoryMethod(): MethodSpec {
return MethodSpec
.methodBuilder("builder")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(builderClass)
.addStatement("return new \$T()", builderClass)
.build()
}

private fun Number.castTo(type: TypeName) =
if (type == TypeName.INT || type == TypeName.INT.box()) {
toInt()
} else if (type == TypeName.FLOAT || type == TypeName.FLOAT.box()) {
toDouble()
} else {
this
}
private fun Number.castTo(type: TypeName): Number {
return if (type == TypeName.INT || type == TypeName.INT.box()) {
toInt()
} else if (type == TypeName.FLOAT || type == TypeName.FLOAT.box()) {
toDouble()
} else {
this
}
}

private fun TypeName.isEnum(typeDeclarations: List<TypeDeclaration>) =
((this is ClassName) && typeDeclarations.count { it.kind == "EnumType" && it.name == simpleName() } > 0)
private fun TypeName.isEnum(typeDeclarations: List<TypeDeclaration>): Boolean {
return ((this is ClassName) && typeDeclarations.count { it.kind == "EnumType" && it.name == simpleName() } > 0)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package com.apollographql.apollo.compiler

import com.apollographql.apollo.api.GraphqlFragment
import com.apollographql.apollo.api.Mutation
import com.apollographql.apollo.api.Operation
import com.apollographql.apollo.api.Query
import com.apollographql.apollo.api.*
import com.apollographql.apollo.api.internal.Optional
import com.apollographql.apollo.api.internal.UnmodifiableMapBuilder
import com.apollographql.apollo.api.internal.Utils
Expand All @@ -29,6 +26,7 @@ object ClassNames {
val JAVA_OPTIONAL: ClassName = ClassName.get("java.util", "Optional")
val API_UTILS: ClassName = ClassName.get(Utils::class.java)
val FRAGMENT: ClassName = ClassName.get(GraphqlFragment::class.java)
val INPUT_TYPE: ClassName = ClassName.get(Input::class.java)

fun <K : Any> parameterizedListOf(type: Class<K>): TypeName =
ParameterizedTypeName.get(LIST, ClassName.get(type))
Expand All @@ -55,13 +53,13 @@ object ClassNames {
fun parameterizedOptional(type: TypeName): TypeName =
ParameterizedTypeName.get(OPTIONAL, type)

fun <K : Any> parameterizedGuavaOptional(type: Class<K>): TypeName =
ParameterizedTypeName.get(GUAVA_OPTIONAL, ClassName.get(type))

fun parameterizedGuavaOptional(type: TypeName): TypeName =
ParameterizedTypeName.get(GUAVA_OPTIONAL, type)

fun parameterizedJavaOptional(type: TypeName): TypeName =
ParameterizedTypeName.get(JAVA_OPTIONAL, type)

fun parameterizedInputType(type: TypeName): TypeName =
ParameterizedTypeName.get(INPUT_TYPE, type)

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,21 @@ class InputFieldSpec(
Type.SCALAR_LIST -> writeScalarList(writerParam)
Type.CUSTOM_LIST -> writeCustomList(writerParam)
Type.OBJECT_LIST -> writeObjectList(writerParam, marshaller)
}.let {
if (javaType.isOptional()) {
CodeBlock.builder()
.beginControlFlow("if (\$L.defined)", name)
.add(it)
.endControlFlow()
.build()
} else {
it
}
}
}

private fun writeScalarCode(writerParam: CodeBlock): CodeBlock {
val valueCode = javaType.unwrapOptionalValue(name)
val valueCode = javaType.unwrapOptionalValue(varName = name, checkIfPresent = false)
return CodeBlock.of("\$L.\$L(\$S, \$L);\n", writerParam, WRITE_METHODS[type], name, valueCode)
}

Expand All @@ -59,7 +69,7 @@ class InputFieldSpec(
}

private fun writeScalarList(writerParam: CodeBlock): CodeBlock {
val rawFieldType = with(javaType) { if (isList()) listParamType() else this }
val rawFieldType = with(javaType.unwrapOptionalType(true)) { if (isList()) listParamType() else this }
val writeMethod = SCALAR_LIST_ITEM_WRITE_METHODS[rawFieldType] ?: "writeString"
val writeStatement = CodeBlock.builder()
.beginControlFlow("for (\$T \$L : \$L)", rawFieldType, "\$item",
Expand Down Expand Up @@ -90,7 +100,7 @@ class InputFieldSpec(
}

private fun writeCustomList(writerParam: CodeBlock): CodeBlock {
val rawFieldType = javaType.let { if (it.isList()) it.listParamType() else it }
val rawFieldType = with(javaType.unwrapOptionalType(true)) { if (isList()) listParamType() else this }
val customScalarEnum = CustomEnumTypeSpecBuilder.className(context)
val customScalarEnumConst = normalizeGraphQlType(graphQLType).toUpperCase(Locale.ENGLISH)
val writeStatement = CodeBlock.builder()
Expand Down Expand Up @@ -118,7 +128,7 @@ class InputFieldSpec(
}

private fun writeObjectList(writerParam: CodeBlock, marshaller: CodeBlock): CodeBlock {
val rawFieldType = with(javaType) { if (isList()) listParamType() else this }
val rawFieldType = with(javaType.unwrapOptionalType(true)) { if (isList()) listParamType() else this }
val writeStatement = CodeBlock.builder()
.beginControlFlow("for (\$T \$L : \$L)", rawFieldType, "\$item",
javaType.unwrapOptionalValue(name, false))
Expand Down Expand Up @@ -170,10 +180,11 @@ class InputFieldSpec(
private val LIST_ITEM_WRITER_PARAM =
ParameterSpec.builder(InputFieldWriter.ListItemWriter::class.java, "listItemWriter").build()

fun build(name: String, graphQLType: String, context: CodeGenerationContext): InputFieldSpec {
fun build(name: String, graphQLType: String, context: CodeGenerationContext,
nullableValueType: NullableValueType = NullableValueType.INPUT_TYPE): InputFieldSpec {
val javaType = JavaTypeResolver(context = context, packageName = "")
.resolve(typeName = graphQLType, nullableValueType = NullableValueType.ANNOTATED)
val normalizedJavaType = javaType.withoutAnnotations()
.resolve(typeName = graphQLType, nullableValueType = nullableValueType)
val normalizedJavaType = javaType.unwrapOptionalType(true)
val type = when {
normalizedJavaType.isList() -> {
val rawFieldType = normalizedJavaType.let { if (it.isList()) it.listParamType() else it }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,25 +40,6 @@ class InputTypeSpecBuilder(
)
}

private fun TypeSpec.Builder.addFieldDefinition(field: TypeDeclarationField): TypeSpec.Builder =
addField(FieldSpec
.builder(field.javaTypeName(context), field.name.decapitalize())
.addModifiers(Modifier.PRIVATE, Modifier.FINAL)
.build())

private fun TypeSpec.Builder.addFieldAccessor(field: TypeDeclarationField) =
addMethod(MethodSpec.methodBuilder(field.name.decapitalize())
.addModifiers(Modifier.PUBLIC)
.returns(field.javaTypeName(context))
.let {
if (!field.description.isNullOrBlank())
it.addJavadoc(CodeBlock.of("\$L\n", field.description))
else
it
}
.addStatement("return this.\$L", field.name.decapitalize())
.build())

private fun TypeSpec.Builder.addBuilder(): TypeSpec.Builder {
if (fields.isEmpty()) {
return this
Expand All @@ -83,7 +64,13 @@ class InputTypeSpecBuilder(

private fun marshallerMethodSpec(): MethodSpec {
val writeCode = fields
.map { InputFieldSpec.build(name = it.name, graphQLType = it.type, context = context) }
.map {
InputFieldSpec.build(
name = it.name,
graphQLType = it.type,
context = context
)
}
.map {
it.writeValueCode(
writerParam = CodeBlock.of("\$L", WRITER_PARAM.name),
Expand Down Expand Up @@ -111,17 +98,43 @@ class InputTypeSpecBuilder(
}

private fun TypeSpec.Builder.addFields(): TypeSpec.Builder {
fun addFieldDefinition(field: TypeDeclarationField) {
addField(FieldSpec
.builder(field.javaTypeName(context), field.name.decapitalize())
.addModifiers(Modifier.PRIVATE, Modifier.FINAL)
.build())
}

fun addFieldAccessor(field: TypeDeclarationField) {
val optional = !field.type.endsWith("!")
addMethod(MethodSpec.methodBuilder(field.name.decapitalize())
.addModifiers(Modifier.PUBLIC)
.returns(field.javaTypeName(context).unwrapOptionalType())
.let {
if (!field.description.isNullOrBlank())
it.addJavadoc(CodeBlock.of("\$L\n", field.description))
else
it
}
.addStatement("return this.\$L\$L", field.name.decapitalize(), if (optional) ".value" else "")
.build())
}

fields.forEach { field ->
addFieldDefinition(field)
addFieldAccessor(field)
}

return this
}

private fun TypeDeclarationField.javaTypeName(context: CodeGenerationContext): TypeName {
return JavaTypeResolver(context, context.typesPackage)
.resolve(typeName = type, nullableValueType = NullableValueType.INPUT_TYPE)
}

companion object {
private val WRITER_PARAM = ParameterSpec.builder(InputFieldWriter::class.java, "writer").build()
private const val MARSHALLER_PARAM_NAME = "marshaller"
private fun TypeDeclarationField.javaTypeName(context: CodeGenerationContext) =
JavaTypeResolver(context, context.typesPackage).resolve(type, !type.endsWith("!")).unwrapOptionalType()
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.apollographql.apollo.compiler

import com.apollographql.apollo.compiler.ClassNames.parameterizedGuavaOptional
import com.apollographql.apollo.compiler.ClassNames.parameterizedInputType
import com.apollographql.apollo.compiler.ClassNames.parameterizedJavaOptional
import com.apollographql.apollo.compiler.ClassNames.parameterizedOptional
import com.apollographql.apollo.compiler.ir.CodeGenerationContext
Expand Down Expand Up @@ -35,6 +36,7 @@ class JavaTypeResolver(
NullableValueType.APOLLO_OPTIONAL -> parameterizedOptional(javaType)
NullableValueType.GUAVA_OPTIONAL -> parameterizedGuavaOptional(javaType)
NullableValueType.JAVA_OPTIONAL -> parameterizedJavaOptional(javaType)
NullableValueType.INPUT_TYPE -> parameterizedInputType(javaType)
else -> javaType.annotated(Annotations.NULLABLE)
}.let {
if (deprecated) it.annotated(Annotations.DEPRECATED) else it
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ enum class NullableValueType(val value: String) {
ANNOTATED("annotated"),
APOLLO_OPTIONAL("apolloOptional"),
GUAVA_OPTIONAL("guavaOptional"),
JAVA_OPTIONAL("javaOptional");
JAVA_OPTIONAL("javaOptional"),
INPUT_TYPE("inputType");

companion object {
fun findByValue(value: String): NullableValueType? = NullableValueType.values().find { it.value == value }
Expand Down
Loading

0 comments on commit 8482d6c

Please sign in to comment.