Skip to content

Commit 12e1480

Browse files
authored
Merge pull request #9 from victorhsr/adding_lombok_support
Adding lombok support
2 parents f2b9d43 + a1efc4c commit 12e1480

File tree

9 files changed

+361
-121
lines changed

9 files changed

+361
-121
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,14 @@ Note that there's no need to instantiate `Address` or `MetaDataWrapper`, and mor
8686
- **@DSLProperty** - Customizes the method name generated for a specific field. The default is the original field name;
8787
- **@DSLIgnore** - Marks a field to be excluded when generating DSL code;
8888

89+
## Prerequisites
90+
91+
The Hermes builder requires elements annotated with **@DSLRoot** to have a public default constructor. Only attributes with a public setter method will have a higher-order function created.
92+
93+
## Lombok Support
94+
95+
The Hermes builder is compatible with the Lombok plugin. It recognizes the **@Data** and **@Setter** annotations from Lombok and uses their generated methods to compose the sources generated by Hermes Builder.
96+
8997
# Comparison with Traditional Java Classes
9098

9199
The equivalent implementation in traditional Java code involves a considerable amount of boilerplate:

pom.xml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
<properties>
1313
<kotlin.code.style>official</kotlin.code.style>
1414
<kotlin.compiler.jvmTarget>11</kotlin.compiler.jvmTarget>
15-
<maven.compiler.source>11</maven.compiler.source>
16-
<maven.compiler.target>11</maven.compiler.target>
15+
<maven.compiler.source>16</maven.compiler.source>
16+
<maven.compiler.target>16</maven.compiler.target>
1717
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
1818
<auto-service.version>1.1.1</auto-service.version>
19-
<kotlin.version>1.9.0</kotlin.version>
19+
<kotlin.version>1.9.21</kotlin.version>
2020
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
2121
<maven-surefire-plugin.version>3.0.0-M3</maven-surefire-plugin.version>
2222
</properties>
@@ -206,6 +206,12 @@
206206
<version>3.8.1</version>
207207
<scope>provided</scope>
208208
</dependency>
209+
<dependency>
210+
<groupId>org.projectlombok</groupId>
211+
<artifactId>lombok</artifactId>
212+
<version>1.18.36</version>
213+
<scope>provided</scope>
214+
</dependency>
209215

210216
<!--kotlin-->
211217
<dependency>

src/main/java/com/github/victorhsr/hermes/sample/ExpectedCustomGenericKeyValuePairDSL.java

