Skip to content

Commit

Permalink
Merge pull request #106 from Virelion/feature/ISSUE_54/dynamic_access…
Browse files Browse the repository at this point in the history
…_with_path

ISSUE-54: Dynamic access with path
  • Loading branch information
Virelion committed Sep 12, 2022
2 parents 1a27fe6 + 3f14206 commit fe889e7
Show file tree
Hide file tree
Showing 44 changed files with 1,169 additions and 184 deletions.
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,22 +53,28 @@ root.withPath().branch.leaf.path().jsonPath // will return "$.branch.leaf"

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

# Dynamic access
# [Dynamic access](docs/dynamic-access.md)

All `@Buildable` classes can be dynamically accessed.

Annotate class:
```kotlin
@DynamicallyAccessible
@Buildable
data class Item(
val value: String
val value: String,
val list: List<Map<String,String>>
// ...
)
```

and access data dynamically with generated accessors:
```kotlin
item.dynamicAccessor["value"] // returns item.value
item.dynamicAccessor["$.list[2]['element']"] // returns item.list[2]["element"]
```

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

# How to set up?
0. Have open source repositories connected to project:
```kotlin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import com.google.devtools.ksp.processing.SymbolProcessor
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

Expand Down Expand Up @@ -67,7 +66,7 @@ class BuildataSymbolProcessor(

// Stream Dynamically accessible code-generated classes
resolver
.getSymbolsWithAnnotation(DYNAMICALLY_ACCESSIBLE_FQNAME)
.getSymbolsWithAnnotation(BUILDABLE_FQNAME)
.apply {
filterIsInstance<KSClassDeclaration>()
.map {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package io.github.virelion.buildata.ksp

import io.github.virelion.buildata.ksp.extensions.typeForDocumentation
import io.github.virelion.buildata.ksp.path.StringNamePathIdentifier
import io.github.virelion.buildata.ksp.utils.CodeBuilder

internal class BuilderClassTemplate(
Expand Down Expand Up @@ -120,14 +121,16 @@ internal class BuilderClassTemplate(
propertiesDocumentation
)
appendln("@BuildataDSL")
indentBlock("class $builderName() : Builder<$originalName>") {
indentBlock("class $builderName() : Builder<$originalName>, StringAccessible") {
properties.forEach {
it.generatePropertyDeclaration(this)
}
emptyLine()
generateBuildFunction()
emptyLine()
generateBuilderPopulateWithFunction()
emptyLine()
generateAccessElementFunction()
}
}

Expand Down Expand Up @@ -183,9 +186,31 @@ internal class BuilderClassTemplate(
}
}

fun CodeBuilder.generateAccessElementFunction() {
appendDocumentation(
"""
Access class element builder using string identifier
@param key element name
@returns element, elements builder or null if not set
""".trimIndent()
)
indentBlock("override fun accessElement(key: String): Any?") {
indentBlock("return when(key)") {
properties.forEach {
val pathIdentifier = StringNamePathIdentifier(it).getPropertyName()
appendln(""""$pathIdentifier" -> ${it.generateDirectAccessLine()}""")
}
appendln("""else -> throw MissingPropertyException(key, "$originalName")""")
}
}
}

companion object {
val imports: List<String> = listOf(
"io.github.virelion.buildata.*",
"io.github.virelion.buildata.access.MissingPropertyException",
"io.github.virelion.buildata.access.StringAccessible",
"kotlin.reflect.KClass"
).sorted()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,16 @@ class ClassProperty(
appendln("$name = it.$name")
}
}

fun generateDirectAccessLine(): String {
return if (buildable) {
if (nullable) {
"if($backingPropName.setToNull) null else $backingPropName.builder"
} else {
"$backingPropName.builder"
}
} else {
"$backingPropName.container"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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 @@ -15,7 +15,6 @@
*/
package io.github.virelion.buildata.ksp

import com.google.devtools.ksp.getDeclaredProperties
import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSNode
Expand All @@ -34,8 +33,7 @@ internal class KSClassDeclarationProcessor(
ksClassDeclaration.apply {
return AccessorExtensionsTemplate(
pkg = this.packageName.asString(),
originalName = this.simpleName.getShortName(),
properties = ksClassDeclaration.getDeclaredProperties().map { it.simpleName.asString() }.toList()
originalName = this.simpleName.getShortName()
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ import io.github.virelion.buildata.ksp.utils.CodeBuilder

class AccessorExtensionsTemplate(
override val pkg: String,
val originalName: String,
val properties: List<String>
val originalName: String
) : GeneratedFileTemplate {
override val name: String get() = "${originalName}_AccessorExtension"

Expand All @@ -34,67 +33,24 @@ class AccessorExtensionsTemplate(
}
emptyLine()
createAccessorExtensionProperty()
emptyLine()
createPropertyAccessExtensionFunction()
emptyLine()
createPropertyValueAccessExtensionFunction()
}
}

private fun CodeBuilder.createAccessorExtensionProperty() {
appendDocumentation(
"""
Accessor that allows for dynamic access (via property name) to properties and values.
Accessor that allows for dynamic access (via property name or path) to properties and values.
""".trimIndent()
)
appendln("val $originalName.dynamicAccessor: DynamicAccessor<$originalName> get() = DynamicAccessor(this)")
}

private fun CodeBuilder.createPropertyAccessExtensionFunction() {
appendDocumentation(
"""
Get property of class under specified name
@param name property name
@throws [io.github.virelion.buildata.access.MissingPropertyException] if property is missing
@returns property object or null in case it is missing
""".trimIndent()
)
indentBlock("fun DynamicAccessor<$originalName>.getProperty(name: String): KProperty0<*>?") {
indentBlock("return when(name)") {
properties.forEach { propertyName ->
appendln(""""$propertyName" -> this.target::`$propertyName`""")
}
appendln("""else -> throw MissingPropertyException(name, "$pkg.$originalName")""")
}
}
}

private fun CodeBuilder.createPropertyValueAccessExtensionFunction() {
appendDocumentation(
"""
Get value of property under specified name
@param name property name
@throws [io.github.virelion.buildata.access.MissingPropertyException] if property is missing
@returns property value
""".trimIndent()
)
indentBlock("operator fun <T> DynamicAccessor<$originalName>.get(name: String): T") {
indentBlock("return when(name)") {
properties.forEach { propertyName ->
appendln(""""$propertyName" -> this.target.`$propertyName`""")
}
appendln("""else -> throw MissingPropertyException(name, "$pkg.$originalName")""")
}
append(" as T")
appendln("val $originalName.dynamicAccessor: DynamicAccessor get() =")
indent {
appendln("DynamicAccessor($originalName::class.builder().also { it.populateWith(this) })")
}
}

companion object {
val imports: List<String> = listOf(
"io.github.virelion.buildata.access.*",
"kotlin.reflect.KProperty0"
"io.github.virelion.buildata.access.*"
).sorted()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ value class StringNamePathIdentifier(
val JACKSON_ALIAS = "com.fasterxml.jackson.annotation.JsonAlias"
}

private fun getPropertyName(): String {
fun getPropertyName(): String {
return getAnnotatedName() ?: classProperty.name
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import kotlin.reflect.KProperty
class BuilderElementProperty<T> : ReadWriteProperty<Any?, T> {
var initialized = false
private set
private var container: T? = null
var container: T? = null

/**
* Sets property value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import kotlin.reflect.KProperty
class BuilderNullableCompositeElementProperty<T : Any, B : Builder<T>>(
val builderProvider: () -> B
) : ReadWriteProperty<Any?, T?> {
private var setToNull = false
var setToNull = false
var initialized = false
private set
var builder: B = builderProvider()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,137 @@
*/
package io.github.virelion.buildata.access

import io.github.virelion.buildata.Builder
import io.github.virelion.buildata.path.IntIndexPathIdentifier
import io.github.virelion.buildata.path.PathIdentifier
import io.github.virelion.buildata.path.RecordedPath
import io.github.virelion.buildata.path.StringIndexPathIdentifier
import io.github.virelion.buildata.path.StringNamePathIdentifier

/**
* Marker for code-generated class dynamic elements access.
*/
class DynamicAccessor<T>(val target: T)
class DynamicAccessor(private val builder: Builder<*>) {
@Throws(ElementNotFoundException::class)
operator fun <R> get(path: String): R? {
return get(*RecordedPath.Parser.parse(path).path.toTypedArray())
}

@Throws(ElementNotFoundException::class)
operator fun <R> get(vararg path: PathIdentifier): R? {
if (path.isEmpty()) return builder.build() as R

val element: PathAccessProcessingAccumulator = path
.fold(PathAccessProcessingAccumulator(builder, listOf())) { acc, pathIdentifier ->
resolvePathIdentifier(pathIdentifier, acc)
}

if (element.item is Builder<*>) {
return element.item.build() as R
}

return element.item as R
}

private data class PathAccessProcessingAccumulator(
val item: Any?,
val pathProcessed: List<PathIdentifier>
)

private fun resolvePathIdentifier(
pathElement: PathIdentifier,
acc: PathAccessProcessingAccumulator
): PathAccessProcessingAccumulator {
return try {
when (pathElement) {
is IntIndexPathIdentifier -> resolveIntIndexPathIdentifier(pathElement, acc)
is StringIndexPathIdentifier -> resolveStringIndexPathIdentifier(pathElement, acc)
is StringNamePathIdentifier -> resolveStringNamePathIdentifier(pathElement, acc)
}
} catch (e: Exception) { handleException(e, pathElement, acc) }
}

private fun resolveIntIndexPathIdentifier(
pathElement: IntIndexPathIdentifier,
acc: PathAccessProcessingAccumulator
): PathAccessProcessingAccumulator {
return when (acc.item) {
is List<*> -> {
// additional check for KotlinJS
if (pathElement.index >= acc.item.size) throw IndexOutOfBoundsException()
acc.item[pathElement.index]
}
is Array<*> -> {
// additional check for KotlinJS
if (pathElement.index >= acc.item.size) throw IndexOutOfBoundsException()
acc.item[pathElement.index]
}
is Map<*, *> -> {
if (pathElement.index !in acc.item) throw IndexOutOfBoundsException()
acc.item[pathElement.index]
}
is IntAccessible -> acc.item.accessElement(pathElement.index)
else -> throw ElementNotFoundException(
pathProcessed = RecordedPath(acc.pathProcessed),
lastItemProcessed = acc.item,
lastProcessedPathIdentifier = pathElement
)
}.let { PathAccessProcessingAccumulator(it, acc.pathProcessed + pathElement) }
}

private fun resolveStringIndexPathIdentifier(
pathElement: StringIndexPathIdentifier,
acc: PathAccessProcessingAccumulator
): PathAccessProcessingAccumulator {
return when (acc.item) {
is Map<*, *> -> {
if (pathElement.index !in acc.item) throw IndexOutOfBoundsException()
acc.item[pathElement.index]
}
is StringAccessible -> acc.item.accessElement(pathElement.index)
else -> throw ElementNotFoundException(
pathProcessed = RecordedPath(acc.pathProcessed),
lastItemProcessed = acc.item,
lastProcessedPathIdentifier = pathElement
)
}.let { PathAccessProcessingAccumulator(it, acc.pathProcessed + pathElement) }
}

private fun resolveStringNamePathIdentifier(
pathElement: StringNamePathIdentifier,
acc: PathAccessProcessingAccumulator
): PathAccessProcessingAccumulator {
if (acc.item !is StringAccessible) {
throw ElementNotFoundException(
pathProcessed = RecordedPath(acc.pathProcessed),
lastItemProcessed = acc.item,
lastProcessedPathIdentifier = pathElement
)
}
return acc.item.accessElement(pathElement.name)
.let { PathAccessProcessingAccumulator(it, acc.pathProcessed + pathElement) }
}

private fun handleException(
e: Exception,
pathElement: PathIdentifier,
acc: PathAccessProcessingAccumulator
): Nothing {
when (e) {
is MissingPropertyException, is IndexOutOfBoundsException -> {
val item = if (acc.item is Builder<*>) {
acc.item.build()
} else {
acc.item
}

throw ElementNotFoundException(
pathProcessed = RecordedPath(acc.pathProcessed),
lastItemProcessed = item,
lastProcessedPathIdentifier = pathElement
)
}
else -> throw e
}
}
}

0 comments on commit fe889e7

Please sign in to comment.