Skip to content
This repository was archived by the owner on Feb 26, 2023. It is now read-only.

Commit 10c921d

Browse files
Added a parameter to the constructor of the builder to take default values from an instance
1 parent 859c293 commit 10c921d

File tree

7 files changed

+128
-6
lines changed

7 files changed

+128
-6
lines changed

README.md

+5-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ dependencies {
2626
<dependency>
2727
<groupId>com.thinkinglogic.builder</groupId>
2828
<artifactId>kotlin-builder-annotation</artifactId>
29-
<version>1.0.3</version>
29+
<version>1.1.0</version>
3030
</dependency>
3131
...
3232
</dependencies>
@@ -46,7 +46,7 @@ dependencies {
4646
<annotationProcessorPath>
4747
<groupId>com.thinkinglogic.builder</groupId>
4848
<artifactId>kotlin-builder-processor</artifactId>
49-
<version>1.0.3</version>
49+
<version>1.1.0</version>
5050
</annotationProcessorPath>
5151
</annotationProcessorPaths>
5252
</configuration>
@@ -87,6 +87,9 @@ The builder will check for required fields, so
8787
`new MyDataClassBuilder().notNullString("Foo").build();`
8888
would return a new instance with a null value for 'nullableString'.
8989

90+
To replace Kotlin's `copy()` (and Lombok's `toBuilder()`) method, clients can pass an instance of the annotated class when constructing a builder:
91+
`new MyDataClassBuilder(myDataClassInstance)` - the builder will be initialised with values from the instance.
92+
9093
#### Default values
9194
Kotlin doesn't retain information about default values after compilation, so it cannot be accessed during annotation processing.
9295
Instead we must use the `@DefaultValue` annotation to tell the builder about it:

build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ buildscript {
2121

2222
allprojects {
2323
group = 'com.thinkinglogic.builder'
24-
version = '1.0.3'
24+
version = '1.1.0'
2525
apply plugin: "kotlin"
2626

2727
repositories {

kotlin-builder-example-usage/src/main/kotlin/com/thinkinglogic/example/ClassWithConstructorParameters.kt

+4
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,8 @@ constructor(
2929
result = 31 * result + fullName.hashCode()
3030
return result
3131
}
32+
33+
override fun toString(): String {
34+
return "ClassWithConstructorParameters(otherName=$otherName, fullName='$fullName')"
35+
}
3236
}

kotlin-builder-example-usage/src/test/kotlin/com/thinkinglogic/example/ClassWithConstructorParametersTest.kt

+21
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,25 @@ internal class ClassWithConstructorParametersTest {
4646
assertThat(actual).isEqualTo(expected)
4747
}
4848

49+
@Test
50+
fun `builder should inherit values from source`() {
51+
// given
52+
val forename = "Jane"
53+
val surname = "Smith"
54+
val expected = ClassWithConstructorParameters(
55+
forename = forename,
56+
surname = surname,
57+
otherName = "Jayne"
58+
)
59+
60+
// when
61+
val actual = ClassWithConstructorParametersBuilder(expected)
62+
.forename(forename)
63+
.surname(surname)
64+
.build()
65+
66+
// then
67+
assertThat(actual).isEqualTo(expected)
68+
}
69+
4970
}

kotlin-builder-example-usage/src/test/kotlin/com/thinkinglogic/example/DataClassWithAdditionalFieldsTest.kt

+22
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package com.thinkinglogic.example
22

3+
import assertk.assertions.contains
4+
import assertk.assertions.isNotNull
5+
import assertk.assertions.message
6+
import assertk.catch
37
import org.assertj.core.api.Assertions.assertThat
48
import org.junit.jupiter.api.Test
59

@@ -24,4 +28,22 @@ internal class DataClassWithAdditionalFieldsTest {
2428
assertThat(actual).isEqualTo(expected)
2529
}
2630

31+
@Test
32+
fun `builder should not inherit private properties from source object`() {
33+
// given
34+
val source = DataClassWithAdditionalFields(
35+
constructorString = "foobar ",
36+
privateString = "barfoo"
37+
)
38+
39+
// when
40+
var expected = catch { DataClassWithAdditionalFieldsBuilder(source).build() }
41+
42+
// then
43+
assertk.assert(expected).isNotNull { e ->
44+
e is IllegalStateException
45+
e.message().isNotNull { it.contains("privateString") }
46+
}
47+
}
48+
2749
}

kotlin-builder-example-usage/src/test/kotlin/com/thinkinglogic/example/SimpleDataClassTest.kt

+44-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.thinkinglogic.example
22

33
import assertk.assert
4-
import assertk.assertions.*
4+
import assertk.assertions.contains
5+
import assertk.assertions.isNotNull
6+
import assertk.assertions.message
57
import assertk.catch
68
import org.assertj.core.api.Assertions.assertThat
79
import org.junit.jupiter.api.Test
@@ -57,6 +59,47 @@ internal class SimpleDataClassTest {
5759
assertThat(actual).isEqualTo(expected)
5860
}
5961

62+
@Test
63+
fun `builder should inherit properties from source`() {
64+
// given
65+
val expected = SimpleDataClass(
66+
notNullString = "not null",
67+
nullableString = null,
68+
notNullLong = 123,
69+
nullableLong = 345,
70+
date = LocalDate.now()
71+
)
72+
73+
// when
74+
val actual = SimpleDataClassBuilder(expected)
75+
.build()
76+
77+
// then
78+
assertThat(actual).isEqualTo(expected)
79+
}
80+
81+
@Test
82+
fun `builder should replace inherited properties`() {
83+
// given
84+
val original = SimpleDataClass(
85+
notNullString = "not null",
86+
nullableString = null,
87+
notNullLong = 123,
88+
nullableLong = 345,
89+
date = LocalDate.now()
90+
)
91+
val newStringValue = "New value"
92+
93+
// when
94+
val actual = SimpleDataClassBuilder(original)
95+
.notNullString(newStringValue)
96+
.build()
97+
98+
// then
99+
assertThat(actual).isNotEqualTo(original)
100+
assertThat(actual.notNullString).isEqualTo(newStringValue)
101+
}
102+
60103
@Test
61104
fun `build method should throw exception if required property not set`() {
62105
// given

kotlin-builder-processor/src/main/kotlin/com/thinkinglogic/builder/processor/BuilderProcessor.kt

+31-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ import javax.lang.model.type.ArrayType
1717
import javax.lang.model.type.DeclaredType
1818
import javax.lang.model.type.PrimitiveType
1919
import javax.lang.model.type.TypeMirror
20-
import javax.lang.model.util.ElementFilter.constructorsIn
21-
import javax.lang.model.util.ElementFilter.fieldsIn
20+
import javax.lang.model.util.ElementFilter.*
2221
import javax.tools.Diagnostic.Kind.ERROR
2322
import javax.tools.Diagnostic.Kind.NOTE
2423

@@ -96,6 +95,8 @@ class BuilderProcessor : AbstractProcessor() {
9695
builderSpec.addFunction(field.asSetterFunctionReturning(builderClass))
9796
}
9897

98+
builderSpec.primaryConstructor(FunSpec.constructorBuilder().build())
99+
builderSpec.addFunction(createConstructor(fields, classToBuild))
99100
builderSpec.addFunction(createBuildFunction(fields, classToBuild))
100101
builderSpec.addFunction(createCheckRequiredFieldsFunction(fields))
101102

@@ -117,6 +118,34 @@ class BuilderProcessor : AbstractProcessor() {
117118
return fields.filter { constructorParamNames.contains(it.simpleName.toString()) }
118119
}
119120

121+
/** Creates a constructor for [classType] that accepts an instance of the class to build, from which default values are obtained. */
122+
private fun createConstructor(fields: List<Element>, classType: TypeElement): FunSpec {
123+
val source = "source"
124+
val sourceParameter = ParameterSpec.builder(source, classType.asKotlinTypeName()).build()
125+
val getterFieldNames = classType.getterFieldNames()
126+
val code = StringBuilder()
127+
fields.forEach { field ->
128+
if (getterFieldNames.contains(field.simpleName.toString())) {
129+
code.append(" this.${field.simpleName} = $source.${field.simpleName}")
130+
.appendln()
131+
}
132+
}
133+
return FunSpec.constructorBuilder()
134+
.addParameter(sourceParameter)
135+
.callThisConstructor()
136+
.addCode(code.toString())
137+
.build()
138+
}
139+
140+
/** Returns a set of the names of fields with getters (actually the names of getter methods with 'get' removed and decapitalised). */
141+
private fun TypeElement.getterFieldNames(): Set<String> {
142+
val allMembers = processingEnv.elementUtils.getAllMembers(this)
143+
return methodsIn(allMembers)
144+
.filter { it.simpleName.startsWith("get") && it.parameters.isEmpty() }
145+
.map { it.simpleName.toString().substringAfter("get").decapitalize() }
146+
.toSet()
147+
}
148+
120149
/** Creates a 'build()' function that will invoke a constructor for [returnType], passing [fields] as arguments and returning the new instance. */
121150
private fun createBuildFunction(fields: List<Element>, returnType: TypeElement): FunSpec {
122151
val code = StringBuilder("$CHECK_REQUIRED_FIELDS_FUNCTION_NAME()")

0 commit comments

Comments
 (0)