diff --git a/README.md b/README.md index 12d9ddc..b038104 100644 --- a/README.md +++ b/README.md @@ -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 { @@ -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. +``` \ No newline at end of file diff --git a/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/AnnotatedClasses.kt b/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/AnnotatedClasses.kt new file mode 100644 index 0000000..25a88c5 --- /dev/null +++ b/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/AnnotatedClasses.kt @@ -0,0 +1,6 @@ +package io.github.virelion.buildata.ksp + +data class AnnotatedClasses( + val buildable: Set, + val pathReflection: Set +) diff --git a/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/BuildataSymbolProcessor.kt b/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/BuildataSymbolProcessor.kt index b3de2c9..8f65ddf 100644 --- a/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/BuildataSymbolProcessor.kt +++ b/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/BuildataSymbolProcessor.kt @@ -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, @@ -20,22 +22,36 @@ class BuildataSymbolProcessor( override fun process(resolver: Resolver): List { 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() - .map { - classProcessor.processBuilderClasses(it) - } - .forEach(streamer::consume) - } + .filterIsInstance() + .toList() + + val pathReflectionAnnotated = resolver + .getSymbolsWithAnnotation(PATH_REFLECTION_FQNAME) + .filterIsInstance() + .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() @@ -45,6 +61,6 @@ class BuildataSymbolProcessor( .forEach(streamer::consume) } - return buildableAnnotated.toList() + dynamicAccessorAnnotated.toList() + return listOf() } } diff --git a/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/ClassProperty.kt b/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/ClassProperty.kt index a74191b..7323ffb 100644 --- a/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/ClassProperty.kt +++ b/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/ClassProperty.kt @@ -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, + 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)") { diff --git a/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/Constants.kt b/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/Constants.kt index f115aa6..7cb1e7c 100644 --- a/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/Constants.kt +++ b/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/Constants.kt @@ -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" } diff --git a/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/KSClassDeclarationProcessor.kt b/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/KSClassDeclarationProcessor.kt index 927f845..80d4550 100644 --- a/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/KSClassDeclarationProcessor.kt +++ b/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/KSClassDeclarationProcessor.kt @@ -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 { @@ -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) @@ -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 { 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) ) } } diff --git a/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/extensions/KSPExtensions.kt b/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/extensions/KSPExtensions.kt index 1cbc537..aa062b4 100644 --- a/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/extensions/KSPExtensions.kt +++ b/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/extensions/KSPExtensions.kt @@ -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) { diff --git a/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/path/CollectionType.kt b/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/path/CollectionType.kt new file mode 100644 index 0000000..5b7f1c8 --- /dev/null +++ b/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/path/CollectionType.kt @@ -0,0 +1,5 @@ +package io.github.virelion.buildata.ksp.path + +enum class CollectionType { + LIST, STRING +} diff --git a/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/path/PathPropertyWrapperTemplate.kt b/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/path/PathPropertyWrapperTemplate.kt new file mode 100644 index 0000000..f7ec797 --- /dev/null +++ b/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/path/PathPropertyWrapperTemplate.kt @@ -0,0 +1,210 @@ +package io.github.virelion.buildata.ksp.path + +import com.google.devtools.ksp.innerArguments +import com.google.devtools.ksp.symbol.KSType +import com.google.devtools.ksp.symbol.Nullability +import io.github.virelion.buildata.ksp.BuildataCodegenException +import io.github.virelion.buildata.ksp.ClassProperty +import io.github.virelion.buildata.ksp.GeneratedFileTemplate +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 +import io.github.virelion.buildata.ksp.utils.isList +import io.github.virelion.buildata.ksp.utils.isMapWithStringKey +import io.github.virelion.buildata.ksp.utils.isScalar +import io.github.virelion.buildata.ksp.utils.nullableIdentifier + +class PathPropertyWrapperTemplate( + override val pkg: String, + val originalName: String, + val properties: List +) : GeneratedFileTemplate { + companion object { + val imports: List = listOf( + "io.github.virelion.buildata.path.*", + "kotlin.reflect.KClass" + ).sorted() + } + + override val name: String = "${originalName}_PathReflectionWrapper" + private val nullableWrapperName: String = "${originalName}_NullablePathReflectionWrapper" + + override fun generateCode(codeBuilder: CodeBuilder): String { + return codeBuilder.build { + appendln("package $pkg") + emptyLine() + imports.forEach { + appendln("import $it // ktlint-disable") + } + emptyLine() + createPathReflectionWrapperClass(nullable = false) + emptyLine() + createPathReflectionWrapperClass(nullable = true) + emptyLine() + createExtensionWithPathMethod() + emptyLine() + createExtensionPathMethod() + emptyLine() + } + } + + private fun CodeBuilder.createPathReflectionWrapperClass(nullable: Boolean) { + val nId = nullableIdentifier(nullable) + val className = if (nullable) { + nullableWrapperName + } else { + name + } + appendDocumentation( + """ + Implementation of [io.github.virelion.buildata.path.PathReflectionWrapper] for${if (!nullable) " not" else ""} nullable item of [$pkg.$originalName] + """.trimIndent() + ) + indentBlock( + "class $className", + enclosingCharacter = "(", + sufix = " : PathReflectionWrapper<$originalName$nId> {" + ) { + appendln("private val __value: $originalName$nId,") + appendln("private val __path: RecordedPath") + } + indent { + properties.forEach { + createPropertyEntry(it, nullable || it.type.nullability == Nullability.NULLABLE) + } + emptyLine() + createOverrides() + } + appendln("}") + } + + private fun CodeBuilder.createPropertyEntry(classProperty: ClassProperty, nullable: Boolean) { + if (classProperty.type.isList()) { + createCollectionPropertyEntry(classProperty, nullable, CollectionType.LIST) + return + } + if (classProperty.type.isMapWithStringKey()) { + createCollectionPropertyEntry(classProperty, nullable, CollectionType.STRING) + return + } + + val nId = nullableIdentifier(nullable) + val wrapperName = getWrapperName(classProperty.type, nullable) + + val wrapperType = if (classProperty.type.isScalar()) { + wrapperName + "<${classProperty.type.typeFQName()}$nId>" + } else { + if (!classProperty.pathReflection) { + throw BuildataCodegenException( + """Cannot create path reflection wrapper for: $originalName. + Member element ${classProperty.name} not annotated for reflection. + Annotate ${classProperty.type.classFQName()} with @PathReflection""" + ) + } + wrapperName + } + + indentBlock("val ${classProperty.name}: $wrapperType by lazy") { + indentBlock(wrapperName, enclosingCharacter = "(") { + appendln("value()$nId.${classProperty.name},") + appendln("path() + ${StringNamePathIdentifier(classProperty)}") + } + } + } + + private fun getWrapperName(type: KSType, nullable: Boolean): String { + return if (type.isScalar()) { + "ScalarPathReflectionWrapper" + } else { + if (nullable) { + type.className() + "_NullablePathReflectionWrapper" + } else { + type.className() + "_PathReflectionWrapper" + } + } + } + + private fun getWrapperType(type: KSType, nullable: Boolean): String { + return if (type.isScalar()) { + getWrapperName(type, nullable) + "<${type.typeFQName()}>" + } else { + getWrapperName(type, nullable) + } + } + + private fun CodeBuilder.createCollectionPropertyEntry( + classProperty: ClassProperty, + nullable: Boolean, + collectionType: CollectionType + ) { + val defaultInitializer = when (collectionType) { + CollectionType.LIST -> "listOf()" + CollectionType.STRING -> "mapOf()" + } + + val recorderClassImpl = when (collectionType) { + CollectionType.LIST -> "PathReflectionList" + CollectionType.STRING -> "PathReflectionMap" + } + + val itemType = + requireNotNull(classProperty.type.innerArguments.last().type?.resolve()) { "Unable to resolve type of list ${classProperty.name} in $name" } + + val itemNId = nullableIdentifier(nullable) + val defaultInitialization = if (nullable) { + " ?: $defaultInitializer" + } else { + "" + } + + val wrapperType = getWrapperType(itemType, true) + indentBlock("val ${classProperty.name}: $recorderClassImpl<${itemType.typeFQName()}, $wrapperType> by lazy") { + indentBlock(recorderClassImpl, enclosingCharacter = "(") { + appendln("value()$itemNId.${classProperty.name}$defaultInitialization,") + appendln("path() + ${StringNamePathIdentifier(classProperty)},") + appendln("::$wrapperType") + } + } + } + + private fun CodeBuilder.createOverrides() { + appendDocumentation( + """ + @returns value stored in wrapper node. Null in case value is null or any previous element was null. + """.trimIndent() + ) + appendln("override fun value() = __value") + emptyLine() + appendDocumentation( + """ + @returns [io.github.virelion.buildata.path.RecordedPath] even for accessed element + """.trimIndent() + ) + appendln("override fun path() = __path") + } + + private fun CodeBuilder.createExtensionWithPathMethod() { + appendDocumentation( + """ + Wrap item in [io.github.virelion.buildata.path.PathReflectionWrapper]. This will treat this item as a root of accessed elements. + + @returns [io.github.virelion.buildata.path.PathReflectionWrapper] implementation for this item, + """.trimIndent() + ) + appendln("fun $originalName.withPath() = $name(this, RecordedPath())") + } + + private fun CodeBuilder.createExtensionPathMethod() { + appendDocumentation( + """ + Creates path builder for [io.github.virelion.buildata.path.RecordedPath] creation + with [$pkg.$originalName] as data root. + Value of every node will be always null. + + @returns path builder + """.trimIndent() + ) + appendln("fun KClass<$originalName>.path() = $nullableWrapperName(null, RecordedPath())") + } +} diff --git a/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/path/StringNamePathIdentifier.kt b/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/path/StringNamePathIdentifier.kt new file mode 100644 index 0000000..819e43b --- /dev/null +++ b/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/path/StringNamePathIdentifier.kt @@ -0,0 +1,45 @@ +package io.github.virelion.buildata.ksp.path + +import com.google.devtools.ksp.symbol.KSAnnotation +import io.github.virelion.buildata.ksp.ClassProperty +import io.github.virelion.buildata.ksp.extensions.classFQName +import org.jetbrains.kotlin.utils.addToStdlib.safeAs + +@JvmInline +value class StringNamePathIdentifier( + private val classProperty: ClassProperty, +) { + companion object { + val PATH_ELEMENT_NAME_FQNAME = "io.github.virelion.buildata.path.PathElementName" + val KOTLINX_SERIALIZATION_SERIAL_NAME = "kotlinx.serialization.SerialName" + val JACKSON_ALIAS = "com.fasterxml.jackson.annotation.JsonAlias" + } + + private fun getPropertyName(): String { + return getAnnotatedName() ?: classProperty.name + } + + private fun getAnnotatedName(): String? { + val propertyAnnotations = classProperty.annotations + .map { it.annotationType.resolve().classFQName() to it } + .toMap() + + propertyAnnotations[PATH_ELEMENT_NAME_FQNAME]?.apply { return getFirstParamOfAnnotationAsString() } + propertyAnnotations[KOTLINX_SERIALIZATION_SERIAL_NAME]?.apply { return getFirstParamOfAnnotationAsString() } + propertyAnnotations[JACKSON_ALIAS]?.apply { return getFirstListElementOfAnnotationAsString() } + + return null + } + + private fun KSAnnotation.getFirstParamOfAnnotationAsString(): String? { + return arguments.firstOrNull()?.value?.safeAs() + } + + private fun KSAnnotation.getFirstListElementOfAnnotationAsString(): String? { + return arguments.firstOrNull()?.value?.safeAs>()?.firstOrNull() + } + + override fun toString(): String { + return """StringNamePathIdentifier("${getPropertyName()}")""" + } +} diff --git a/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/utils/CodeBuilder.kt b/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/utils/CodeBuilder.kt index 4ddf7be..e63c2dc 100644 --- a/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/utils/CodeBuilder.kt +++ b/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/utils/CodeBuilder.kt @@ -44,20 +44,21 @@ class CodeBuilder(private val indentationDelta: String = " ") { } fun indent(block: CodeBuilder.() -> Unit) { - indentBlock(openingLine = "", enclosingCharacter = "", separator = "", block) + indentBlock(openingLine = "", enclosingCharacter = "", separator = "", sufix = "", block) } fun indentBlock( openingLine: String, enclosingCharacter: String = "{", separator: String = " ", + sufix: String = "", block: CodeBuilder.() -> Unit ) { appendln(openingLine.trim() + separator + enclosingCharacter) indent++ this.block() indent-- - appendln(REVERSE_ENCLOSING_CHARACTER[enclosingCharacter]) + appendln(REVERSE_ENCLOSING_CHARACTER[enclosingCharacter] + sufix) } fun build(block: CodeBuilder.() -> Unit): String { @@ -75,7 +76,8 @@ class CodeBuilder(private val indentationDelta: String = " ") { "{" to "}", "[" to "]", "(" to ")", - "<" to ">" + "<" to ">", + "" to "" ) } } diff --git a/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/utils/TypeUtils.kt b/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/utils/TypeUtils.kt new file mode 100644 index 0000000..66ab1e6 --- /dev/null +++ b/buildata-ksp-plugin/src/main/kotlin/io/github/virelion/buildata/ksp/utils/TypeUtils.kt @@ -0,0 +1,44 @@ +package io.github.virelion.buildata.ksp.utils + +import com.google.devtools.ksp.innerArguments +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.utils.TypeConstants.SCALARS + +object TypeConstants { + val SCALARS = setOf( + "String", + "Unit", + "Boolean", + "Int", + "UInt", + "Long", + "ULong", + "UByte", + "Byte", + "Short", + "UShort", + "Float", + "Double", + "Any" + ) +} + +fun KSType.isScalar(): Boolean { + return this.className() in SCALARS +} + +fun nullableIdentifier(nullable: Boolean) = if (nullable) "?" else "" + +fun KSType.isList(): Boolean { + return "kotlin.collections.List" == this.classFQName() +} + +fun KSType.isMap(): Boolean { + return "kotlin.collections.Map" == this.classFQName() +} + +fun KSType.isMapWithStringKey(): Boolean { + return isMap() && this.innerArguments.first().type?.resolve()?.classFQName() == "kotlin.String" +} diff --git a/buildata-runtime/src/commonMain/kotlin/io/github/virelion/buildata/Buildable.kt b/buildata-runtime/src/commonMain/kotlin/io/github/virelion/buildata/Buildable.kt index 99e64d3..1cd031a 100644 --- a/buildata-runtime/src/commonMain/kotlin/io/github/virelion/buildata/Buildable.kt +++ b/buildata-runtime/src/commonMain/kotlin/io/github/virelion/buildata/Buildable.kt @@ -19,6 +19,6 @@ package io.github.virelion.buildata * ) * ``` */ -@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE) +@Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) annotation class Buildable diff --git a/buildata-runtime/src/commonMain/kotlin/io/github/virelion/buildata/path/PathElementName.kt b/buildata-runtime/src/commonMain/kotlin/io/github/virelion/buildata/path/PathElementName.kt new file mode 100644 index 0000000..5cc12d1 --- /dev/null +++ b/buildata-runtime/src/commonMain/kotlin/io/github/virelion/buildata/path/PathElementName.kt @@ -0,0 +1,5 @@ +package io.github.virelion.buildata.path + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER) +annotation class PathElementName(val name: String) diff --git a/buildata-runtime/src/commonMain/kotlin/io/github/virelion/buildata/path/PathIdentifier.kt b/buildata-runtime/src/commonMain/kotlin/io/github/virelion/buildata/path/PathIdentifier.kt new file mode 100644 index 0000000..a2b0112 --- /dev/null +++ b/buildata-runtime/src/commonMain/kotlin/io/github/virelion/buildata/path/PathIdentifier.kt @@ -0,0 +1,45 @@ +package io.github.virelion.buildata.path + +/** + * Abstraction for various path element identification. + */ +sealed class PathIdentifier + +/** + * Element accessed as [Int] index. + * + * Occurs when accessed element is part of the List. + * ```kotlin + * @PathReflection + * data class Data(val list: List) + * + * KClass.path().list[2] + * ``` + */ +class IntIndexPathIdentifier(val index: Int) : PathIdentifier() + +/** + * Element accessed as [String] index. + * + * Occurs when accessed element is part of the Map with String keys. + * ```kotlin + * @PathReflection + * data class Data(val map: Map) + * + * KClass.path().map["element"] + * ``` + */ +class StringIndexPathIdentifier(val index: String) : PathIdentifier() + +/** + * Element accessed as [String] property name. + * + * Occurs when accessed element is regular class member. + * ```kotlin + * @PathReflection + * data class Data(val str: String) + * + * KClass.path().str + * ``` + */ +class StringNamePathIdentifier(val name: String) : PathIdentifier() diff --git a/buildata-runtime/src/commonMain/kotlin/io/github/virelion/buildata/path/PathReflection.kt b/buildata-runtime/src/commonMain/kotlin/io/github/virelion/buildata/path/PathReflection.kt new file mode 100644 index 0000000..ee1732d --- /dev/null +++ b/buildata-runtime/src/commonMain/kotlin/io/github/virelion/buildata/path/PathReflection.kt @@ -0,0 +1,21 @@ +package io.github.virelion.buildata.path + +/** + * Use this annotation to mark data class to have path reflection capabilities + * + * This will make codegen create path solving capabilities. + * + * Annotating data class + * ```kotlin + * @PathReflection + * data class Data(val item: String) + * ``` + * + * will create functions: + * - ```fun Data.withPath()``` + * - ```fun KClass.path()``` + * + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.CLASS) +annotation class PathReflection diff --git a/buildata-runtime/src/commonMain/kotlin/io/github/virelion/buildata/path/PathReflectionList.kt b/buildata-runtime/src/commonMain/kotlin/io/github/virelion/buildata/path/PathReflectionList.kt new file mode 100644 index 0000000..d014776 --- /dev/null +++ b/buildata-runtime/src/commonMain/kotlin/io/github/virelion/buildata/path/PathReflectionList.kt @@ -0,0 +1,33 @@ +package io.github.virelion.buildata.path + +/** + * List implementation for seamless path wrapped elements access + * + * Accessing element that is out of bound will return nullable wrapper. + */ +class PathReflectionList> internal constructor( + private val delegate: List, + private val nullWrapperProvider: (Int) -> Wrapper +) : List by delegate { + constructor( + originalList: List, + pathToList: RecordedPath, + wrapperProvider: (Type?, RecordedPath) -> Wrapper + ) : this( + delegate = originalList.mapIndexed { index, item -> + wrapperProvider( + item, + pathToList + IntIndexPathIdentifier(index) + ) + }, + nullWrapperProvider = { index -> wrapperProvider(null, pathToList + IntIndexPathIdentifier(index)) } + ) + + override fun get(index: Int): Wrapper { + if (index >= size) { + return nullWrapperProvider(index) + } + + return delegate[index] + } +} diff --git a/buildata-runtime/src/commonMain/kotlin/io/github/virelion/buildata/path/PathReflectionMap.kt b/buildata-runtime/src/commonMain/kotlin/io/github/virelion/buildata/path/PathReflectionMap.kt new file mode 100644 index 0000000..fdd7bc2 --- /dev/null +++ b/buildata-runtime/src/commonMain/kotlin/io/github/virelion/buildata/path/PathReflectionMap.kt @@ -0,0 +1,34 @@ +package io.github.virelion.buildata.path + +/** + * Map implementation for seamless path wrapped elements access + * + * Accessing element that is not in original map will return nullable wrapper. + */ +class PathReflectionMap> internal constructor( + private val delegate: Map, + private val nullWrapperProvider: (String) -> Wrapper +) : Map by delegate { + + constructor( + originalMap: Map, + pathToList: RecordedPath, + wrapperProvider: (Type?, RecordedPath) -> Wrapper + ) : this( + delegate = originalMap.mapValues { (key, value) -> + wrapperProvider( + value, + pathToList + StringIndexPathIdentifier(key) + ) + }, + nullWrapperProvider = { key -> wrapperProvider(null, pathToList + StringIndexPathIdentifier(key)) } + ) + + override fun get(key: String): Wrapper { + if (key !in delegate) { + return nullWrapperProvider(key) + } + + return delegate[key] ?: nullWrapperProvider(key) + } +} diff --git a/buildata-runtime/src/commonMain/kotlin/io/github/virelion/buildata/path/PathReflectionWrapper.kt b/buildata-runtime/src/commonMain/kotlin/io/github/virelion/buildata/path/PathReflectionWrapper.kt new file mode 100644 index 0000000..ea85fda --- /dev/null +++ b/buildata-runtime/src/commonMain/kotlin/io/github/virelion/buildata/path/PathReflectionWrapper.kt @@ -0,0 +1,9 @@ +package io.github.virelion.buildata.path + +/** + * Value paired with [RecordedPath] + */ +interface PathReflectionWrapper { + fun value(): T + fun path(): RecordedPath +} diff --git a/buildata-runtime/src/commonMain/kotlin/io/github/virelion/buildata/path/RecordedPath.kt b/buildata-runtime/src/commonMain/kotlin/io/github/virelion/buildata/path/RecordedPath.kt new file mode 100644 index 0000000..b32d920 --- /dev/null +++ b/buildata-runtime/src/commonMain/kotlin/io/github/virelion/buildata/path/RecordedPath.kt @@ -0,0 +1,29 @@ +package io.github.virelion.buildata.path + +import kotlin.jvm.JvmInline + +/** + * List of [PathIdentifier]. + * + * Elements are ordered as they were accessed: first element accessed is first element of collection, etc. + */ +@JvmInline +value class RecordedPath(private val item: List = listOf()) { + operator fun plus(identifier: PathIdentifier): RecordedPath { + return RecordedPath(item + identifier) + } + + val jsonPath: String get() { + val builder = StringBuilder() + builder.append("$") + item.forEach { + when (it) { + is IntIndexPathIdentifier -> builder.append("[${it.index}]") + is StringIndexPathIdentifier -> builder.append("['${it.index}']") + is StringNamePathIdentifier -> builder.append(".${it.name}") + } + } + + return builder.toString() + } +} diff --git a/buildata-runtime/src/commonMain/kotlin/io/github/virelion/buildata/path/ScalarPathReflectionWrapper.kt b/buildata-runtime/src/commonMain/kotlin/io/github/virelion/buildata/path/ScalarPathReflectionWrapper.kt new file mode 100644 index 0000000..b7e0049 --- /dev/null +++ b/buildata-runtime/src/commonMain/kotlin/io/github/virelion/buildata/path/ScalarPathReflectionWrapper.kt @@ -0,0 +1,12 @@ +package io.github.virelion.buildata.path + +/** + * [PathReflectionWrapper] implementation for nullable and non-nullable scalars. + */ +data class ScalarPathReflectionWrapper( + val __value: T, + val __path: RecordedPath +) : PathReflectionWrapper { + override fun value() = __value + override fun path() = __path +} diff --git a/docs/data-tree-building.md b/docs/data-tree-building.md new file mode 100644 index 0000000..ee363f2 --- /dev/null +++ b/docs/data-tree-building.md @@ -0,0 +1,59 @@ +# Building data tree +Mark data classes as `@Buildable` to create a builder DSL. + +See example: +```kotlin +@Buildable +data class InnerItem( + val property: String +) + +@Buildable +data class ParentDataClass( + val innerItem: InnerItem +) +``` + +```kotlin +val parentDataClassBuilder = ParentDataClass::class.builder() + +// ... +parentDataClassBuilder { + innerItem { + property = "InnerProperty" + } +} + +// later in the code +parentDataClassBuilder { + property = "RootProperty" +} + +val parentDataClass = parentDataClassBuilder.build() +parentDataClass.innerItem.property // returns "InnerProperty" +parentDataClass.property // returns "RootProperty" +``` + +This feature allows for creating complex builder structures for tree like `data class` +and make mutation easy during tree building process. + +Helps avoid using `copy()` data class, which can be unadvised for large data trees, and still keeps end result immutable. + +## 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" +``` \ No newline at end of file diff --git a/docs/path-reflection.md b/docs/path-reflection.md new file mode 100644 index 0000000..107bc41 --- /dev/null +++ b/docs/path-reflection.md @@ -0,0 +1,107 @@ +# Path reflection +Path reflection is a feature that allows for figuring out what part of tree data structure is being accessed. + +## Usage + +Let's consider following data tree structure. + +```kotlin +@PathReflection +data class Root( + val branch1: Branch, + val branch2: Branch?, + val list: List, + val map: Map +) + +@PathReflection +data class Branch( + val stringValue: String +) +``` + +`@PathReflection` annotation will allow codegen engine to create wrappers +and create `fun Root.withPath()` and `fun Branch.withPath` functions. +Using this function allows for accessing parts of data class and gathering information about location in tree. + +```kotlin +fun exampleUse(root: Root) { + val wrapper = root.withPath().branch1.stringValue // generated wrapper is returned + val recordedPath: RecordedPath = wrapper.path() // path collection that allows for further analysis + recordedPath.jsonPath // will return "$.branch1.stringValue" string + + wrapper.value() // returns root.branch1.stringValue +} +``` + +**NOTE!** After using `withPath()` operations are no longer acting on data class, +but rather on wrapper structures that were generated to mimic access. + +## Nullability +Because operation are done on wrapper classes, those are always will be not null. +which means accessing members of `branch2` looks like this. + +```kotlin +fun exampleUse(root: Root) { + val wrapper = root.withPath().branch2.stringValue // no '?' safe call is needed + wrapper.path().jsonPath // will return "$.branch2.stringValue" string, even if branch2 is null + + wrapper.value() // will be null if any tree node is null +} +``` + +## Collections +Here is how collections such as lists and maps can be accessed. +So far Lists and Maps (with `String` key) are supported. + +```kotlin +fun exampleUse(root: Root) { + val root = Root( + //... + list = listOf(Branch("content")) + ) + with(root.withPath().list[0].stringValue) { + value() // will return "content" + path().jsonPath // will return "$.list[0].stringValue" + } +} +``` + +Missing elements in collections do not throw any exceptions, but rather are treated as null. Therefore: +```kotlin +fun exampleUse(root: Root) { + val root = Root( + //... + list = listOf() //empty list + ) + with(root.withPath().list[0].stringValue) { // stringValue is now treated as nullable since it can be missing from the list + value() // will return null + path().jsonPath // will return "$.list[0].stringValue" // will still return full path + } +} +``` + +## Changing name in path +Using `@PathElementName("newName")` can change the string that will appear in path. + +See example: +```kotlin +@PathReflection +data class Data( + @PathElementName("newName") + val stringValue: String +) + +fun exampleUse(data: Data) { + val wrapper = data.withPath().stringValue.path().jsonPath // will return "$.newName" +} +``` + +### Json serialization engines support +Using annotations from popular serialization engines is also supported. + +Annotations: +- `@kotlinx.serialization.SerialName("newName")` +- `@com.fasterxml.jackson.annotation.JsonAlias("newName")` + +are supported out of the box. \ No newline at end of file diff --git a/integration-test-project/benchmark/src/main/kotlin/io/github/virelion/buildata/benchmark/TreeClasses.kt b/integration-test-project/benchmark/src/main/kotlin/io/github/virelion/buildata/benchmark/TreeClasses.kt index e1ea9aa..fe90729 100644 --- a/integration-test-project/benchmark/src/main/kotlin/io/github/virelion/buildata/benchmark/TreeClasses.kt +++ b/integration-test-project/benchmark/src/main/kotlin/io/github/virelion/buildata/benchmark/TreeClasses.kt @@ -4,16 +4,16 @@ import io.github.virelion.buildata.Buildable @Buildable data class Root( - val a: @Buildable Lvl1, - val b: @Buildable Lvl1, - val c: @Buildable Lvl1, - val d: @Buildable Lvl1, - val e: @Buildable Lvl1, - val f: @Buildable Lvl1, - val g: @Buildable Lvl1, - val i: @Buildable Lvl1, - val j: @Buildable Lvl1, - val k: @Buildable Lvl1 + val a: Lvl1, + val b: Lvl1, + val c: Lvl1, + val d: Lvl1, + val e: Lvl1, + val f: Lvl1, + val g: Lvl1, + val i: Lvl1, + val j: Lvl1, + val k: Lvl1 ) fun root() = Root(lvl1(), lvl1(), lvl1(), lvl1(), lvl1(), lvl1(), lvl1(), lvl1(), lvl1(), lvl1()) @@ -35,16 +35,16 @@ fun Root_Builder.fill() { @Buildable data class Lvl1( - val a: @Buildable Lvl2, - val b: @Buildable Lvl2, - val c: @Buildable Lvl2, - val d: @Buildable Lvl2, - val e: @Buildable Lvl2, - val f: @Buildable Lvl2, - val g: @Buildable Lvl2, - val i: @Buildable Lvl2, - val j: @Buildable Lvl2, - val k: @Buildable Lvl2 + val a: Lvl2, + val b: Lvl2, + val c: Lvl2, + val d: Lvl2, + val e: Lvl2, + val f: Lvl2, + val g: Lvl2, + val i: Lvl2, + val j: Lvl2, + val k: Lvl2 ) fun lvl1() = Lvl1(lvl2(), lvl2(), lvl2(), lvl2(), lvl2(), lvl2(), lvl2(), lvl2(), lvl2(), lvl2()) @@ -66,16 +66,16 @@ fun Lvl1_Builder.fill() { @Buildable data class Lvl2( - val a: @Buildable Lvl3, - val b: @Buildable Lvl3, - val c: @Buildable Lvl3, - val d: @Buildable Lvl3, - val e: @Buildable Lvl3, - val f: @Buildable Lvl3, - val g: @Buildable Lvl3, - val i: @Buildable Lvl3, - val j: @Buildable Lvl3, - val k: @Buildable Lvl3 + val a: Lvl3, + val b: Lvl3, + val c: Lvl3, + val d: Lvl3, + val e: Lvl3, + val f: Lvl3, + val g: Lvl3, + val i: Lvl3, + val j: Lvl3, + val k: Lvl3 ) fun lvl2() = Lvl2(lvl3(), lvl3(), lvl3(), lvl3(), lvl3(), lvl3(), lvl3(), lvl3(), lvl3(), lvl3()) @@ -97,16 +97,16 @@ inline fun Lvl2_Builder.fill() { @Buildable data class Lvl3( - val a: @Buildable Lvl4, - val b: @Buildable Lvl4, - val c: @Buildable Lvl4, - val d: @Buildable Lvl4, - val e: @Buildable Lvl4, - val f: @Buildable Lvl4, - val g: @Buildable Lvl4, - val i: @Buildable Lvl4, - val j: @Buildable Lvl4, - val k: @Buildable Lvl4 + val a: Lvl4, + val b: Lvl4, + val c: Lvl4, + val d: Lvl4, + val e: Lvl4, + val f: Lvl4, + val g: Lvl4, + val i: Lvl4, + val j: Lvl4, + val k: Lvl4 ) fun lvl3() = Lvl3(lvl4(), lvl4(), lvl4(), lvl4(), lvl4(), lvl4(), lvl4(), lvl4(), lvl4(), lvl4()) @@ -128,16 +128,16 @@ inline fun Lvl3_Builder.fill() { @Buildable data class Lvl4( - val a: @Buildable Lvl5, - val b: @Buildable Lvl5, - val c: @Buildable Lvl5, - val d: @Buildable Lvl5, - val e: @Buildable Lvl5, - val f: @Buildable Lvl5, - val g: @Buildable Lvl5, - val i: @Buildable Lvl5, - val j: @Buildable Lvl5, - val k: @Buildable Lvl5 + val a: Lvl5, + val b: Lvl5, + val c: Lvl5, + val d: Lvl5, + val e: Lvl5, + val f: Lvl5, + val g: Lvl5, + val i: Lvl5, + val j: Lvl5, + val k: Lvl5 ) fun lvl4() = Lvl4(lvl5(), lvl5(), lvl5(), lvl5(), lvl5(), lvl5(), lvl5(), lvl5(), lvl5(), lvl5()) @@ -159,16 +159,16 @@ inline fun Lvl4_Builder.fill() { @Buildable data class Lvl5( - val a: @Buildable Lvl6, - val b: @Buildable Lvl6, - val c: @Buildable Lvl6, - val d: @Buildable Lvl6, - val e: @Buildable Lvl6, - val f: @Buildable Lvl6, - val g: @Buildable Lvl6, - val i: @Buildable Lvl6, - val j: @Buildable Lvl6, - val k: @Buildable Lvl6 + val a: Lvl6, + val b: Lvl6, + val c: Lvl6, + val d: Lvl6, + val e: Lvl6, + val f: Lvl6, + val g: Lvl6, + val i: Lvl6, + val j: Lvl6, + val k: Lvl6 ) fun lvl5() = Lvl5(lvl6(), lvl6(), lvl6(), lvl6(), lvl6(), lvl6(), lvl6(), lvl6(), lvl6(), lvl6()) diff --git a/integration-test-project/benchmark/src/main/kotlin/io/github/virelion/buildata/benchmark/TreeClassesWithDefaults.kt b/integration-test-project/benchmark/src/main/kotlin/io/github/virelion/buildata/benchmark/TreeClassesWithDefaults.kt index 6cb8100..f1d0217 100644 --- a/integration-test-project/benchmark/src/main/kotlin/io/github/virelion/buildata/benchmark/TreeClassesWithDefaults.kt +++ b/integration-test-project/benchmark/src/main/kotlin/io/github/virelion/buildata/benchmark/TreeClassesWithDefaults.kt @@ -4,16 +4,16 @@ import io.github.virelion.buildata.Buildable @Buildable data class RootWithDefaults( - val a: @Buildable Lvl1WithDefaults = Lvl1WithDefaults(), - val b: @Buildable Lvl1WithDefaults = Lvl1WithDefaults(), - val c: @Buildable Lvl1WithDefaults = Lvl1WithDefaults(), - val d: @Buildable Lvl1WithDefaults = Lvl1WithDefaults(), - val e: @Buildable Lvl1WithDefaults = Lvl1WithDefaults(), - val f: @Buildable Lvl1WithDefaults = Lvl1WithDefaults(), - val g: @Buildable Lvl1WithDefaults = Lvl1WithDefaults(), - val i: @Buildable Lvl1WithDefaults = Lvl1WithDefaults(), - val j: @Buildable Lvl1WithDefaults = Lvl1WithDefaults(), - val k: @Buildable Lvl1WithDefaults = Lvl1WithDefaults() + val a: Lvl1WithDefaults = Lvl1WithDefaults(), + val b: Lvl1WithDefaults = Lvl1WithDefaults(), + val c: Lvl1WithDefaults = Lvl1WithDefaults(), + val d: Lvl1WithDefaults = Lvl1WithDefaults(), + val e: Lvl1WithDefaults = Lvl1WithDefaults(), + val f: Lvl1WithDefaults = Lvl1WithDefaults(), + val g: Lvl1WithDefaults = Lvl1WithDefaults(), + val i: Lvl1WithDefaults = Lvl1WithDefaults(), + val j: Lvl1WithDefaults = Lvl1WithDefaults(), + val k: Lvl1WithDefaults = Lvl1WithDefaults() ) fun RootWithDefaults_Builder.fill() { @@ -33,16 +33,16 @@ fun RootWithDefaults_Builder.fill() { @Buildable data class Lvl1WithDefaults( - val a: @Buildable Lvl2WithDefaults = Lvl2WithDefaults(), - val b: @Buildable Lvl2WithDefaults = Lvl2WithDefaults(), - val c: @Buildable Lvl2WithDefaults = Lvl2WithDefaults(), - val d: @Buildable Lvl2WithDefaults = Lvl2WithDefaults(), - val e: @Buildable Lvl2WithDefaults = Lvl2WithDefaults(), - val f: @Buildable Lvl2WithDefaults = Lvl2WithDefaults(), - val g: @Buildable Lvl2WithDefaults = Lvl2WithDefaults(), - val i: @Buildable Lvl2WithDefaults = Lvl2WithDefaults(), - val j: @Buildable Lvl2WithDefaults = Lvl2WithDefaults(), - val k: @Buildable Lvl2WithDefaults = Lvl2WithDefaults() + val a: Lvl2WithDefaults = Lvl2WithDefaults(), + val b: Lvl2WithDefaults = Lvl2WithDefaults(), + val c: Lvl2WithDefaults = Lvl2WithDefaults(), + val d: Lvl2WithDefaults = Lvl2WithDefaults(), + val e: Lvl2WithDefaults = Lvl2WithDefaults(), + val f: Lvl2WithDefaults = Lvl2WithDefaults(), + val g: Lvl2WithDefaults = Lvl2WithDefaults(), + val i: Lvl2WithDefaults = Lvl2WithDefaults(), + val j: Lvl2WithDefaults = Lvl2WithDefaults(), + val k: Lvl2WithDefaults = Lvl2WithDefaults() ) fun Lvl1WithDefaults_Builder.fill() { @@ -62,16 +62,16 @@ fun Lvl1WithDefaults_Builder.fill() { @Buildable data class Lvl2WithDefaults( - val a: @Buildable Lvl3WithDefaults = Lvl3WithDefaults(), - val b: @Buildable Lvl3WithDefaults = Lvl3WithDefaults(), - val c: @Buildable Lvl3WithDefaults = Lvl3WithDefaults(), - val d: @Buildable Lvl3WithDefaults = Lvl3WithDefaults(), - val e: @Buildable Lvl3WithDefaults = Lvl3WithDefaults(), - val f: @Buildable Lvl3WithDefaults = Lvl3WithDefaults(), - val g: @Buildable Lvl3WithDefaults = Lvl3WithDefaults(), - val i: @Buildable Lvl3WithDefaults = Lvl3WithDefaults(), - val j: @Buildable Lvl3WithDefaults = Lvl3WithDefaults(), - val k: @Buildable Lvl3WithDefaults = Lvl3WithDefaults() + val a: Lvl3WithDefaults = Lvl3WithDefaults(), + val b: Lvl3WithDefaults = Lvl3WithDefaults(), + val c: Lvl3WithDefaults = Lvl3WithDefaults(), + val d: Lvl3WithDefaults = Lvl3WithDefaults(), + val e: Lvl3WithDefaults = Lvl3WithDefaults(), + val f: Lvl3WithDefaults = Lvl3WithDefaults(), + val g: Lvl3WithDefaults = Lvl3WithDefaults(), + val i: Lvl3WithDefaults = Lvl3WithDefaults(), + val j: Lvl3WithDefaults = Lvl3WithDefaults(), + val k: Lvl3WithDefaults = Lvl3WithDefaults() ) fun Lvl2WithDefaults_Builder.fill() { @@ -91,16 +91,16 @@ fun Lvl2WithDefaults_Builder.fill() { @Buildable data class Lvl3WithDefaults( - val a: @Buildable Lvl4WithDefaults = Lvl4WithDefaults(), - val b: @Buildable Lvl4WithDefaults = Lvl4WithDefaults(), - val c: @Buildable Lvl4WithDefaults = Lvl4WithDefaults(), - val d: @Buildable Lvl4WithDefaults = Lvl4WithDefaults(), - val e: @Buildable Lvl4WithDefaults = Lvl4WithDefaults(), - val f: @Buildable Lvl4WithDefaults = Lvl4WithDefaults(), - val g: @Buildable Lvl4WithDefaults = Lvl4WithDefaults(), - val i: @Buildable Lvl4WithDefaults = Lvl4WithDefaults(), - val j: @Buildable Lvl4WithDefaults = Lvl4WithDefaults(), - val k: @Buildable Lvl4WithDefaults = Lvl4WithDefaults() + val a: Lvl4WithDefaults = Lvl4WithDefaults(), + val b: Lvl4WithDefaults = Lvl4WithDefaults(), + val c: Lvl4WithDefaults = Lvl4WithDefaults(), + val d: Lvl4WithDefaults = Lvl4WithDefaults(), + val e: Lvl4WithDefaults = Lvl4WithDefaults(), + val f: Lvl4WithDefaults = Lvl4WithDefaults(), + val g: Lvl4WithDefaults = Lvl4WithDefaults(), + val i: Lvl4WithDefaults = Lvl4WithDefaults(), + val j: Lvl4WithDefaults = Lvl4WithDefaults(), + val k: Lvl4WithDefaults = Lvl4WithDefaults() ) fun Lvl3WithDefaults_Builder.fill() { @@ -120,16 +120,16 @@ fun Lvl3WithDefaults_Builder.fill() { @Buildable data class Lvl4WithDefaults( - val a: @Buildable Lvl5WithDefaults = Lvl5WithDefaults(), - val b: @Buildable Lvl5WithDefaults = Lvl5WithDefaults(), - val c: @Buildable Lvl5WithDefaults = Lvl5WithDefaults(), - val d: @Buildable Lvl5WithDefaults = Lvl5WithDefaults(), - val e: @Buildable Lvl5WithDefaults = Lvl5WithDefaults(), - val f: @Buildable Lvl5WithDefaults = Lvl5WithDefaults(), - val g: @Buildable Lvl5WithDefaults = Lvl5WithDefaults(), - val i: @Buildable Lvl5WithDefaults = Lvl5WithDefaults(), - val j: @Buildable Lvl5WithDefaults = Lvl5WithDefaults(), - val k: @Buildable Lvl5WithDefaults = Lvl5WithDefaults() + val a: Lvl5WithDefaults = Lvl5WithDefaults(), + val b: Lvl5WithDefaults = Lvl5WithDefaults(), + val c: Lvl5WithDefaults = Lvl5WithDefaults(), + val d: Lvl5WithDefaults = Lvl5WithDefaults(), + val e: Lvl5WithDefaults = Lvl5WithDefaults(), + val f: Lvl5WithDefaults = Lvl5WithDefaults(), + val g: Lvl5WithDefaults = Lvl5WithDefaults(), + val i: Lvl5WithDefaults = Lvl5WithDefaults(), + val j: Lvl5WithDefaults = Lvl5WithDefaults(), + val k: Lvl5WithDefaults = Lvl5WithDefaults() ) fun Lvl4WithDefaults_Builder.fill() { @@ -149,16 +149,16 @@ fun Lvl4WithDefaults_Builder.fill() { @Buildable data class Lvl5WithDefaults( - val a: @Buildable Lvl6WithDefaults = Lvl6WithDefaults(), - val b: @Buildable Lvl6WithDefaults = Lvl6WithDefaults(), - val c: @Buildable Lvl6WithDefaults = Lvl6WithDefaults(), - val d: @Buildable Lvl6WithDefaults = Lvl6WithDefaults(), - val e: @Buildable Lvl6WithDefaults = Lvl6WithDefaults(), - val f: @Buildable Lvl6WithDefaults = Lvl6WithDefaults(), - val g: @Buildable Lvl6WithDefaults = Lvl6WithDefaults(), - val i: @Buildable Lvl6WithDefaults = Lvl6WithDefaults(), - val j: @Buildable Lvl6WithDefaults = Lvl6WithDefaults(), - val k: @Buildable Lvl6WithDefaults = Lvl6WithDefaults() + val a: Lvl6WithDefaults = Lvl6WithDefaults(), + val b: Lvl6WithDefaults = Lvl6WithDefaults(), + val c: Lvl6WithDefaults = Lvl6WithDefaults(), + val d: Lvl6WithDefaults = Lvl6WithDefaults(), + val e: Lvl6WithDefaults = Lvl6WithDefaults(), + val f: Lvl6WithDefaults = Lvl6WithDefaults(), + val g: Lvl6WithDefaults = Lvl6WithDefaults(), + val i: Lvl6WithDefaults = Lvl6WithDefaults(), + val j: Lvl6WithDefaults = Lvl6WithDefaults(), + val k: Lvl6WithDefaults = Lvl6WithDefaults() ) fun Lvl5WithDefaults_Builder.fill() { diff --git a/integration-test-project/benchmark/src/test/kotlin/io/github/virelion/buildata/benchmark/ScenarioTest.kt b/integration-test-project/benchmark/src/test/kotlin/io/github/virelion/buildata/benchmark/ScenarioTest.kt index 919004d..eaffa54 100644 --- a/integration-test-project/benchmark/src/test/kotlin/io/github/virelion/buildata/benchmark/ScenarioTest.kt +++ b/integration-test-project/benchmark/src/test/kotlin/io/github/virelion/buildata/benchmark/ScenarioTest.kt @@ -1,6 +1,6 @@ package io.github.virelion.buildata.benchmark -import org.junit.Test +import kotlin.test.Test class ScenarioTest { @Test diff --git a/integration-test-project/project-types/jvm/build.gradle.kts b/integration-test-project/project-types/jvm/build.gradle.kts index 7810b09..57a04f9 100644 --- a/integration-test-project/project-types/jvm/build.gradle.kts +++ b/integration-test-project/project-types/jvm/build.gradle.kts @@ -15,6 +15,8 @@ val buildataRuntimeVersion = "0.0.0-SNAPSHOT" dependencies { implementation("io.github.virelion:buildata-runtime:$buildataRuntimeVersion") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1") + implementation("com.fasterxml.jackson.core:jackson-databind:2.11.1") testImplementation(kotlin("test-junit")) } diff --git a/integration-test-project/project-types/jvm/src/main/kotlin/io/github/virelion/buildata/demo/CompositeDataClass.kt b/integration-test-project/project-types/jvm/src/main/kotlin/io/github/virelion/buildata/demo/CompositeDataClass.kt index cfe1ee3..e2fcab5 100644 --- a/integration-test-project/project-types/jvm/src/main/kotlin/io/github/virelion/buildata/demo/CompositeDataClass.kt +++ b/integration-test-project/project-types/jvm/src/main/kotlin/io/github/virelion/buildata/demo/CompositeDataClass.kt @@ -4,21 +4,21 @@ import io.github.virelion.buildata.Buildable @Buildable data class CompositeDataClass( - val innerClass: @Buildable Level1Class? + val innerClass: Level1Class? ) @Buildable data class Level1Class( - val level2: @Buildable Level2Class, + val level2: Level2Class, val value: String, ) @Buildable data class Level2Class( - val level3: @Buildable Level3Class, - val level3WithDefault: @Buildable Level3Class = Level3Class("DEFAULT"), - val nullableLevel3WithDefault: @Buildable Level3Class? = Level3Class("DEFAULT"), - val nullableLevel3WithNullDefault: @Buildable Level3Class? = null, + val level3: Level3Class, + val level3WithDefault: Level3Class = Level3Class("DEFAULT"), + val nullableLevel3WithDefault: Level3Class? = Level3Class("DEFAULT"), + val nullableLevel3WithNullDefault: Level3Class? = null, val value: String ) diff --git a/integration-test-project/project-types/jvm/src/main/kotlin/io/github/virelion/buildata/demo/PathCalculationClasses.kt b/integration-test-project/project-types/jvm/src/main/kotlin/io/github/virelion/buildata/demo/PathCalculationClasses.kt new file mode 100644 index 0000000..29fc3c9 --- /dev/null +++ b/integration-test-project/project-types/jvm/src/main/kotlin/io/github/virelion/buildata/demo/PathCalculationClasses.kt @@ -0,0 +1,83 @@ +package io.github.virelion.buildata.demo + +import com.fasterxml.jackson.annotation.JsonAlias +import io.github.virelion.buildata.Buildable +import io.github.virelion.buildata.path.PathElementName +import io.github.virelion.buildata.path.PathReflection +import kotlinx.serialization.SerialName + +@Buildable +@PathReflection +data class Root( + val inner1: Inner1 = Inner1(), + val nullableLeaf: LeafNode? = null +) + +@Buildable +@PathReflection +data class Inner1( + val inner2: Inner2 = Inner2(), + val leafWithNullables: LeafWithNullables = LeafWithNullables(), + // lists + val innerList: List = listOf(), + val listOfNullables: List = listOf(null), + val nullableList: List? = null, + val nullableListOfNullables: List? = listOf(null), + // map + val innerMap: Map = mapOf(), + val mapOfNullables: Map = mapOf("null" to null), + val nullableMap: Map? = null, + val nullableMapOfNullables: Map? = mapOf("null" to null) +) + +@Buildable +@PathReflection +data class Inner2( + val leaf: LeafNode = LeafNode() +) + +@Buildable +@PathReflection +data class LeafNode( + val string: String = "", + val boolean: Boolean = false, + val int: Int = 0, + val uInt: UInt = 0u, + val long: Long = 0L, + val uLong: ULong = 0uL, + val byte: Byte = 0, + val uByte: UByte = 0x0u, + val short: Short = 0, + val uShort: UShort = 0u, + val float: Float = 0.0f, + val double: Double = 0.0 +) + +@Buildable +@PathReflection +data class LeafWithNullables( + val string: String? = "", + val boolean: Boolean? = false, + val int: Int? = 0, + val uInt: UInt? = 0u, + val long: Long? = 0L, + val uLong: ULong? = 0uL, + val byte: Byte? = 0, + val uByte: UByte? = 0x0u, + val short: Short? = 0, + val uShort: UShort? = 0u, + val float: Float? = 0.0f, + val double: Double? = 0.0 +) + +@PathReflection +data class AnnotatedLeaf( + @PathElementName("PATH_ELEMENT_NAME") + val pathElementNameAnnotated: String, + + @JsonAlias("JACKSON_JSON_ALIAS") + val jacksonAnnotatedWithAlias: String, + + @SerialName("KOTLINX_SERIALIZATION_SERIAL_NAME") + val kotlinxSerializationAnnotated: String +) diff --git a/integration-test-project/project-types/jvm/src/test/kotlin/io/github/virelion/buildata/demo/ListPathCalculation.kt b/integration-test-project/project-types/jvm/src/test/kotlin/io/github/virelion/buildata/demo/ListPathCalculation.kt new file mode 100644 index 0000000..ed51296 --- /dev/null +++ b/integration-test-project/project-types/jvm/src/test/kotlin/io/github/virelion/buildata/demo/ListPathCalculation.kt @@ -0,0 +1,76 @@ +package io.github.virelion.buildata.demo + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class ListPathCalculation { + @Test + fun testListAccess() { + val root = Root::class.build { + inner1 { + innerList = listOf(Inner2()) + } + } + + with(root.withPath().inner1.innerList[0].leaf.string) { + assertEquals("", value()) + assertEquals("$.inner1.innerList[0].leaf.string", path().jsonPath) + } + } + + @Test + fun testListAccessOutOfBoundsElement() { + val root = Root::class.build { + inner1 { + innerList = listOf(Inner2()) + } + } + with(root.withPath().inner1.innerList[1].leaf.string) { + assertNull(value()) + assertEquals("$.inner1.innerList[1].leaf.string", path().jsonPath) + } + } + + @Test + fun testListOfNullablesAccess() { + val root = Root::class.build { + inner1 { + listOfNullables = listOf(null) + } + } + + with(root.withPath().inner1.listOfNullables[0].leaf.string) { + assertEquals(null, value()) + assertEquals("$.inner1.listOfNullables[0].leaf.string", path().jsonPath) + } + } + + @Test + fun testNullableListAccess() { + val root = Root::class.build { + inner1 { + nullableList = listOf(Inner2()) + } + } + + with(root.withPath().inner1.nullableList[0].leaf.string) { + assertEquals("", value()) + assertEquals("$.inner1.nullableList[0].leaf.string", path().jsonPath) + } + } + + @Test + fun testNullableListOfNullablesAccess() { + val root = Root::class.build { + inner1 { + nullableListOfNullables = listOf(Inner2()) + } + } + + with(root.withPath().inner1.nullableListOfNullables[0].leaf.string) { + assertEquals("", value()) + assertEquals("$.inner1.nullableListOfNullables[0].leaf.string", path().jsonPath) + } + } +} diff --git a/integration-test-project/project-types/jvm/src/test/kotlin/io/github/virelion/buildata/demo/MapPathCalculation.kt b/integration-test-project/project-types/jvm/src/test/kotlin/io/github/virelion/buildata/demo/MapPathCalculation.kt new file mode 100644 index 0000000..5853a85 --- /dev/null +++ b/integration-test-project/project-types/jvm/src/test/kotlin/io/github/virelion/buildata/demo/MapPathCalculation.kt @@ -0,0 +1,76 @@ +package io.github.virelion.buildata.demo + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class MapPathCalculation { + @Test + fun testMapAccess() { + val root = Root::class.build { + inner1 { + innerMap = mapOf("key" to Inner2()) + } + } + + with(root.withPath().inner1.innerMap["key"].leaf.string) { + assertEquals("", value()) + assertEquals("$.inner1.innerMap['key'].leaf.string", path().jsonPath) + } + } + + @Test + fun testListAccessOutOfBoundsElement() { + val root = Root::class.build { + inner1 { + innerMap = mapOf("key" to Inner2()) + } + } + with(root.withPath().inner1.innerMap["missingKey"].leaf.string) { + assertNull(value()) + assertEquals("$.inner1.innerMap['missingKey'].leaf.string", path().jsonPath) + } + } + + @Test + fun testMapOfNullablesAccess() { + val root = Root::class.build { + inner1 { + mapOfNullables = mapOf("null" to null) + } + } + + with(root.withPath().inner1.mapOfNullables["null"].leaf.string) { + assertEquals(null, value()) + assertEquals("$.inner1.mapOfNullables['null'].leaf.string", path().jsonPath) + } + } + + @Test + fun testNullableMapAccess() { + val root = Root::class.build { + inner1 { + nullableMap = mapOf("key" to Inner2()) + } + } + + with(root.withPath().inner1.nullableMap["key"].leaf.string) { + assertEquals("", value()) + assertEquals("$.inner1.nullableMap['key'].leaf.string", path().jsonPath) + } + } + + @Test + fun testNullableMapOfNullablesAccess() { + val root = Root::class.build { + inner1 { + nullableMapOfNullables = mapOf("key" to Inner2()) + } + } + + with(root.withPath().inner1.nullableMapOfNullables["key"].leaf.string) { + assertEquals("", value()) + assertEquals("$.inner1.nullableMapOfNullables['key'].leaf.string", path().jsonPath) + } + } +} diff --git a/integration-test-project/project-types/jvm/src/test/kotlin/io/github/virelion/buildata/demo/PathCalculation.kt b/integration-test-project/project-types/jvm/src/test/kotlin/io/github/virelion/buildata/demo/PathCalculation.kt new file mode 100644 index 0000000..b2f2d68 --- /dev/null +++ b/integration-test-project/project-types/jvm/src/test/kotlin/io/github/virelion/buildata/demo/PathCalculation.kt @@ -0,0 +1,228 @@ +package io.github.virelion.buildata.demo + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class PathCalculation { + @Test + fun testScalarsValuesWithPaths() { + val root = Root::class.build { + inner1 { + inner2 { + leaf { + string = "Test" + boolean = true + int = 1 + uInt = 2u + long = 3 + uLong = 4uL + byte = 0x5 + uByte = 0x6u + short = 7 + uShort = 8u + float = 9.0f + double = 10.0 + } + } + } + } + + with(root.withPath().inner1.inner2.leaf) { + with(string) { + assertEquals("Test", value()) + assertEquals("$.inner1.inner2.leaf.string", path().jsonPath) + } + with(boolean) { + assertEquals(true, value()) + assertEquals("$.inner1.inner2.leaf.boolean", path().jsonPath) + } + with(int) { + assertEquals(1, value()) + assertEquals("$.inner1.inner2.leaf.int", path().jsonPath) + } + with(uInt) { + assertEquals(2u, value()) + assertEquals("$.inner1.inner2.leaf.uInt", path().jsonPath) + } + with(long) { + assertEquals(3, value()) + assertEquals("$.inner1.inner2.leaf.long", path().jsonPath) + } + with(uLong) { + assertEquals(4u, value()) + assertEquals("$.inner1.inner2.leaf.uLong", path().jsonPath) + } + with(byte) { + assertEquals(0x5, value()) + assertEquals("$.inner1.inner2.leaf.byte", path().jsonPath) + } + with(uByte) { + assertEquals(0x6u, value()) + assertEquals("$.inner1.inner2.leaf.uByte", path().jsonPath) + } + with(short) { + assertEquals(7, value()) + assertEquals("$.inner1.inner2.leaf.short", path().jsonPath) + } + with(uShort) { + assertEquals(8u, value()) + assertEquals("$.inner1.inner2.leaf.uShort", path().jsonPath) + } + with(float) { + assertEquals(9.0f, value()) + assertEquals("$.inner1.inner2.leaf.float", path().jsonPath) + } + with(double) { + assertEquals(10.0, value()) + assertEquals("$.inner1.inner2.leaf.double", path().jsonPath) + } + } + } + + @Test + fun testNullableScalarsValuesWithPaths() { + val root = Root::class.build { + inner1 { + leafWithNullables { + string = "Test" + boolean = true + int = 1 + uInt = 2u + long = 3 + uLong = 4uL + byte = 0x5 + uByte = 0x6u + short = 7 + uShort = 8u + float = 9.0f + double = 10.0 + } + } + } + + with(root.withPath().inner1.leafWithNullables) { + with(string) { + assertEquals("Test", value()) + assertEquals("$.inner1.leafWithNullables.string", path().jsonPath) + } + with(boolean) { + assertEquals(true, value()) + assertEquals("$.inner1.leafWithNullables.boolean", path().jsonPath) + } + with(int) { + assertEquals(1, value()) + assertEquals("$.inner1.leafWithNullables.int", path().jsonPath) + } + with(uInt) { + assertEquals(2u, value()) + assertEquals("$.inner1.leafWithNullables.uInt", path().jsonPath) + } + with(long) { + assertEquals(3, value()) + assertEquals("$.inner1.leafWithNullables.long", path().jsonPath) + } + with(uLong) { + assertEquals(4u, value()) + assertEquals("$.inner1.leafWithNullables.uLong", path().jsonPath) + } + with(byte) { + assertEquals(0x5, value()) + assertEquals("$.inner1.leafWithNullables.byte", path().jsonPath) + } + with(uByte) { + assertEquals(0x6u, value()) + assertEquals("$.inner1.leafWithNullables.uByte", path().jsonPath) + } + with(short) { + assertEquals(7, value()) + assertEquals("$.inner1.leafWithNullables.short", path().jsonPath) + } + with(uShort) { + assertEquals(8u, value()) + assertEquals("$.inner1.leafWithNullables.uShort", path().jsonPath) + } + with(float) { + assertEquals(9.0f, value()) + assertEquals("$.inner1.leafWithNullables.float", path().jsonPath) + } + with(double) { + assertEquals(10.0, value()) + assertEquals("$.inner1.leafWithNullables.double", path().jsonPath) + } + } + } + + @Test + fun testScalarsFromNullNode() { + val root = Root() + + with(root.withPath().nullableLeaf) { + with(string) { + assertNull(value()) + assertEquals("$.nullableLeaf.string", path().jsonPath) + } + with(boolean) { + assertNull(value()) + assertEquals("$.nullableLeaf.boolean", path().jsonPath) + } + with(int) { + assertNull(value()) + assertEquals("$.nullableLeaf.int", path().jsonPath) + } + with(uInt) { + assertNull(value()) + assertEquals("$.nullableLeaf.uInt", path().jsonPath) + } + with(long) { + assertNull(value()) + assertEquals("$.nullableLeaf.long", path().jsonPath) + } + with(uLong) { + assertNull(value()) + assertEquals("$.nullableLeaf.uLong", path().jsonPath) + } + with(byte) { + assertNull(value()) + assertEquals("$.nullableLeaf.byte", path().jsonPath) + } + with(uByte) { + assertNull(value()) + assertEquals("$.nullableLeaf.uByte", path().jsonPath) + } + with(short) { + assertNull(value()) + assertEquals("$.nullableLeaf.short", path().jsonPath) + } + with(uShort) { + assertNull(value()) + assertEquals("$.nullableLeaf.uShort", path().jsonPath) + } + with(float) { + assertNull(value()) + assertEquals("$.nullableLeaf.float", path().jsonPath) + } + with(double) { + assertNull(value()) + assertEquals("$.nullableLeaf.double", path().jsonPath) + } + } + } + + @Test + fun testPathCalculationOnEmptyNode() { + with(Root::class.path().inner1.innerList[42].leaf.boolean) { + assertNull(value()) + assertEquals("$.inner1.innerList[42].leaf.boolean", path().jsonPath) + } + } + + @Test + fun testCustomNamesAnnotated() { + with(AnnotatedLeaf::class.path()) { + assertEquals("$.PATH_ELEMENT_NAME", pathElementNameAnnotated.path().jsonPath) + assertEquals("$.JACKSON_JSON_ALIAS", jacksonAnnotatedWithAlias.path().jsonPath) + assertEquals("$.KOTLINX_SERIALIZATION_SERIAL_NAME", kotlinxSerializationAnnotated.path().jsonPath) + } + } +} diff --git a/integration-test-project/project-types/multiplatform/build.gradle.kts b/integration-test-project/project-types/multiplatform/build.gradle.kts index 4ea17f7..25f01fe 100644 --- a/integration-test-project/project-types/multiplatform/build.gradle.kts +++ b/integration-test-project/project-types/multiplatform/build.gradle.kts @@ -40,6 +40,7 @@ kotlin { val commonMain by getting { dependencies { implementation("io.github.virelion:buildata-runtime:$buildataRuntimeVersion") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1") } } diff --git a/integration-test-project/project-types/multiplatform/src/commonMain/kotlin/io/github/virelion/buildata/demo/CompositeDataClass.kt b/integration-test-project/project-types/multiplatform/src/commonMain/kotlin/io/github/virelion/buildata/demo/CompositeDataClass.kt index cfe1ee3..e2fcab5 100644 --- a/integration-test-project/project-types/multiplatform/src/commonMain/kotlin/io/github/virelion/buildata/demo/CompositeDataClass.kt +++ b/integration-test-project/project-types/multiplatform/src/commonMain/kotlin/io/github/virelion/buildata/demo/CompositeDataClass.kt @@ -4,21 +4,21 @@ import io.github.virelion.buildata.Buildable @Buildable data class CompositeDataClass( - val innerClass: @Buildable Level1Class? + val innerClass: Level1Class? ) @Buildable data class Level1Class( - val level2: @Buildable Level2Class, + val level2: Level2Class, val value: String, ) @Buildable data class Level2Class( - val level3: @Buildable Level3Class, - val level3WithDefault: @Buildable Level3Class = Level3Class("DEFAULT"), - val nullableLevel3WithDefault: @Buildable Level3Class? = Level3Class("DEFAULT"), - val nullableLevel3WithNullDefault: @Buildable Level3Class? = null, + val level3: Level3Class, + val level3WithDefault: Level3Class = Level3Class("DEFAULT"), + val nullableLevel3WithDefault: Level3Class? = Level3Class("DEFAULT"), + val nullableLevel3WithNullDefault: Level3Class? = null, val value: String ) diff --git a/integration-test-project/project-types/multiplatform/src/commonMain/kotlin/io/github/virelion/buildata/demo/PathCalculationClasses.kt b/integration-test-project/project-types/multiplatform/src/commonMain/kotlin/io/github/virelion/buildata/demo/PathCalculationClasses.kt new file mode 100644 index 0000000..60bdc79 --- /dev/null +++ b/integration-test-project/project-types/multiplatform/src/commonMain/kotlin/io/github/virelion/buildata/demo/PathCalculationClasses.kt @@ -0,0 +1,79 @@ +package io.github.virelion.buildata.demo + +import io.github.virelion.buildata.Buildable +import io.github.virelion.buildata.path.PathElementName +import io.github.virelion.buildata.path.PathReflection +import kotlinx.serialization.SerialName + +@Buildable +@PathReflection +data class Root( + val inner1: Inner1 = Inner1(), + val nullableLeaf: LeafNode? = null +) + +@Buildable +@PathReflection +data class Inner1( + val inner2: Inner2 = Inner2(), + val leafWithNullables: LeafWithNullables = LeafWithNullables(), + // lists + val innerList: List = listOf(), + val listOfNullables: List = listOf(null), + val nullableList: List? = null, + val nullableListOfNullables: List? = listOf(null), + // map + val innerMap: Map = mapOf(), + val mapOfNullables: Map = mapOf("null" to null), + val nullableMap: Map? = null, + val nullableMapOfNullables: Map? = mapOf("null" to null) +) + +@Buildable +@PathReflection +data class Inner2( + val leaf: LeafNode = LeafNode() +) + +@Buildable +@PathReflection +data class LeafNode( + val string: String = "", + val boolean: Boolean = false, + val int: Int = 0, + val uInt: UInt = 0u, + val long: Long = 0L, + val uLong: ULong = 0uL, + val byte: Byte = 0, + val uByte: UByte = 0x0u, + val short: Short = 0, + val uShort: UShort = 0u, + val float: Float = 0.0f, + val double: Double = 0.0 +) + +@Buildable +@PathReflection +data class LeafWithNullables( + val string: String? = "", + val boolean: Boolean? = false, + val int: Int? = 0, + val uInt: UInt? = 0u, + val long: Long? = 0L, + val uLong: ULong? = 0uL, + val byte: Byte? = 0, + val uByte: UByte? = 0x0u, + val short: Short? = 0, + val uShort: UShort? = 0u, + val float: Float? = 0.0f, + val double: Double? = 0.0 +) + +@PathReflection +data class AnnotatedLeaf( + @PathElementName("PATH_ELEMENT_NAME") + val pathElementNameAnnotated: String, + + @SerialName("KOTLINX_SERIALIZATION_SERIAL_NAME") + val kotlinxSerializationAnnotated: String +) diff --git a/integration-test-project/project-types/multiplatform/src/commonTest/kotlin/io/github/virelion/buildata/demo/ListPathCalculation.kt b/integration-test-project/project-types/multiplatform/src/commonTest/kotlin/io/github/virelion/buildata/demo/ListPathCalculation.kt new file mode 100644 index 0000000..ed51296 --- /dev/null +++ b/integration-test-project/project-types/multiplatform/src/commonTest/kotlin/io/github/virelion/buildata/demo/ListPathCalculation.kt @@ -0,0 +1,76 @@ +package io.github.virelion.buildata.demo + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class ListPathCalculation { + @Test + fun testListAccess() { + val root = Root::class.build { + inner1 { + innerList = listOf(Inner2()) + } + } + + with(root.withPath().inner1.innerList[0].leaf.string) { + assertEquals("", value()) + assertEquals("$.inner1.innerList[0].leaf.string", path().jsonPath) + } + } + + @Test + fun testListAccessOutOfBoundsElement() { + val root = Root::class.build { + inner1 { + innerList = listOf(Inner2()) + } + } + with(root.withPath().inner1.innerList[1].leaf.string) { + assertNull(value()) + assertEquals("$.inner1.innerList[1].leaf.string", path().jsonPath) + } + } + + @Test + fun testListOfNullablesAccess() { + val root = Root::class.build { + inner1 { + listOfNullables = listOf(null) + } + } + + with(root.withPath().inner1.listOfNullables[0].leaf.string) { + assertEquals(null, value()) + assertEquals("$.inner1.listOfNullables[0].leaf.string", path().jsonPath) + } + } + + @Test + fun testNullableListAccess() { + val root = Root::class.build { + inner1 { + nullableList = listOf(Inner2()) + } + } + + with(root.withPath().inner1.nullableList[0].leaf.string) { + assertEquals("", value()) + assertEquals("$.inner1.nullableList[0].leaf.string", path().jsonPath) + } + } + + @Test + fun testNullableListOfNullablesAccess() { + val root = Root::class.build { + inner1 { + nullableListOfNullables = listOf(Inner2()) + } + } + + with(root.withPath().inner1.nullableListOfNullables[0].leaf.string) { + assertEquals("", value()) + assertEquals("$.inner1.nullableListOfNullables[0].leaf.string", path().jsonPath) + } + } +} diff --git a/integration-test-project/project-types/multiplatform/src/commonTest/kotlin/io/github/virelion/buildata/demo/MapPathCalculation.kt b/integration-test-project/project-types/multiplatform/src/commonTest/kotlin/io/github/virelion/buildata/demo/MapPathCalculation.kt new file mode 100644 index 0000000..5853a85 --- /dev/null +++ b/integration-test-project/project-types/multiplatform/src/commonTest/kotlin/io/github/virelion/buildata/demo/MapPathCalculation.kt @@ -0,0 +1,76 @@ +package io.github.virelion.buildata.demo + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class MapPathCalculation { + @Test + fun testMapAccess() { + val root = Root::class.build { + inner1 { + innerMap = mapOf("key" to Inner2()) + } + } + + with(root.withPath().inner1.innerMap["key"].leaf.string) { + assertEquals("", value()) + assertEquals("$.inner1.innerMap['key'].leaf.string", path().jsonPath) + } + } + + @Test + fun testListAccessOutOfBoundsElement() { + val root = Root::class.build { + inner1 { + innerMap = mapOf("key" to Inner2()) + } + } + with(root.withPath().inner1.innerMap["missingKey"].leaf.string) { + assertNull(value()) + assertEquals("$.inner1.innerMap['missingKey'].leaf.string", path().jsonPath) + } + } + + @Test + fun testMapOfNullablesAccess() { + val root = Root::class.build { + inner1 { + mapOfNullables = mapOf("null" to null) + } + } + + with(root.withPath().inner1.mapOfNullables["null"].leaf.string) { + assertEquals(null, value()) + assertEquals("$.inner1.mapOfNullables['null'].leaf.string", path().jsonPath) + } + } + + @Test + fun testNullableMapAccess() { + val root = Root::class.build { + inner1 { + nullableMap = mapOf("key" to Inner2()) + } + } + + with(root.withPath().inner1.nullableMap["key"].leaf.string) { + assertEquals("", value()) + assertEquals("$.inner1.nullableMap['key'].leaf.string", path().jsonPath) + } + } + + @Test + fun testNullableMapOfNullablesAccess() { + val root = Root::class.build { + inner1 { + nullableMapOfNullables = mapOf("key" to Inner2()) + } + } + + with(root.withPath().inner1.nullableMapOfNullables["key"].leaf.string) { + assertEquals("", value()) + assertEquals("$.inner1.nullableMapOfNullables['key'].leaf.string", path().jsonPath) + } + } +} diff --git a/integration-test-project/project-types/multiplatform/src/commonTest/kotlin/io/github/virelion/buildata/demo/PathCalculation.kt b/integration-test-project/project-types/multiplatform/src/commonTest/kotlin/io/github/virelion/buildata/demo/PathCalculation.kt new file mode 100644 index 0000000..e4b4a84 --- /dev/null +++ b/integration-test-project/project-types/multiplatform/src/commonTest/kotlin/io/github/virelion/buildata/demo/PathCalculation.kt @@ -0,0 +1,227 @@ +package io.github.virelion.buildata.demo + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class PathCalculation { + @Test + fun testScalarsValuesWithPaths() { + val root = Root::class.build { + inner1 { + inner2 { + leaf { + string = "Test" + boolean = true + int = 1 + uInt = 2u + long = 3 + uLong = 4uL + byte = 0x5 + uByte = 0x6u + short = 7 + uShort = 8u + float = 9.0f + double = 10.0 + } + } + } + } + + with(root.withPath().inner1.inner2.leaf) { + with(string) { + assertEquals("Test", value()) + assertEquals("$.inner1.inner2.leaf.string", path().jsonPath) + } + with(boolean) { + assertEquals(true, value()) + assertEquals("$.inner1.inner2.leaf.boolean", path().jsonPath) + } + with(int) { + assertEquals(1, value()) + assertEquals("$.inner1.inner2.leaf.int", path().jsonPath) + } + with(uInt) { + assertEquals(2u, value()) + assertEquals("$.inner1.inner2.leaf.uInt", path().jsonPath) + } + with(long) { + assertEquals(3, value()) + assertEquals("$.inner1.inner2.leaf.long", path().jsonPath) + } + with(uLong) { + assertEquals(4u, value()) + assertEquals("$.inner1.inner2.leaf.uLong", path().jsonPath) + } + with(byte) { + assertEquals(0x5, value()) + assertEquals("$.inner1.inner2.leaf.byte", path().jsonPath) + } + with(uByte) { + assertEquals(0x6u, value()) + assertEquals("$.inner1.inner2.leaf.uByte", path().jsonPath) + } + with(short) { + assertEquals(7, value()) + assertEquals("$.inner1.inner2.leaf.short", path().jsonPath) + } + with(uShort) { + assertEquals(8u, value()) + assertEquals("$.inner1.inner2.leaf.uShort", path().jsonPath) + } + with(float) { + assertEquals(9.0f, value()) + assertEquals("$.inner1.inner2.leaf.float", path().jsonPath) + } + with(double) { + assertEquals(10.0, value()) + assertEquals("$.inner1.inner2.leaf.double", path().jsonPath) + } + } + } + + @Test + fun testNullableScalarsValuesWithPaths() { + val root = Root::class.build { + inner1 { + leafWithNullables { + string = "Test" + boolean = true + int = 1 + uInt = 2u + long = 3 + uLong = 4uL + byte = 0x5 + uByte = 0x6u + short = 7 + uShort = 8u + float = 9.0f + double = 10.0 + } + } + } + + with(root.withPath().inner1.leafWithNullables) { + with(string) { + assertEquals("Test", value()) + assertEquals("$.inner1.leafWithNullables.string", path().jsonPath) + } + with(boolean) { + assertEquals(true, value()) + assertEquals("$.inner1.leafWithNullables.boolean", path().jsonPath) + } + with(int) { + assertEquals(1, value()) + assertEquals("$.inner1.leafWithNullables.int", path().jsonPath) + } + with(uInt) { + assertEquals(2u, value()) + assertEquals("$.inner1.leafWithNullables.uInt", path().jsonPath) + } + with(long) { + assertEquals(3, value()) + assertEquals("$.inner1.leafWithNullables.long", path().jsonPath) + } + with(uLong) { + assertEquals(4u, value()) + assertEquals("$.inner1.leafWithNullables.uLong", path().jsonPath) + } + with(byte) { + assertEquals(0x5, value()) + assertEquals("$.inner1.leafWithNullables.byte", path().jsonPath) + } + with(uByte) { + assertEquals(0x6u, value()) + assertEquals("$.inner1.leafWithNullables.uByte", path().jsonPath) + } + with(short) { + assertEquals(7, value()) + assertEquals("$.inner1.leafWithNullables.short", path().jsonPath) + } + with(uShort) { + assertEquals(8u, value()) + assertEquals("$.inner1.leafWithNullables.uShort", path().jsonPath) + } + with(float) { + assertEquals(9.0f, value()) + assertEquals("$.inner1.leafWithNullables.float", path().jsonPath) + } + with(double) { + assertEquals(10.0, value()) + assertEquals("$.inner1.leafWithNullables.double", path().jsonPath) + } + } + } + + @Test + fun testScalarsFromNullNode() { + val root = Root() + + with(root.withPath().nullableLeaf) { + with(string) { + assertNull(value()) + assertEquals("$.nullableLeaf.string", path().jsonPath) + } + with(boolean) { + assertNull(value()) + assertEquals("$.nullableLeaf.boolean", path().jsonPath) + } + with(int) { + assertNull(value()) + assertEquals("$.nullableLeaf.int", path().jsonPath) + } + with(uInt) { + assertNull(value()) + assertEquals("$.nullableLeaf.uInt", path().jsonPath) + } + with(long) { + assertNull(value()) + assertEquals("$.nullableLeaf.long", path().jsonPath) + } + with(uLong) { + assertNull(value()) + assertEquals("$.nullableLeaf.uLong", path().jsonPath) + } + with(byte) { + assertNull(value()) + assertEquals("$.nullableLeaf.byte", path().jsonPath) + } + with(uByte) { + assertNull(value()) + assertEquals("$.nullableLeaf.uByte", path().jsonPath) + } + with(short) { + assertNull(value()) + assertEquals("$.nullableLeaf.short", path().jsonPath) + } + with(uShort) { + assertNull(value()) + assertEquals("$.nullableLeaf.uShort", path().jsonPath) + } + with(float) { + assertNull(value()) + assertEquals("$.nullableLeaf.float", path().jsonPath) + } + with(double) { + assertNull(value()) + assertEquals("$.nullableLeaf.double", path().jsonPath) + } + } + } + + @Test + fun testPathCalculationOnEmptyNode() { + with(Root::class.path().inner1.innerList[42].leaf.boolean) { + assertNull(value()) + assertEquals("$.inner1.innerList[42].leaf.boolean", path().jsonPath) + } + } + + @Test + fun testCustomNamesAnnotated() { + with(AnnotatedLeaf::class.path()) { + assertEquals("$.PATH_ELEMENT_NAME", pathElementNameAnnotated.path().jsonPath) + assertEquals("$.KOTLINX_SERIALIZATION_SERIAL_NAME", kotlinxSerializationAnnotated.path().jsonPath) + } + } +}