Skip to content

Commit 7dfef1f

Browse files
authored
close #1, #3
1 parent aa4d1fe commit 7dfef1f

13 files changed

+452
-34
lines changed

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# implier
44

5-
Kotlin Symbol Processor plugin to create `Mutable` and `Immutable` variants of objects.
5+
Kotlin Symbol Processor plugin for creating [**Mutable**](https://github.com/y9vad9/implier/blob/fb5cba3c62defe23ce5773287fc9f37367d800fd/src/main/kotlin/com/y9vad9/implier/annotations.kt#L10), [**Immutable**](https://github.com/y9vad9/implier/blob/fb5cba3c62defe23ce5773287fc9f37367d800fd/src/main/kotlin/com/y9vad9/implier/annotations.kt#L18), [**Builders**](https://github.com/y9vad9/implier/blob/fb5cba3c62defe23ce5773287fc9f37367d800fd/src/main/kotlin/com/y9vad9/implier/annotations.kt#L35), [**DSL Builders**](https://github.com/y9vad9/implier/blob/1.0.1/src/main/kotlin/com/y9vad9/implier/annotations.kt#L50) from interfaces & abstract classes with properties.
66

77
## Examples
88

@@ -17,15 +17,15 @@ public interface Sample {
1717
Will generate next classes and functions:
1818

1919
```kotlin
20-
public fun Sample.toImmutable(): ImmutableSample = ImmutableSample(sample)
20+
fun Sample.toImmutable(): ImmutableSample = ImmutableSample(sample)
2121

22-
public class ImmutableSample(
22+
class ImmutableSample(
2323
public override val sample: String
2424
) : Sample
2525

26-
public fun Sample.toMutable(): MutableSample = MutableSample(sample)
26+
fun Sample.toMutable(): MutableSample = MutableSample(sample)
2727

28-
public class MutableSample(
28+
class MutableSample(
2929
public override var sample: String
3030
) : Sample
3131
```
Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,49 @@
11
package com.y9vad9.implier
22

33
import com.google.devtools.ksp.KspExperimental
4+
import com.google.devtools.ksp.getAnnotationsByType
45
import com.google.devtools.ksp.isAnnotationPresent
56
import com.google.devtools.ksp.processing.CodeGenerator
6-
import com.google.devtools.ksp.processing.Dependencies
77
import com.google.devtools.ksp.symbol.KSClassDeclaration
88
import com.google.devtools.ksp.symbol.KSVisitorVoid
9-
import java.io.OutputStreamWriter
9+
import com.y9vad9.implier.annotations.processor.*
1010

1111
class AnnotationsVisitor(private val codeGenerator: CodeGenerator) : KSVisitorVoid() {
1212
@OptIn(KspExperimental::class)
1313
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
1414
super.visitAnnotated(classDeclaration, data)
1515
if (classDeclaration.isAnnotationPresent(MutableImpl::class)) {
16-
codeGenerator.createNewFile(
17-
Dependencies(false),
18-
classDeclaration.packageName.asString(),
19-
"Mutable${classDeclaration.simpleName.asString()}"
20-
).use { output ->
21-
OutputStreamWriter(output).use { writer ->
22-
generateVariant("Mutable", true, classDeclaration).writeTo(writer)
23-
}
24-
}
16+
MutableAnnotatedClassProcessor.process(
17+
classDeclaration.getAnnotationsByType(MutableImpl::class).first(),
18+
codeGenerator,
19+
classDeclaration
20+
)
2521
}
2622
if (classDeclaration.isAnnotationPresent(ImmutableImpl::class)) {
27-
codeGenerator.createNewFile(
28-
Dependencies(false),
29-
classDeclaration.packageName.asString(),
30-
"Immutable${classDeclaration.simpleName.asString()}"
31-
).use { output ->
32-
OutputStreamWriter(output).use { writer ->
33-
generateVariant("Immutable", false, classDeclaration).writeTo(writer)
34-
}
35-
}
23+
ImmutableAnnotatedClassProcessor.process(
24+
classDeclaration.getAnnotationsByType(ImmutableImpl::class).first(), codeGenerator, classDeclaration
25+
)
26+
}
27+
if (classDeclaration.isAnnotationPresent(FactoryFunctionImpl::class)) {
28+
FactoryFunctionAnnotatedClassProcessor.process(
29+
classDeclaration.getAnnotationsByType(FactoryFunctionImpl::class).first(),
30+
codeGenerator,
31+
classDeclaration
32+
)
33+
}
34+
if (classDeclaration.isAnnotationPresent(BuilderImpl::class)) {
35+
BuilderImplAnnotatedClassProcessor.process(
36+
classDeclaration.getAnnotationsByType(BuilderImpl::class).first(),
37+
codeGenerator,
38+
classDeclaration
39+
)
40+
}
41+
if (classDeclaration.isAnnotationPresent(DSLImpl::class)) {
42+
DSLImplAnnotatedClassProcessor.process(
43+
classDeclaration.getAnnotationsByType(DSLImpl::class).first(),
44+
codeGenerator,
45+
classDeclaration
46+
)
3647
}
3748
}
3849
}

ksp/src/main/kotlin/com/y9vad9/implier/ImplierAnnotationProcessor.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ class ImplierAnnotationProcessor(private val codeGenerator: CodeGenerator) : Sym
1515
override fun process(resolver: Resolver): List<KSAnnotated> {
1616
val annotations: Sequence<KSDeclaration> =
1717
resolver.getAllFiles().flatMap { it.declarations }.filter {
18-
it.isAnnotationPresent(MutableImpl::class) || it.isAnnotationPresent(ImmutableImpl::class)
18+
it.isAnnotationPresent(MutableImpl::class)
19+
|| it.isAnnotationPresent(ImmutableImpl::class)
20+
|| it.isAnnotationPresent(FactoryFunctionImpl::class
21+
)
1922
}
2023

2124
if (invoked) {
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.y9vad9.implier.annotations.processor
2+
3+
import com.google.devtools.ksp.processing.CodeGenerator
4+
import com.google.devtools.ksp.symbol.KSClassDeclaration
5+
6+
interface AnnotatedClassProcessor<T : Annotation> {
7+
fun process(annotation: T, codeGenerator: CodeGenerator, classDeclaration: KSClassDeclaration)
8+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package com.y9vad9.implier.annotations.processor
2+
3+
import com.google.devtools.ksp.KspExperimental
4+
import com.google.devtools.ksp.isAnnotationPresent
5+
import com.google.devtools.ksp.processing.CodeGenerator
6+
import com.google.devtools.ksp.processing.Dependencies
7+
import com.google.devtools.ksp.symbol.KSClassDeclaration
8+
import com.squareup.kotlinpoet.*
9+
import com.y9vad9.implier.BuilderImpl
10+
import com.y9vad9.implier.ImmutableImpl
11+
import com.y9vad9.implier.MutableImpl
12+
import java.io.OutputStreamWriter
13+
import java.util.*
14+
15+
object BuilderImplAnnotatedClassProcessor : AnnotatedClassProcessor<BuilderImpl> {
16+
@OptIn(KspExperimental::class)
17+
override fun process(annotation: BuilderImpl, codeGenerator: CodeGenerator, classDeclaration: KSClassDeclaration) {
18+
if (!(classDeclaration.isAnnotationPresent(ImmutableImpl::class)
19+
&& classDeclaration.isAnnotationPresent(MutableImpl::class))
20+
)
21+
throw IllegalStateException("Unable to create builder implementation without Immutable / Mutable implementations.")
22+
codeGenerator.createNewFile(
23+
Dependencies(false),
24+
classDeclaration.packageName.asString(),
25+
classDeclaration.simpleName.asString().plus("Builder")
26+
).use { output ->
27+
OutputStreamWriter(output).use { writer ->
28+
generateBuilderImplementation(
29+
type = annotation.type,
30+
initVariantCode = (if (!(classDeclaration.isAnnotationPresent(ImmutableImpl::class))) "Immutable" else "Mutable").plus(
31+
classDeclaration.simpleName.asString()
32+
),
33+
classDeclaration
34+
).writeTo(writer)
35+
}
36+
}
37+
}
38+
}
39+
40+
private fun generateBuilderImplementation(
41+
type: BuilderImpl.Type,
42+
initVariantCode: String,
43+
declaration: KSClassDeclaration
44+
): FileSpec {
45+
val file = FileSpec.builder(
46+
declaration.packageName.asString(),
47+
declaration.simpleName.asString().plus("Builder")
48+
)
49+
val builderClassName = ClassName(declaration.packageName.asString(), declaration.simpleName.asString().plus("Builder"))
50+
val builderClass = TypeSpec.classBuilder(file.name)
51+
for (member in declaration.getAllProperties()) {
52+
val resolvedMember = member.type.resolve()
53+
val memberType = ClassName(
54+
resolvedMember.declaration.packageName.asString(),
55+
resolvedMember.declaration.simpleName.asString()
56+
)
57+
builderClass.addProperty(
58+
PropertySpec.builder(member.simpleName.asString(), memberType)
59+
.mutable(true)
60+
.delegate("kotlin.properties.Delegates.notNull()")
61+
.addModifiers(KModifier.PRIVATE)
62+
.build()
63+
)
64+
builderClass.addFunction(
65+
FunSpec.builder(
66+
if (type == BuilderImpl.Type.WITHOUT_ACCESSORS) member.simpleName.asString() else "set${
67+
member.simpleName.asString()
68+
.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
69+
}"
70+
).addParameter(
71+
"value",
72+
ClassName(
73+
resolvedMember.declaration.packageName.asString(),
74+
resolvedMember.declaration.simpleName.asString()
75+
)
76+
)
77+
.returns(builderClassName)
78+
.addCode("${member.simpleName.asString()} = value\n")
79+
.addCode("return this")
80+
.build()
81+
)
82+
}
83+
builderClass.addFunction(
84+
FunSpec.builder("build")
85+
.addCode(
86+
"return $initVariantCode(${
87+
declaration.getAllProperties().joinToString(",") { it.simpleName.asString() }
88+
})"
89+
)
90+
.returns(
91+
ClassName(
92+
declaration.packageName.asString(),
93+
declaration.simpleName.asString()
94+
)
95+
)
96+
.build()
97+
)
98+
file.addType(builderClass.build())
99+
return file.build()
100+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package com.y9vad9.implier.annotations.processor
2+
3+
import com.google.devtools.ksp.KspExperimental
4+
import com.google.devtools.ksp.isAnnotationPresent
5+
import com.google.devtools.ksp.processing.CodeGenerator
6+
import com.google.devtools.ksp.processing.Dependencies
7+
import com.google.devtools.ksp.symbol.KSClassDeclaration
8+
import com.squareup.kotlinpoet.*
9+
import com.y9vad9.implier.DSLImpl
10+
import com.y9vad9.implier.ImmutableImpl
11+
import com.y9vad9.implier.MutableImpl
12+
import java.io.OutputStreamWriter
13+
import java.util.*
14+
15+
object DSLImplAnnotatedClassProcessor : AnnotatedClassProcessor<DSLImpl> {
16+
@OptIn(KspExperimental::class)
17+
override fun process(annotation: DSLImpl, codeGenerator: CodeGenerator, classDeclaration: KSClassDeclaration) {
18+
if (!(classDeclaration.isAnnotationPresent(ImmutableImpl::class)
19+
&& classDeclaration.isAnnotationPresent(MutableImpl::class))
20+
)
21+
throw IllegalStateException("Unable to create builder implementation without Immutable / Mutable implementations.")
22+
codeGenerator.createNewFile(
23+
Dependencies(false),
24+
classDeclaration.packageName.asString(),
25+
classDeclaration.simpleName.asString().plus("BuilderScope")
26+
).use { output ->
27+
OutputStreamWriter(output).use { writer ->
28+
generateDSLImplementation(
29+
type = annotation.type,
30+
initVariantCode = (if (!(classDeclaration.isAnnotationPresent(ImmutableImpl::class))) "Immutable" else "Mutable").plus(
31+
classDeclaration.simpleName.asString()
32+
),
33+
functionName = annotation.functionName,
34+
declaration = classDeclaration
35+
).writeTo(writer)
36+
}
37+
}
38+
}
39+
}
40+
41+
private fun generateDSLImplementation(
42+
type: DSLImpl.Type,
43+
functionName: String,
44+
initVariantCode: String,
45+
declaration: KSClassDeclaration
46+
): FileSpec {
47+
val name = declaration.simpleName.asString().plus("BuilderScope")
48+
val file = FileSpec.builder(
49+
declaration.packageName.asString(),
50+
name
51+
)
52+
val builderClassName = ClassName(declaration.packageName.asString(), name)
53+
val builderClass = TypeSpec.classBuilder(file.name)
54+
for (member in declaration.getAllProperties()) {
55+
val resolvedMember = member.type.resolve()
56+
val memberType = ClassName(
57+
resolvedMember.declaration.packageName.asString(),
58+
resolvedMember.declaration.simpleName.asString()
59+
)
60+
when (type) {
61+
DSLImpl.Type.PROPERTY_ACCESS -> builderClass.addProperty(
62+
PropertySpec.builder(member.simpleName.asString(), memberType)
63+
.mutable(true)
64+
.delegate("kotlin.properties.Delegates.notNull()")
65+
.build()
66+
)
67+
else -> {
68+
builderClass.addProperty(
69+
PropertySpec.builder(member.simpleName.asString(), memberType)
70+
.mutable(true)
71+
.delegate("kotlin.properties.Delegates.notNull()")
72+
.addModifiers(KModifier.PUBLIC)
73+
.build()
74+
)
75+
builderClass.addFunction(
76+
FunSpec.builder(
77+
if (type == DSLImpl.Type.WITHOUT_ACCESSORS) member.simpleName.asString() else "set${
78+
member.simpleName.asString()
79+
.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
80+
}"
81+
).addParameter(
82+
"value",
83+
ClassName(
84+
resolvedMember.declaration.packageName.asString(),
85+
resolvedMember.declaration.simpleName.asString()
86+
)
87+
)
88+
.returns(builderClassName)
89+
.addCode("${member.simpleName.asString()} = value\n")
90+
.addCode("return this")
91+
.build()
92+
)
93+
}
94+
}
95+
}
96+
file.addFunction(
97+
FunSpec.builder(functionName)
98+
.addParameter(
99+
"block",
100+
LambdaTypeName.get(receiver = builderClassName, returnType = Unit::class.asTypeName())
101+
)
102+
.addCode(
103+
"""
104+
val dslBuilder = $name()
105+
dslBuilder.apply(block)
106+
return $initVariantCode(${
107+
declaration.getAllProperties().joinToString(",") { "dslBuilder." + it.simpleName.asString() }
108+
})
109+
""".trimIndent()
110+
)
111+
.returns(
112+
ClassName(
113+
declaration.packageName.asString(),
114+
declaration.simpleName.asString()
115+
)
116+
)
117+
.build()
118+
)
119+
file.addType(builderClass.build())
120+
return file.build()
121+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.y9vad9.implier.annotations.processor
2+
3+
import com.google.devtools.ksp.KspExperimental
4+
import com.google.devtools.ksp.isAnnotationPresent
5+
import com.google.devtools.ksp.processing.CodeGenerator
6+
import com.google.devtools.ksp.processing.Dependencies
7+
import com.google.devtools.ksp.symbol.KSClassDeclaration
8+
import com.y9vad9.implier.*
9+
import com.y9vad9.implier.generateFactory
10+
import java.io.OutputStreamWriter
11+
12+
object FactoryFunctionAnnotatedClassProcessor : AnnotatedClassProcessor<FactoryFunctionImpl> {
13+
@OptIn(KspExperimental::class)
14+
override fun process(annotation: FactoryFunctionImpl, codeGenerator: CodeGenerator, classDeclaration: KSClassDeclaration) {
15+
val variantName = if(classDeclaration.isAnnotationPresent(ImmutableImpl::class))
16+
"Immutable"
17+
else if(classDeclaration.isAnnotationPresent(MutableImpl::class))
18+
"Mutable"
19+
else throw IllegalStateException(
20+
"Unable to create factory function for interface that does not have Mutable or Immutable realization"
21+
)
22+
if(classDeclaration.isAnnotationPresent(ImmutableImpl::class)) {
23+
codeGenerator.createNewFile(
24+
Dependencies(false),
25+
classDeclaration.packageName.asString(),
26+
classDeclaration.simpleName.asString().plus("Factory")
27+
).use { output ->
28+
OutputStreamWriter(output).use { writer ->
29+
generateFactory(variantName, classDeclaration).writeTo(writer)
30+
}
31+
}
32+
}
33+
}
34+
}

0 commit comments

Comments
 (0)