Skip to content

Commit

Permalink
deferToFormatWhenVariableColumns added
Browse files Browse the repository at this point in the history
  • Loading branch information
UnknownJoe796 committed Jun 6, 2023
1 parent fb90332 commit b6e88eb
Show file tree
Hide file tree
Showing 11 changed files with 138 additions and 14 deletions.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version=2.0.4-SNAPSHOT
version=2.0.5-SNAPSHOT
kotlin.code.style=official

# Disable generation of metadata sha256/sha512 checksum
Expand Down
1 change: 1 addition & 0 deletions library/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ dependencies {

testImplementation(kotlin("test-junit5"))
testImplementation("org.junit.jupiter:junit-jupiter:5.7.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serializationVersion")
}

java {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package kotlinx.serialization.csv.config

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.StringFormat
import kotlinx.serialization.csv.Csv
import kotlinx.serialization.modules.SerializersModule

Expand Down Expand Up @@ -71,6 +72,11 @@ class CsvBuilder internal constructor(conf: CsvConfig = CsvConfig.Default) {
*/
var serializersModule: SerializersModule = conf.serializersModule

/**
* An alternative format to use when serializing items with variable size, such as lists.
*/
var deferToFormatWhenVariableColumns: StringFormat? = null

@OptIn(ExperimentalSerializationApi::class)
internal fun build(): CsvConfig {
require(delimiter != quoteChar) {
Expand Down Expand Up @@ -118,6 +124,7 @@ class CsvBuilder internal constructor(conf: CsvConfig = CsvConfig.Default) {
ignoreUnknownColumns = ignoreUnknownColumns,
hasTrailingDelimiter = hasTrailingDelimiter,
serializersModule = serializersModule,
deferToFormatWhenVariableColumns = deferToFormatWhenVariableColumns,
)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package kotlinx.serialization.csv.config

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.StringFormat
import kotlinx.serialization.modules.EmptySerializersModule
import kotlinx.serialization.modules.SerializersModule

Expand Down Expand Up @@ -34,6 +35,7 @@ internal data class CsvConfig(
val ignoreUnknownColumns: Boolean = false,
val hasTrailingDelimiter: Boolean = false,
val serializersModule: SerializersModule = EmptySerializersModule,
val deferToFormatWhenVariableColumns: StringFormat? = null
) {

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package kotlinx.serialization.csv.decode

import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.csv.Csv
import kotlinx.serialization.csv.HeadersNotSupportedForSerialDescriptorException
import kotlinx.serialization.csv.UnknownColumnHeaderException
import kotlinx.serialization.csv.UnsupportedSerialDescriptorException
import kotlinx.serialization.csv.config.CsvConfig
Expand Down Expand Up @@ -196,4 +198,17 @@ internal abstract class CsvDecoder(
subHeaders[key] = value
}
}

override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T {
return if (config.deferToFormatWhenVariableColumns != null) {
when (deserializer.descriptor.kind) {
is StructureKind.LIST,
is StructureKind.MAP,
is PolymorphicKind.OPEN -> {
config.deferToFormatWhenVariableColumns!!.decodeFromString(deserializer, decodeColumn())
}
else -> super.decodeSerializableValue(deserializer)
}
} else super.decodeSerializableValue(deserializer)
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package kotlinx.serialization.csv.decode

import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.csv.Csv
import kotlinx.serialization.descriptors.PolymorphicKind
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.StructureKind
import kotlinx.serialization.encoding.CompositeDecoder
Expand Down Expand Up @@ -51,4 +53,15 @@ internal class RootCsvDecoder(
readTrailingDelimiter()
return value
}

override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T {
return if (config.deferToFormatWhenVariableColumns != null) {
when (deserializer.descriptor.kind) {
is PolymorphicKind.OPEN -> {
config.deferToFormatWhenVariableColumns!!.decodeFromString(deserializer, decodeColumn())
}
else -> deserializer.deserialize(this)
}
} else deserializer.deserialize(this)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package kotlinx.serialization.csv.encode

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.csv.Csv
import kotlinx.serialization.csv.HeadersNotSupportedForSerialDescriptorException
import kotlinx.serialization.csv.UnsupportedSerialDescriptorException
Expand Down Expand Up @@ -117,19 +118,17 @@ internal abstract class CsvEncoder(
}

private fun printHeader(prefix: String, descriptor: SerialDescriptor) {
when (descriptor.kind) {
is StructureKind.LIST,
is StructureKind.MAP,
is PolymorphicKind.OPEN -> {
throw HeadersNotSupportedForSerialDescriptorException(descriptor)
}
}

for (i in 0 until descriptor.elementsCount) {
val name = prefix + descriptor.getElementName(i)
val childDesc = descriptor.getElementDescriptor(i)

when {
(childDesc.kind is StructureKind.LIST ||
childDesc.kind is StructureKind.MAP ||
childDesc.kind is PolymorphicKind.OPEN) && config.deferToFormatWhenVariableColumns != null -> {
writer.printColumn(name)
}

// TODO Check
childDesc.kind is SerialKind.CONTEXTUAL ->
writer.printColumn(name)
Expand Down Expand Up @@ -168,4 +167,21 @@ internal abstract class CsvEncoder(
) {
writer.printColumn(value, isNumeric, isNull)
}

override fun <T> encodeSerializableValue(serializer: SerializationStrategy<T>, value: T) {
if (config.deferToFormatWhenVariableColumns != null) {
when (serializer.descriptor.kind) {
is StructureKind.LIST,
is StructureKind.MAP,
is PolymorphicKind.OPEN -> {
encodeColumn(
value = config.deferToFormatWhenVariableColumns!!.encodeToString(serializer, value),
isNumeric = false,
isNull = false
)
}
else -> super.encodeSerializableValue(serializer, value)
}
} else super.encodeSerializableValue(serializer, value)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,11 @@ internal class CsvWriter(private val sb: Appendable, private val config: CsvConf
/** Check if given [value] contains reserved chars that require quoting. */
private fun requiresQuoting(value: String): Boolean {
val chars = with(config) { "${delimiter}${quoteChar}${recordSeparator}" }
return value.contains("[${Regex.escape(chars)}]".toRegex())
return value.contains("[${Regex.escape(chars)}]".toRegex()) || (
config.trimUnquotedWhitespace &&
value.length > 1 &&
(value.first().isWhitespace() || value.last().isWhitespace())
)
}

/** Escape all [escapeCharacters] in this [String] using [escapeChar]. */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package kotlinx.serialization.csv.encode

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.csv.Csv
import kotlinx.serialization.descriptors.PolymorphicKind
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.StructureKind
import kotlinx.serialization.encoding.CompositeEncoder
Expand Down Expand Up @@ -52,4 +54,19 @@ internal class RootCsvEncoder(
super.encodeColumn(value, isNumeric, isNull)
writer.endRecord()
}

override fun <T> encodeSerializableValue(serializer: SerializationStrategy<T>, value: T) {
if (config.deferToFormatWhenVariableColumns != null) {
when (serializer.descriptor.kind) {
is PolymorphicKind.OPEN -> {
encodeColumn(
value = config.deferToFormatWhenVariableColumns!!.encodeToString(serializer, value),
isNumeric = false,
isNull = false
)
}
else -> serializer.serialize(this, value)
}
} else serializer.serialize(this, value)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,12 @@ class CsvHasHeaderRecordTest {
)

@Test
fun testMultipleColumnsRN() = Csv {
fun testMultipleColumnsTrim() = Csv {
hasHeaderRecord = true
trimUnquotedWhitespace = true
}.assertDecode(
"a,b\r\n1,testing",
IntStringRecord(1, "testing"),
}.assertEncodeAndDecode(
"a,b\n1,\" testing \"",
IntStringRecord(1, " testing "),
IntStringRecord.serializer()
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.*
import kotlinx.serialization.csv.Csv
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.test.assertEncodeAndDecode
import kotlin.test.Test

Expand Down Expand Up @@ -118,9 +120,56 @@ class CsvCollectionsTest {
),
ListSerializer(Record.serializer())
)

@Test fun deferToFormatWhenVariableColumns() = Csv {
this.deferToFormatWhenVariableColumns = Json
}.assertEncodeAndDecode(
expected = """
0,[]
1,[1]
2,"[1,2]"
3,"[1,2,3]"
4,"[1,2,3,4]"
""".trimIndent(),
original = listOf(
Record2(0, listOf()),
Record2(1, listOf(1)),
Record2(2, listOf(1, 2)),
Record2(3, listOf(1, 2, 3)),
Record2(4, listOf(1, 2, 3, 4)),
),
serializer = ListSerializer(Record2.serializer())
)
@Test fun deferToFormatWhenVariableColumnsHeaders() = Csv {
this.deferToFormatWhenVariableColumns = Json
this.hasHeaderRecord = true
}.assertEncodeAndDecode(
expected = """
simple,list
0,[]
1,[1]
2,"[1,2]"
3,"[1,2,3]"
4,"[1,2,3,4]"
""".trimIndent(),
original = listOf(
Record2(0, listOf()),
Record2(1, listOf(1)),
Record2(2, listOf(1, 2)),
Record2(3, listOf(1, 2, 3)),
Record2(4, listOf(1, 2, 3, 4)),
),
serializer = ListSerializer(Record2.serializer())
)
}

@Serializable
data class Record(
val map: Map<List<Int>, List<Int>>
)

@Serializable
data class Record2(
val simple: Int,
val list: List<Int>
)

0 comments on commit b6e88eb

Please sign in to comment.