From 1dd615160999fee42b4e3c1104184a4212cb462b Mon Sep 17 00:00:00 2001 From: Jolan Rensen Date: Sat, 6 Apr 2024 14:17:26 +0200 Subject: [PATCH] linting and generating sources --- .../kotlinx/dataframe/api/toDataFrame.kt | 17 +- .../dataframe/codeGen/MarkersExtractor.kt | 4 +- .../jetbrains/kotlinx/dataframe/impl/Utils.kt | 16 +- .../kotlinx/dataframe/impl/api/toDataFrame.kt | 164 +++++++++++++----- .../kotlinx/dataframe/impl/schema/Utils.kt | 17 +- .../kotlinx/dataframe/api/JavaPojo.java | 58 +++++++ .../kotlinx/dataframe/api/toDataFrame.kt | 60 +++++++ .../kotlinx/dataframe/impl/api/toDataFrame.kt | 99 ++++++++--- 8 files changed, 358 insertions(+), 77 deletions(-) create mode 100644 core/generated-sources/src/test/java/org/jetbrains/kotlinx/dataframe/api/JavaPojo.java diff --git a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/toDataFrame.kt b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/toDataFrame.kt index ec3832563..43fbc3c60 100644 --- a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/toDataFrame.kt +++ b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/toDataFrame.kt @@ -13,6 +13,7 @@ import org.jetbrains.kotlinx.dataframe.impl.asList import org.jetbrains.kotlinx.dataframe.impl.columnName import org.jetbrains.kotlinx.dataframe.impl.columns.guessColumnType import org.jetbrains.kotlinx.dataframe.index +import kotlin.reflect.KCallable import kotlin.reflect.KClass import kotlin.reflect.KProperty @@ -115,24 +116,26 @@ public fun Iterable>>.toDataFrameFromPairs(): AnyFra public interface TraversePropertiesDsl { /** - * Skip given [classes] during recursive (dfs) traversal + * Skip given [classes] during recursive (dfs) traversal. */ public fun exclude(vararg classes: KClass<*>) /** - * Skip given [properties] during recursive (dfs) traversal + * Skip given [properties] during recursive (dfs) traversal. + * These can also be getter-like functions. */ - public fun exclude(vararg properties: KProperty<*>) + public fun exclude(vararg properties: KCallable<*>) /** - * Store given [classes] in ValueColumns without transformation into ColumnGroups or FrameColumns + * Store given [classes] in ValueColumns without transformation into ColumnGroups or FrameColumns. */ public fun preserve(vararg classes: KClass<*>) /** - * Store given [properties] in ValueColumns without transformation into ColumnGroups or FrameColumns + * Store given [properties] in ValueColumns without transformation into ColumnGroups or FrameColumns. + * These can also be getter-like functions. */ - public fun preserve(vararg properties: KProperty<*>) + public fun preserve(vararg properties: KCallable<*>) } public inline fun TraversePropertiesDsl.preserve(): Unit = preserve(T::class) @@ -148,7 +151,7 @@ public abstract class CreateDataFrameDsl : TraversePropertiesDsl { public infix fun AnyBaseCol.into(path: ColumnPath): Unit = add(this, path) public abstract fun properties( - vararg roots: KProperty<*>, + vararg roots: KCallable<*>, maxDepth: Int = 0, body: (TraversePropertiesDsl.() -> Unit)? = null, ) diff --git a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/codeGen/MarkersExtractor.kt b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/codeGen/MarkersExtractor.kt index ef883a33e..833862643 100644 --- a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/codeGen/MarkersExtractor.kt +++ b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/codeGen/MarkersExtractor.kt @@ -4,7 +4,7 @@ import org.jetbrains.kotlinx.dataframe.DataFrame import org.jetbrains.kotlinx.dataframe.DataRow import org.jetbrains.kotlinx.dataframe.annotations.ColumnName import org.jetbrains.kotlinx.dataframe.annotations.DataSchema -import org.jetbrains.kotlinx.dataframe.impl.schema.getPropertiesOrder +import org.jetbrains.kotlinx.dataframe.impl.schema.getPropertyOrderFromPrimaryConstructor import org.jetbrains.kotlinx.dataframe.schema.ColumnSchema import kotlin.reflect.KClass import kotlin.reflect.KType @@ -53,7 +53,7 @@ internal object MarkersExtractor { } private fun getFields(markerClass: KClass<*>, nullableProperties: Boolean): List { - val order = getPropertiesOrder(markerClass) + val order = getPropertyOrderFromPrimaryConstructor(markerClass) ?: emptyMap() return markerClass.memberProperties.sortedBy { order[it.name] ?: Int.MAX_VALUE }.mapIndexed { _, it -> val fieldName = ValidFieldName.of(it.name) val columnName = it.findAnnotation()?.name ?: fieldName.unquoted diff --git a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/Utils.kt b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/Utils.kt index 6d4b89a8f..1051712ef 100644 --- a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/Utils.kt +++ b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/Utils.kt @@ -14,8 +14,9 @@ import org.jetbrains.kotlinx.dataframe.impl.columns.toColumnSet import org.jetbrains.kotlinx.dataframe.nrow import java.math.BigDecimal import java.math.BigInteger +import kotlin.reflect.KCallable import kotlin.reflect.KClass -import kotlin.reflect.KProperty +import kotlin.reflect.KFunction import kotlin.reflect.KType import kotlin.reflect.KTypeProjection import kotlin.reflect.KVariance @@ -338,4 +339,15 @@ internal fun List.joinToCamelCaseString(): String { } @PublishedApi -internal val KProperty.columnName: String get() = findAnnotation()?.name ?: name +internal val KCallable.columnName: String + get() = findAnnotation()?.name + ?: when (this) { + // for defining the column names based on a getter-function, we use the function name minus the get/is prefix + is KFunction<*> -> + name + .removePrefix("get") + .removePrefix("is") + .replaceFirstChar { it.lowercase() } + + else -> name + } diff --git a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/toDataFrame.kt b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/toDataFrame.kt index 56f99abd0..ee1a3ff42 100644 --- a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/toDataFrame.kt +++ b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/toDataFrame.kt @@ -19,16 +19,20 @@ import org.jetbrains.kotlinx.dataframe.impl.columnName import org.jetbrains.kotlinx.dataframe.impl.emptyPath import org.jetbrains.kotlinx.dataframe.impl.getListType import org.jetbrains.kotlinx.dataframe.impl.projectUpTo -import org.jetbrains.kotlinx.dataframe.impl.schema.getPropertiesOrder +import org.jetbrains.kotlinx.dataframe.impl.schema.getPropertyOrderFromAnyConstructor +import org.jetbrains.kotlinx.dataframe.impl.schema.getPropertyOrderFromPrimaryConstructor import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method import java.time.temporal.Temporal +import kotlin.reflect.KCallable import kotlin.reflect.KClass import kotlin.reflect.KProperty import kotlin.reflect.KVisibility import kotlin.reflect.full.isSubclassOf +import kotlin.reflect.full.memberFunctions import kotlin.reflect.full.memberProperties import kotlin.reflect.full.primaryConstructor +import kotlin.reflect.full.valueParameters import kotlin.reflect.full.withNullability import kotlin.reflect.jvm.isAccessible import kotlin.reflect.jvm.javaField @@ -43,11 +47,12 @@ internal val valueTypes = setOf( kotlinx.datetime.Instant::class, ) -internal val KClass<*>.isValueType: Boolean get() = - this in valueTypes || - this.isSubclassOf(Number::class) || - this.isSubclassOf(Enum::class) || - this.isSubclassOf(Temporal::class) +internal val KClass<*>.isValueType: Boolean + get() = + this in valueTypes || + this.isSubclassOf(Number::class) || + this.isSubclassOf(Enum::class) || + this.isSubclassOf(Temporal::class) internal class CreateDataFrameDslImpl( override val source: Iterable, @@ -72,13 +77,13 @@ internal class CreateDataFrameDslImpl( internal class TraverseConfiguration : TraversePropertiesDsl { - val excludeProperties = mutableSetOf>() + val excludeProperties = mutableSetOf>() val excludeClasses = mutableSetOf>() val preserveClasses = mutableSetOf>() - val preserveProperties = mutableSetOf>() + val preserveProperties = mutableSetOf>() fun clone(): TraverseConfiguration = TraverseConfiguration().also { it.excludeClasses.addAll(excludeClasses) @@ -87,7 +92,7 @@ internal class CreateDataFrameDslImpl( it.preserveClasses.addAll(preserveClasses) } - override fun exclude(vararg properties: KProperty<*>) { + override fun exclude(vararg properties: KCallable<*>) { excludeProperties.addAll(properties) } @@ -99,17 +104,25 @@ internal class CreateDataFrameDslImpl( preserveClasses.addAll(classes) } - override fun preserve(vararg properties: KProperty<*>) { + override fun preserve(vararg properties: KCallable<*>) { preserveProperties.addAll(properties) } } - override fun properties(vararg roots: KProperty<*>, maxDepth: Int, body: (TraversePropertiesDsl.() -> Unit)?) { + override fun properties(vararg roots: KCallable<*>, maxDepth: Int, body: (TraversePropertiesDsl.() -> Unit)?) { val dsl = configuration.clone() if (body != null) { body(dsl) } - val df = convertToDataFrame(source, clazz, roots.toList(), dsl.excludeProperties, dsl.preserveClasses, dsl.preserveProperties, maxDepth) + val df = convertToDataFrame( + data = source, + clazz = clazz, + roots = roots.toList(), + excludes = dsl.excludeProperties, + preserveClasses = dsl.preserveClasses, + preserveProperties = dsl.preserveProperties, + maxDepth = maxDepth, + ) df.columns().forEach { add(it) } @@ -117,7 +130,10 @@ internal class CreateDataFrameDslImpl( } @PublishedApi -internal fun Iterable.createDataFrameImpl(clazz: KClass<*>, body: CreateDataFrameDslImpl.() -> Unit): DataFrame { +internal fun Iterable.createDataFrameImpl( + clazz: KClass<*>, + body: CreateDataFrameDslImpl.() -> Unit, +): DataFrame { val builder = CreateDataFrameDslImpl(this, clazz) builder.body() return builder.columns.toDataFrameFromPairs() @@ -127,18 +143,47 @@ internal fun Iterable.createDataFrameImpl(clazz: KClass<*>, body: CreateD internal fun convertToDataFrame( data: Iterable<*>, clazz: KClass<*>, - roots: List>, - excludes: Set>, + roots: List>, + excludes: Set>, preserveClasses: Set>, - preserveProperties: Set>, - maxDepth: Int + preserveProperties: Set>, + maxDepth: Int, ): AnyFrame { - val order = getPropertiesOrder(clazz) + val primaryConstructorOrder = getPropertyOrderFromPrimaryConstructor(clazz) + val anyConstructorOrder = getPropertyOrderFromAnyConstructor(clazz) + + val properties: List> = roots + .ifEmpty { + clazz.memberProperties + .filter { it.visibility == KVisibility.PUBLIC && it.valueParameters.isEmpty() } + } + + // fall back to getter functions for pojo-like classes + .ifEmpty { + clazz.memberFunctions + .filter { + it.visibility == KVisibility.PUBLIC && + it.valueParameters.isEmpty() && + (it.name.startsWith("get") || it.name.startsWith("is")) + } + } + + // sort properties by order of primary-ish constructor + .let { + val names = it.map { it.columnName }.toSet() - val properties = roots.ifEmpty { - clazz.memberProperties - .filter { it.visibility == KVisibility.PUBLIC && it.parameters.toList().size == 1 } - }.sortedBy { order[it.name] ?: Int.MAX_VALUE } + // use the primary constructor order if it's available, + // else try to find a constructor that matches all properties + val order = primaryConstructorOrder + ?: anyConstructorOrder.firstOrNull { map -> + names.all { it in map.keys } + } + if (order != null) { + it.sortedBy { order[it.columnName] ?: Int.MAX_VALUE } + } else { + it + } + } val columns = properties.mapNotNull { val property = it @@ -148,8 +193,9 @@ internal fun convertToDataFrame( val valueClassConverter = (it.returnType.classifier as? KClass<*>)?.let { kClass -> if (!kClass.isValue) null else { - val constructor = - requireNotNull(kClass.primaryConstructor) { "value class $kClass is expected to have primary constructor, but couldn't obtain it" } + val constructor = requireNotNull(kClass.primaryConstructor) { + "value class $kClass is expected to have primary constructor, but couldn't obtain it" + } val parameter = constructor.parameters.singleOrNull() ?: error("conversion of value class $kClass with multiple parameters in constructor is not yet supported") // there's no need to unwrap if underlying field is nullable @@ -162,7 +208,7 @@ internal fun convertToDataFrame( valueClassConverter } } - property.javaField?.isAccessible = true + (property as? KProperty<*>)?.javaField?.isAccessible = true property.isAccessible = true var nullable = false @@ -208,18 +254,34 @@ internal fun convertToDataFrame( val kclass = (returnType.classifier as KClass<*>) when { hasExceptions -> DataColumn.createWithTypeInference(it.columnName, values, nullable) - kclass == Any::class || preserveClasses.contains(kclass) || preserveProperties.contains(property) || (maxDepth <= 0 && !returnType.shouldBeConvertedToFrameColumn() && !returnType.shouldBeConvertedToColumnGroup()) || kclass.isValueType -> - DataColumn.createValueColumn(it.columnName, values, returnType.withNullability(nullable)) - kclass == DataFrame::class && !nullable -> DataColumn.createFrameColumn(it.columnName, values as List) - kclass == DataRow::class -> DataColumn.createColumnGroup(it.columnName, (values as List).concat()) + + kclass == Any::class || + preserveClasses.contains(kclass) || + preserveProperties.contains(property) || + (maxDepth <= 0 && !returnType.shouldBeConvertedToFrameColumn() && !returnType.shouldBeConvertedToColumnGroup()) || + kclass.isValueType -> + DataColumn.createValueColumn( + name = it.columnName, + values = values, + type = returnType.withNullability(nullable), + ) + + kclass == DataFrame::class && !nullable -> + DataColumn.createFrameColumn( + name = it.columnName, + groups = values as List + ) + + kclass == DataRow::class -> + DataColumn.createColumnGroup( + name = it.columnName, + df = (values as List).concat(), + ) + kclass.isSubclassOf(Iterable::class) -> { val elementType = returnType.projectUpTo(Iterable::class).arguments.firstOrNull()?.type if (elementType == null) { - DataColumn.createValueColumn( - it.columnName, - values, - returnType.withNullability(nullable) - ) + DataColumn.createValueColumn(it.columnName, values, returnType.withNullability(nullable)) } else { val elementClass = (elementType.classifier as? KClass<*>) @@ -231,6 +293,7 @@ internal fun convertToDataFrame( DataColumn.createWithTypeInference(it.columnName, listValues) } + elementClass.isValueType -> { val listType = getListType(elementType).withNullability(nullable) val listValues = values.map { @@ -238,12 +301,22 @@ internal fun convertToDataFrame( } DataColumn.createValueColumn(it.columnName, listValues, listType) } + else -> { val frames = values.map { - if (it == null) DataFrame.empty() - else { + if (it == null) { + DataFrame.empty() + } else { require(it is Iterable<*>) - convertToDataFrame(it, elementClass, emptyList(), excludes, preserveClasses, preserveProperties, maxDepth - 1) + convertToDataFrame( + data = it, + clazz = elementClass, + roots = emptyList(), + excludes = excludes, + preserveClasses = preserveClasses, + preserveProperties = preserveProperties, + maxDepth = maxDepth - 1, + ) } } DataColumn.createFrameColumn(it.columnName, frames) @@ -251,13 +324,24 @@ internal fun convertToDataFrame( } } } + else -> { - val df = convertToDataFrame(values, kclass, emptyList(), excludes, preserveClasses, preserveProperties, maxDepth - 1) - DataColumn.createColumnGroup(it.columnName, df) + val df = convertToDataFrame( + data = values, + clazz = kclass, + roots = emptyList(), + excludes = excludes, + preserveClasses = preserveClasses, + preserveProperties = preserveProperties, + maxDepth = maxDepth - 1, + ) + DataColumn.createColumnGroup(name = it.columnName, df = df) } } } return if (columns.isEmpty()) { DataFrame.empty(data.count()) - } else dataFrameOf(columns) + } else { + dataFrameOf(columns) + } } diff --git a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/schema/Utils.kt b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/schema/Utils.kt index 16acc7aba..02f8e5f19 100644 --- a/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/schema/Utils.kt +++ b/core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/schema/Utils.kt @@ -141,5 +141,18 @@ internal fun DataFrameSchema.createEmptyDataFrame(numberOfRows: Int): AnyFrame = internal fun createEmptyDataFrameOf(clazz: KClass<*>): AnyFrame = MarkersExtractor.get(clazz).schema.createEmptyDataFrame() -internal fun getPropertiesOrder(clazz: KClass<*>): Map = - clazz.primaryConstructor?.parameters?.mapNotNull { it.name }?.mapIndexed { i, v -> v to i }?.toMap() ?: emptyMap() +internal fun getPropertyOrderFromPrimaryConstructor(clazz: KClass<*>): Map? = + clazz.primaryConstructor + ?.parameters + ?.mapNotNull { it.name } + ?.mapIndexed { i, v -> v to i } + ?.toMap() + +internal fun getPropertyOrderFromAnyConstructor(clazz: KClass<*>): List> = + clazz.constructors + .map { constructor -> + constructor.parameters + .mapNotNull { it.name } + .mapIndexed { i, v -> v to i } + .toMap() + } diff --git a/core/generated-sources/src/test/java/org/jetbrains/kotlinx/dataframe/api/JavaPojo.java b/core/generated-sources/src/test/java/org/jetbrains/kotlinx/dataframe/api/JavaPojo.java new file mode 100644 index 000000000..dddcbd572 --- /dev/null +++ b/core/generated-sources/src/test/java/org/jetbrains/kotlinx/dataframe/api/JavaPojo.java @@ -0,0 +1,58 @@ +package org.jetbrains.kotlinx.dataframe.api; + +import java.util.Objects; + +public class JavaPojo { + + private int a; + private String b; + + public JavaPojo() {} + + public JavaPojo(int a, String b) { + this.a = a; + this.b = b; + } + + public int getA() { + return a; + } + + public void setA(int a) { + this.a = a; + } + + public String getB() { + return b; + } + + public void setB(String b) { + this.b = b; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + JavaPojo testPojo = (JavaPojo) o; + + if (a != testPojo.a) return false; + return Objects.equals(b, testPojo.b); + } + + @Override + public int hashCode() { + int result = a; + result = 31 * result + (b != null ? b.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "TestPojo{" + + "a=" + a + + ", b='" + b + '\'' + + '}'; + } +} diff --git a/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/toDataFrame.kt b/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/toDataFrame.kt index f5247ccc6..1eebb0bb7 100644 --- a/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/toDataFrame.kt +++ b/core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/toDataFrame.kt @@ -312,4 +312,64 @@ class CreateDataFrameTests { fun `convert private class with public members`() { listOf(PrivateClass(1)).toDataFrame() shouldBe dataFrameOf("a")(1) } + + class KotlinPojo { + + private var a: Int = 0 + private var b: String = "" + + constructor() + + constructor(a: Int, b: String) { + this.a = a + this.b = b + } + + fun getA(): Int = a + fun setA(a: Int) { + this.a = a + } + + fun getB(): String = b + fun setB(b: String) { + this.b = b + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is KotlinPojo) return false + + if (a != other.a) return false + if (b != other.b) return false + + return true + } + + override fun hashCode(): Int { + var result = a + result = 31 * result + b.hashCode() + return result + } + + override fun toString(): String { + return "FakePojo(a=$a, b='$b')" + } + } + + @Test + fun `convert POJO to DF`() { + listOf(KotlinPojo(1, "a")).toDataFrame() shouldBe dataFrameOf("a", "b")(1, "a") + listOf(JavaPojo(1, "a")).toDataFrame() shouldBe dataFrameOf("a", "b")(1, "a") + + listOf(KotlinPojo(1, "a")).toDataFrame { properties(KotlinPojo::getA) } shouldBe dataFrameOf("a")(1) + listOf(KotlinPojo(1, "a")).toDataFrame { properties(KotlinPojo::getB) } shouldBe dataFrameOf("b")("a") + + listOf(JavaPojo(1, "a")).toDataFrame { + properties(JavaPojo::getA) + } shouldBe dataFrameOf("a")(1) + + listOf(JavaPojo(1, "a")).toDataFrame { + properties(JavaPojo::getB) + } shouldBe dataFrameOf("b")("a") + } } diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/toDataFrame.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/toDataFrame.kt index cd59ad05f..ee1a3ff42 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/toDataFrame.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/toDataFrame.kt @@ -47,11 +47,12 @@ internal val valueTypes = setOf( kotlinx.datetime.Instant::class, ) -internal val KClass<*>.isValueType: Boolean get() = - this in valueTypes || - this.isSubclassOf(Number::class) || - this.isSubclassOf(Enum::class) || - this.isSubclassOf(Temporal::class) +internal val KClass<*>.isValueType: Boolean + get() = + this in valueTypes || + this.isSubclassOf(Number::class) || + this.isSubclassOf(Enum::class) || + this.isSubclassOf(Temporal::class) internal class CreateDataFrameDslImpl( override val source: Iterable, @@ -113,7 +114,15 @@ internal class CreateDataFrameDslImpl( if (body != null) { body(dsl) } - val df = convertToDataFrame(source, clazz, roots.toList(), dsl.excludeProperties, dsl.preserveClasses, dsl.preserveProperties, maxDepth) + val df = convertToDataFrame( + data = source, + clazz = clazz, + roots = roots.toList(), + excludes = dsl.excludeProperties, + preserveClasses = dsl.preserveClasses, + preserveProperties = dsl.preserveProperties, + maxDepth = maxDepth, + ) df.columns().forEach { add(it) } @@ -121,7 +130,10 @@ internal class CreateDataFrameDslImpl( } @PublishedApi -internal fun Iterable.createDataFrameImpl(clazz: KClass<*>, body: CreateDataFrameDslImpl.() -> Unit): DataFrame { +internal fun Iterable.createDataFrameImpl( + clazz: KClass<*>, + body: CreateDataFrameDslImpl.() -> Unit, +): DataFrame { val builder = CreateDataFrameDslImpl(this, clazz) builder.body() return builder.columns.toDataFrameFromPairs() @@ -181,8 +193,9 @@ internal fun convertToDataFrame( val valueClassConverter = (it.returnType.classifier as? KClass<*>)?.let { kClass -> if (!kClass.isValue) null else { - val constructor = - requireNotNull(kClass.primaryConstructor) { "value class $kClass is expected to have primary constructor, but couldn't obtain it" } + val constructor = requireNotNull(kClass.primaryConstructor) { + "value class $kClass is expected to have primary constructor, but couldn't obtain it" + } val parameter = constructor.parameters.singleOrNull() ?: error("conversion of value class $kClass with multiple parameters in constructor is not yet supported") // there's no need to unwrap if underlying field is nullable @@ -241,18 +254,34 @@ internal fun convertToDataFrame( val kclass = (returnType.classifier as KClass<*>) when { hasExceptions -> DataColumn.createWithTypeInference(it.columnName, values, nullable) - kclass == Any::class || preserveClasses.contains(kclass) || preserveProperties.contains(property) || (maxDepth <= 0 && !returnType.shouldBeConvertedToFrameColumn() && !returnType.shouldBeConvertedToColumnGroup()) || kclass.isValueType -> - DataColumn.createValueColumn(it.columnName, values, returnType.withNullability(nullable)) - kclass == DataFrame::class && !nullable -> DataColumn.createFrameColumn(it.columnName, values as List) - kclass == DataRow::class -> DataColumn.createColumnGroup(it.columnName, (values as List).concat()) + + kclass == Any::class || + preserveClasses.contains(kclass) || + preserveProperties.contains(property) || + (maxDepth <= 0 && !returnType.shouldBeConvertedToFrameColumn() && !returnType.shouldBeConvertedToColumnGroup()) || + kclass.isValueType -> + DataColumn.createValueColumn( + name = it.columnName, + values = values, + type = returnType.withNullability(nullable), + ) + + kclass == DataFrame::class && !nullable -> + DataColumn.createFrameColumn( + name = it.columnName, + groups = values as List + ) + + kclass == DataRow::class -> + DataColumn.createColumnGroup( + name = it.columnName, + df = (values as List).concat(), + ) + kclass.isSubclassOf(Iterable::class) -> { val elementType = returnType.projectUpTo(Iterable::class).arguments.firstOrNull()?.type if (elementType == null) { - DataColumn.createValueColumn( - it.columnName, - values, - returnType.withNullability(nullable) - ) + DataColumn.createValueColumn(it.columnName, values, returnType.withNullability(nullable)) } else { val elementClass = (elementType.classifier as? KClass<*>) @@ -264,6 +293,7 @@ internal fun convertToDataFrame( DataColumn.createWithTypeInference(it.columnName, listValues) } + elementClass.isValueType -> { val listType = getListType(elementType).withNullability(nullable) val listValues = values.map { @@ -271,12 +301,22 @@ internal fun convertToDataFrame( } DataColumn.createValueColumn(it.columnName, listValues, listType) } + else -> { val frames = values.map { - if (it == null) DataFrame.empty() - else { + if (it == null) { + DataFrame.empty() + } else { require(it is Iterable<*>) - convertToDataFrame(it, elementClass, emptyList(), excludes, preserveClasses, preserveProperties, maxDepth - 1) + convertToDataFrame( + data = it, + clazz = elementClass, + roots = emptyList(), + excludes = excludes, + preserveClasses = preserveClasses, + preserveProperties = preserveProperties, + maxDepth = maxDepth - 1, + ) } } DataColumn.createFrameColumn(it.columnName, frames) @@ -284,13 +324,24 @@ internal fun convertToDataFrame( } } } + else -> { - val df = convertToDataFrame(values, kclass, emptyList(), excludes, preserveClasses, preserveProperties, maxDepth - 1) - DataColumn.createColumnGroup(it.columnName, df) + val df = convertToDataFrame( + data = values, + clazz = kclass, + roots = emptyList(), + excludes = excludes, + preserveClasses = preserveClasses, + preserveProperties = preserveProperties, + maxDepth = maxDepth - 1, + ) + DataColumn.createColumnGroup(name = it.columnName, df = df) } } } return if (columns.isEmpty()) { DataFrame.empty(data.count()) - } else dataFrameOf(columns) + } else { + dataFrameOf(columns) + } }