Skip to content

Commit

Permalink
Initial implementation of parsing of ArrayOfTables (#100)
Browse files Browse the repository at this point in the history
### What's done:
- Small refactoring related to sealed classes limitations
- Initial test implemetation of parking for array of tables
- Kotlin update to 1.6.10
  • Loading branch information
orchestr7 authored Feb 11, 2022
1 parent 3b2b702 commit 1fd5e11
Show file tree
Hide file tree
Showing 40 changed files with 603 additions and 318 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ As this young and big project [is needed](https://github.com/Kotlin/kotlinx.seri
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:
**Thanks!** :pray: :partying_face:

## Acknowledgement
Special thanks to those awesome developers who give us great suggestions, help us to maintain and improve this project:
Expand Down
4 changes: 4 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ allprojects {
}
configureDiktat()
configureDetekt()

tasks.withType<org.cqfn.diktat.plugin.gradle.DiktatJavaExecTaskBase> {
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
}
}
createDiktatTask()
createDetektTask()
Expand Down
4 changes: 2 additions & 2 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ repositories {

dependencies {
// this hack prevents the following bug: https://github.com/gradle/gradle/issues/9770
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.0")
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10")

implementation("org.cqfn.diktat:diktat-gradle-plugin:1.0.0-rc.3")
implementation("org.cqfn.diktat:diktat-gradle-plugin:1.0.2")
implementation("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.15.0")
implementation("io.github.gradle-nexus:publish-plugin:1.1.0")
implementation("org.ajoberstar.reckon:reckon-gradle:0.13.0")
Expand Down
4 changes: 2 additions & 2 deletions buildSrc/src/main/kotlin/com/akuleshov7/Versions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
// now it is just a workaround

object Versions {
const val KOTLIN = "1.6.0"
const val KOTLIN = "1.6.10"
const val JUNIT = "5.7.1"
const val OKIO = "3.0.0"
const val SERIALIZATION = "1.3.1"
const val SERIALIZATION = "1.3.2"
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ fun Project.configureDiktat() {
apply<DiktatGradlePlugin>()
configure<DiktatExtension> {
diktatConfigFile = rootProject.file("diktat-analysis.yml")
inputs = files(
"src/**/*.kt"
)
excludes = files("src/commonTest")
inputs {
include("src/**/*.kt", "*.kts", "src/**/*.kts")
exclude("$projectDir/build/**", "src/commonTest/**/*.kt")
}
}
}

Expand All @@ -33,10 +33,15 @@ fun Project.createDiktatTask() {
apply<DiktatGradlePlugin>()
configure<DiktatExtension> {
diktatConfigFile = rootProject.file("diktat-analysis.yml")
inputs = files(
"$rootDir/buildSrc/src/**/*.kt"
)
excludes = files("src/commonTest")
inputs {
include(
"$rootDir/buildSrc/src/**/*.kt",
"$rootDir/buildSrc/src/**/*.kts",
"$rootDir/*.kts",
"$rootDir/buildSrc/*.kts"
)
exclude("$rootDir/build", "$rootDir/buildSrc/build")
}
}
}
tasks.register("diktatCheckAll") {
Expand Down
2 changes: 1 addition & 1 deletion diktat-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
configuration:
useRecommendedImportsOrder: true
- name: FILE_WILDCARD_IMPORTS
enabled: true
enabled: false
configuration:
allowedWildcards: "kotlinx.serialization.*"
- name: BRACES_BLOCK_STRUCTURE_ERROR
Expand Down
2 changes: 0 additions & 2 deletions ktoml-core/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import com.akuleshov7.buildutils.configurePublishing
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform.getCurrentOperatingSystem
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jetbrains.kotlin.gradle.targets.jvm.tasks.KotlinJvmTest

plugins {
Expand Down
7 changes: 5 additions & 2 deletions ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/Toml.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.akuleshov7.ktoml
import com.akuleshov7.ktoml.decoders.TomlMainDecoder
import com.akuleshov7.ktoml.exceptions.MissingRequiredPropertyException
import com.akuleshov7.ktoml.parsers.TomlParser
import com.akuleshov7.ktoml.tree.TableType
import com.akuleshov7.ktoml.tree.TomlFile
import com.akuleshov7.ktoml.writers.TomlWriter

Expand Down Expand Up @@ -89,7 +90,7 @@ public open class Toml(
tomlTableName: String,
config: TomlConfig = TomlConfig()
): T {
val fakeFileNode = generateFakeTomlStructureForPartialParsing(toml, tomlTableName, config, TomlParser::parseString)
val fakeFileNode = generateFakeTomlStructureForPartialParsing(toml, tomlTableName, config, TableType.PRIMITIVE, TomlParser::parseString)
return TomlMainDecoder.decode(deserializer, fakeFileNode, this.config)
}

Expand Down Expand Up @@ -117,6 +118,7 @@ public open class Toml(
toml.joinToString("\n"),
tomlTableName,
config,
TableType.PRIMITIVE,
TomlParser::parseString,
)
return TomlMainDecoder.decode(deserializer, fakeFileNode, this.config)
Expand All @@ -128,10 +130,11 @@ public open class Toml(
toml: String,
tomlTableName: String,
config: TomlConfig = TomlConfig(),
type: TableType,
parsingFunction: (TomlParser, String) -> TomlFile
): TomlFile {
val parsedToml = parsingFunction(TomlParser(this.config), toml)
.findTableInAstByName(tomlTableName, tomlTableName.count { it == '.' } + 1)
.findTableInAstByName(tomlTableName, tomlTableName.count { it == '.' } + 1, type)
?: throw MissingRequiredPropertyException(
"Cannot find table with name <$tomlTableName> in the toml input. " +
"Not able to decode this toml part."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public class TomlMainDecoder(
is TomlKeyValueArray -> node
// empty nodes will be filtered by iterateUntilWillFindAnyKnownName() method, but in case we came into this
// branch, we should throw an exception as it is not expected at all and we should catch this in tests
is TomlStubEmptyNode, is TomlTable, is TomlFile ->
else ->
throw InternalDecodingException(
"This kind of node should not be processed in TomlDecoder.decodeValue(): ${node.content}"
)
Expand Down Expand Up @@ -201,7 +201,7 @@ public class TomlMainDecoder(
when (nextProcessingNode) {
is TomlKeyValueArray -> TomlArrayDecoder(nextProcessingNode, config)
is TomlKeyValuePrimitive, is TomlStubEmptyNode -> TomlMainDecoder(nextProcessingNode, config)
is TomlTable -> {
is TomlTablePrimitive -> {
val firstTableChild = nextProcessingNode.getFirstChild() ?: throw InternalDecodingException(
"Decoding process failed due to invalid structure of parsed AST tree: missing children" +
" in a table <${nextProcessingNode.fullTableName}>"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ package com.akuleshov7.ktoml.parsers
import com.akuleshov7.ktoml.exceptions.ParseException

/**
* Splitting dot-separated string to tokens:
* Splitting dot-separated string to the list of tokens:
* a.b.c -> [a, b, c]; a."b.c".d -> [a, "b.c", d];
*
* @param lineNo - the line number in toml
Expand Down Expand Up @@ -74,6 +74,14 @@ internal fun String.trimQuotes(): String = trimSymbols(this, "\"", "\"")
*/
internal fun String.trimBrackets(): String = trimSymbols(this, "[", "]")

/**
* If this string starts and end with a pair brackets([[]]) - will return the string with brackets removed
* Otherwise, returns this string.
*
* @return string with the result
*/
internal fun String.trimDoubleBrackets(): String = trimSymbols(this, "[[", "]]")

private fun String.validateSpaces(lineNo: Int, fullKey: String) {
if (this.trim().count { it == ' ' } > 0 && this.isNotQuoted()) {
throw ParseException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,8 @@ package com.akuleshov7.ktoml.parsers

import com.akuleshov7.ktoml.TomlConfig
import com.akuleshov7.ktoml.exceptions.InternalAstException
import com.akuleshov7.ktoml.tree.TomlFile
import com.akuleshov7.ktoml.tree.*
import com.akuleshov7.ktoml.tree.TomlKeyValue
import com.akuleshov7.ktoml.tree.TomlKeyValueArray
import com.akuleshov7.ktoml.tree.TomlKeyValuePrimitive
import com.akuleshov7.ktoml.tree.TomlNode
import com.akuleshov7.ktoml.tree.TomlStubEmptyNode
import com.akuleshov7.ktoml.tree.TomlTable
import com.akuleshov7.ktoml.tree.splitKeyValue
import kotlin.jvm.JvmInline

/**
Expand Down Expand Up @@ -38,45 +32,60 @@ public value class TomlParser(private val config: TomlConfig) {
* @return the root node of the resulted toml tree
* @throws InternalAstException - if toml node does not inherit TomlNode class
*/
@Suppress("TOO_LONG_FUNCTION")
public fun parseStringsToTomlTree(tomlLines: List<String>, config: TomlConfig): TomlFile {
var currentParent: TomlNode = TomlFile(config)
val tomlFileHead = currentParent as TomlFile
var currentParentalNode: TomlNode = TomlFile(config)
val tomlFileHead = currentParentalNode as TomlFile
// need to trim empty lines BEFORE the start of processing
val mutableTomlLines = tomlLines.toMutableList().trimEmptyLines()

mutableTomlLines.forEachIndexed { index, line ->
val lineNo = index + 1
// 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
if (!line.isComment() && !line.isEmptyLine()) {
if (line.isTableNode()) {
val tableSection = TomlTable(line, lineNo, config)
// if the table is the last line in toml, than it has no children and we need to
// add at least fake node as a child
if (index == mutableTomlLines.lastIndex) {
tableSection.appendChild(TomlStubEmptyNode(lineNo, config))
}
// covering the case when processed table contains no key-value pairs or no tables (after our insertion)
// adding fake nodes to a previous table (it has no children because we have found another table right after)
if (currentParent.hasNoChildren()) {
currentParent.appendChild(TomlStubEmptyNode(currentParent.lineNo, config))
if (line.isArrayOfTables()) {
// TomlArrayOfTables contains all information about the ArrayOfTables ([[array of tables]])
val tableArray = TomlArrayOfTables(line, lineNo, config)
val arrayOfTables = tomlFileHead.insertTableToTree(tableArray, TableType.ARRAY)
// creating a new empty element that will be used as an element in array and the parent for next key-value records
val newArrayElement = TomlArrayOfTablesElement(lineNo, config)
// adding this element as a child to the array of tables
arrayOfTables.appendChild(newArrayElement)
// and setting this element as a current parent, so new key-records will be added to this bucket
currentParentalNode = newArrayElement
} else {
val tableSection = TomlTablePrimitive(line, lineNo, config)
// if the table is the last line in toml, then it has no children, and we need to
// add at least fake node as a child
if (index == mutableTomlLines.lastIndex) {
tableSection.appendChild(TomlStubEmptyNode(lineNo, config))
}
// covering the case when the processed table does not contain nor key-value pairs neither tables (after our insertion)
// adding fake nodes to a previous table (it has no children because we have found another table right after)
if (currentParentalNode.hasNoChildren()) {
currentParentalNode.appendChild(TomlStubEmptyNode(currentParentalNode.lineNo, config))
}
currentParentalNode = tomlFileHead.insertTableToTree(tableSection, TableType.PRIMITIVE)
}
currentParent = tomlFileHead.insertTableToTree(tableSection)
} else {
val keyValue = line.parseTomlKeyValue(lineNo, config)
if (keyValue !is TomlNode) {
throw InternalAstException("All Toml nodes should always inherit TomlNode class." +
" Check [${keyValue.key}] with $keyValue type")
}

// inserting the key-value record to the tree
if (keyValue.key.isDotted) {
// in case parser has faced dot-separated complex key (a.b.c) it should create proper table [a.b],
// because table is the same as dotted key
val newTableSection = keyValue.createTomlTableFromDottedKey(currentParent, config)
val newTableSection = keyValue.createTomlTableFromDottedKey(currentParentalNode, config)
tomlFileHead
.insertTableToTree(newTableSection)
.insertTableToTree(newTableSection, TableType.PRIMITIVE)
.appendChild(keyValue)
} else {
// otherwise it should simply append the keyValue to the parent
currentParent.appendChild(keyValue)
currentParentalNode.appendChild(keyValue)
}
}
}
Expand Down Expand Up @@ -109,6 +118,11 @@ public value class TomlParser(private val config: TomlConfig) {
}
}

private fun String.isArrayOfTables(): Boolean {
val trimmed = this.trim()
return trimmed.startsWith("[[") && trimmed.endsWith("]]")
}

private fun String.isTableNode(): Boolean {
val trimmed = this.trim()
return trimmed.startsWith("[") && trimmed.endsWith("]")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* Array of tables https://toml.io/en/v1.0.0#array-of-tables
*/

package com.akuleshov7.ktoml.tree

import com.akuleshov7.ktoml.TomlConfig
import com.akuleshov7.ktoml.exceptions.ParseException
import com.akuleshov7.ktoml.parsers.splitKeyToTokens
import com.akuleshov7.ktoml.parsers.trimDoubleBrackets
import com.akuleshov7.ktoml.parsers.trimQuotes

/**
* @property isSynthetic
*/
// FixMe: this class is mostly identical to the TomlTable - we should unify them together
public class TomlArrayOfTables(
content: String,
lineNo: Int,
config: TomlConfig = TomlConfig(),
public val isSynthetic: Boolean = false
) : TomlTable(
content,
lineNo,
config
) {
public override val type: TableType = TableType.ARRAY

// short table name (only the name without parental prefix, like a - it is used in decoder and encoder)
override val name: String

// list of tables (including sub-tables) that are included in this table (e.g.: {a, a.b, a.b.c} in a.b.c)
public override lateinit var tablesList: List<String>

// full name of the table (like a.b.c.d)
public override lateinit var fullTableName: String

init {
// getting the content inside brackets ([a.b] -> a.b)
val sectionFromContent = content.trim().trimDoubleBrackets().trim()

if (sectionFromContent.isBlank()) {
throw ParseException("Incorrect blank name for array of tables: $content", lineNo)
}

fullTableName = sectionFromContent

val sectionsList = sectionFromContent.splitKeyToTokens(lineNo)
name = sectionsList.last().trimQuotes()
tablesList = sectionsList.mapIndexed { index, _ ->
(0..index).joinToString(".") { sectionsList[it] }
}
}
}

/**
* This class is used to store elements of array of tables (bucket for key-value records)
*/
public class TomlArrayOfTablesElement(lineNo: Int, config: TomlConfig = TomlConfig()) : TomlNode(
EMPTY_TECHNICAL_NODE,
lineNo,
config
) {
override val name: String = EMPTY_TECHNICAL_NODE
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.akuleshov7.ktoml.tree

import com.akuleshov7.ktoml.TomlConfig
import com.akuleshov7.ktoml.exceptions.InternalAstException

/**
* A root node for TOML Abstract Syntax Tree
*/
public class TomlFile(config: TomlConfig = TomlConfig()) : TomlNode(
"rootNode",
0,
config
) {
override val name: String = "rootNode"

override fun getNeighbourNodes(): MutableSet<TomlNode> =
throw InternalAstException("Invalid call to getNeighbourNodes() for TomlFile node")
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,17 @@ internal interface TomlKeyValue {
* @param config
* @return the table that is parsed from a dotted key
*/
fun createTomlTableFromDottedKey(parentNode: TomlNode, config: TomlConfig = TomlConfig()): TomlTable {
fun createTomlTableFromDottedKey(parentNode: TomlNode, config: TomlConfig = TomlConfig()): TomlTablePrimitive {
// for a key: a.b.c it will be [a, b]
val syntheticTablePrefix = this.key.keyParts.dropLast(1)
// creating new key with the last dot-separated fragment
val realKeyWithoutDottedPrefix = TomlKey(key.content, lineNo)
// updating current KeyValue with this key
this.key = realKeyWithoutDottedPrefix
// tables should contain fully qualified name, so we need to add parental name
val parentalPrefix = if (parentNode is TomlTable) "${parentNode.fullTableName}." else ""
val parentalPrefix = if (parentNode is TomlTablePrimitive) "${parentNode.fullTableName}." else ""
// and creating a new table that will be created from dotted key
return TomlTable(
return TomlTablePrimitive(
"[$parentalPrefix${syntheticTablePrefix.joinToString(".")}]",
lineNo,
config,
Expand Down
Loading

0 comments on commit 1fd5e11

Please sign in to comment.