From 3be0331cb50cc556fa7616967edebe2f27dea1a2 Mon Sep 17 00:00:00 2001 From: Andrey Kuleshov Date: Sun, 11 Jul 2021 02:41:43 +0300 Subject: [PATCH 1/4] Cosmetic preparations and bug-fixing before the 0.2.6 release ### What's done: - Changed the API and public API names - Fixed bugs related to Array parsing - Changed the decoding logic: beginStructure - Updated the readme and added more test examples --- README.md | 182 +++++++++++----- .../kotlin/com/akuleshov7/ktoml/Ktoml.kt | 66 +++--- .../kotlin/com/akuleshov7/ktoml/KtomlConf.kt | 10 + .../ktoml/{KtomlSerializer.kt => Toml.kt} | 35 ++-- .../akuleshov7/ktoml/decoders/DecoderConf.kt | 6 - .../akuleshov7/ktoml/decoders/TomlDecoder.kt | 41 ++-- .../ktoml/decoders/TomlListDecoder.kt | 3 +- .../akuleshov7/ktoml/parsers/ParserConf.kt | 7 - .../akuleshov7/ktoml/parsers/TomlParser.kt | 19 +- .../ktoml/parsers/node/TomlKeyValue.kt | 22 +- .../ktoml/parsers/node/TomlValue.kt | 8 +- .../kotlin/decoder/ArrayDecoderTest.kt | 116 ++++++++++- .../kotlin/decoder/DottedKeysDecoderTest.kt | 44 ++-- .../kotlin/decoder/GeneralDecoderTest.kt | 196 ++++++++---------- .../kotlin/decoder/PartialDecoderTest.kt | 8 +- .../kotlin/decoder/ReadMeExampleTest.kt | 70 +++++++ .../kotlin/file/TomlFileParserTest.kt | 13 +- .../kotlin/node/parser/ValueParserTest.kt | 12 +- 18 files changed, 557 insertions(+), 301 deletions(-) create mode 100644 ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/KtomlConf.kt rename ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/{KtomlSerializer.kt => Toml.kt} (78%) delete mode 100644 ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/DecoderConf.kt delete mode 100644 ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/ParserConf.kt create mode 100644 ktoml-core/src/commonTest/kotlin/decoder/ReadMeExampleTest.kt diff --git a/README.md b/README.md index 713d13fe..8c266e24 100644 --- a/README.md +++ b/README.md @@ -3,39 +3,68 @@ ![Releases](https://img.shields.io/github/v/release/akuleshov7/ktoml) ![Maven Central](https://img.shields.io/maven-central/v/com.akuleshov7/ktoml-core) ![License](https://img.shields.io/github/license/akuleshov7/ktoml) - ![Build and test](https://github.com/akuleshov7/ktoml/actions/workflows/build_and_test.yml/badge.svg?branch=main) - ![Lines of code](https://img.shields.io/tokei/lines/github/akuleshov7/ktoml) ![Hits-of-Code](https://hitsofcode.com/github/akuleshov7/ktoml?branch=main) ![GitHub repo size](https://img.shields.io/github/repo-size/akuleshov7/ktoml) - ![codebeat badge](https://codebeat.co/badges/0518ea49-71ed-4bfd-8dd3-62da7034eebd) ![maintainability](https://api.codeclimate.com/v1/badges/c75d2d6b0d44cea7aefe/maintainability) -Absolutely Native and multiplatform Kotlin serialization library for serialization/deserialization of [toml](https://toml.io/en/) format. Uses `kotlinx.serialization`; uses only native serialization provided by Kotlin, no Java code. +Fully Native and Multiplatform Kotlin serialization library for serialization/deserialization of [toml](https://toml.io/en/) format. +Uses native `kotlinx.serialization`, provided by Kotlin. This library contains no Java code or Java dependencies. +We believe that TOML is actually the most readable and user-friendly **configuration file** format. +So we decided to support this format in the kotlinx serialization library. -## Supported platforms +### Contribution +As this young and big project [is needed](https://github.com/Kotlin/kotlinx.serialization/issues/1092) by the Kotlin community, we need your help. +We will be glad if you will test `ktoml` or contribute to this project. +In case you don't have much time for this - at least spend 5 seconds to give us a star to attract other contributors! +**Thanks!** :pray: -All the code is written in Kotlin common module. That means that it can be built for each and every native platform. But to reduce the scope, ktoml supports only the following platforms (if needed - other platfroms could be added later): +## Supported platforms +All the code is written in Kotlin **common** module. This means that it can be built for each and every Kotlin native platform. +However, to reduce the scope, ktoml now supports only the following platforms: - jvm - mingwx64 - linuxx64 - macosx64 +Other platforms could be added later on the demand or easily built by users. + +## Current limitations +**General** \ +We are still developing and testing this library, so it has several limitations: \ +:white_check_mark: deserialization (with some parsing limitations) \ +:x: serialization (not implemented [yet](https://github.com/akuleshov7/ktoml/issues/11), less important for TOML config-files) + +**Parsing** \ +:white_check_mark: Table sections (single and dotted) \ +:white_check_mark: Key-value pairs (single and dotted) \ +:white_check_mark: Integer type \ +:white_check_mark: Float type \ +:white_check_mark: String type \ +:white_check_mark: Float type \ +:white_check_mark: Boolean type \ +:white_check_mark: Simple Arrays \ +:white_check_mark: Comments \ +:x: Arrays: nested; multiline; of Different Types \ +:x: Literal Strings \ +:x: Multiline Strings \ +:x: Inline Tables \ +:x: Array of Tables \ +:x: Offset Date-Time, Local: Date-Time; Date; Time ## Dependency +The library is hosted on the [Maven Central](https://search.maven.org/artifact/com.akuleshov7/ktoml-core). To import `ktoml` library you need to add following dependencies to your code: -
Maven - ```pom com.akuleshov7 ktoml-core - 0.2.2 + 0.2.6 ```
@@ -44,7 +73,7 @@ To import `ktoml` library you need to add following dependencies to your code: Gradle Groovy ```groovy -implementation 'com.akuleshov7:ktoml-core:0.2.2' +implementation 'com.akuleshov7:ktoml-core:0.2.6' ``` @@ -52,11 +81,18 @@ implementation 'com.akuleshov7:ktoml-core:0.2.2' Gradle Kotlin ```kotlin -implementation("com.akuleshov7:ktoml-core:0.2.2") +implementation("com.akuleshov7:ktoml-core:0.2.6") ``` + ## How to use +:heavy_exclamation_mark: as TOML is a foremost configuration language, we also have supported the deserialization from file. +However, to read the file we are using [okio](https://github.com/square/okio), so it will be added as a dependency. + +:bangbang: there are two ways of calling TOML serialization API +- Fast and mutable: you need to create a serialization instance once and call serialization methods on it later; +- Slow and immutable: in case you need to read a few TOML configurations you can use String extension methods; **Deserialization:** ```kotlin @@ -66,22 +102,31 @@ import com.akuleshov7.ktoml.deserialize data class MyClass(/* your fields */) // to deserialize toml input in a string format (separated by newlines '\n') -val result = deserialize(/* string with a toml input */) +val result = /* string with a toml input */.deserializeToml() // to deserialize toml input from file -val result = deserializeFile(/* string with a path to a toml file */) +val result = /* string with path to a toml file */.deserializeTomlFile() + +// in case you need optimized code, you can create serialization object once: +val mySerializer = Toml() +val result = mySerializer.deserializeToml( /* string with a toml input */ ) +``` +**Partial Deserialization:** +Partial Deserialization is useful when you would like to deserialize only ONE table and you do not want + to reproduce whole object structure in the code. + +```kotlin // if you need to deserialize only some part of the toml - provide the full name of the toml table // the deserializer will work only with children of this table -// For example: +// For example if you have the following toml, but want only to decode [c.d.e.f]: // [a] // b = 1 // [c.d.e.f] -// d = 5 - -val result = deserialize(/* string with a toml input */, "c.d.e.f") -val result = deserializeFile(/* string with a path to a toml file */, "c.d.e.f") +// d = "5" +val result = /* string with a toml input */.deserializeToml("c.d.e.f") +val result = /* string with path to a toml file */.deserializeTomlFile("c.d.e.f") ``` **Parser to AST:** @@ -92,52 +137,95 @@ TomlParser(/* path to your file */).readAndParseFile() TomlParser(/* the string that you will try to parse */).parseString() ``` -## How it works with examples +## How ktoml works: examples This tool natively deserializes toml expressions using native Kotlin compiler plug-in and [kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serialization-guide.md). -from toml test: -```text -e = 777 +The following example: +```toml +someBooleanProperty = true [table1] -a = 5 -b = 6 - +property1 = 5 +property2 = 6 + [table2] -a = 5 - - [table2.inlineTable] - a = "a" +someNumber = 5 + [table2."akuleshov7.com"] + name = "my name" + configurationList = ["a", "b", "c"] + +# such redeclaration of table2 +# is prohibited in toml specification; +# but ktoml is allowing it in non-strict mode: +[table2] +otherNumber = 5.56 ``` -to `MyClass` +can be deserialized to `MyClass`: ```kotlin @Serializable - data class MyClass(val e: Int, val table1: Table1, val table2: Table2) + data class MyClass( + val someBooleanProperty: Boolean, + val table1: Table1, + val table2: Table2 + ) @Serializable - data class Table1(val a: Int, val b: Int) + data class Table1(val property1: Int, val property2: Int) @Serializable - data class Table2(val a: Int, val inlineTable: InlineTable) + data class Table2( + val someNumber: Int, + @SerialName("akuleshov7.com") + val inlineTable: InlineTable, + val otherNumber: Double + ) @Serializable - data class InlineTable(val a: String) + data class InlineTable( + val name: String, + @SerialName("configurationList") + val overriddenName: List + ) ``` -Or in json-terminology: +with the following code: +```kotlin +stringWhereTomlIsStored.deserialize() +``` + +The example above in json-terminology: ```json - { - "table1": { - "a": 5, - "b": 5 - }, - "table2": { - "a": 5, - "inlineTable": { - "a": "a", - } - } - } +{ + "someBooleanProperty": true, + "table1": { + "property1": 5, + "property2": 5 + }, + "table2": { + "someNumber": 5, + "akuleshov7.com": { + "name": "my name", + "configurationList": ["a", "b", "c"], + "otherNumber": 5.56 + } + } +} ``` + +You can check how this example works in [ReadMeExampleTest](ktoml-core/src/commonTest/kotlin/decoder/ReadMeExampleTest.kt). + +### Configuration +Ktoml parsing and deserialization was made configurable to fit all the requirements from users. +We have created a special configuration class that can be passed to the decoder method: +```kotlin +stringWhereTomlIsStored.deserialize( + ktomlConf = KtomlConf( + // allow/prohibit unknown names during the deserialization + ignoreUnknownNames= false, + // allow/prohibit empty values like "a = # comment" + emptyValuesAllowed = true + ) +) +``` diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/Ktoml.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/Ktoml.kt index 42ac29b8..5dd654d0 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/Ktoml.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/Ktoml.kt @@ -4,8 +4,6 @@ package com.akuleshov7.ktoml -import com.akuleshov7.ktoml.decoders.DecoderConf - import okio.ExperimentalFileSystem import kotlinx.serialization.ExperimentalSerializationApi @@ -14,57 +12,65 @@ import kotlinx.serialization.serializer /** * simple deserializer of a string in a toml format (separated by newlines) * - * @param request - string in toml format with '\n' or '\r\n' separation - * @param decoderConfig - optional config to configure extra options (not required) + * this: request-string in toml format with '\n' or '\r\n' separation + * + * @param ktomlConfig - optional config to configure extra options (not required) * @return deserialized object of type T */ @OptIn(ExperimentalSerializationApi::class) -inline fun deserialize( - request: String, - decoderConfig: DecoderConf = DecoderConf() -): T = KtomlSerializer(decoderConfig).decodeFromString(serializer(), request) +inline fun String.deserializeToml( + ktomlConfig: KtomlConf = KtomlConf() +): T = Toml(ktomlConfig).decodeFromString(serializer(), this) /** - * partial deserializer of a string in a toml format (separated by newlines). Will deserialize only the part presented - * under the tomlTableName table. If such table is missing in he input - will throw an exception + * partial deserializer of a string in a toml format (separated by newlines). + * Will deserialize only the part presented under the tomlTableName table. + * If such table is missing in he input - will throw an exception + * + * (!) Useful when you would like to deserialize only ONE table + * and you do not want to reproduce whole object structure in the code + * + * this: request-string in toml format with '\n' or '\r\n' separation * - * @param request - string in toml format with '\n' or '\r\n' separation - * @param decoderConfig - optional config to configure extra options (not required) + * @param ktomlConfig - optional config to configure extra options (not required) * @param tomlTableName fully qualified name of the toml table (it should be the full name - a.b.c.d) * @return deserialized object of type T */ @OptIn(ExperimentalSerializationApi::class) -inline fun deserialize( - request: String, +inline fun String.deserializeToml( tomlTableName: String, - decoderConfig: DecoderConf = DecoderConf() -): T = KtomlSerializer(decoderConfig).decodeFromString(serializer(), request, tomlTableName) + ktomlConfig: KtomlConf = KtomlConf() +): T = Toml(ktomlConfig).partiallyDecodeFromString(serializer(), this, tomlTableName) /** * simple deserializer of a file that contains toml. Reading file with okio native library * - * @param tomlFilePath - path to the file where toml is stored - * @param decoderConfig - optional config to configure extra options (not required) + * this: path to the file where toml is stored + * + * @param ktomlConfig - optional config to configure extra options (not required) * @return deserialized object of type T */ @OptIn(ExperimentalFileSystem::class, ExperimentalSerializationApi::class) -inline fun deserializeFile( - tomlFilePath: String, - decoderConfig: DecoderConf = DecoderConf() -): T = KtomlSerializer(decoderConfig).decodeFromFile(serializer(), tomlFilePath) +inline fun String.deserializeTomlFile( + ktomlConfig: KtomlConf = KtomlConf() +): T = Toml(ktomlConfig).decodeFromFile(serializer(), this) /** - * partial deserializer of a file that contains toml. Reading file with okio native library. Will deserialize only the part presented - * under the tomlTableName table. If such table is missing in he input - will throw an exception. + * partial deserializer of a file that contains toml. Reading file with okio native library. + * Will deserialize only the part presented under the tomlTableName table. + * If such table is missing in he input - will throw an exception. + * + * (!) Useful when you would like to deserialize only ONE table + * and you do not want to reproduce whole object structure in the code + * + * this: path to the file where toml is stored * - * @param tomlFilePath - path to the file where toml is stored - * @param decoderConfig - optional config to configure extra options (not required) + * @param ktomlConfig - optional config to configure extra options (not required) * @param tomlTableName fully qualified name of the toml table (it should be the full name - a.b.c.d) * @return deserialized object of type T */ @OptIn(ExperimentalFileSystem::class, ExperimentalSerializationApi::class) -inline fun deserializeFile( - tomlFilePath: String, +inline fun String.deserializeTomlFile( tomlTableName: String, - decoderConfig: DecoderConf = DecoderConf() -): T = KtomlSerializer(decoderConfig).decodeFromFile(serializer(), tomlFilePath, tomlTableName) + ktomlConfig: KtomlConf = KtomlConf() +): T = Toml(ktomlConfig).partiallyDecodeFromFile(serializer(), this, tomlTableName) diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/KtomlConf.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/KtomlConf.kt new file mode 100644 index 00000000..536158e9 --- /dev/null +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/KtomlConf.kt @@ -0,0 +1,10 @@ +package com.akuleshov7.ktoml + +/** + * @property ignoreUnknownNames - a control to allow/prohibit unknown names during the deserialization + * @property emptyValuesAllowed - a control to allow/prohibit empty values: a = # comment + */ +data class KtomlConf( + val ignoreUnknownNames: Boolean = false, + val emptyValuesAllowed: Boolean = true +) diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/KtomlSerializer.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/Toml.kt similarity index 78% rename from ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/KtomlSerializer.kt rename to ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/Toml.kt index 46d50e6c..5ca34390 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/KtomlSerializer.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/Toml.kt @@ -1,6 +1,5 @@ package com.akuleshov7.ktoml -import com.akuleshov7.ktoml.decoders.DecoderConf import com.akuleshov7.ktoml.decoders.TomlDecoder import com.akuleshov7.ktoml.exceptions.MissingRequiredFieldException import com.akuleshov7.ktoml.parsers.TomlParser @@ -13,30 +12,39 @@ import kotlinx.serialization.modules.EmptySerializersModule import kotlinx.serialization.modules.SerializersModule /** - * KtomlSerializer class - is a general class, that should be used to serialize/deserialize TOML file or string + * KtomlSerializer class - is a general class in the core, that is used to serialize/deserialize TOML file or string * * @property config - configuration for the serialization * @property serializersModule - default overridden */ @ExperimentalSerializationApi -public class KtomlSerializer( - private val config: DecoderConf = DecoderConf(), +class Toml( + private val config: KtomlConf = KtomlConf(), override val serializersModule: SerializersModule = EmptySerializersModule ) : StringFormat { + // this is moved to properties to reduce the number of created classes for each toml + val tomlParser = TomlParser(config) + // FixMe: need to fix code duplication here - // ================== decoders =============== + // ================== basic overrides =============== override fun decodeFromString(deserializer: DeserializationStrategy, string: String): T { - val parsedToml = TomlParser(string).parseString() + val parsedToml = tomlParser.parseString(string) return TomlDecoder.decode(deserializer, parsedToml, config) } + override fun encodeToString(serializer: SerializationStrategy, value: T): String { + TODO("Not yet implemented") + } + + // ================== custom decoding methods =============== + /** * @param deserializer * @param toml * @param tomlTableName * @return decoded object of type T */ - fun decodeFromString( + fun partiallyDecodeFromString( deserializer: DeserializationStrategy, toml: String, tomlTableName: String @@ -52,7 +60,7 @@ public class KtomlSerializer( */ @ExperimentalFileSystem fun decodeFromFile(deserializer: DeserializationStrategy, tomlFilePath: String): T { - val parsedToml = TomlParser(tomlFilePath).readAndParseFile() + val parsedToml = TomlParser(config).readAndParseFile(tomlFilePath) return TomlDecoder.decode(deserializer, parsedToml, config) } @@ -63,7 +71,7 @@ public class KtomlSerializer( * @return decoded object of type T */ @ExperimentalFileSystem - fun decodeFromFile( + fun partiallyDecodeFromFile( deserializer: DeserializationStrategy, tomlFilePath: String, tomlTableName: String @@ -73,12 +81,13 @@ public class KtomlSerializer( return TomlDecoder.decode(deserializer, fakeFileNode, config) } + @Suppress("TYPE_ALIAS") private fun generateFakeTomlStructureForPartialParsing( toml: String, tomlTableName: String, - parsingFunction: (TomlParser) -> TomlFile + parsingFunction: (TomlParser, String) -> TomlFile ): TomlFile { - val parsedToml = parsingFunction(TomlParser(toml)) + val parsedToml = parsingFunction(TomlParser(config), toml) .findTableInAstByName(tomlTableName, tomlTableName.count { it == '.' } + 1) ?: throw MissingRequiredFieldException( "Table with <$tomlTableName> name is missing in the toml input. " + @@ -93,8 +102,4 @@ public class KtomlSerializer( return fakeFileNode } - - override fun encodeToString(serializer: SerializationStrategy, value: T): String { - TODO("Not yet implemented") - } } diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/DecoderConf.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/DecoderConf.kt deleted file mode 100644 index 49a7f63f..00000000 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/DecoderConf.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.akuleshov7.ktoml.decoders - -/** - * @property ignoreUnknownNames - */ -data class DecoderConf(val ignoreUnknownNames: Boolean = false) diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlDecoder.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlDecoder.kt index 243904a3..f076af96 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlDecoder.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlDecoder.kt @@ -2,6 +2,7 @@ package com.akuleshov7.ktoml.decoders +import com.akuleshov7.ktoml.KtomlConf import com.akuleshov7.ktoml.exceptions.* import com.akuleshov7.ktoml.parsers.node.* import kotlinx.serialization.* @@ -16,7 +17,7 @@ import kotlinx.serialization.modules.* @ExperimentalSerializationApi public class TomlDecoder( val rootNode: TomlNode, - val config: DecoderConf, + val config: KtomlConf, ) : AbstractDecoder() { private var elementIndex = 0 override val serializersModule: SerializersModule = EmptySerializersModule @@ -124,20 +125,20 @@ public class TomlDecoder( } } + /** + * this method does all the iteration logic for processing code structures and collections + * treat it as an !entry point! and the orchestrator of the decoding + */ override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder = when (rootNode) { is TomlFile -> { checkMissingRequiredField(rootNode.children, descriptor) - val firstChild = rootNode.getFirstChild() ?: throw InternalDecodingException( + val firstFileChild = rootNode.getFirstChild() ?: throw InternalDecodingException( "Missing child nodes (tales, key-values) for TomlFile." + " Empty toml was provided to the input?" ) - TomlDecoder(firstChild, config) + TomlDecoder(firstFileChild, config) } - - is TomlKeyValueList -> TomlListDecoder(rootNode, config) - - // need to move on here, but also need to pass children into the function - is TomlTable, is TomlKeyValueSimple, is TomlStubEmptyNode -> { + else -> { // this is a little bit tricky index calculation, suggest not to change // we are using the previous node to get all neighbour nodes: // (parentNode) @@ -145,13 +146,23 @@ public class TomlDecoder( val nextProcessingNode = rootNode .getNeighbourNodes() .elementAt(elementIndex - 1) - .getFirstChild() ?: throw InternalDecodingException( - "Decoding process failed due to invalid structure of parsed AST tree: missing children" - ) - - checkMissingRequiredField(nextProcessingNode.getNeighbourNodes(), descriptor) - TomlDecoder(nextProcessingNode, config) + when (nextProcessingNode) { + is TomlKeyValueList -> TomlListDecoder(nextProcessingNode, config) + is TomlKeyValueSimple, is TomlStubEmptyNode -> TomlDecoder(nextProcessingNode, config) + is TomlTable -> { + val firstTableChild = nextProcessingNode.getFirstChild() ?: throw InternalDecodingException( + "Decoding process failed due to invalid structure of parsed AST tree: missing children" + + " in a table <${nextProcessingNode.fullTableName}>" + ) + checkMissingRequiredField(firstTableChild.getNeighbourNodes(), descriptor) + TomlDecoder(firstTableChild, config) + } + else -> throw InternalDecodingException( + "Incorrect decdong state in the beginStructure()" + + " with $nextProcessingNode (${nextProcessingNode.content})[${nextProcessingNode.name}]" + ) + } } } @@ -183,7 +194,7 @@ public class TomlDecoder( fun decode( deserializer: DeserializationStrategy, rootNode: TomlNode, - config: DecoderConf = DecoderConf() + config: KtomlConf = KtomlConf() ): T { val decoder = TomlDecoder(rootNode, config) return decoder.decodeSerializableValue(deserializer) diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlListDecoder.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlListDecoder.kt index 358b7388..6ffe996f 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlListDecoder.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlListDecoder.kt @@ -1,5 +1,6 @@ package com.akuleshov7.ktoml.decoders +import com.akuleshov7.ktoml.KtomlConf import com.akuleshov7.ktoml.parsers.node.TomlKeyValueList import com.akuleshov7.ktoml.parsers.node.TomlValue import kotlinx.serialization.ExperimentalSerializationApi @@ -17,7 +18,7 @@ import kotlinx.serialization.modules.SerializersModule @Suppress("UNCHECKED_CAST") public class TomlListDecoder( val rootNode: TomlKeyValueList, - val config: DecoderConf, + val config: KtomlConf, ) : AbstractDecoder() { private var nextElementIndex = 0 private val list = rootNode.value.content as List diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/ParserConf.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/ParserConf.kt deleted file mode 100644 index f3ae2a10..00000000 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/ParserConf.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.akuleshov7.ktoml.parsers - -/** - * @param emptyValuesAllowed - control to allow/prohibit the following: a = # comment - * @property emptyValuesAllowed - */ -data class ParserConf(val emptyValuesAllowed: Boolean = true) diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/TomlParser.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/TomlParser.kt index 463f8bf0..caf12e93 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/TomlParser.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/TomlParser.kt @@ -1,5 +1,6 @@ package com.akuleshov7.ktoml.parsers +import com.akuleshov7.ktoml.KtomlConf import com.akuleshov7.ktoml.exceptions.InternalAstException import com.akuleshov7.ktoml.parsers.node.TomlFile import com.akuleshov7.ktoml.parsers.node.TomlKeyValue @@ -15,19 +16,18 @@ import okio.FileSystem import okio.Path.Companion.toPath /** - * @property toml - this argument can be a string with path to a toml file or a raw string in the toml format, - * depending on how you plan to work with it. - * @property parserConf - object that stores configuration options for a parser + * @property ktomlConf - object that stores configuration options for a parser */ -public class TomlParser(val toml: String, val parserConf: ParserConf = ParserConf()) { +public inline class TomlParser(val ktomlConf: KtomlConf) { /** * Method for parsing of TOML file (reading line by line and parsing to a special TOML AST tree) * + * @param toml - a string with path to a toml file * @return the TomlFile root node * @throws e: FileNotFoundException if the toml file is missing */ @ExperimentalFileSystem - fun readAndParseFile(): TomlFile { + fun readAndParseFile(toml: String): TomlFile { try { val ktomlPath = toml.toPath() val ktomlLinesFromFile = FileSystem.SYSTEM.read(ktomlPath) { @@ -44,9 +44,10 @@ public class TomlParser(val toml: String, val parserConf: ParserConf = ParserCon /** * Method for parsing of TOML string (this string should be split with newlines \n or \r\n) * + * @param toml a raw string in the toml format with '\n' separator * @return the root TomlFile node of the Tree that we have built after parsing */ - fun parseString(): TomlFile { + fun parseString(toml: String): TomlFile { // It looks like we need this hack to process line separator properly, as we don't have System.lineSeparator() val tomlString = toml.replace("\\r\\n", "\n") return parseStringsToTomlNode(tomlString.split("\n")) @@ -77,7 +78,7 @@ public class TomlParser(val toml: String, val parserConf: ParserConf = ParserCon } currentParent = newParent } else { - val keyValue = line.parseTomlKeyValue(lineNo, parserConf) + val keyValue = line.parseTomlKeyValue(lineNo, ktomlConf) if (keyValue !is TomlNode) { throw InternalAstException("All Toml nodes should always inherit TomlNode class") } @@ -110,8 +111,8 @@ public class TomlParser(val toml: String, val parserConf: ParserConf = ParserCon /** * factory adaptor to split the logic of parsing simple values from the logic of parsing collections (like Arrays) */ - private fun String.parseTomlKeyValue(lineNo: Int, parserConf: ParserConf): TomlKeyValue { - val keyValuePair = this.splitKeyValue(lineNo, parserConf) + private fun String.parseTomlKeyValue(lineNo: Int, ktomlConf: KtomlConf): TomlKeyValue { + val keyValuePair = this.splitKeyValue(lineNo, ktomlConf) return when { keyValuePair.second.startsWith("[") -> TomlKeyValueList(keyValuePair, lineNo) else -> TomlKeyValueSimple(keyValuePair, lineNo) diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/node/TomlKeyValue.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/node/TomlKeyValue.kt index 9f1bf771..753f3fd7 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/node/TomlKeyValue.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/node/TomlKeyValue.kt @@ -1,7 +1,7 @@ package com.akuleshov7.ktoml.parsers.node +import com.akuleshov7.ktoml.KtomlConf import com.akuleshov7.ktoml.exceptions.TomlParsingException -import com.akuleshov7.ktoml.parsers.ParserConf /** * Interface that contains all common methods that are used in KeyValue nodes @@ -53,11 +53,11 @@ interface TomlKeyValue { * parse and split a string in a key-value format * * @param lineNo - * @param parserConf + * @param ktomlConf * @return a resulted key-value pair * @throws TomlParsingException */ -fun String.splitKeyValue(lineNo: Int, parserConf: ParserConf): Pair { +fun String.splitKeyValue(lineNo: Int, ktomlConf: KtomlConf): Pair { // FixMe: need to cover a case, when '#' symbol is used inside the string ( a = "# hi") (supported by the spec) val keyValue = this.substringBefore("#") .split("=") @@ -65,13 +65,15 @@ fun String.splitKeyValue(lineNo: Int, parserConf: ParserConf): Pair, but was: $this", + "Incorrect format of Key-Value pair." + + " Should be , but was: $this." + + " If you wanted to define table - use brackets []", lineNo ) } - val keyStr = keyValue.getKeyValuePart("key", 0, this, parserConf, lineNo) - val valueStr = keyValue.getKeyValuePart("value", 1, this, parserConf, lineNo) + val keyStr = keyValue.getKeyValuePart("key", 0, this, ktomlConf, lineNo) + val valueStr = keyValue.getKeyValuePart("value", 1, this, ktomlConf, lineNo) return Pair(keyStr, valueStr) } @@ -81,19 +83,19 @@ fun String.splitKeyValue(lineNo: Int, parserConf: ParserConf): Pair.getKeyValuePart( log: String, index: Int, content: String, - parserConf: ParserConf, + ktomlConf: KtomlConf, lineNo: Int) = this[index].trim().also { // key should never be empty, but the value can be empty (and treated as null) // see the discussion: https://github.com/toml-lang/toml/issues/30 - if ((!parserConf.emptyValuesAllowed || index == 0) && it.isBlank()) { + if ((!ktomlConf.emptyValuesAllowed || index == 0) && it.isBlank()) { throw TomlParsingException( "Incorrect format of Key-Value pair. It has empty $log: $content", lineNo @@ -118,7 +120,7 @@ fun String.parseValue(lineNo: Int) = when (this) { TomlInt(this, lineNo) } catch (e: NumberFormatException) { try { - TomlFloat(this, lineNo) + TomlDouble(this, lineNo) } catch (e: NumberFormatException) { TomlBasicString(this, lineNo) } diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/node/TomlValue.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/node/TomlValue.kt index b0903b4f..9c295632 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/node/TomlValue.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/node/TomlValue.kt @@ -86,10 +86,12 @@ class TomlInt(content: String, lineNo: Int) : TomlValue(lineNo) { } /** - * Toml AST Node for a representation of float types: key = 1.01 + * Toml AST Node for a representation of float types: key = 1.01. + * Toml specification requires floating point numbers to be IEEE 754 binary64 values, + * so it should be Kotlin Double (64 bits) */ -class TomlFloat(content: String, lineNo: Int) : TomlValue(lineNo) { - override var content: Any = content.toFloat() +class TomlDouble(content: String, lineNo: Int) : TomlValue(lineNo) { + override var content: Any = content.toDouble() } /** diff --git a/ktoml-core/src/commonTest/kotlin/decoder/ArrayDecoderTest.kt b/ktoml-core/src/commonTest/kotlin/decoder/ArrayDecoderTest.kt index 0456801a..f2801f92 100644 --- a/ktoml-core/src/commonTest/kotlin/decoder/ArrayDecoderTest.kt +++ b/ktoml-core/src/commonTest/kotlin/decoder/ArrayDecoderTest.kt @@ -1,6 +1,7 @@ package decoder -import com.akuleshov7.ktoml.deserialize +import com.akuleshov7.ktoml.deserializeToml +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlin.test.Ignore import kotlin.test.Test @@ -12,24 +13,123 @@ data class SimpleArray(val a: List) @Serializable data class NestedArray(val a: List>) +@Serializable +data class ArrayInInlineTable(val table: InlineTable) + +@Serializable +data class InlineTable( + val name: String, + @SerialName("configurationList") + val overriddenName: List +) + +@Serializable +data class TestArrays( + @SerialName("configurationList1") + val overriddenName1: List, + @SerialName("configurationList2") + val overriddenName2: List, + val table: Table +) + +@Serializable +data class Table(val name: String) + +@Serializable +data class TestArraysAndSimple( + val name1: String, + @SerialName("configurationList1") + val overriddenName1: List, + @SerialName("configurationList2") + val overriddenName2: List, + val table: Table, + val name2: String +) + + class SimpleArrayDecoderTest { @Test fun testSimpleArrayDecoder() { - val test = deserialize( - "a = [1, 2, 3]" + val test = "a = [1, 2, 3]".deserializeToml() + assertEquals(SimpleArray(listOf(1, 2, 3)), test) + } + + @Test + fun testSimpleArrayDecoderInNestedTable() { + var test = """ + |[table] + |name = "my name" + |configurationList = ["a", "b", "c"] + """.trimMargin() + .deserializeToml() + + println(test) + + assertEquals( + ArrayInInlineTable( + table = InlineTable(name = "my name", overriddenName = listOf("a", "b", "c")) + ), test ) - assertEquals(SimpleArray(listOf(1, 2, 3)), test) + test = + """ + |[table] + |configurationList = ["a", "b", "c"] + |name = "my name" + """.trimMargin() + .deserializeToml() + + println(test) + + assertEquals( + ArrayInInlineTable( + table = InlineTable(name = "my name", overriddenName = listOf("a", "b", "c")) + ), test + ) + + val testTable = """ + |configurationList1 = ["a", "b", "c"] + |configurationList2 = ["a", "b", "c"] + |[table] + |name = "my name" + """.trimMargin() + .deserializeToml() + + println(testTable) + + assertEquals( + TestArrays( + overriddenName1 = listOf("a", "b", "c"), + overriddenName2 = listOf("a", "b", "c"), + table = Table(name = "my name") + ), testTable + ) + + val testTableAndVariables = """ + |name1 = "simple" + |configurationList1 = ["a", "b", "c"] + |name2 = "simple" + |configurationList2 = ["a", "b", "c"] + |[table] + |name = "my name" + """.trimMargin() + .deserializeToml() + + println(testTableAndVariables) + + assertEquals( + TestArraysAndSimple( + name1 = "simple", overriddenName1 = listOf("a", "b", "c"), + overriddenName2 = listOf("a", "b", "c"), table = Table(name = "my name"), name2 = "simple" + ), testTableAndVariables + ) } @Test @Ignore fun testNestedArrayDecoder() { // FixMe: nested array decoding causes issues and is not supported yet - val test = deserialize( - "a = [[1, 2], [3, 4]]" - ) - + val test = "a = [[1, 2], [3, 4]]".deserializeToml() assertEquals(NestedArray(listOf(listOf(1, 2), listOf(3, 4))), test) } } diff --git a/ktoml-core/src/commonTest/kotlin/decoder/DottedKeysDecoderTest.kt b/ktoml-core/src/commonTest/kotlin/decoder/DottedKeysDecoderTest.kt index a4bc6282..5fb1ce44 100644 --- a/ktoml-core/src/commonTest/kotlin/decoder/DottedKeysDecoderTest.kt +++ b/ktoml-core/src/commonTest/kotlin/decoder/DottedKeysDecoderTest.kt @@ -1,9 +1,7 @@ package decoder -import com.akuleshov7.ktoml.decoders.DecoderConf -import com.akuleshov7.ktoml.deserialize -import com.akuleshov7.ktoml.parsers.TomlParser -import com.akuleshov7.ktoml.test.decoder.GeneralDecoderTest +import com.akuleshov7.ktoml.KtomlConf +import com.akuleshov7.ktoml.deserializeToml import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -103,8 +101,7 @@ class DottedKeysDecoderTest { @ExperimentalSerializationApi @Test fun testDottedKeys() { - deserialize( - """ + """ |table2.b.d = 2 |[table1] |a.c = 1 @@ -114,38 +111,39 @@ class DottedKeysDecoderTest { |b.f = 2 |table2."foo bar".d = 2 |[table3] - """.trimMargin(), - DecoderConf(true) - ) + """.trimMargin() + .deserializeToml( + KtomlConf(true) + ) } @Test fun tableTest() { - deserialize( - """ + """ |table2.b.d = 2 |[table2] |e = 5 |b.f = 2 - """.trimMargin(), - DecoderConf(true) - ) + """.trimMargin() + .deserializeToml( + KtomlConf(true) + ) } @Test fun tableAndDottedKeys() { - deserialize( - """ + """ |[table2] |table2."foo bar".d = 2 |e = 6 |[table2.b] |d = 2 |f = 7 - """.trimMargin(), - DecoderConf(true) - ) + """.trimMargin() + .deserializeToml( + KtomlConf(true) + ) } @Serializable @@ -198,8 +196,7 @@ class DottedKeysDecoderTest { ) ) ), - deserialize( - """ + """ |[a."b.c..".d."e.f"] | val = 1 | [a] @@ -207,8 +204,7 @@ class DottedKeysDecoderTest { | val = 2 | [a."b.c..".inner] | val = 3 - """.trimMargin() - ) + """.trimMargin().deserializeToml() ) } @@ -236,7 +232,7 @@ class DottedKeysDecoderTest { fun decodeQuotedKey() { assertEquals( QuotedKey(a = AQ(b = ABCQ(b = BQ(d = 123)))), - deserialize("a.\"a.b.c\".b.d = 123") + "a.\"a.b.c\".b.d = 123".deserializeToml() ) } } diff --git a/ktoml-core/src/commonTest/kotlin/decoder/GeneralDecoderTest.kt b/ktoml-core/src/commonTest/kotlin/decoder/GeneralDecoderTest.kt index 74126b55..97cd09ac 100644 --- a/ktoml-core/src/commonTest/kotlin/decoder/GeneralDecoderTest.kt +++ b/ktoml-core/src/commonTest/kotlin/decoder/GeneralDecoderTest.kt @@ -1,16 +1,14 @@ package com.akuleshov7.ktoml.test.decoder -import com.akuleshov7.ktoml.decoders.DecoderConf -import com.akuleshov7.ktoml.deserialize +import com.akuleshov7.ktoml.KtomlConf +import com.akuleshov7.ktoml.deserializeToml import com.akuleshov7.ktoml.exceptions.InvalidEnumValueException import com.akuleshov7.ktoml.exceptions.MissingRequiredFieldException import com.akuleshov7.ktoml.exceptions.UnknownNameDecodingException import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable -import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertFails import kotlin.test.assertFailsWith @ExperimentalSerializationApi @@ -70,7 +68,7 @@ class GeneralDecoderTest { @Test fun testForSimpleTomlCase() { println("table1: (a:5, b:6)") - val test = deserialize("[table1]\n b = 6 \n a = 5 ") + val test = "[table1]\n b = 6 \n a = 5 ".deserializeToml() println(test) assertEquals(SimpleTomlCase(Table1(5, 6)), test) } @@ -78,16 +76,15 @@ class GeneralDecoderTest { @Test fun testForTwoTomlTablesCase() { println("table1: (b:6, a:5), table2:(c:7, d:8)") - val test = deserialize( - "[table1]\n" + - " b = 6 \n" + - " a = 5 \n " + - - "[table2] \n" + - " c = 7 \n" + - " d = 8 \n" + - " e = 9 \n" - ) + val test = ("[table1]\n" + + " b = 6 \n" + + " a = 5 \n " + + + "[table2] \n" + + " c = 7 \n" + + " d = 8 \n" + + " e = 9 \n") + .deserializeToml() println(test) assertEquals(TwoTomlTables(Table1(5, 6), Table2(7, 9, 8)), test) } @@ -96,14 +93,14 @@ class GeneralDecoderTest { fun testForComplexTypes() { assertFailsWith { println("table3: (a:true, d:5, e:\"my test\", b: H)") - deserialize("[table3] \n a = true \n d = 5 \n e = \"my test\" \n b = \"H\"") + "[table3] \n a = true \n d = 5 \n e = \"my test\" \n b = \"H\"".deserializeToml() } } @Test fun testForComplexTypesExceptionOnEnums() { println("table3: (a:true, d:5, e:\"my test\", b = A)") - val test = deserialize("[table3] \n a = true \n d = 5 \n e = \"my test\" \n b = \"A\"") + val test = "[table3] \n a = true \n d = 5 \n e = \"my test\" \n b = \"A\"".deserializeToml() println(test) assertEquals(ComplexPlainTomlCase(Table3(true, "my test", 5, b = TestEnum.A)), test) } @@ -112,22 +109,18 @@ class GeneralDecoderTest { fun testUnknownFieldInToml() { assertFailsWith { println("table3: (a:true, d:5, e:\"my test\", b:A, c:unknown)") - deserialize( - "[table3] \n a = true \n d = 5 \n" + - " c = \"unknown\" \n e = \"my test\" \n b = \"A\" " - ) + ("[table3] \n a = true \n d = 5 \n" + + " c = \"unknown\" \n e = \"my test\" \n b = \"A\" ").deserializeToml() } } @Test fun testForSimpleNestedTable() { println("c: 5, table1: (b:6, a:5)") - val test = deserialize( - "c = 5 \n" + - "[table1] \n" + - " b = 6 \n" + - " a = 5 \n " - ) + val test = ("c = 5 \n" + + "[table1] \n" + + " b = 6 \n" + + " a = 5 \n ").deserializeToml() println(test) assertEquals(NestedSimpleTable(5, Table1(5, 6)), test) } @@ -135,20 +128,19 @@ class GeneralDecoderTest { @Test fun testForNestedTables() { println("c:5, table1: (b:6, a:5), table4:(c:7, e:9 d:8, table1: (b:6, a:5))") - val test = deserialize( - "c = 5 \n" + - "[table1] \n" + - " b = 6 \n" + - " a = 5 \n " + - - "[table4] \n" + - " c = 7 \n" + - " d = 8 \n" + - " e = 9 \n" + - " [table4.table1] \n" + - " b = 6 \n" + - " a = 5 \n " - ) + val test = ("c = 5 \n" + + "[table1] \n" + + " b = 6 \n" + + " a = 5 \n " + + + "[table4] \n" + + " c = 7 \n" + + " d = 8 \n" + + " e = 9 \n" + + " [table4.table1] \n" + + " b = 6 \n" + + " a = 5 \n ") + .deserializeToml() println(test) assertEquals(TwoNestedTables(c = 5, Table1(5, 6), Table4(7, 9, 8, Table1(5, 6))), test) } @@ -156,12 +148,11 @@ class GeneralDecoderTest { @Test fun testWithoutTables() { println("a:true, b:A, e: my string, d: 55") - val test = deserialize( - "a = true \n" + - " b = \"A\"\n" + - " e = \"my string\"\n" + - " d = 55" - ) + val test = ("a = true \n" + + " b = \"A\"\n" + + " e = \"my string\"\n" + + " d = 55") + .deserializeToml() println(test) assertEquals(Table3(true, "my string", 55, TestEnum.A), test) } @@ -169,12 +160,10 @@ class GeneralDecoderTest { @Test fun testForQuotes() { println("a:true, b:A, e: my string, d: 55") - val test = deserialize( - "a = true \n" + - " b = \"A\"\n" + - " e = \"my string\"\n" + - " d = 55" - ) + val test = ("a = true \n" + + " b = \"A\"\n" + + " e = \"my string\"\n" + + " d = 55").deserializeToml() println(test) assertEquals(Table3(true, "my string", 55, TestEnum.A), test) } @@ -188,12 +177,11 @@ class GeneralDecoderTest { ) { // 'err' key is unknown, but this should not trigger an error becuase of ignoreUnknown // 'b' key is not provided and should trigger an error - deserialize( - " a = true \n" + - " d = 5 \n" + - " e = \"my test\"\n" + - " err = \"B\"", - DecoderConf(true) + (" a = true \n" + + " d = 5 \n" + + " e = \"my test\"\n" + + " err = \"B\"").deserializeToml( + KtomlConf(true) ) } } @@ -205,26 +193,23 @@ class GeneralDecoderTest { "Invalid number of arguments provided for deserialization." + " Missing required field in the input" ) { - deserialize( - "[tableUNKNOWN] \n" + - " a = true \n" + - " d = 5 \n" + - " e = \"my test\" \n" + - " b = \"B\"", - DecoderConf(true) + ("[tableUNKNOWN] \n" + + " a = true \n" + + " d = 5 \n" + + " e = \"my test\" \n" + + " b = \"B\"").deserializeToml( + KtomlConf(true) ) } println("a:true, b:A, d: 55, t: 7777") // e is missing, because it has a default value // t - is new unknown field - val test = deserialize( - " t = \"7777\" \n" + - "a = true \n" + - " b = \"A\" \n" + - " d = 55 \n", - - DecoderConf(true) + val test = (" t = \"7777\" \n" + + "a = true \n" + + " b = \"A\" \n" + + " d = 55 \n").deserializeToml( + KtomlConf(true) ) assertEquals(Table3(true, b = TestEnum.A, d = 55), test) @@ -236,12 +221,10 @@ class GeneralDecoderTest { "Invalid number of arguments provided for deserialization." + " Missing required field in the input" ) { - deserialize( - " t = \"7777\" \n" + - "a = true \n" + - " b = \"A\" \n", - - DecoderConf(true) + (" t = \"7777\" \n" + + "a = true \n" + + " b = \"A\" \n").deserializeToml( + KtomlConf(true) ) } } @@ -249,14 +232,13 @@ class GeneralDecoderTest { @Test fun nullableFields() { println("a = null, b = NULL, c = nil") - val test = deserialize( - "a = null \n " + - "b = NULL \n" + - "c = nil \n" + - "d = # hi \n" + - "e = \n" + - "f = NIL\n" - ) + val test = ("a = null \n " + + "b = NULL \n" + + "c = nil \n" + + "d = # hi \n" + + "e = \n" + + "f = NIL\n") + .deserializeToml() println(test) assertEquals(NullableValues(null, null, null, null, null, null), test) } @@ -268,9 +250,8 @@ class GeneralDecoderTest { "Invalid number of arguments provided for deserialization." + " Missing required field in the input" ) { - deserialize( - "[table3] \n a = true", - DecoderConf(true) + "[table3] \n a = true".deserializeToml( + KtomlConf(true) ) } } @@ -282,9 +263,8 @@ class GeneralDecoderTest { "Invalid number of arguments provided for deserialization." + " Missing required field in the input" ) { - deserialize( - "[table1] \n a = 5 \n b = 6", - DecoderConf(true) + "[table1] \n a = 5 \n b = 6".deserializeToml( + KtomlConf(true) ) } } @@ -293,9 +273,9 @@ class GeneralDecoderTest { fun testForMissingRequiredFieldWithDefaultValue() { // e - has default value and is missing in the input println("table3: (a:true, d:5, b: B)") - val test = deserialize( - "[table3] \n a = true \n b = \"B\" \n d = 5", - DecoderConf(true) + val test = "[table3] \n a = true \n b = \"B\" \n d = 5".deserializeToml( + + KtomlConf(true) ) println(test) @@ -304,14 +284,13 @@ class GeneralDecoderTest { @Test fun testChildTableBeforeParent() { - val test = deserialize( - """ + val test = """ |[a.b] | c = 5 | [a] | a = true - """.trimMargin(), - DecoderConf(true) + """.trimMargin().deserializeToml( + KtomlConf(true) ) println(test) assertEquals(ChildTableBeforeParent(A(B(5), true)), test) @@ -321,22 +300,19 @@ class GeneralDecoderTest { fun testIncorrectEnumValue() { println("a:true, b:F, e: my string, d: 55") assertFailsWith { - deserialize( - "a = true \n" + - " b = \"F\"\n" + - " e = \"my string\"\n" + - " d = 55" - ) + ("a = true \n" + + " b = \"F\"\n" + + " e = \"my string\"\n" + + " d = 55").deserializeToml() } } @Test fun kotlinRegressionTest() { // this test is NOT failing on JVM but fails on mingw64 with 39 SYMBOLS and NOT failing with 38 - val test = deserialize( - "[general] \n" + - "execCmd = \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"" - ) + val test = ("[general] \n" + + "execCmd = \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"").deserializeToml() + assertEquals( Regression( General("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") diff --git a/ktoml-core/src/commonTest/kotlin/decoder/PartialDecoderTest.kt b/ktoml-core/src/commonTest/kotlin/decoder/PartialDecoderTest.kt index 90c3b626..4d69abaf 100644 --- a/ktoml-core/src/commonTest/kotlin/decoder/PartialDecoderTest.kt +++ b/ktoml-core/src/commonTest/kotlin/decoder/PartialDecoderTest.kt @@ -1,7 +1,7 @@ package com.akuleshov7.ktoml.test.decoder -import com.akuleshov7.ktoml.deserialize -import com.akuleshov7.ktoml.deserializeFile +import com.akuleshov7.ktoml.deserializeToml +import com.akuleshov7.ktoml.deserializeTomlFile import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlin.test.Test @@ -22,7 +22,7 @@ class PartialDecoderTest { fun testPartialDecoding() { val test = TwoTomlTables(Table1(1, 2), Table2(1,2,3)) assertEquals(test.table1, - deserialize("[table1] \n a = 1 \n b = 2 \n [table2] \n c = 1 \n e = 2 \n d = 3", "table1")) + "[table1] \n a = 1 \n b = 2 \n [table2] \n c = 1 \n e = 2 \n d = 3".deserializeToml("table1")) } @Test @@ -30,7 +30,7 @@ class PartialDecoderTest { val file = "src/commonTest/resources/partial_decoder.toml" val test = TwoTomlTables(Table1(1, 2), Table2(1,2,3)) assertEquals(test.table1, - deserializeFile(file, "table1") + file.deserializeTomlFile("table1") ) } } diff --git a/ktoml-core/src/commonTest/kotlin/decoder/ReadMeExampleTest.kt b/ktoml-core/src/commonTest/kotlin/decoder/ReadMeExampleTest.kt new file mode 100644 index 00000000..99ffc33d --- /dev/null +++ b/ktoml-core/src/commonTest/kotlin/decoder/ReadMeExampleTest.kt @@ -0,0 +1,70 @@ +package decoder + +import com.akuleshov7.ktoml.deserializeToml +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlin.test.Test +import kotlin.test.assertEquals + +class ReadMeExampleTest { + @Serializable + data class MyClass(val someBooleanProperty: Boolean, val table1: Table1, val table2: Table2) + + @Serializable + data class Table1(val property1: Int, val property2: Int) + + @Serializable + data class Table2( + val someNumber: Int, + @SerialName("akuleshov7.com") + val inlineTable: InlineTable, + val otherNumber: Double + ) + + @Serializable + data class InlineTable( + val name: String, + @SerialName("configurationList") + val overriddenName: List + ) + + @Test + fun readmeExampleTest() { + val test = + """ + |someBooleanProperty = true + | + |[table1] + |property1 = 5 + |property2 = 6 + | + |[table2] + |someNumber = 5 + | [table2."akuleshov7.com"] + | name = "my name" + | configurationList = ["a", "b", "c"] + | + |# such redeclaration of table2 + |# is prohibited in toml specification; + |# but ktoml is allowing it in non-strict mode: + |[table2] + |otherNumber = 5.56 + | + """.trimMargin() + .deserializeToml() + + assertEquals( + MyClass( + someBooleanProperty = true, + table1 = Table1(property1 = 5, property2 = 6), + table2 = Table2( + someNumber = 5, + inlineTable = InlineTable(name = "my name", overriddenName = listOf("a", "b", "c")), + otherNumber = 5.56 + ) + ), + test + ) + } +} + diff --git a/ktoml-core/src/commonTest/kotlin/file/TomlFileParserTest.kt b/ktoml-core/src/commonTest/kotlin/file/TomlFileParserTest.kt index 39f2c8c1..dca731bf 100644 --- a/ktoml-core/src/commonTest/kotlin/file/TomlFileParserTest.kt +++ b/ktoml-core/src/commonTest/kotlin/file/TomlFileParserTest.kt @@ -1,6 +1,7 @@ package com.akuleshov7.ktoml.test.file -import com.akuleshov7.ktoml.deserializeFile +import com.akuleshov7.ktoml.KtomlConf +import com.akuleshov7.ktoml.deserializeTomlFile import com.akuleshov7.ktoml.exceptions.NonNullableValueException import com.akuleshov7.ktoml.parsers.TomlParser import kotlinx.serialization.ExperimentalSerializationApi @@ -50,7 +51,7 @@ class TomlFileParserTest { "192.168.1.1" ) ) - assertEquals(test, deserializeFile("src/commonTest/resources/simple_example.toml")) + assertEquals(test, "src/commonTest/resources/simple_example.toml".deserializeTomlFile()) } // ================ @@ -79,9 +80,9 @@ class TomlFileParserTest { val file = "src/commonTest/resources/complex_toml_tables.toml" // ==== reading from file val test = MyTableTest(A(Ab(InnerTest("Undefined")), InnerTest("Undefined")), D(InnerTest("Undefined"))) - assertEquals(test, deserializeFile(file)) + assertEquals(test, file.deserializeTomlFile()) // ==== checking how table discovery works - val parsedResult = TomlParser(file).readAndParseFile() + val parsedResult = TomlParser(KtomlConf()).readAndParseFile(file) assertEquals(listOf("a", "a.b.c", "a.d", "d", "d.a"), parsedResult.getRealTomlTables().map { it.fullTableName }) } @@ -93,7 +94,7 @@ class TomlFileParserTest { fun regressionCast1Test() { assertFailsWith { val file = "src/commonTest/resources/class_cast_regression1.toml" - deserializeFile(file) + file.deserializeTomlFile() } } @@ -101,7 +102,7 @@ class TomlFileParserTest { @Test fun regressionCast2Test() { val file = "src/commonTest/resources/class_cast_regression2.toml" - val parsedResult = deserializeFile(file) + val parsedResult = file.deserializeTomlFile() assertEquals(RegressionTest(null, 1, 2, null), parsedResult) } } diff --git a/ktoml-core/src/commonTest/kotlin/node/parser/ValueParserTest.kt b/ktoml-core/src/commonTest/kotlin/node/parser/ValueParserTest.kt index 36a6c2bb..3a97e59f 100644 --- a/ktoml-core/src/commonTest/kotlin/node/parser/ValueParserTest.kt +++ b/ktoml-core/src/commonTest/kotlin/node/parser/ValueParserTest.kt @@ -1,7 +1,7 @@ package com.akuleshov7.ktoml.test.node.parser +import com.akuleshov7.ktoml.KtomlConf import com.akuleshov7.ktoml.exceptions.TomlParsingException -import com.akuleshov7.ktoml.parsers.ParserConf import com.akuleshov7.ktoml.parsers.node.* import kotlin.test.Test import kotlin.test.assertEquals @@ -68,7 +68,7 @@ class ValueParserTest { // regression test related to comments with an equals symbol after it val pairTest = - "lineCaptureGroup = 1 # index `warningTextHasLine = false`\n".splitKeyValue(0, parserConf = ParserConf()) + "lineCaptureGroup = 1 # index `warningTextHasLine = false`\n".splitKeyValue(0, ktomlConf = KtomlConf()) assertEquals(1, TomlKeyValueSimple(pairTest, 0).value.content) } @@ -76,9 +76,9 @@ class ValueParserTest { @Test fun parsingIssueValue() { - assertFailsWith { " a = b = c".splitKeyValue(0, parserConf = ParserConf()) } - assertFailsWith { " = false".splitKeyValue(0, parserConf = ParserConf()) } - assertFailsWith { " just false".splitKeyValue(0, parserConf = ParserConf()) } + assertFailsWith { " a = b = c".splitKeyValue(0, ktomlConf = KtomlConf()) } + assertFailsWith { " = false".splitKeyValue(0, ktomlConf = KtomlConf()) } + assertFailsWith { " just false".splitKeyValue(0, ktomlConf = KtomlConf()) } assertFailsWith { TomlKeyValueSimple(Pair("a", "\"\\hello tworld\""), 0) } } } @@ -91,7 +91,7 @@ fun getNodeType(v: TomlValue): NodeType = when (v) { is TomlBasicString -> NodeType.STRING is TomlNull -> NodeType.NULL is TomlInt -> NodeType.INT - is TomlFloat -> NodeType.FLOAT + is TomlDouble -> NodeType.FLOAT is TomlBoolean -> NodeType.BOOLEAN else -> NodeType.INCORRECT } From 8c09f0a117660e0ecd291e95fbbc752ab8ed9225 Mon Sep 17 00:00:00 2001 From: Andrey Kuleshov Date: Sun, 11 Jul 2021 12:59:40 +0300 Subject: [PATCH 2/4] Update README.md --- README.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 8c266e24..6dd634f6 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,13 @@ ![maintainability](https://api.codeclimate.com/v1/badges/c75d2d6b0d44cea7aefe/maintainability) Fully Native and Multiplatform Kotlin serialization library for serialization/deserialization of [toml](https://toml.io/en/) format. -Uses native `kotlinx.serialization`, provided by Kotlin. This library contains no Java code or Java dependencies. +Uses native `kotlinx.serialization`, provided by Kotlin. This library contains no Java code and no Java dependencies. We believe that TOML is actually the most readable and user-friendly **configuration file** format. -So we decided to support this format in the kotlinx serialization library. +So we decided to support this format for the `kotlinx` serialization library. ### Contribution As this young and big project [is needed](https://github.com/Kotlin/kotlinx.serialization/issues/1092) by the Kotlin community, we need your help. -We will be glad if you will test `ktoml` or contribute to this project. +We will be glad if you will test `ktoml` or contribute to this project. In case you don't have much time for this - at least spend 5 seconds to give us a star to attract other contributors! **Thanks!** :pray: @@ -29,7 +29,7 @@ However, to reduce the scope, ktoml now supports only the following platforms: - linuxx64 - macosx64 -Other platforms could be added later on the demand or easily built by users. +Other platforms could be added later on the demand or easily built by users on their machines. ## Current limitations **General** \ @@ -87,12 +87,12 @@ implementation("com.akuleshov7:ktoml-core:0.2.6") ## How to use -:heavy_exclamation_mark: as TOML is a foremost configuration language, we also have supported the deserialization from file. -However, to read the file we are using [okio](https://github.com/square/okio), so it will be added as a dependency. +:heavy_exclamation_mark: as TOML is a foremost language for config files, we have also supported the deserialization from file. +However, we are using [okio](https://github.com/square/okio) to read the file, so it will be added as a dependency to your project. -:bangbang: there are two ways of calling TOML serialization API -- Fast and mutable: you need to create a serialization instance once and call serialization methods on it later; -- Slow and immutable: in case you need to read a few TOML configurations you can use String extension methods; +:bangbang: there are two ways of calling TOML serialization API: +- Mutable: you need to create a serialization instance once and call serialization methods on it later (1); +- Immutable: in case you need to read a few TOML configurations you can use String extension API methods (2); **Deserialization:** ```kotlin @@ -101,25 +101,25 @@ import com.akuleshov7.ktoml.deserialize @Serializable data class MyClass(/* your fields */) -// to deserialize toml input in a string format (separated by newlines '\n') +// to deserialize toml input in a string format (separated by newlines '\n') (2) val result = /* string with a toml input */.deserializeToml() -// to deserialize toml input from file +// to deserialize toml input from file (2) val result = /* string with path to a toml file */.deserializeTomlFile() -// in case you need optimized code, you can create serialization object once: +// in case you need optimized code, you can create serialization object once (1): val mySerializer = Toml() val result = mySerializer.deserializeToml( /* string with a toml input */ ) ``` **Partial Deserialization:** -Partial Deserialization is useful when you would like to deserialize only ONE table and you do not want - to reproduce whole object structure in the code. +Partial Deserialization can be useful when you would like to deserialize only **one single** table and you do not want +to reproduce whole object structure in your code. ```kotlin -// if you need to deserialize only some part of the toml - provide the full name of the toml table -// the deserializer will work only with children of this table -// For example if you have the following toml, but want only to decode [c.d.e.f]: +// If you need to deserialize only some part of the toml - provide the full name of the toml table. +// The deserializer will work only with this table and it's children. +// For example if you have the following toml, but you want only to decode [c.d.e.f] table: // [a] // b = 1 // [c.d.e.f] @@ -133,8 +133,8 @@ val result = /* string with path to a toml file */.deserializeTomlFile() ``` -The example above in json-terminology: +Translation of the example above to json-terminology: ```json { "someBooleanProperty": true, From fa794955a42158c45e934b0ca54138e18c3b12cf Mon Sep 17 00:00:00 2001 From: Andrey Kuleshov Date: Sun, 11 Jul 2021 13:11:58 +0300 Subject: [PATCH 3/4] Update README.md --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6dd634f6..4afac48e 100644 --- a/README.md +++ b/README.md @@ -107,9 +107,11 @@ val result = /* string with a toml input */.deserializeToml() // to deserialize toml input from file (2) val result = /* string with path to a toml file */.deserializeTomlFile() -// in case you need optimized code, you can create serialization object once (1): -val mySerializer = Toml() -val result = mySerializer.deserializeToml( /* string with a toml input */ ) +// in case you need optimized code, you can create object of serialization class only once (1) +// it is a useful optimization for loops +val myTomlSerializer = Toml() +val resultFirst: MyClass1 = myTomlSerializer.decodeFromString(serializer(), /* string with a toml input */ ) +val resultSecond: MyClass2 = myTomlSerializer.decodeFromString(serializer(), /* string with a toml input */ ) ``` **Partial Deserialization:** From 84e6afcf8176bdb2d81ea75306d745a8244fe095 Mon Sep 17 00:00:00 2001 From: Andrey Kuleshov Date: Sun, 11 Jul 2021 13:15:05 +0300 Subject: [PATCH 4/4] Update TomlDecoder.kt --- .../kotlin/com/akuleshov7/ktoml/decoders/TomlDecoder.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlDecoder.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlDecoder.kt index f076af96..9f9b3c2b 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlDecoder.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlDecoder.kt @@ -141,8 +141,8 @@ public class TomlDecoder( else -> { // this is a little bit tricky index calculation, suggest not to change // we are using the previous node to get all neighbour nodes: - // (parentNode) - // neighbourNodes: (current rootNode) (next node which we would like to use) + // | (parentNode) + // |--- neighbourNodes: (current rootNode) (next node which we would like to process now) val nextProcessingNode = rootNode .getNeighbourNodes() .elementAt(elementIndex - 1)