Skip to content

Commit

Permalink
Make generateModelBuilder generate deep builders (instead of shallow)
Browse files Browse the repository at this point in the history
For each builder generate additional setter with `Consumer<*.Builder>` as an argument:

```
public Builder hero(@nonnull Consumer<Hero.Builder> mutator) {
  Hero.Builder builder = this.hero != null ? this.hero.toBuilder() : Hero.builder();

  mutator.accept(builder);

  this.hero = builder.build();
  return this;
}
```

That will allow user to do next:
```
data.toBuilder()
  .hero(someHero)
  // or
  .hero(new Consumer<>() {
    void accept(Hero.Builder builder) {
      builder.name("Joe")
       .age(31)
    }
  })
```

This will be even better with lambda support:
```
data.toBuilder()
  .hero(someHero)
  // or
  .hero(builder -> builder
       .name("Joe")
       .age(31)
    }
  })
```

Closes #643
  • Loading branch information
sav007 committed Oct 5, 2017
1 parent 64a6f81 commit 34e3c18
Show file tree
Hide file tree
Showing 9 changed files with 273 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.apollographql.apollo.api.internal;

/**
* Represents an operation that accepts a single input argument and returns no result. Unlike most other functional
* interfaces, {@code Consumer} is expected to operate via side-effects.
*
* <p>This is a <a href="package-summary.html">functional interface</a> whose functional method is {@link
* #accept(Object)}.
*
* @param <T> the type of the input to the operation
*/
public interface Consumer<T> {

/**
* Performs this operation on the given argument.
*
* @param t the input argument
*/
void accept(T t);

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.apollographql.apollo.compiler

import com.apollographql.apollo.compiler.ir.Fragment
import com.apollographql.apollo.compiler.ir.TypeDeclaration
import com.squareup.javapoet.*
import java.util.*
Expand All @@ -10,19 +11,21 @@ class BuilderTypeSpecBuilder(
val fields: List<Pair<String, TypeName>>,
val fieldDefaultValues: Map<String, Any?>,
val fieldJavaDocs: Map<String, String>,
val typeDeclarations: List<TypeDeclaration>
val typeDeclarations: List<TypeDeclaration>,
val buildableTypes: List<TypeName> = emptyList()
) {
fun build(): TypeSpec {
return TypeSpec.classBuilder(builderClass)
return TypeSpec.classBuilder(ClassNames.BUILDER.simpleName())
.addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
.addBuilderFields()
.apply { addFields(builderFields()) }
.addMethod(MethodSpec.constructorBuilder().build())
.addBuilderMethods()
.addBuilderBuildMethod()
.apply { addMethods(setFieldMethods()) }
.apply { addMethods(setFieldWithMutatorMethods()) }
.addMethod(buildMethod())
.build()
}

private fun TypeSpec.Builder.addBuilderFields(): TypeSpec.Builder {
private fun builderFields(): List<FieldSpec> {
fun valueCode(value: Any, type: TypeName): CodeBlock = when {
value is Number -> CodeBlock.of("\$L", value.castTo(type))
type.isEnum(typeDeclarations) -> CodeBlock.of("\$T.\$L", type, value)
Expand All @@ -41,7 +44,7 @@ class BuilderTypeSpecBuilder(
.build()
}

return addFields(fields.map { (fieldName, fieldType) ->
return fields.map { (fieldName, fieldType) ->
val rawFieldType = fieldType.unwrapOptionalType(true).let {
if (it.isList()) it.listParamType() else it
}
Expand All @@ -59,55 +62,134 @@ class BuilderTypeSpecBuilder(
.addModifiers(Modifier.PRIVATE)
.initializer(initializer ?: fieldType.defaultOptionalValue())
.build()
})
}
}

private fun TypeSpec.Builder.addBuilderMethods(): TypeSpec.Builder {
return addMethods(fields.map { (fieldName, fieldType) ->
private fun setFieldMethods(): List<MethodSpec> {
return fields.map { (fieldName, fieldType) ->
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
setFieldMethod(fieldName, fieldType, javaDoc)
}
}

private fun setFieldMethod(fieldName: String, fieldType: TypeName, javaDoc: String?): MethodSpec {
return 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(ClassNames.BUILDER)
.addStatement("this.\$L = \$L", fieldName, fieldType.wrapOptionalValue(CodeBlock.of("\$L", fieldName)))
.addStatement("return this")
.build()
}

private fun setFieldWithMutatorMethods(): List<MethodSpec> {
return fields
.map { (fieldName, fieldType) ->
fieldName to fieldType.withoutAnnotations()
}
.filter { (_, type) ->
if (type.isList()) {
buildableTypes.contains(type.listParamType())
} else {
buildableTypes.contains(type)
}
.returns(builderClass)
.addStatement("this.\$L = \$L", fieldName, fieldType.wrapOptionalValue(CodeBlock.of("\$L", fieldName)))
}
.map { (fieldName, fieldType) ->
setFieldWithMutatorMethod(fieldName, fieldType)
}
}

private fun setFieldWithMutatorMethod(fieldName: String, fieldType: TypeName): MethodSpec {
fun setFieldCode(mutatorParam: ParameterSpec): CodeBlock {
return CodeBlock.builder()
.addStatement("\$T.\$L builder = this.\$L != null ? this.\$L.\$L() : \$T.\$L()", fieldType,
ClassNames.BUILDER.simpleName(), fieldName, fieldName, TO_BUILDER_METHOD_NAME, fieldType,
ClassNames.BUILDER.simpleName().decapitalize())
.addStatement("\$L.accept(builder)", mutatorParam.name)
.addStatement("this.\$L = builder.build()", fieldName)
.addStatement("return this")
.build()
})
}

fun setListFieldCode(mutatorParam: ParameterSpec): CodeBlock {
return CodeBlock.builder()
.addStatement("\$T<\$T.\$L> builders = new \$T<>()", ClassNames.LIST, fieldType.listParamType(),
ClassNames.BUILDER.simpleName(), ClassNames.ARRAY_LIST)
.beginControlFlow("if (this.\$L != null)", fieldName)
.beginControlFlow("for (\$T item : this.\$L)", fieldType.listParamType(), fieldName)
.addStatement("builders.add(item != null ? item.toBuilder() : null)")
.endControlFlow()
.endControlFlow()
.addStatement("\$L.accept(builders)", mutatorParam.name)
.addStatement("\$T<\$T> \$L = new \$T<>()", ClassNames.LIST, fieldType.listParamType(), fieldName,
ClassNames.ARRAY_LIST)
.beginControlFlow("for (\$T.\$L item : builders)", fieldType.listParamType(), ClassNames.BUILDER.simpleName())
.addStatement("\$L.add(item != null ? item.build() : null)", fieldName)
.endControlFlow()
.addStatement("this.\$L = \$L", fieldName, fieldName)
.addStatement("return this")
.build()
}

val javaDoc = fieldJavaDocs[fieldName]
val mutatorParam = mutatorParam(fieldType)
return MethodSpec.methodBuilder(fieldName)
.addModifiers(Modifier.PUBLIC)
.addParameter(mutatorParam)
.apply { if (!javaDoc.isNullOrBlank()) addJavadoc(CodeBlock.of("\$L\n", javaDoc)) }
.returns(ClassNames.BUILDER)
.addStatement("\$T.checkNotNull(\$L, \$S)", ClassNames.API_UTILS, mutatorParam.name,
"${mutatorParam.name} == null")
.addCode(if (fieldType.isList()) setListFieldCode(mutatorParam) else setFieldCode(mutatorParam))
.build()
}

private fun TypeSpec.Builder.addBuilderBuildMethod(): TypeSpec.Builder {
private fun buildMethod(): MethodSpec {
val validationCodeBuilder = fields.filter { (_, fieldType) ->
!fieldType.isPrimitive && fieldType.annotations.contains(Annotations.NONNULL)
}.map { (fieldName, _) ->
CodeBlock.of("\$T.checkNotNull(\$L, \$S);\n", ClassNames.API_UTILS, fieldName, "$fieldName == null")
}.fold(CodeBlock.builder(), CodeBlock.Builder::add)

return addMethod(MethodSpec
return MethodSpec
.methodBuilder("build")
.addModifiers(Modifier.PUBLIC)
.returns(targetObjectClassName)
.addCode(validationCodeBuilder.build())
.addStatement("return new \$T\$L", targetObjectClassName,
fields.map { it.first }.joinToString(prefix = "(", separator = ", ", postfix = ")"))
.build())
.build()
}

companion object {
val CLASS_NAME: String = "Builder"
private val builderClass = ClassName.get("", CLASS_NAME)
val TO_BUILDER_METHOD_NAME = "toBuilder"

private fun mutatorParam(fieldType: TypeName): ParameterSpec {
val fieldBuilderType = if (fieldType.isList()) {
ParameterizedTypeName.get(ClassNames.LIST,
ClassName.get("",
"${(fieldType.listParamType() as ClassName).simpleName()}.${ClassNames.BUILDER.simpleName()}"))
} else {
ClassName.get("", "${(fieldType as ClassName).simpleName()}.${ClassNames.BUILDER.simpleName()}")
}
return ParameterSpec.builder(
ParameterizedTypeName.get(ClassNames.CONSUMER, fieldBuilderType),
"mutator"
).addAnnotation(Annotations.NONNULL).build()
}

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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.apollographql.apollo.compiler

import com.apollographql.apollo.api.*
import com.apollographql.apollo.api.internal.Consumer
import com.apollographql.apollo.api.internal.Optional
import com.apollographql.apollo.api.internal.UnmodifiableMapBuilder
import com.apollographql.apollo.api.internal.Utils
Expand All @@ -13,6 +14,7 @@ object ClassNames {
val OBJECT: ClassName = ClassName.get(Object::class.java)
val STRING: ClassName = ClassName.get(String::class.java)
val LIST: ClassName = ClassName.get(List::class.java)
val ARRAY_LIST: ClassName = ClassName.get(ArrayList::class.java)
val GRAPHQL_OPERATION: ClassName = ClassName.get(Operation::class.java)
val GRAPHQL_QUERY: ClassName = ClassName.get(Query::class.java)
val GRAPHQL_MUTATION: ClassName = ClassName.get(Mutation::class.java)
Expand All @@ -27,6 +29,8 @@ object ClassNames {
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)
val BUILDER: ClassName = ClassName.get("", "Builder")
val CONSUMER: ClassName = ClassName.get(Consumer::class.java)

fun <K : Any> parameterizedListOf(type: Class<K>): TypeName =
ParameterizedTypeName.get(LIST, ClassName.get(type))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class OperationTypeSpecBuilder(
.flatten(excludeTypeNames = listOf(
Util.RESPONSE_FIELD_MAPPER_TYPE_NAME,
(SchemaTypeSpecBuilder.FRAGMENTS_FIELD.type as ClassName).simpleName(),
BuilderTypeSpecBuilder.CLASS_NAME
ClassNames.BUILDER.simpleName()
))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,13 @@ class SchemaTypeSpecBuilder(
.withToStringImplementation()
.withEqualsImplementation()
.withHashCodeImplementation()
.let {
if (context.generateModelBuilder) {
it.withBuilder()
} else {
it
}
}
}

private fun formatUniqueTypeName(typeName: String, reservedTypeNames: List<String>): String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,10 +330,9 @@ fun TypeSpec.withBuilder(): TypeSpec {
val fields = fieldSpecs
.filter { !it.modifiers.contains(Modifier.STATIC) }
.filterNot { it.name.startsWith(prefix = "$") }

val builderVariable = BuilderTypeSpecBuilder.CLASS_NAME.decapitalize()
val builderClass = ClassName.get("", BuilderTypeSpecBuilder.CLASS_NAME)
val toBuilderMethod = MethodSpec.methodBuilder("toBuilder")
val builderVariable = ClassNames.BUILDER.simpleName().decapitalize()
val builderClass = ClassName.get("", ClassNames.BUILDER.simpleName())
val toBuilderMethod = MethodSpec.methodBuilder(BuilderTypeSpecBuilder.TO_BUILDER_METHOD_NAME)
.addModifiers(Modifier.PUBLIC)
.returns(builderClass)
.addStatement("\$T \$L = new \$T()", builderClass, builderVariable, builderClass)
Expand All @@ -344,6 +343,10 @@ fun TypeSpec.withBuilder(): TypeSpec {
)
.addStatement("return \$L", builderVariable)
.build()
val buildableTypes = typeSpecs.filter {
it.typeSpecs.find { it.name == ClassNames.BUILDER.simpleName() } != null
}.map { ClassName.get("", it.name) }

return toBuilder()
.addMethod(toBuilderMethod)
.addMethod(BuilderTypeSpecBuilder.builderFactoryMethod())
Expand All @@ -353,7 +356,8 @@ fun TypeSpec.withBuilder(): TypeSpec {
fields = fields.map { it.name to it.type.unwrapOptionalType() },
fieldDefaultValues = emptyMap(),
fieldJavaDocs = emptyMap(),
typeDeclarations = emptyList()
typeDeclarations = emptyList(),
buildableTypes = buildableTypes
).build()
)
.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ data class Fragment(
.flatten(excludeTypeNames = listOf(
Util.RESPONSE_FIELD_MAPPER_TYPE_NAME,
(SchemaTypeSpecBuilder.FRAGMENTS_FIELD.type as ClassName).simpleName(),
BuilderTypeSpecBuilder.CLASS_NAME
ClassNames.BUILDER.simpleName()
))
.let {
if (context.generateModelBuilder) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.apollographql.apollo.api.ResponseFieldMarshaller;
import com.apollographql.apollo.api.ResponseReader;
import com.apollographql.apollo.api.ResponseWriter;
import com.apollographql.apollo.api.internal.Consumer;
import com.apollographql.apollo.api.internal.Optional;
import com.apollographql.apollo.api.internal.Utils;
import com.example.fragment_with_inline_fragment.fragment.HeroDetails;
Expand Down Expand Up @@ -193,6 +194,14 @@ public Builder hero(@Nullable Hero hero) {
return this;
}

public Builder hero(@Nonnull Consumer<Hero.Builder> mutator) {
Utils.checkNotNull(mutator, "mutator == null");
Hero.Builder builder = this.hero != null ? this.hero.toBuilder() : Hero.builder();
mutator.accept(builder);
this.hero = builder.build();
return this;
}

public Data build() {
return new Data(hero);
}
Expand Down Expand Up @@ -391,6 +400,16 @@ public int hashCode() {
return $hashCode;
}

public Builder toBuilder() {
Builder builder = new Builder();
builder.heroDetails = heroDetails;
return builder;
}

public static Builder builder() {
return new Builder();
}

public static final class Mapper implements FragmentResponseFieldMapper<Fragments> {
final HeroDetails.Mapper heroDetailsFieldMapper = new HeroDetails.Mapper();

Expand All @@ -403,6 +422,23 @@ public static final class Mapper implements FragmentResponseFieldMapper<Fragment
return new Fragments(Utils.checkNotNull(heroDetails, "heroDetails == null"));
}
}

public static final class Builder {
private @Nonnull HeroDetails heroDetails;

Builder() {
}

public Builder heroDetails(@Nonnull HeroDetails heroDetails) {
this.heroDetails = heroDetails;
return this;
}

public Fragments build() {
Utils.checkNotNull(heroDetails, "heroDetails == null");
return new Fragments(heroDetails);
}
}
}

public static final class Mapper implements ResponseFieldMapper<Hero> {
Expand Down Expand Up @@ -460,6 +496,14 @@ public Builder fragments(@Nonnull Fragments fragments) {
return this;
}

public Builder fragments(@Nonnull Consumer<Fragments.Builder> mutator) {
Utils.checkNotNull(mutator, "mutator == null");
Fragments.Builder builder = this.fragments != null ? this.fragments.toBuilder() : Fragments.builder();
mutator.accept(builder);
this.fragments = builder.build();
return this;
}

public Hero build() {
Utils.checkNotNull(__typename, "__typename == null");
Utils.checkNotNull(name, "name == null");
Expand Down
Loading

0 comments on commit 34e3c18

Please sign in to comment.