Lines changed: 0 additions & 28 deletions
This file was deleted.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.github.victorhsr.hermes.sample.lombok;
2+
3+
import com.github.victorhsr.hermes.core.annotations.DSLRoot;
4+
import lombok.Data;
5+
import lombok.EqualsAndHashCode;
6+
7+
@Data
8+
@DSLRoot
9+
@EqualsAndHashCode
10+
public class DataAnnotationPojo {
11+
12+
private String foo;
13+
private Boolean bar;
14+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.github.victorhsr.hermes.sample.lombok;
2+
3+
import com.github.victorhsr.hermes.core.annotations.DSLRoot;
4+
import lombok.EqualsAndHashCode;
5+
import lombok.Setter;
6+
7+
@DSLRoot
8+
@EqualsAndHashCode
9+
public class SetterAnnotationOnFieldPojo {
10+
11+
@Setter
12+
private String someString;
13+
@Setter
14+
private Boolean someBoolean;
15+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.github.victorhsr.hermes.sample.lombok;
2+
3+
import com.github.victorhsr.hermes.core.annotations.DSLRoot;
4+
import lombok.EqualsAndHashCode;
5+
import lombok.Setter;
6+
7+
@Setter
8+
@DSLRoot
9+
@EqualsAndHashCode
10+
public class SetterAnnotationPojo {
11+
12+
private Boolean fieldWithoutAnnotation;
13+
}

src/main/kotlin/com/github/victorhsr/hermes/maven/element/builder/FieldFinder.kt

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,66 @@ import javax.lang.model.element.Element
66
import javax.lang.model.element.ElementKind
77
import javax.lang.model.element.TypeElement
88

9-
object FieldFinder {
9+
private const val SET_METHOD_PREFIX = "set"
10+
private const val LOMBOK_SETTER_ANNOTATION = "lombok.Setter"
11+
private const val LOMBOK_DATA_ANNOTATION = "lombok.Data"
12+
private val LOMBOK_CLASS_ANNOTATIONS = setOf(LOMBOK_SETTER_ANNOTATION, LOMBOK_DATA_ANNOTATION)
1013

11-
private const val SET_METHOD_PREFIX = "set"
14+
object FieldFinder {
1215

1316
fun getFieldsFromClazz(clazz: TypeElement): List<Element> {
1417
return resolveFields(clazz)
1518
.filter { it.getAnnotation(DSLIgnore::class.java) == null }
1619
}
1720

1821
private fun resolveFields(clazz: TypeElement): List<Element> {
19-
val setMethodsMap = getSetMethods(clazz)
22+
val setMethods = getSetMethodsForFields(clazz)
2023

2124
return clazz.enclosedElements
2225
.filter { it.kind.isField }
23-
.filter { setMethodsMap.containsKey(buildSetMethodName(it.simpleName.toString())) }
26+
.filter { setMethods.contains(buildSetMethodName(it.simpleName.toString())) }
2427
.toList()
2528
}
2629

27-
private fun getSetMethods(clazz: TypeElement): Map<String, List<Element>> {
30+
private fun getSetMethodsForFields(clazz: TypeElement): Set<String> {
31+
val setMethodsFromFieldsAnnotatedWithLombok = getAnnotatedFieldsWithLombok(clazz)
32+
val setMethodsFromClassAnnotationOrRegularMethods =
33+
if (isLombokManagedClass(clazz))
34+
return getSetMethodsFromClassAnnotation(clazz)
35+
else getSetMethodsFromRegularMethods(clazz)
36+
37+
return setMethodsFromFieldsAnnotatedWithLombok + setMethodsFromClassAnnotationOrRegularMethods
38+
}
39+
40+
private fun getSetMethodsFromRegularMethods(clazz: TypeElement): Set<String> {
2841
return clazz.enclosedElements
2942
.filter { it.kind == ElementKind.METHOD }
3043
.filter { it.simpleName.startsWith(SET_METHOD_PREFIX) }
31-
.groupBy { it.simpleName.toString() }
44+
.map { it.simpleName.toString() }
45+
.toSet()
46+
}
47+
48+
private fun getSetMethodsFromClassAnnotation(clazz: TypeElement): Set<String> {
49+
return clazz.enclosedElements
50+
.filter { it.kind == ElementKind.FIELD }
51+
.map { buildSetMethodName(it.simpleName.toString()) }
52+
.toSet()
53+
}
54+
55+
private fun getAnnotatedFieldsWithLombok(clazz: TypeElement): Set<String> {
56+
return clazz.enclosedElements
57+
.filter { it.kind == ElementKind.FIELD }
58+
.filter { field -> field.annotationMirrors.any { it.annotationType.toString() == LOMBOK_SETTER_ANNOTATION } }
59+
.map { buildSetMethodName(it.simpleName.toString()) }
60+
.toSet()
61+
}
62+
63+
private fun isLombokManagedClass(clazz: TypeElement): Boolean {
64+
return clazz.annotationMirrors.any { LOMBOK_CLASS_ANNOTATIONS.contains(it.annotationType.toString()) }
3265
}
3366

3467
private fun buildSetMethodName(fieldName: String): String {
35-
return "${SET_METHOD_PREFIX}${fieldName.myCapitalize()}"
68+
return "$SET_METHOD_PREFIX${fieldName.myCapitalize()}"
3669
}
3770

3871
}

src/test/kotlin/com/github/victorhsr/hermes/maven/element/builder/FieldFinderTest.kt

Lines changed: 131 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,54 +5,161 @@ import com.github.victorhsr.hermes.maven.util.TestName
55
import io.mockk.every
66
import io.mockk.mockk
77
import org.assertj.core.api.Assertions.assertThat
8+
import org.junit.jupiter.api.Nested
89
import org.junit.jupiter.api.Test
10+
import javax.lang.model.element.AnnotationMirror
911
import javax.lang.model.element.Element
1012
import javax.lang.model.element.ElementKind
1113
import javax.lang.model.element.TypeElement
14+
import javax.lang.model.type.DeclaredType
1215

1316
class FieldFinderTest {
1417

15-
companion object {
16-
private const val SET_NAME_METHOD_NAME = "setName"
17-
private const val SET_AGE_METHOD_NAME = "setAge"
18-
private const val DO_SOMETHING_METHOD_NAME = "doSomething"
19-
private const val NAME_FIELD_NAME = "name"
20-
private const val AGE_FIELD_NAME = "age"
21-
private const val ADDRESS_FIELD_NAME = "address"
18+
@Nested
19+
inner class PlainJavaTests {
20+
@Test
21+
fun `getFieldsFromClazz should identify fields that have a setter method`() {
22+
// given
23+
val fieldWithSetterName = "fieldA";
24+
val fieldWithoutSetterName = "fieldB";
25+
val setterName = "setFieldA"
26+
val fieldWithSetter = mockElement(ElementKind.FIELD, fieldWithSetterName)
27+
val fieldWithoutSetter = mockElement(ElementKind.FIELD, fieldWithoutSetterName)
28+
val setterMethod = mockElement(ElementKind.METHOD, setterName)
29+
val clazz = mockClazz(fieldWithSetter, fieldWithoutSetter, setterMethod)
30+
val expected = listOf(fieldWithSetter)
31+
32+
// when
33+
val actual = FieldFinder.getFieldsFromClazz(clazz)
34+
35+
// then
36+
assertThat(actual).isEqualTo(expected)
37+
}
38+
39+
@Test
40+
fun `getFieldsFromClazz should filter classes annotated with DSLIgnore out`() {
41+
// given
42+
val fieldWithSetterName = "fieldA";
43+
val fieldWithoutSetterName = "fieldB";
44+
val setterName = "setFieldA"
45+
val fieldWithSetter = mockElement(ElementKind.FIELD, fieldWithSetterName, true)
46+
val fieldWithoutSetter = mockElement(ElementKind.FIELD, fieldWithoutSetterName)
47+
val setterMethod = mockElement(ElementKind.METHOD, setterName)
48+
val clazz = mockClazz(fieldWithSetter, fieldWithoutSetter, setterMethod)
49+
val expected = listOf<Element>()
50+
51+
// when
52+
val actual = FieldFinder.getFieldsFromClazz(clazz)
53+
54+
// then
55+
assertThat(actual).isEqualTo(expected)
56+
}
2257
}
2358

24-
@Test
25-
fun `should filter elements that are fields and have 'set' methods and ignore the ones annoted with DSLIgnore`() {
26-
// given
27-
val clazz: TypeElement = mockk<TypeElement>()
2859

29-
val nameSetMethod = mockElement(ElementKind.METHOD, SET_NAME_METHOD_NAME)
30-
val ageSetMethod = mockElement(ElementKind.METHOD, SET_AGE_METHOD_NAME)
31-
val doSomethingMethod = mockElement(ElementKind.METHOD, DO_SOMETHING_METHOD_NAME)
32-
val nameField = mockElement(ElementKind.FIELD, NAME_FIELD_NAME)
33-
val ageField = mockElement(ElementKind.FIELD, AGE_FIELD_NAME, true)
34-
val addressField = mockElement(ElementKind.FIELD, ADDRESS_FIELD_NAME)
60+
@Nested
61+
inner class LombokTests {
62+
63+
@Nested
64+
inner class ClazzLevelAnnotationTests {
3565

36-
val enclosedElements = listOf(nameField, nameSetMethod, ageField, ageSetMethod, doSomethingMethod, addressField)
37-
val expectedResult = listOf(nameField)
66+
@Test
67+
fun `getFieldsFromClazz should identify @Setter on class level`() {
68+
// given
69+
val fieldWithSetterName = "fieldA";
70+
val fieldWithSetter = mockElement(ElementKind.FIELD, fieldWithSetterName)
71+
val clazz = mockClazz("lombok.Setter", fieldWithSetter)
72+
val expected = listOf(fieldWithSetter)
3873

39-
every { clazz.enclosedElements } returns enclosedElements
74+
// when
75+
val actual = FieldFinder.getFieldsFromClazz(clazz)
4076

41-
// when
42-
val actual: List<Element> = FieldFinder.getFieldsFromClazz(clazz)
77+
// then
78+
assertThat(actual).isEqualTo(expected)
79+
}
4380

44-
// then
45-
assertThat(actual).hasSameElementsAs(expectedResult)
81+
@Test
82+
fun `getFieldsFromClazz should identify @Data on class level`() {
83+
// given
84+
val fieldWithSetterName = "fieldA";
85+
val anotherFieldWithSetterName = "fieldB";
86+
val fieldWithSetter = mockElement(ElementKind.FIELD, fieldWithSetterName)
87+
val anotherFieldWithSetter = mockElement(ElementKind.FIELD, anotherFieldWithSetterName)
88+
val clazz = mockClazz("lombok.Data", fieldWithSetter, anotherFieldWithSetter)
89+
val expected = listOf(fieldWithSetter, anotherFieldWithSetter)
90+
91+
// when
92+
val actual = FieldFinder.getFieldsFromClazz(clazz)
93+
94+
// then
95+
assertThat(actual).isEqualTo(expected)
96+
}
97+
}
98+
99+
@Nested
100+
inner class FiledLevelAnnotationTests {
101+
@Test
102+
fun `getFieldsFromClazz should identify @Setter on class level`() {
103+
// given
104+
val fieldWithSetterName = "fieldA";
105+
val fieldWithoutSetterName = "fieldB";
106+
val fieldWithSetter = mockElement("lombok.Setter", ElementKind.FIELD, fieldWithSetterName)
107+
val fieldWithoutSetter = mockElement(ElementKind.FIELD, fieldWithoutSetterName)
108+
val clazz = mockClazz(fieldWithSetter, fieldWithoutSetter)
109+
val expected = listOf(fieldWithSetter)
110+
111+
// when
112+
val actual = FieldFinder.getFieldsFromClazz(clazz)
113+
114+
// then
115+
assertThat(actual).isEqualTo(expected)
116+
}
117+
}
46118
}
47119

48120
private fun mockElement(elementKind: ElementKind, name: String, shouldBeIgnored: Boolean = false): Element {
121+
return mockElement(null, elementKind, name, shouldBeIgnored)
122+
}
123+
124+
private fun mockElement(annotation: String?, elementKind: ElementKind, name: String, shouldBeIgnored: Boolean = false): Element {
49125
val element: Element = mockk<Element>()
50126

51127
every { element.kind } returns elementKind
52128
every { element.simpleName } returns TestName(name)
129+
every { element.annotationMirrors } returns
130+
if (annotation == null) mutableListOf()
131+
else mutableListOf(mockAnnotationMirror(annotation))
53132
every { element.getAnnotation(any<Class<DSLIgnore>>()) } returns if (shouldBeIgnored) mockk<DSLIgnore>() else null
54133

55134
return element
56135
}
57136

137+
private fun mockClazz(vararg field: Element): TypeElement {
138+
return mockClazz(null, *field)
139+
}
140+
141+
private fun mockClazz(annotation: String? = null, vararg field: Element): TypeElement {
142+
val typeElement = mockk<TypeElement>()
143+
every { typeElement.enclosedElements } returns field.toList()
144+
every { typeElement.annotationMirrors } returns
145+
if (annotation == null) mutableListOf()
146+
else mutableListOf(mockAnnotationMirror(annotation))
147+
every { typeElement.getAnnotation(any<Class<DSLIgnore>>()) } returns null
148+
149+
return typeElement
150+
}
151+
152+
private fun mockAnnotationMirror(annotation: String): AnnotationMirror {
153+
val annotationMirror = mockk<AnnotationMirror>()
154+
every { annotationMirror.annotationType } returns mockAnnotationType(annotation)
155+
156+
return annotationMirror
157+
}
158+
159+
private fun mockAnnotationType(annotation: String): DeclaredType {
160+
val declaredType = mockk<DeclaredType>()
161+
every { declaredType.toString() } returns annotation
162+
163+
return declaredType
164+
}
58165
}

0 commit comments

Comments
 (0)