diff --git a/README.md b/README.md index 64966448..d5524be2 100644 --- a/README.md +++ b/README.md @@ -71,9 +71,9 @@ We are still developing and testing this library, so it has several limitations: :white_check_mark: Local Date (to `LocalDate` of [kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime)) \ :white_check_mark: Local Time (to `LocalTime` of [kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime)) \ :white_check_mark: Multiline Strings \ -:white_check_mark: Arrays (including multiline arrays) \ +:white_check_mark: Arrays (including multiline and nested arrays) \ :white_check_mark: Maps (for anonymous key-value pairs) \ -:x: Arrays: nested; of Different Types \ +:x: Arrays: of Different Types \ :x: Nested Inline Tables \ :x: Array of Tables \ :x: Inline Array of Tables diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlArrayDecoder.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlArrayDecoder.kt index 1ac5e872..c3d493b3 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlArrayDecoder.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlArrayDecoder.kt @@ -4,6 +4,7 @@ import com.akuleshov7.ktoml.TomlInputConfig import com.akuleshov7.ktoml.tree.nodes.TomlKeyValue import com.akuleshov7.ktoml.tree.nodes.TomlKeyValueArray import com.akuleshov7.ktoml.tree.nodes.TomlKeyValuePrimitive +import com.akuleshov7.ktoml.tree.nodes.pairs.values.TomlArray import com.akuleshov7.ktoml.tree.nodes.pairs.values.TomlNull import com.akuleshov7.ktoml.tree.nodes.pairs.values.TomlValue import kotlinx.serialization.DeserializationStrategy @@ -26,7 +27,7 @@ public class TomlArrayDecoder( private var nextElementIndex = 0 private val list = rootNode.value.content as List override val serializersModule: SerializersModule = EmptySerializersModule() - private lateinit var currentElementDecoder: TomlPrimitiveDecoder + private lateinit var currentElementDecoder: TomlAbstractDecoder private lateinit var currentPrimitiveElementOfArray: TomlValue private fun haveStartedReadingElements() = nextElementIndex > 0 @@ -40,16 +41,29 @@ public class TomlArrayDecoder( currentPrimitiveElementOfArray = list[nextElementIndex] - currentElementDecoder = TomlPrimitiveDecoder( - // a small hack that creates a PrimitiveKeyValue node that is used in the decoder - TomlKeyValuePrimitive( - rootNode.key, - currentPrimitiveElementOfArray, - rootNode.lineNo, - comments = emptyList(), - inlineComment = "", + currentElementDecoder = if (currentPrimitiveElementOfArray is TomlArray) { + TomlArrayDecoder( + TomlKeyValueArray( + rootNode.key, + currentPrimitiveElementOfArray, + rootNode.lineNo, + comments = emptyList(), + inlineComment = "", + ), + config ) - ) + } else { + TomlPrimitiveDecoder( + // a small hack that creates a PrimitiveKeyValue node that is used in the decoder + TomlKeyValuePrimitive( + rootNode.key, + currentPrimitiveElementOfArray, + rootNode.lineNo, + comments = emptyList(), + inlineComment = "", + ) + ) + } return nextElementIndex++ } diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/StringUtils.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/StringUtils.kt index c367542e..3425747c 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/StringUtils.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/StringUtils.kt @@ -143,16 +143,16 @@ internal fun String.trimDoubleBrackets(): String = trimSymbols(this, "[[", "]]") * @param allowEscapedQuotesInLiteralStrings value from TomlInputConfig * @return The text before a comment, i.e. * ```kotlin - * "a = 0 # Comment".takeBeforeComment() == "a = 0" + * "a = 0 # Comment".takeBeforeComment() == "a = 0 " * ``` */ internal fun String.takeBeforeComment(allowEscapedQuotesInLiteralStrings: Boolean): String { val commentStartIndex = getCommentStartIndex(allowEscapedQuotesInLiteralStrings) return if (commentStartIndex == -1) { - this.trim() + this } else { - this.substring(0, commentStartIndex).trim() + this.substring(0, commentStartIndex) } } diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/TomlMultilineString.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/TomlMultilineString.kt new file mode 100644 index 00000000..3c52965c --- /dev/null +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/TomlMultilineString.kt @@ -0,0 +1,195 @@ +package com.akuleshov7.ktoml.parsers + +import com.akuleshov7.ktoml.TomlInputConfig +import com.akuleshov7.ktoml.exceptions.ParseException +import com.akuleshov7.ktoml.parsers.enums.MultilineType +import com.akuleshov7.ktoml.utils.LinesIteratorWrapper +import com.akuleshov7.ktoml.utils.newLineChar + +/** + * @param config + * @param linesIteratorWrapper - iterator with the rest of the toml data + * @param firstLine - first line of multiline value where it was detected + */ +internal class TomlMultilineString( + private val config: TomlInputConfig, + private val linesIteratorWrapper: LinesIteratorWrapper, + firstLine: String, +) { + private val comments: MutableList = mutableListOf() + private val lines: MutableList = mutableListOf() + private val startLineNo = linesIteratorWrapper.lineNo + private val multilineType = getMultilineType(firstLine, config) + private var isInMultilineBasic = false + private var isInMultilineLiteral = false + + // If isNested is null, we don't know yet if the type is nested + private var isNested = if (multilineType.isNestedSupported) null else false + + init { + if (multilineType == MultilineType.NOT_A_MULTILINE) { + throw ParseException("Internal parse exception", startLineNo) + } + trackMultilineString(firstLine) + lines.add(firstLine.takeBeforeComment(config.allowEscapedQuotesInLiteralStrings)) + parseMultiline() + } + + fun getLine(): String = if (multilineType == MultilineType.ARRAY) { + lines.joinToString(newLineChar().toString()) { + it.takeBeforeComment(config.allowEscapedQuotesInLiteralStrings) + } + } else { + // we can't have comments inside multi-line basic/literal string + lines.joinToString(newLineChar().toString()) + } + + fun getComments(): List = comments + + private fun parseMultiline() { + var hasFoundEnd = false + + while (linesIteratorWrapper.hasNext()) { + val line = linesIteratorWrapper.next() + trackMultilineString(line) + + if (!stringTypes.contains(multilineType)) { + if (!isInMultilineString()) { + comments.add(line.trimComment(config.allowEscapedQuotesInLiteralStrings)) + lines.add(line.takeBeforeComment(config.allowEscapedQuotesInLiteralStrings)) + } else { + // We're inside multiline basic/literal string element, so there's no comments + lines.add(line) + } + } else { + // We have multiline basic/literal string MultilineType; They don't have comments inside + lines.add(line) + } + + if (!isInMultilineString() && isEndOfMultilineValue(multilineType)) { + hasFoundEnd = true + break + } + } + + if (!hasFoundEnd) { + throw ParseException( + "Expected (${multilineType.closingSymbols}) in the end of ${multilineType.name}", + startLineNo, + ) + } + } + + /** + * When we have an array with multiline strings, and we're parsing line X + * we want to know if multiline string was open before line X + */ + private fun trackMultilineString(line: String) { + if (stringTypes.contains(multilineType)) { + return + } + for (i in 0..line.length - 3) { + // Stumbled upon a comment, no need to analyze for the rest of the line + if (!isInMultilineBasic && !isInMultilineLiteral && line[i] == '#') { + break + } + + if (!isInMultilineLiteral && isNextThreeQuotes(line, i, '"')) { + isInMultilineBasic = !isInMultilineBasic + } else if (!isInMultilineBasic && isNextThreeQuotes(line, i, '\'')) { + isInMultilineLiteral = !isInMultilineLiteral + } + } + } + + private fun isNextThreeQuotes( + line: String, + index: Int, + quote: Char + ): Boolean = line[index] == quote && line[index + 1] == quote && line[index + 2] == quote + + private fun isInMultilineString(): Boolean = isInMultilineBasic || isInMultilineLiteral + + /** + * @return true if string is a last line of multiline value declaration + */ + private fun isEndOfMultilineValue(multilineType: MultilineType): Boolean { + if (multilineType == MultilineType.NOT_A_MULTILINE) { + throw ParseException("Internal parse exception", startLineNo) + } + isNested ?: run { + isNested = hasTwoConsecutiveSymbolsIgnoreWhitespaces(getLine(), multilineType.openSymbols[0]) + } + + return if (isNested == true) { + lines.joinToString("") + .trim() + .endsWith(multilineType.closingSymbols + multilineType.closingSymbols) + } else { + lines.last() + .trim() + .endsWith(multilineType.closingSymbols) + } + } + + private fun hasTwoConsecutiveSymbolsIgnoreWhitespaces(value: String, searchSymbol: Char): Boolean? { + val firstIndex = value.indexOf(searchSymbol) + if (firstIndex == -1) { + return false + } + + val nextIndex = value.indexOf(searchSymbol, firstIndex + 1) + + if (nextIndex != -1) { + val between = value.substring(firstIndex + 1, nextIndex) + return between.all { it.isWhitespace() } + } + + val isRestHasOnlyWhitespaces = !value.substring(firstIndex + 1).any { !it.isWhitespace() } + return if (isRestHasOnlyWhitespaces) { + null + } else { + false + } + } + + companion object { + private val stringTypes = listOf(MultilineType.BASIC_STRING, MultilineType.LITERAL_STRING) + + /** + * Important! We treat a multi-line that is declared in one line ("""abc""") as a regular not multiline string + * + * @param line + * @param config + * @return MultilineType + */ + fun getMultilineType(line: String, config: TomlInputConfig): MultilineType { + val line = line.takeBeforeComment(config.allowEscapedQuotesInLiteralStrings) + val firstEqualsSign = line.indexOfFirst { it == '=' } + if (firstEqualsSign == -1) { + return MultilineType.NOT_A_MULTILINE + } + val value = line.substring(firstEqualsSign + 1).trim() + + if (value.startsWith(MultilineType.ARRAY.openSymbols) && + !value.endsWith(MultilineType.ARRAY.closingSymbols) + ) { + return MultilineType.ARRAY + } + + // If we have more than 1 combination of (""") - it means that + // multi-line is declared in one line, and we can handle it as not a multi-line + if (value.startsWith(MultilineType.BASIC_STRING.openSymbols) && value.getCountOfOccurrencesOfSubstring(MultilineType.BASIC_STRING.openSymbols) == 1 + ) { + return MultilineType.BASIC_STRING + } + if (value.startsWith(MultilineType.LITERAL_STRING.openSymbols) && + value.getCountOfOccurrencesOfSubstring(MultilineType.LITERAL_STRING.openSymbols) == 1 + ) { + return MultilineType.LITERAL_STRING + } + + return MultilineType.NOT_A_MULTILINE + } + } +} 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 285b03d3..71d59ecd 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 @@ -2,8 +2,9 @@ package com.akuleshov7.ktoml.parsers import com.akuleshov7.ktoml.TomlInputConfig import com.akuleshov7.ktoml.exceptions.InternalDecodingException -import com.akuleshov7.ktoml.exceptions.ParseException +import com.akuleshov7.ktoml.parsers.enums.MultilineType import com.akuleshov7.ktoml.tree.nodes.* +import com.akuleshov7.ktoml.utils.LinesIteratorWrapper import com.akuleshov7.ktoml.utils.newLineChar import kotlin.jvm.JvmInline @@ -54,12 +55,11 @@ public value class TomlParser(private val config: TomlInputConfig) { var latestCreatedBucket: TomlArrayOfTablesElement? = null val comments: MutableList = mutableListOf() - var index = 0 - val linesIterator = trimmedTomlLines.iterator() + val linesIterator = LinesIteratorWrapper(trimmedTomlLines.iterator()) // all lines will be streamed sequentially while (linesIterator.hasNext()) { val line = linesIterator.next() - val lineNo = index + 1 + val lineNo = linesIterator.lineNo // comments and empty lines can easily be ignored in the TomlTree, but we cannot filter them out in mutableTomlLines // because we need to calculate and save lineNo @@ -69,23 +69,11 @@ public value class TomlParser(private val config: TomlInputConfig) { // Parse the inline comment if any val inlineComment = line.trimComment(config.allowEscapedQuotesInLiteralStrings) - val multilineType = line.getMultilineType() + val multilineType = TomlMultilineString.getMultilineType(line, config) val tomlLine = if (multilineType != MultilineType.NOT_A_MULTILINE) { - // first line from multiline is already taken from the sequence and can be processed - val collectedMultiline = StringBuilder() - collectLineWithComments(collectedMultiline, comments, multilineType, line) - collectedMultiline.append(newLineChar()) - - // processing remaining lines from a multiline - val indexAtTheEndOfMultiline = collectMultiline( - linesIterator, - collectedMultiline, - index, - multilineType, - comments - ) - index = indexAtTheEndOfMultiline - collectedMultiline.toString() + val tomlMultilineString = TomlMultilineString(config, linesIterator, line) + comments += tomlMultilineString.getComments() + tomlMultilineString.getLine() } else { line } @@ -140,7 +128,6 @@ public value class TomlParser(private val config: TomlInputConfig) { } comments.clear() } - index++ } return tomlFileHead } @@ -215,100 +202,6 @@ public value class TomlParser(private val config: TomlInputConfig) { } } - /** - * @param collectedMultiline append all multi-lines to this argument - * @return index at the end of multiline - */ - private fun collectMultiline( - linesIterator: Iterator, - collectedMultiline: StringBuilder, - startIndex: Int, - multilineType: MultilineType, - comments: MutableList - ): Int { - var index = startIndex - var line: String - var hasFoundEnd = false - - // all lines will be streamed sequentially - while (linesIterator.hasNext()) { - line = linesIterator.next() - collectLineWithComments(collectedMultiline, comments, multilineType, line) - - if (line.isEndOfMultilineValue(multilineType, index + 1)) { - hasFoundEnd = true - break - } - // append new line to collect string as is - collectedMultiline.append(newLineChar()) - index++ - } - - if (!hasFoundEnd) { - throw ParseException( - "Expected (${multilineType.closingSymbols}) in the end of ${multilineType.name}", - startIndex + 1 - ) - } - return index - } - - private fun collectLineWithComments( - collectTo: StringBuilder, - comments: MutableList, - multilineType: MultilineType, - line: String - ) { - if (multilineType == MultilineType.ARRAY) { - collectTo.append(line.takeBeforeComment(config.allowEscapedQuotesInLiteralStrings)) - comments += line.trimComment(config.allowEscapedQuotesInLiteralStrings) - } else { - // we can't have comments inside a multi-line basic/literal string - collectTo.append(line) - } - } - - /** - * Important! We treat a multi-line that is declared in one line ("""abc""") as a regular not multiline string - */ - private fun String.getMultilineType(): MultilineType { - val line = this.takeBeforeComment(config.allowEscapedQuotesInLiteralStrings) - val firstEqualsSign = line.indexOfFirst { it == '=' } - if (firstEqualsSign == -1) { - return MultilineType.NOT_A_MULTILINE - } - val value = line.substring(firstEqualsSign + 1).trim() - - if (value.startsWith("[") && !value.endsWith("]")) { - return MultilineType.ARRAY - } - - // If we have more than 1 combination of (""") - it means that - // multi-line is declared in one line, and we can handle it as not a multi-line - if (value.startsWith("\"\"\"") && value.getCountOfOccurrencesOfSubstring("\"\"\"") == 1) { - return MultilineType.BASIC_STRING - } - if (value.startsWith("'''") && value.getCountOfOccurrencesOfSubstring("\'\'\'") == 1) { - return MultilineType.LITERAL_STRING - } - - // Otherwise, the string isn't a multi-line declaration - return MultilineType.NOT_A_MULTILINE - } - - /** - * @return true if string is a last line of multiline value declaration - */ - private fun String.isEndOfMultilineValue(multilineType: MultilineType, lineNo: Int): Boolean { - if (multilineType == MultilineType.NOT_A_MULTILINE) { - throw ParseException("Internal parse exception", lineNo) - } - - return this.takeBeforeComment(config.allowEscapedQuotesInLiteralStrings) - .trim() - .endsWith(multilineType.closingSymbols) - } - private fun TomlNode.insertStub() { if (this.hasNoChildren() && this !is TomlFile && this !is TomlArrayOfTablesElement) { this.appendChild(TomlStubEmptyNode(this.lineNo)) @@ -339,17 +232,6 @@ public value class TomlParser(private val config: TomlInputConfig) { private fun String.isComment() = this.trim().startsWith("#") private fun String.isEmptyLine() = this.trim().isEmpty() - - /** - * @property closingSymbols - symbols indicating that the multi-line is closed - */ - private enum class MultilineType(val closingSymbols: String) { - ARRAY("]"), - BASIC_STRING("\"\"\""), - LITERAL_STRING("'''"), - NOT_A_MULTILINE(""), - ; - } } /** diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/enums/MultilineType.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/enums/MultilineType.kt new file mode 100644 index 00000000..daf14776 --- /dev/null +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/enums/MultilineType.kt @@ -0,0 +1,34 @@ +package com.akuleshov7.ktoml.parsers.enums + +/** + * @property openSymbols - symbols indicating that the multi-line is opened + * @property closingSymbols - symbols indicating that the multi-line is closed + * @property isNestedSupported - if the multi-line type can be nested + */ +internal enum class MultilineType( + val openSymbols: String, + val closingSymbols: String, + val isNestedSupported: Boolean, +) { + ARRAY( + "[", + "]", + true + ), + BASIC_STRING( + "\"\"\"", + "\"\"\"", + false + ), + LITERAL_STRING( + "'''", + "'''", + false + ), + NOT_A_MULTILINE( + "", + "", + false + ), + ; +} diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlArray.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlArray.kt index 2ddba889..1f7aa7cc 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlArray.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlArray.kt @@ -22,9 +22,7 @@ public class TomlArray internal constructor( rawContent: String, lineNo: Int, config: TomlInputConfig - ) : this(rawContent.parse(lineNo, config)) { - validateQuotes(rawContent, lineNo) - } + ) : this(rawContent.parse(lineNo, config)) @Deprecated( message = "TomlConfig is deprecated; use TomlInputConfig instead. Will be removed in next releases." @@ -66,8 +64,8 @@ public class TomlArray internal constructor( emitter.startArray() val content = (content as List).map { - if (it is List<*>) { - TomlArray(it, multiline) + if (it is TomlArray) { + TomlArray(it.content, multiline) } else { it as TomlValue } @@ -114,15 +112,21 @@ public class TomlArray internal constructor( * recursively parse TOML array from the string: [ParsingArray -> Trimming values -> Parsing Nested Arrays] */ private fun String.parse(lineNo: Int, config: TomlInputConfig = TomlInputConfig()): List = - this.parseArray() + this.parseArray(lineNo) .map { it.trim() } - .map { if (it.startsWith("[")) it.parse(lineNo, config) else it.parseValue(lineNo, config) } + .map { + if (it.startsWith("[")) { + TomlArray(it, lineNo, config) + } else { + it.parseValue(lineNo, config) + } + } /** * method for splitting the string to the array: "[[a, b], [c], [d]]" to -> [a,b] [c] [d] */ @Suppress("NESTED_BLOCK", "TOO_LONG_FUNCTION") - private fun String.parseArray(): MutableList { + private fun String.parseArray(lineNo: Int): MutableList { val trimmed = trimBrackets().trim().removeTrailingComma() // covering cases when the array is intentionally blank: myArray = []. It should be empty and not contain null if (trimmed.isBlank()) { @@ -138,11 +142,15 @@ public class TomlArray internal constructor( for (i in trimmed.indices) { when (val current = trimmed[i]) { '[' -> { - nbBrackets++ + if (!isInBasicString && !isInLiteralString) { + nbBrackets++ + } bufferBetweenCommas.append(current) } ']' -> { - nbBrackets-- + if (!isInBasicString && !isInLiteralString) { + nbBrackets-- + } bufferBetweenCommas.append(current) } '\'' -> { @@ -172,20 +180,14 @@ public class TomlArray internal constructor( else -> bufferBetweenCommas.append(current) } } - result.add(bufferBetweenCommas.toString()) - return result - } - - /** - * small validation for quotes: each quote should be closed in a key - */ - private fun validateQuotes(rawContent: String, lineNo: Int) { - if (rawContent.count { it == '\"' } % 2 != 0 || rawContent.count { it == '\'' } % 2 != 0) { + if (isInBasicString || isInLiteralString) { throw ParseException( - "Not able to parse the key: [$rawContent] as it does not have closing quote", - lineNo + "Not able to parse the array: [$this] as it does not have closing quote", + lineNo, ) } + result.add(bufferBetweenCommas.toString()) + return result } } } diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/utils/LinesIteratorWrapper.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/utils/LinesIteratorWrapper.kt new file mode 100644 index 00000000..20f9c1ac --- /dev/null +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/utils/LinesIteratorWrapper.kt @@ -0,0 +1,19 @@ +package com.akuleshov7.ktoml.utils + +/** + * @param T + * @param iterator + */ +internal class LinesIteratorWrapper( + private val iterator: Iterator +) : Iterator { + internal var lineNo: Int = 0 + private set + + override fun hasNext(): Boolean = iterator.hasNext() + + override fun next(): T { + lineNo++ + return iterator.next() + } +} diff --git a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/ArrayDecoderTest.kt b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/ArrayDecoderTest.kt index 19df99aa..41eeb6b6 100644 --- a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/ArrayDecoderTest.kt +++ b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/ArrayDecoderTest.kt @@ -2,15 +2,12 @@ package com.akuleshov7.ktoml.decoders import com.akuleshov7.ktoml.Toml import com.akuleshov7.ktoml.exceptions.IllegalTypeException -import com.akuleshov7.ktoml.exceptions.ParseException -import kotlinx.serialization.decodeFromString import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlin.test.Ignore +import kotlinx.serialization.decodeFromString import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith -import kotlin.test.assertTrue @Serializable data class SimpleArray(val a: List) @@ -24,6 +21,9 @@ data class SimpleStringArray(val a: List) @Serializable data class NestedArray(val a: List>) +@Serializable +data class NestedArrayOfStrings(val a: List>) + @Serializable data class ArrayInInlineTable(val table: InlineTable) @@ -161,11 +161,17 @@ class SimpleArrayDecoderTest { } @Test - @Ignore fun testNestedArrayDecoder() { - // FixMe: nested array decoding causes issues and is not supported yet val test = "a = [[1, 2], [3, 4]]" - assertEquals(NestedArray(listOf(listOf(1, 2), listOf(3, 4))), Toml.decodeFromString(test)) + assertEquals( + NestedArray( + listOf( + listOf(1, 2), + listOf(3, 4) + ) + ), + Toml.decodeFromString(test) + ) } @Test diff --git a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/multiline/ArrayOfStringsDecoderTest.kt b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/multiline/ArrayOfStringsDecoderTest.kt new file mode 100644 index 00000000..a9960365 --- /dev/null +++ b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/multiline/ArrayOfStringsDecoderTest.kt @@ -0,0 +1,156 @@ +package com.akuleshov7.ktoml.decoders.multiline + +import com.akuleshov7.ktoml.Toml +import com.akuleshov7.ktoml.decoders.NestedArrayOfStrings +import com.akuleshov7.ktoml.decoders.SimpleStringArray +import kotlinx.serialization.decodeFromString +import kotlin.test.Test +import kotlin.test.assertEquals + +class ArrayOfStringsDecoderTest { + private val tripleQuotes = "\"\"\"" + private val tripleSingleQuotes = "'''" + + @Test + fun testPositiveCase() { + var test = """ + a = [ + ''' + hey + ''', + + ''' + hi + ''' + ] + """.trimIndent() + assertEquals( + SimpleStringArray(listOf(" hey\n", " hi\n")), + Toml.decodeFromString(test), + ) + + test = """ + a = [''' + hey + ''',''' + hi + ''' + ] + """.trimIndent() + assertEquals( + SimpleStringArray(listOf(" hey\n", " hi\n")), + Toml.decodeFromString(test), + ) + + test = "a = [ $tripleQuotes\n hey\n$tripleQuotes, $tripleQuotes\n hi\n$tripleQuotes]" + assertEquals( + SimpleStringArray(listOf(" hey\n", " hi\n")), + Toml.decodeFromString(test), + ) + } + + @Test + fun testIgnoreClosingSymbolInsideString() { + var test = """ + a = [''' + Index: [0] + ''' + ] + """.trimIndent() + assertEquals( + SimpleStringArray(listOf(" Index: [0]\n")), + Toml.decodeFromString(test), + ) + + test = """ + a = [ + ''' + Index: [0] + ''' + ] + """.trimIndent() + assertEquals( + SimpleStringArray(listOf(" Index: [0]\n")), + Toml.decodeFromString(test), + ) + + test = "a = [ $tripleQuotes\n Index: [0] \n$tripleQuotes\n]" + assertEquals( + SimpleStringArray(listOf(" Index: [0] \n")), + Toml.decodeFromString(test), + ) + } + + @Test + fun testMoreThanOneValuesInArray() { + val test = """ + a = [ + ["1", "2"], + ["3", "4"] + ] + """.trimIndent() + + assertEquals( + NestedArrayOfStrings( + listOf( + listOf("1", "2"), + listOf("3", "4") + ) + ), + Toml.decodeFromString(test) + ) + } + + @Test + fun testWithMultipleBrackets() { + val expected = NestedArrayOfStrings( + listOf( + listOf("]123", "[]123"), + listOf("[asd]", "[asd]") + ) + ) + var test = """ + a = [["]123", "[]123"], + ["[asd]", "[asd]"]] + """.trimIndent() + + assertEquals(expected, Toml.decodeFromString(test)) + + test = """ + a = [ + # comment + ["]123", "[]123"],# comment + # comment + ["[asd]", "[asd]"# comment + ] + + ] + """.trimIndent() + assertEquals(expected, Toml.decodeFromString(test)) + } + + @Test + fun testQuotesInsideOtherQuotes() { + var test = """ + a = [ + $tripleQuotes ''' $tripleQuotes + ] + """.trimIndent() + + assertEquals( + SimpleStringArray(listOf(" ''' ")), + Toml.decodeFromString(test), + ) + + test = """ + a = [ + $tripleSingleQuotes " $tripleSingleQuotes + ] + """.trimIndent() + + assertEquals( + SimpleStringArray(listOf(" \" ")), + Toml.decodeFromString(test), + ) + } +} diff --git a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/multiline/MultilineArrayTest.kt b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/multiline/MultilineArrayTest.kt index 035401f5..99ba7cc3 100644 --- a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/multiline/MultilineArrayTest.kt +++ b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/multiline/MultilineArrayTest.kt @@ -1,6 +1,7 @@ package com.akuleshov7.ktoml.decoders.multiline import com.akuleshov7.ktoml.Toml +import com.akuleshov7.ktoml.decoders.NestedArray import com.akuleshov7.ktoml.exceptions.ParseException import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString @@ -275,4 +276,54 @@ class MultilineArrayTest { """.trimIndent() assertEquals(SimpleArrayWithNullableValues(listOf(1, 2, 3)), Toml.decodeFromString(test)) } + + @Test + fun testMultilineNestedArray() { + var test = """ + a = [[ + 1, + 2, + ], [ + 3, + 4, + ]] + """.trimIndent() + + assertEquals( + NestedArray( + listOf( + listOf(1, 2), + listOf(3, 4) + ) + ), + Toml.decodeFromString(test) + ) + + val expected = NestedArray( + listOf( + listOf(1, 2), + listOf(3, 4) + ) + ) + test = """ + a = [ + [1, 2], + [3, 4] + ] + """.trimIndent() + assertEquals(expected, Toml.decodeFromString(test)) + + test = """ + a = [ + # comment + [1, 2], + # comment + [3, 4 # comment + ] + + ] + """.trimIndent() + + assertEquals(expected, Toml.decodeFromString(test)) + } } \ No newline at end of file diff --git a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/parsers/CommonParserTest.kt b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/parsers/CommonParserTest.kt index 6a8a82b6..e499b54a 100644 --- a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/parsers/CommonParserTest.kt +++ b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/parsers/CommonParserTest.kt @@ -17,10 +17,10 @@ class CommonParserTest { (TomlArray("[[\"a\", [\"b\"]], \"c\", \"d\"]", 0, TomlInputConfig()).content as List).forEach { when (it) { is TomlBasicString -> highLevelValues++ - is List<*> -> it.forEach { + is TomlArray -> (it.content as List).forEach { when (it) { is TomlBasicString -> midLevelValues++ - is List<*> -> it.forEach { _ -> + is TomlArray -> (it.content as List).forEach { _ -> lowLevelValues++ } } diff --git a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/parsers/SetLineNoTest.kt b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/parsers/SetLineNoTest.kt index b0e3c3ff..744fa86a 100644 --- a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/parsers/SetLineNoTest.kt +++ b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/parsers/SetLineNoTest.kt @@ -50,7 +50,7 @@ class SetLineNoTest { | 3 | |''')[line:13] - | - TomlKeyValueArray (mla=[ "a", "b", "c" ])[line:18] + | - TomlKeyValueArray (mla=[ "a", "b", "c" ])[line:19] | """.trimMargin(), parsedToml.prettyStr(true) diff --git a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/parsers/StringUtilsTest.kt b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/parsers/StringUtilsTest.kt index 4716f236..40b30552 100644 --- a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/parsers/StringUtilsTest.kt +++ b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/parsers/StringUtilsTest.kt @@ -6,26 +6,26 @@ import kotlin.test.assertEquals class StringUtilsTest { @Test fun testForTakeBeforeComment() { - var lineWithoutComment = "test_key = \"test_value\" # \" some comment".takeBeforeComment(false) + var lineWithoutComment = "test_key = \"test_value\"# \" some comment".takeBeforeComment(false) assertEquals("test_key = \"test_value\"", lineWithoutComment) - lineWithoutComment = "key = \"\"\"value\"\"\" # \"".takeBeforeComment(false) + lineWithoutComment = "key = \"\"\"value\"\"\"# \"".takeBeforeComment(false) assertEquals("key = \"\"\"value\"\"\"", lineWithoutComment) - lineWithoutComment = "key = 123 # \"\"\"abc".takeBeforeComment(false) + lineWithoutComment = "key = 123# \"\"\"abc".takeBeforeComment(false) assertEquals("key = 123", lineWithoutComment) lineWithoutComment = "key = \"ab\\\"#cdef\"#123".takeBeforeComment(false) assertEquals("key = \"ab\\\"#cdef\"", lineWithoutComment) - lineWithoutComment = " \t#123".takeBeforeComment(false) + lineWithoutComment = "#123".takeBeforeComment(false) assertEquals("", lineWithoutComment) - lineWithoutComment = "key = \"ab\'c\" # ".takeBeforeComment(false) + lineWithoutComment = "key = \"ab\'c\"# ".takeBeforeComment(false) assertEquals("key = \"ab\'c\"", lineWithoutComment) lineWithoutComment = """ - a = 'C:\some\path\' #\abc + a = 'C:\some\path\'#\abc """.trimIndent().takeBeforeComment(true) assertEquals("""a = 'C:\some\path\'""", lineWithoutComment) }