Skip to content

Commit

Permalink
Merge pull request #49 from Virelion/path_support
Browse files Browse the repository at this point in the history
Path support
  • Loading branch information
Virelion committed Sep 26, 2021
2 parents 4e74fe5 + 2ada92c commit a7ffcf1
Show file tree
Hide file tree
Showing 38 changed files with 1,861 additions and 243 deletions.
137 changes: 58 additions & 79 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,64 @@
![version](https://img.shields.io/github/v/tag/Virelion/buildata)
![last-commit](https://img.shields.io/github/last-commit/Virelion/buildata)

Kotlin multiplatform builder generator.
Kotlin multiplatform code-generator for typed tree data class structures.

# How to use?
# [Builder generator](docs/data-tree-building.md)
Generate builders for your immutable data classes.
Annotate class:
```kotlin
@Buildable
data class Root(
//...
)
```

and use builders:
```kotlin
Root::class.build {
branch {
leaf = "My value"
}
}
```

See more in [data-tree-building.md](docs/data-tree-building.md)

# [Path reflection](docs/path-reflection.md)
Generate builders for your immutable data classes.

Annotate class:
```kotlin
@PathReflection
data class Root(
//...
)
```

and automatically gather information about the path to the value:
```kotlin
root.withPath().branch.leaf.path().jsonPath // will return "$.branch.leaf"
```

See more in [path-reflection.md](docs/path-reflection.md)

# Dynamic access

Annotate class:
```kotlin
@DynamicallyAccessible
data class Item(
val value: String
// ...
)
```

and access data dynamically with generated accessors:
```kotlin
item.dynamicAccessor["value"] // returns item.value
```

# How to set up?
0. Have open source repositories connected to project:
```kotlin
buildscript {
Expand Down Expand Up @@ -54,80 +109,4 @@ kotlin {
// ...
}
}
```

3. Add annotation to your `data class`
```kotlin

import io.github.virelion.buildata.Buildable
// ...

@Buildable
data class MyDataClass(
val property: String
)
```

4. Codegen will generate a builder you can use:
```kotlin
val myDataClass = MyDataClass::class.build {
property = "Example"
}
```
```kotlin
val builder = MyDataClass::class.builder()
val myDataClass = builder {
property = "Example"
}.build()
```

# Features
## Default values support
Builders will honor default values during the building.
```kotlin
@Buildable
data class MyDataClass(
val propertyWithDefault: String = "DEFAULT",
val property: String
)
```

```kotlin
val myDataClass = MyDataClass::class.build {
property = "Example"
}

myDataClass.property // returns "Example"
myDataClass.propertyWithDefault // returns "DEFAULT"
```

## Composite builders support
You can mark item as `@Buildable` to allow accessing inner builders.

```kotlin
@Buildable
data class InnerItem(
val property: String
)

@Buildable
data class ParentDataClass(
val innerItem: @Buildable InnerItem
)
```

```kotlin
val parentDataClassBuilder = ParentDataClass::class.builder()

parentDataClassBuilder {
innerItem {
property = "Example"
}
}

val parentDataClass = parentDataClassBuilder.build()
parentDataClass.innerItem.property // returns "Example"
```

This feature allows for creating complex builder structures for tree like `data class`
and make mutation easy during tree building process.
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.github.virelion.buildata.ksp

data class AnnotatedClasses(
val buildable: Set<String>,
val pathReflection: Set<String>
)
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSClassDeclaration
import io.github.virelion.buildata.ksp.Constants.BUILDABLE_FQNAME
import io.github.virelion.buildata.ksp.Constants.DYNAMICALLY_ACCESSIBLE_FQNAME
import io.github.virelion.buildata.ksp.Constants.PATH_REFLECTION_FQNAME
import io.github.virelion.buildata.ksp.extensions.printableFqName

class BuildataSymbolProcessor(
val logger: KSPLogger,
Expand All @@ -20,22 +22,36 @@ class BuildataSymbolProcessor(
override fun process(resolver: Resolver): List<KSAnnotated> {
logger.info("BuildataSymbolProcessor processing started")

val classProcessor = KSClassDeclarationProcessor(logger)
val streamer = PackageStreamer(buildataCodegenDir)

// Stream Buildable code-generated classes
val buildableAnnotated = resolver
.getSymbolsWithAnnotation(BUILDABLE_FQNAME)
.apply {
filterIsInstance<KSClassDeclaration>()
.map {
classProcessor.processBuilderClasses(it)
}
.forEach(streamer::consume)
}
.filterIsInstance<KSClassDeclaration>()
.toList()

val pathReflectionAnnotated = resolver
.getSymbolsWithAnnotation(PATH_REFLECTION_FQNAME)
.filterIsInstance<KSClassDeclaration>()
.toList()

val annotatedClasses = AnnotatedClasses(
buildableAnnotated.map { it.printableFqName }.toSet(),
pathReflectionAnnotated.map { it.printableFqName }.toSet()
)

val classProcessor = KSClassDeclarationProcessor(logger, annotatedClasses)

buildableAnnotated.forEach {
streamer.consume(classProcessor.processBuilderClasses(it))
}

pathReflectionAnnotated.forEach {
streamer.consume(classProcessor.processPathWrapperClasses(it))
}

// Stream Dynamically accessible code-generated classes
val dynamicAccessorAnnotated = resolver
resolver
.getSymbolsWithAnnotation(DYNAMICALLY_ACCESSIBLE_FQNAME)
.apply {
filterIsInstance<KSClassDeclaration>()
Expand All @@ -45,6 +61,6 @@ class BuildataSymbolProcessor(
.forEach(streamer::consume)
}

return buildableAnnotated.toList() + dynamicAccessorAnnotated.toList()
return listOf()
}
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
package io.github.virelion.buildata.ksp

import com.google.devtools.ksp.symbol.KSAnnotation
import com.google.devtools.ksp.symbol.KSType
import io.github.virelion.buildata.ksp.extensions.classFQName
import io.github.virelion.buildata.ksp.extensions.className
import io.github.virelion.buildata.ksp.extensions.typeFQName
import io.github.virelion.buildata.ksp.utils.CodeBuilder

internal class ClassProperty(
class ClassProperty(
val name: String,
val type: KSType,
val hasDefaultValue: Boolean,
val nullable: Boolean,
val buildable: Boolean
val annotations: Sequence<KSAnnotation>,
val buildable: Boolean,
val pathReflection: Boolean
) {
val backingPropName = "${name}_Element"

fun generatePropertyDeclaration(codeBuilder: CodeBuilder) {
codeBuilder.build {
if (buildable) {
val builderName = BuilderClassTemplate.createBuilderName(type.classFQName())
val builderName = BuilderClassTemplate.createBuilderName(type.className())
val elementPropName = if (nullable) {
"BuilderNullableCompositeElementProperty"
} else {
"BuilderCompositeElementProperty"
}
appendln("private val $backingPropName = $elementPropName<${type.classFQName()}, $builderName> { $builderName() }")
appendln("private val $backingPropName = $elementPropName<${type.className()}, $builderName> { $builderName() }")
appendln("var $name by $backingPropName")
appendln("@BuildataDSL")
indentBlock("fun $name(block: $builderName.() -> Unit)") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ package io.github.virelion.buildata.ksp
object Constants {
val BUILDABLE_FQNAME = "io.github.virelion.buildata.Buildable"
val DYNAMICALLY_ACCESSIBLE_FQNAME = "io.github.virelion.buildata.access.DynamicallyAccessible"
val PATH_REFLECTION_FQNAME = "io.github.virelion.buildata.path.PathReflection"
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import com.google.devtools.ksp.symbol.KSNode
import com.google.devtools.ksp.symbol.Modifier
import com.google.devtools.ksp.symbol.Nullability
import io.github.virelion.buildata.ksp.access.AccessorExtensionsTemplate
import io.github.virelion.buildata.ksp.extensions.classFQName
import io.github.virelion.buildata.ksp.extensions.printableFqName
import io.github.virelion.buildata.ksp.extensions.typeFQName
import io.github.virelion.buildata.ksp.path.PathPropertyWrapperTemplate

internal class KSClassDeclarationProcessor(
val logger: KSPLogger
val logger: KSPLogger,
val annotatedClasses: AnnotatedClasses
) {
fun processAccessorClasses(ksClassDeclaration: KSClassDeclaration): AccessorExtensionsTemplate {
ksClassDeclaration.apply {
Expand All @@ -23,7 +25,9 @@ internal class KSClassDeclarationProcessor(
}
}

fun processBuilderClasses(ksClassDeclaration: KSClassDeclaration): BuilderClassTemplate {
fun processBuilderClasses(
ksClassDeclaration: KSClassDeclaration
): BuilderClassTemplate {
ksClassDeclaration.apply {
if (Modifier.DATA !in this.modifiers) {
error("Cannot add create builder for non data class", this)
Expand All @@ -36,21 +40,36 @@ internal class KSClassDeclarationProcessor(
}
}

fun processPathWrapperClasses(
ksClassDeclaration: KSClassDeclaration
): PathPropertyWrapperTemplate {
ksClassDeclaration.apply {
if (Modifier.DATA !in this.modifiers) {
error("Cannot add create builder for non data class", this)
}
return PathPropertyWrapperTemplate(
pkg = this.packageName.asString(),
originalName = this.simpleName.getShortName(),
properties = getClassProperties()
)
}
}

fun KSClassDeclaration.getClassProperties(): List<ClassProperty> {
return requireNotNull(primaryConstructor, this) { "$printableFqName needs to have primary constructor to be @Buildable" }
.parameters
.filter { it.isVar || it.isVal }
.map { parameter ->
val type = parameter.type.resolve()

ClassProperty(
name = requireNotNull(parameter.name?.getShortName(), parameter) { "$printableFqName contains nameless property" },
type = type,
hasDefaultValue = parameter.hasDefault,
nullable = type.nullability == Nullability.NULLABLE,
buildable = type.annotations
.any { annotation ->
annotation.annotationType.resolve().typeFQName() == Constants.BUILDABLE_FQNAME
}
annotations = parameter.annotations,
buildable = (type.classFQName() in annotatedClasses.buildable),
pathReflection = (type.classFQName() in annotatedClasses.pathReflection)
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,34 @@
package io.github.virelion.buildata.ksp.extensions

import com.google.devtools.ksp.innerArguments
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.Nullability

fun KSType.typeFQName(): String {
return "${declaration.qualifiedName!!.getQualifier()}.${classFQName()}${nullability.toCode()}"
val genericTypes = if (this.innerArguments.isNotEmpty()) {
this.innerArguments.mapNotNull { it.type?.resolve()?.typeFQName() }
.joinToString(prefix = "<", postfix = ">", separator = ", ")
} else ""
return "${declaration.qualifiedName!!.getQualifier()}.${className()}$genericTypes${nullability.toCode()}"
}

fun KSType.classFQName(): String {
return "${declaration.qualifiedName!!.getQualifier()}.${className()}"
}

fun KSType.className(): String {
return this.declaration.qualifiedName!!.getShortName()
}

fun KSType.typeForDocumentation(): String {
return "[${declaration.qualifiedName!!.getQualifier()}.${classFQName()}]${nullability.toCode()}"
return "[${declaration.qualifiedName!!.getQualifier()}.${className()}]${nullability.toCode()}"
}

val KSClassDeclaration.printableFqName: String get() {
return this.packageName.asString() + "." + this.simpleName.getShortName()
}
val KSClassDeclaration.printableFqName: String
get() {
return this.packageName.asString() + "." + this.simpleName.getShortName()
}

fun Nullability.toCode(): String {
return when (this) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.github.virelion.buildata.ksp.path

enum class CollectionType {
LIST, STRING
}
Loading

0 comments on commit a7ffcf1

Please sign in to comment.