Skip to content

Commit

Permalink
Merge pull request #9 from victorhsr/adding_lombok_support
Browse files Browse the repository at this point in the history
Adding lombok support
  • Loading branch information
victorhsr authored Feb 17, 2025
2 parents f2b9d43 + a1efc4c commit 12e1480
Show file tree
Hide file tree
Showing 9 changed files with 361 additions and 121 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ Note that there's no need to instantiate `Address` or `MetaDataWrapper`, and mor
- **@DSLProperty** - Customizes the method name generated for a specific field. The default is the original field name;
- **@DSLIgnore** - Marks a field to be excluded when generating DSL code;

## Prerequisites

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.

## Lombok Support

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.

# Comparison with Traditional Java Classes

The equivalent implementation in traditional Java code involves a considerable amount of boilerplate:
Expand Down
12 changes: 9 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
<properties>
<kotlin.code.style>official</kotlin.code.style>
<kotlin.compiler.jvmTarget>11</kotlin.compiler.jvmTarget>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<maven.compiler.source>16</maven.compiler.source>
<maven.compiler.target>16</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<auto-service.version>1.1.1</auto-service.version>
<kotlin.version>1.9.0</kotlin.version>
<kotlin.version>1.9.21</kotlin.version>
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
<maven-surefire-plugin.version>3.0.0-M3</maven-surefire-plugin.version>
</properties>
Expand Down Expand Up @@ -206,6 +206,12 @@
<version>3.8.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
<scope>provided</scope>
</dependency>

<!--kotlin-->
<dependency>
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.github.victorhsr.hermes.sample.lombok;

import com.github.victorhsr.hermes.core.annotations.DSLRoot;
import lombok.Data;
import lombok.EqualsAndHashCode;

