Skip to content

Commit

Permalink
Add date-time handling (#119)
Browse files Browse the repository at this point in the history
  • Loading branch information
Valentin Rocher authored Feb 28, 2022
1 parent 46740fb commit baed32d
Show file tree
Hide file tree
Showing 10 changed files with 2,095 additions and 17 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,15 @@ We are still developing and testing this library, so it has several limitations:
:white_check_mark: Comments \
:white_check_mark: Literal Strings \
:white_check_mark: Inline Tables \
:white_check_mark: Offset Date-Time (to `Instant` of [kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime)) \
:white_check_mark: Local Date-Time (to `LocalDateTime` of [kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime)) \
:white_check_mark: Local Date (to `LocalDate` of [kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime)) \
:x: Arrays: nested; multiline; of Different Types \
:x: Multiline Strings \
:x: Nested Inline Tables \
:x: Array of Tables \
:x: Inline Array of Tables \
:x: Offset Date-Time, Local: Date-Time; Date; Time
:x: Local Time

## Dependency
The library is hosted on the [Maven Central](https://search.maven.org/artifact/com.akuleshov7/ktoml-core).
Expand Down
1,918 changes: 1,918 additions & 0 deletions kotlin-js-store/yarn.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions ktoml-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ kotlin {
val commonMain by getting {
dependencies {
api("org.jetbrains.kotlinx:kotlinx-serialization-core:${Versions.SERIALIZATION}")
api("org.jetbrains.kotlinx:kotlinx-datetime:0.3.2")
implementation("org.jetbrains.kotlin:kotlin-stdlib:${Versions.KOTLIN}")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ package com.akuleshov7.ktoml.decoders
import com.akuleshov7.ktoml.exceptions.CastException
import com.akuleshov7.ktoml.exceptions.IllegalTypeException
import com.akuleshov7.ktoml.tree.TomlKeyValue
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.encoding.AbstractDecoder

Expand All @@ -12,6 +16,10 @@ import kotlinx.serialization.encoding.AbstractDecoder
*/
@ExperimentalSerializationApi
public abstract class TomlAbstractDecoder : AbstractDecoder() {
private val instantSerializer = Instant.serializer()
private val localDateTimeSerializer = LocalDateTime.serializer()
private val localDateSerializer = LocalDate.serializer()

// Invalid Toml primitive types, we will simply throw an error for them
override fun decodeByte(): Byte = invalidType("Byte", "Long")
override fun decodeShort(): Short = invalidType("Short", "Long")
Expand All @@ -25,6 +33,20 @@ public abstract class TomlAbstractDecoder : AbstractDecoder() {
override fun decodeDouble(): Double = decodePrimitiveType()
override fun decodeString(): String = decodePrimitiveType()

protected fun DeserializationStrategy<*>.isDateTime(): Boolean =
descriptor == instantSerializer.descriptor ||
descriptor == localDateTimeSerializer.descriptor ||
descriptor == localDateSerializer.descriptor

// Cases for date-time types
@Suppress("UNCHECKED_CAST")
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T = when (deserializer.descriptor) {
instantSerializer.descriptor -> decodePrimitiveType<Instant>() as T
localDateTimeSerializer.descriptor -> decodePrimitiveType<LocalDateTime>() as T
localDateSerializer.descriptor -> decodePrimitiveType<LocalDate>() as T
else -> super.decodeSerializableValue(deserializer)
}

internal abstract fun decodeKeyValue(): TomlKeyValue

private fun invalidType(typeName: String, requiredType: String): Nothing {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.akuleshov7.ktoml.tree.TomlKeyValueArray
import com.akuleshov7.ktoml.tree.TomlKeyValuePrimitive
import com.akuleshov7.ktoml.tree.TomlNull
import com.akuleshov7.ktoml.tree.TomlValue
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.CompositeDecoder
Expand Down Expand Up @@ -72,6 +73,11 @@ public class TomlArrayDecoder(
override fun decodeBoolean(): Boolean = currentElementDecoder.decodeBoolean()
override fun decodeChar(): Char = currentElementDecoder.decodeChar()
override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = currentElementDecoder.decodeEnum(enumDescriptor)
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T = if (deserializer.isDateTime()) {
currentElementDecoder.decodeSerializableValue(deserializer)
} else {
super.decodeSerializableValue(deserializer)
}

// this should be applied to [currentPrimitiveElementOfArray] and not to the [rootNode], because
override fun decodeNotNullMark(): Boolean = currentPrimitiveElementOfArray !is TomlNull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import com.akuleshov7.ktoml.TomlConfig
import com.akuleshov7.ktoml.exceptions.ParseException
import com.akuleshov7.ktoml.parsers.findBeginningOfTheComment

private typealias ValueCreator = (String, Int) -> TomlValue

/**
* Interface that contains all common methods that are used in KeyValue nodes
*/
Expand Down Expand Up @@ -114,25 +116,29 @@ public fun String.parseValue(lineNo: Int, config: TomlConfig): TomlValue = when
"true", "false" -> TomlBoolean(this, lineNo)
else -> when (this[0]) {
// ===== literal strings
'\'' -> if (this.startsWith("'''")) TomlBasicString(this, lineNo) else TomlLiteralString(this, lineNo, config)
'\'' -> if (this.startsWith("'''")) {
TomlBasicString(this, lineNo)
} else {
TomlLiteralString(this, lineNo, config)
}
// ===== basic strings
'\"' -> TomlBasicString(this, lineNo)
else ->
try {
// ===== integer values
TomlLong(this, lineNo)
} catch (e: NumberFormatException) {
try {
// ===== float values
TomlDouble(this, lineNo)
} catch (e: NumberFormatException) {
// ===== fallback strategy in case of invalid value
TomlBasicString(this, lineNo)
}
}
else -> tryParseValue<NumberFormatException>(lineNo, ::TomlLong) // ==== integer values
?: tryParseValue<NumberFormatException>(lineNo, ::TomlDouble) // ===== float values
?: tryParseValue<IllegalArgumentException>(lineNo, ::TomlDateTime) // ===== date-time values
?: TomlBasicString(this, lineNo) // ===== fallback strategy in case of invalid value
}
}

private inline fun <reified E : Throwable> String.tryParseValue(
lineNo: Int,
transform: ValueCreator
): TomlValue? = try {
transform(this, lineNo)
} catch (e: Throwable) {
if (e is E) null else throw e
}

/**
* method to get proper value from content to get key or value
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.akuleshov7.ktoml.exceptions.ParseException
import com.akuleshov7.ktoml.parsers.trimBrackets
import com.akuleshov7.ktoml.parsers.trimQuotes
import com.akuleshov7.ktoml.parsers.trimSingleQuotes
import kotlinx.datetime.*

/**
* Base class for all nodes that represent values
Expand Down Expand Up @@ -179,6 +180,38 @@ internal constructor(
public constructor(content: String, lineNo: Int) : this(content.toBoolean(), lineNo)
}

/**
* Toml AST Node for a representation of date-time types (offset date-time, local date-time, local date)
* @property content
*/
public class TomlDateTime
internal constructor(
override var content: Any,
lineNo: Int
) : TomlValue(lineNo) {
public constructor(content: String, lineNo: Int) : this(content.trim().parseToDateTime(), lineNo)

public companion object {
private fun String.parseToDateTime(): Any = try {
// Offset date-time
toInstant()
} catch (e: IllegalArgumentException) {
try {
// TOML spec allows a space instead of the T, try replacing the first space by a T
replaceFirst(' ', 'T').toInstant()
} catch (e: IllegalArgumentException) {
try {
// Local date-time
toLocalDateTime()
} catch (e: IllegalArgumentException) {
// Local date
toLocalDate()
}
}
}
}
}

/**
* Toml AST Node for a representation of null:
* null, nil, NULL, NIL or empty (key = )
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.akuleshov7.ktoml.decoders

import com.akuleshov7.ktoml.Toml
import com.akuleshov7.ktoml.exceptions.ParseException
import kotlinx.datetime.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

class DateTimeDecoderTest {

@Serializable
data class TimeTable(
val instants: List<Instant>,
val localDateTimes: List<LocalDateTime>,
val localDate: LocalDate,
val dateInString: String
)

@Test
fun testDateParsing() {
val toml = """
instants = [1979-05-27T07:32:00Z, 1979-05-27T00:32:00-07:00, 1979-05-27T00:32:00.999999-07:00, 1979-05-27 07:32:00Z]
localDateTimes = [1979-05-27T07:32:00, 1979-05-27T00:32:00.999999]
localDate = 1979-05-27
dateInString = "1979-05-27T00:32:00-07:00"
""".trimIndent()
val expectedInstants = listOf(
LocalDateTime(1979, 5, 27, 7, 32, 0)
.toInstant(TimeZone.UTC),
LocalDateTime(1979, 5, 27, 0, 32, 0)
.toInstant(TimeZone.of("UTC-7")),
LocalDateTime(1979, 5, 27, 0, 32, 0, 999999000)
.toInstant(TimeZone.of("UTC-7")),
LocalDateTime(1979, 5, 27, 7, 32, 0)
.toInstant(TimeZone.UTC),
)
val expectedLocalDateTimes = listOf(
LocalDateTime(1979, 5, 27, 7, 32, 0),
LocalDateTime(1979, 5, 27, 0, 32, 0, 999999000)
)
val expectedLocalDate = LocalDate(1979, 5, 27)
assertEquals(
TimeTable(
expectedInstants,
expectedLocalDateTimes,
expectedLocalDate,
"1979-05-27T00:32:00-07:00"
),
Toml().decodeFromString(toml)
)
}

@Serializable
data class InvalidInstant(val instant: Instant)

@Serializable
data class InvalidDateTime(val dateTime: LocalDateTime)

@Serializable
data class InvalidDate(val date: LocalDate)

@Test
fun testInvalidData() {
assertFailsWith<ParseException> {
Toml.decodeFromString<InvalidInstant>("instant=1979-05-27T07:32:00INVALID")
}
assertFailsWith<ParseException> {
Toml.decodeFromString<InvalidDateTime>("dateTime=1979/05/27T07:32:00")
}
assertFailsWith<ParseException> {
Toml.decodeFromString<InvalidDate>("date=1979/05/27")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import com.akuleshov7.ktoml.exceptions.InvalidEnumValueException
import com.akuleshov7.ktoml.exceptions.MissingRequiredPropertyException
import com.akuleshov7.ktoml.exceptions.ParseException
import com.akuleshov7.ktoml.exceptions.UnknownNameException
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ class ValueParserTest {
testTomlValue(Pair("a", "\'false\'"), NodeType.LITERAL_STRING)
}

@Test
fun dateTimeParsingTest() {
testTomlValue("a" to "1979-05-27T07:32:00Z", NodeType.DATE_TIME)
testTomlValue("a" to "1979-05-27T00:32:00-07:00", NodeType.DATE_TIME)
testTomlValue("a" to "1979-05-27T00:32:00.999999-07:00", NodeType.DATE_TIME)
testTomlValue("a" to "1979-05-27 07:32:00Z", NodeType.DATE_TIME)
testTomlValue("a" to "1979-05-27T07:32:00", NodeType.DATE_TIME)
testTomlValue("a" to "1979-05-27T00:32:00.999999", NodeType.DATE_TIME)
testTomlValue("a" to "1979-05-27", NodeType.DATE_TIME)
}

@Test
fun nullParsingTest() {
testTomlValue("a" to "null", NodeType.NULL)
Expand Down Expand Up @@ -98,7 +109,7 @@ class ValueParserTest {
}

enum class NodeType {
STRING, NULL, INT, FLOAT, BOOLEAN, INCORRECT, LITERAL_STRING
STRING, NULL, INT, FLOAT, BOOLEAN, INCORRECT, LITERAL_STRING, DATE_TIME
}

fun getNodeType(v: TomlValue): NodeType = when (v) {
Expand All @@ -108,6 +119,7 @@ fun getNodeType(v: TomlValue): NodeType = when (v) {
is TomlDouble -> NodeType.FLOAT
is TomlBoolean -> NodeType.BOOLEAN
is TomlLiteralString -> NodeType.LITERAL_STRING
is TomlDateTime -> NodeType.DATE_TIME
else -> NodeType.INCORRECT
}

Expand Down

0 comments on commit baed32d

Please sign in to comment.