@Data
@DSLRoot
@EqualsAndHashCode
public class DataAnnotationPojo {

private String foo;
private Boolean bar;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.github.victorhsr.hermes.sample.lombok;

import com.github.victorhsr.hermes.core.annotations.DSLRoot;
import lombok.EqualsAndHashCode;
import lombok.Setter;

@DSLRoot
@EqualsAndHashCode
public class SetterAnnotationOnFieldPojo {

@Setter
private String someString;
@Setter
private Boolean someBoolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.github.victorhsr.hermes.sample.lombok;

import com.github.victorhsr.hermes.core.annotations.DSLRoot;
import lombok.EqualsAndHashCode;
import lombok.Setter;

@Setter
@DSLRoot
@EqualsAndHashCode
public class SetterAnnotationPojo {

private Boolean fieldWithoutAnnotation;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,66 @@ import javax.lang.model.element.Element
import javax.lang.model.element.ElementKind
import javax.lang.model.element.TypeElement

object FieldFinder {
private const val SET_METHOD_PREFIX = "set"
private const val LOMBOK_SETTER_ANNOTATION = "lombok.Setter"
private const val LOMBOK_DATA_ANNOTATION = "lombok.Data"
private val LOMBOK_CLASS_ANNOTATIONS = setOf(LOMBOK_SETTER_ANNOTATION, LOMBOK_DATA_ANNOTATION)

private const val SET_METHOD_PREFIX = "set"
object FieldFinder {

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

private fun resolveFields(clazz: TypeElement): List<Element> {
val setMethodsMap = getSetMethods(clazz)
val setMethods = getSetMethodsForFields(clazz)

return clazz.enclosedElements
.filter { it.kind.isField }
.filter { setMethodsMap.containsKey(buildSetMethodName(it.simpleName.toString())) }
.filter { setMethods.contains(buildSetMethodName(it.simpleName.toString())) }
.toList()
}

private fun getSetMethods(clazz: TypeElement): Map<String, List<Element>> {
private fun getSetMethodsForFields(clazz: TypeElement): Set<String> {
val setMethodsFromFieldsAnnotatedWithLombok = getAnnotatedFieldsWithLombok(clazz)
val setMethodsFromClassAnnotationOrRegularMethods =
if (isLombokManagedClass(clazz))
return getSetMethodsFromClassAnnotation(clazz)
else getSetMethodsFromRegularMethods(clazz)

return setMethodsFromFieldsAnnotatedWithLombok + setMethodsFromClassAnnotationOrRegularMethods
}

private fun getSetMethodsFromRegularMethods(clazz: TypeElement): Set<String> {
return clazz.enclosedElements
.filter { it.kind == ElementKind.METHOD }
.filter { it.simpleName.startsWith(SET_METHOD_PREFIX) }
.groupBy { it.simpleName.toString() }
.map { it.simpleName.toString() }
.toSet()
}

private fun getSetMethodsFromClassAnnotation(clazz: TypeElement): Set<String> {
return clazz.enclosedElements
.filter { it.kind == ElementKind.FIELD }
.map { buildSetMethodName(it.simpleName.toString()) }
.toSet()
}

private fun getAnnotatedFieldsWithLombok(clazz: TypeElement): Set<String> {
return clazz.enclosedElements
.filter { it.kind == ElementKind.FIELD }
.filter { field -> field.annotationMirrors.any { it.annotationType.toString() == LOMBOK_SETTER_ANNOTATION } }
.map { buildSetMethodName(it.simpleName.toString()) }
.toSet()
}

private fun isLombokManagedClass(clazz: TypeElement): Boolean {
return clazz.annotationMirrors.any { LOMBOK_CLASS_ANNOTATIONS.contains(it.annotationType.toString()) }
}

private fun buildSetMethodName(fieldName: String): String {
return "${SET_METHOD_PREFIX}${fieldName.myCapitalize()}"
return "$SET_METHOD_PREFIX${fieldName.myCapitalize()}"
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,54 +5,161 @@ import com.github.victorhsr.hermes.maven.util.TestName
import io.mockk.every
import io.mockk.mockk
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import javax.lang.model.element.AnnotationMirror
import javax.lang.model.element.Element
import javax.lang.model.element.ElementKind
import javax.lang.model.element.TypeElement
import javax.lang.model.type.DeclaredType

class FieldFinderTest {

companion object {
private const val SET_NAME_METHOD_NAME = "setName"
private const val SET_AGE_METHOD_NAME = "setAge"
private const val DO_SOMETHING_METHOD_NAME = "doSomething"
private const val NAME_FIELD_NAME = "name"
private const val AGE_FIELD_NAME = "age"
private const val ADDRESS_FIELD_NAME = "address"
@Nested
inner class PlainJavaTests {
@Test
fun `getFieldsFromClazz should identify fields that have a setter method`() {
// given
val fieldWithSetterName = "fieldA";
val fieldWithoutSetterName = "fieldB";
val setterName = "setFieldA"
val fieldWithSetter = mockElement(ElementKind.FIELD, fieldWithSetterName)
val fieldWithoutSetter = mockElement(ElementKind.FIELD, fieldWithoutSetterName)
val setterMethod = mockElement(ElementKind.METHOD, setterName)
val clazz = mockClazz(fieldWithSetter, fieldWithoutSetter, setterMethod)
val expected = listOf(fieldWithSetter)

// when
val actual = FieldFinder.getFieldsFromClazz(clazz)

// then
assertThat(actual).isEqualTo(expected)
}

@Test
fun `getFieldsFromClazz should filter classes annotated with DSLIgnore out`() {
// given
val fieldWithSetterName = "fieldA";
val fieldWithoutSetterName = "fieldB";
val setterName = "setFieldA"
val fieldWithSetter = mockElement(ElementKind.FIELD, fieldWithSetterName, true)
val fieldWithoutSetter = mockElement(ElementKind.FIELD, fieldWithoutSetterName)
val setterMethod = mockElement(ElementKind.METHOD, setterName)
val clazz = mockClazz(fieldWithSetter, fieldWithoutSetter, setterMethod)
val expected = listOf<Element>()

// when
val actual = FieldFinder.getFieldsFromClazz(clazz)

// then
assertThat(actual).isEqualTo(expected)
}
}

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

val nameSetMethod = mockElement(ElementKind.METHOD, SET_NAME_METHOD_NAME)
val ageSetMethod = mockElement(ElementKind.METHOD, SET_AGE_METHOD_NAME)
val doSomethingMethod = mockElement(ElementKind.METHOD, DO_SOMETHING_METHOD_NAME)
val nameField = mockElement(ElementKind.FIELD, NAME_FIELD_NAME)
val ageField = mockElement(ElementKind.FIELD, AGE_FIELD_NAME, true)
val addressField = mockElement(ElementKind.FIELD, ADDRESS_FIELD_NAME)
@Nested
inner class LombokTests {

@Nested
inner class ClazzLevelAnnotationTests {

val enclosedElements = listOf(nameField, nameSetMethod, ageField, ageSetMethod, doSomethingMethod, addressField)
val expectedResult = listOf(nameField)
@Test
fun `getFieldsFromClazz should identify @Setter on class level`() {
// given
val fieldWithSetterName = "fieldA";
val fieldWithSetter = mockElement(ElementKind.FIELD, fieldWithSetterName)
val clazz = mockClazz("lombok.Setter", fieldWithSetter)
val expected = listOf(fieldWithSetter)

every { clazz.enclosedElements } returns enclosedElements
// when
val actual = FieldFinder.getFieldsFromClazz(clazz)

// when
val actual: List<Element> = FieldFinder.getFieldsFromClazz(clazz)
// then
assertThat(actual).isEqualTo(expected)
}

// then
assertThat(actual).hasSameElementsAs(expectedResult)
@Test
fun `getFieldsFromClazz should identify @Data on class level`() {
// given
val fieldWithSetterName = "fieldA";
val anotherFieldWithSetterName = "fieldB";
val fieldWithSetter = mockElement(ElementKind.FIELD, fieldWithSetterName)
val anotherFieldWithSetter = mockElement(ElementKind.FIELD, anotherFieldWithSetterName)
val clazz = mockClazz("lombok.Data", fieldWithSetter, anotherFieldWithSetter)
val expected = listOf(fieldWithSetter, anotherFieldWithSetter)

// when
val actual = FieldFinder.getFieldsFromClazz(clazz)

// then
assertThat(actual).isEqualTo(expected)
}
}

@Nested
inner class FiledLevelAnnotationTests {
@Test
fun `getFieldsFromClazz should identify @Setter on class level`() {
// given
val fieldWithSetterName = "fieldA";
val fieldWithoutSetterName = "fieldB";
val fieldWithSetter = mockElement("lombok.Setter", ElementKind.FIELD, fieldWithSetterName)
val fieldWithoutSetter = mockElement(ElementKind.FIELD, fieldWithoutSetterName)
val clazz = mockClazz(fieldWithSetter, fieldWithoutSetter)
val expected = listOf(fieldWithSetter)

// when
val actual = FieldFinder.getFieldsFromClazz(clazz)

// then
assertThat(actual).isEqualTo(expected)
}
}
}

private fun mockElement(elementKind: ElementKind, name: String, shouldBeIgnored: Boolean = false): Element {
return mockElement(null, elementKind, name, shouldBeIgnored)
}

private fun mockElement(annotation: String?, elementKind: ElementKind, name: String, shouldBeIgnored: Boolean = false): Element {
val element: Element = mockk<Element>()

every { element.kind } returns elementKind
every { element.simpleName } returns TestName(name)
every { element.annotationMirrors } returns
if (annotation == null) mutableListOf()
else mutableListOf(mockAnnotationMirror(annotation))
every { element.getAnnotation(any<Class<DSLIgnore>>()) } returns if (shouldBeIgnored) mockk<DSLIgnore>() else null

return element
}

private fun mockClazz(vararg field: Element): TypeElement {
return mockClazz(null, *field)
}

private fun mockClazz(annotation: String? = null, vararg field: Element): TypeElement {
val typeElement = mockk<TypeElement>()
every { typeElement.enclosedElements } returns field.toList()
every { typeElement.annotationMirrors } returns
if (annotation == null) mutableListOf()
else mutableListOf(mockAnnotationMirror(annotation))
every { typeElement.getAnnotation(any<Class<DSLIgnore>>()) } returns null

return typeElement
}

private fun mockAnnotationMirror(annotation: String): AnnotationMirror {
val annotationMirror = mockk<AnnotationMirror>()
every { annotationMirror.annotationType } returns mockAnnotationType(annotation)

return annotationMirror
}

private fun mockAnnotationType(annotation: String): DeclaredType {
val declaredType = mockk<DeclaredType>()
every { declaredType.toString() } returns annotation

return declaredType
}
}
Loading

0 comments on commit 12e1480

Please sign in to comment.