11package org.openprojectx.bigdata.test.core.config
22
3+ import io.ous.jtoml.JToml
4+ import java.math.BigDecimal
35import java.nio.file.Files
46import java.nio.file.Path
7+ import java.util.Date
58import org.openprojectx.bigdata.test.core.BigDataHealthCheckMode
69import org.openprojectx.bigdata.test.core.BigDataHealthCheckOptions
710import org.openprojectx.bigdata.test.core.BigDataService
@@ -323,7 +326,7 @@ class BigDataTestConfigLoader(
323326 }
324327
325328 fun load (location : String ): BigDataTestConfig {
326- val tables = parseTables(readText(location))
329+ val tables = TomlConfigParser . parseTables(readText(location))
327330 val images = tables[" images" ].orEmpty()
328331 val services = tables[" services" ].orEmpty()
329332 val kerberos = tables[" kerberos" ].orEmpty()
@@ -466,84 +469,7 @@ class BigDataTestConfigLoader(
466469 else -> Files .readString(Path .of(location))
467470 }
468471
469- private fun parseTables (text : String ): Map <String , Map <String , TomlValue >> {
470- val tables = linkedMapOf<String , MutableMap <String , TomlValue >>()
471- var table = " "
472- tables[table] = linkedMapOf()
473- text.lineSequence().forEachIndexed { index, raw ->
474- val line = stripComment(raw).trim()
475- if (line.isBlank()) return @forEachIndexed
476- if (line.startsWith(" [" ) && line.endsWith(" ]" )) {
477- table = line.removePrefix(" [" ).removeSuffix(" ]" ).trim()
478- require(table.isNotBlank()) { " TOML table name is blank at line ${index + 1 } " }
479- tables.getOrPut(table) { linkedMapOf() }
480- return @forEachIndexed
481- }
482- val separator = line.indexOf(' =' )
483- require(separator > 0 ) { " TOML line ${index + 1 } : expected key = value" }
484- val key = parseKey(line.substring(0 , separator).trim(), index + 1 )
485- val value = line.substring(separator + 1 ).trim()
486- tables.getValue(table)[key] = parseValue(value, index + 1 )
487- }
488- return tables
489- }
490-
491- private fun stripComment (raw : String ): String {
492- var inString = false
493- var escaped = false
494- raw.forEachIndexed { index, char ->
495- when {
496- escaped -> escaped = false
497- char == ' \\ ' && inString -> escaped = true
498- char == ' "' -> inString = ! inString
499- char == ' #' && ! inString -> return raw.substring(0 , index)
500- }
501- }
502- return raw
503- }
504-
505- private fun parseKey (key : String , line : Int ): String =
506- if (key.startsWith(' "' )) parseString(key, line) else key
507-
508- private fun parseValue (value : String , line : Int ): TomlValue =
509- when {
510- value.startsWith(' "' ) -> TomlValue .StringValue (parseString(value, line))
511- value == " true" -> TomlValue .BooleanValue (true )
512- value == " false" -> TomlValue .BooleanValue (false )
513- value.matches(Regex (" [0-9]+" )) -> TomlValue .IntValue (value.toInt())
514- else -> error(" TOML line $line : unsupported value '$value '" )
515- }
516-
517- private fun parseString (value : String , line : Int ): String {
518- require(value.length >= 2 && value.first() == ' "' && value.last() == ' "' ) {
519- " TOML line $line : string values must be quoted"
520- }
521- return buildString {
522- var escaped = false
523- value.substring(1 , value.length - 1 ).forEach { char ->
524- if (escaped) {
525- append(
526- when (char) {
527- ' "' -> ' "'
528- ' \\ ' -> ' \\ '
529- ' n' -> ' \n '
530- ' r' -> ' \r '
531- ' t' -> ' \t '
532- else -> char
533- },
534- )
535- escaped = false
536- } else if (char == ' \\ ' ) {
537- escaped = true
538- } else {
539- append(char)
540- }
541- }
542- require(! escaped) { " TOML line $line : unterminated escape in string" }
543- }
544- }
545-
546- private fun httpTls (values : Map <String , TomlValue >): BigDataTestHttpTlsConfig =
472+ private fun httpTls (values : Map <String , Any ?>): BigDataTestHttpTlsConfig =
547473 BigDataTestHttpTlsConfig (
548474 enabled = values.boolean(" enabled" ),
549475 domain = values.string(" domain" ),
@@ -564,7 +490,7 @@ class BigDataTestConfigLoader(
564490 }
565491
566492 private fun parseContainerCustomizations (
567- tables : Map <String , Map <String , TomlValue >>,
493+ tables : Map <String , Map <String , Any ? >>,
568494 ): Map <BigDataService , ContainerCustomizationOptions > {
569495 val customizations = linkedMapOf<BigDataService , ContainerCustomizationOptions >()
570496 tables.forEach { (table, values) ->
@@ -590,7 +516,7 @@ class BigDataTestConfigLoader(
590516 return customizations
591517 }
592518
593- private fun parseContainerLogLevels (tables : Map <String , Map <String , TomlValue >>): Map <BigDataService , String > {
519+ private fun parseContainerLogLevels (tables : Map <String , Map <String , Any ? >>): Map <BigDataService , String > {
594520 val values = tables[" containerLogLevels" ].orEmpty()
595521 return values.mapKeys { (serviceName, _) -> parseService(serviceName, " containerLogLevels" ) }
596522 .mapValues { (service, value) -> normalizeContainerLogLevel(value.asString(" container log level '$service '" )) }
@@ -605,7 +531,7 @@ class BigDataTestConfigLoader(
605531 return normalized
606532 }
607533
608- private fun parseHealthChecks (tables : Map <String , Map <String , TomlValue >>): Map <BigDataService , BigDataHealthCheckOptions > {
534+ private fun parseHealthChecks (tables : Map <String , Map <String , Any ? >>): Map <BigDataService , BigDataHealthCheckOptions > {
609535 val modes = tables[" healthChecks" ].orEmpty()
610536 val timeouts = tables[" healthCheckTimeouts" ].orEmpty()
611537 val healthChecks = linkedMapOf<BigDataService , BigDataHealthCheckOptions >()
@@ -625,7 +551,7 @@ class BigDataTestConfigLoader(
625551 return SERVICE_NAMES [normalized] ? : error(" TOML table '$table ' uses unknown service '$name '" )
626552 }
627553
628- private fun Map <String , TomlValue >.containerFiles (): List <ContainerFileTransferOptions > =
554+ private fun Map <String , Any ? >.containerFiles (): List <ContainerFileTransferOptions > =
629555 map { (containerPath, value) ->
630556 val source = value.asString(" container file '$containerPath '" )
631557 when {
@@ -645,15 +571,15 @@ class BigDataTestConfigLoader(
645571 }
646572 }
647573
648- private fun Map <String , TomlValue >.containerMounts (): List <ContainerMountOptions > =
574+ private fun Map <String , Any ? >.containerMounts (): List <ContainerMountOptions > =
649575 map { (hostPath, value) ->
650576 val target = value.asString(" container mount '$hostPath '" )
651577 val readOnly = ! target.endsWith(" :rw" )
652578 val containerPath = target.removeSuffix(" :ro" ).removeSuffix(" :rw" )
653579 ContainerMountOptions (hostPath = hostPath, containerPath = containerPath, readOnly = readOnly)
654580 }
655581
656- private fun Map <String , TomlValue >.containerPorts (table : String ): List <ContainerPortOptions > =
582+ private fun Map <String , Any ? >.containerPorts (table : String ): List <ContainerPortOptions > =
657583 map { (containerPort, value) ->
658584 ContainerPortOptions (
659585 containerPort = containerPort.toIntOrNull()
@@ -662,41 +588,35 @@ class BigDataTestConfigLoader(
662588 )
663589 }
664590
665- private fun Map <String , TomlValue >.stringMap (table : String ): Map <String , String > =
591+ private fun Map <String , Any ? >.stringMap (table : String ): Map <String , String > =
666592 mapValues { (key, value) -> value.asString(" TOML key '$table .$key '" ) }
667593
668- private fun Map <String , TomlValue >.string (key : String ): String? =
594+ private fun Map <String , Any ? >.string (key : String ): String? =
669595 this [key]?.let {
670- require(it is TomlValue . StringValue ) { " TOML key '$key ' must be a quoted string" }
671- it.value
596+ require(it is String ) { " TOML key '$key ' must be a quoted string" }
597+ it
672598 }
673599
674- private fun Map <String , TomlValue >.boolean (key : String ): Boolean? =
600+ private fun Map <String , Any ? >.boolean (key : String ): Boolean? =
675601 this [key]?.let {
676- require(it is TomlValue . BooleanValue ) { " TOML key '$key ' must be true or false" }
677- it.value
602+ require(it is Boolean ) { " TOML key '$key ' must be true or false" }
603+ it
678604 }
679605
680- private fun Map <String , TomlValue >.int (key : String ): Int? =
606+ private fun Map <String , Any ? >.int (key : String ): Int? =
681607 this [key]?.let {
682- require(it is TomlValue . IntValue ) { " TOML key '$key ' must be an integer" }
683- it.value
608+ require(it is Number ) { " TOML key '$key ' must be an integer" }
609+ it.toInt()
684610 }
685611
686- private fun TomlValue.asString (name : String ): String {
687- require(this is TomlValue .StringValue ) { " $name must be a quoted string" }
688- return value
689- }
690-
691- private fun TomlValue.asInt (name : String ): Int {
692- require(this is TomlValue .IntValue ) { " $name must be an integer" }
693- return value
612+ private fun Any?.asString (name : String ): String {
613+ require(this is String ) { " $name must be a quoted string" }
614+ return this
694615 }
695616
696- private sealed interface TomlValue {
697- data class StringValue (val value : String ) : TomlValue
698- data class BooleanValue (val value : Boolean ) : TomlValue
699- data class IntValue (val value : Int ) : TomlValue
617+ private fun Any?.asInt (name : String ): Int {
618+ require(this is Number ) { " $name must be an integer" }
619+ return toInt()
700620 }
701621
702622 private companion object {
@@ -713,3 +633,53 @@ class BigDataTestConfigLoader(
713633 )
714634 }
715635}
636+
637+ private object TomlConfigParser {
638+ private val trailingArrayComma = Regex (" ,\\ s*]" )
639+
640+ fun parseTables (text : String ): Map <String , Map <String , Any ?>> {
641+ val root = JToml .parseString(text.replace(trailingArrayComma, " \n ]" ))
642+ val tables = linkedMapOf<String , Map <String , Any ?>>()
643+ flattenTable(" " , root, tables)
644+ return tables
645+ }
646+
647+ private fun flattenTable (
648+ path : String ,
649+ values : Map <* , * >,
650+ tables : MutableMap <String , Map <String , Any ?>>,
651+ ) {
652+ val scalars = linkedMapOf<String , Any ?>()
653+ values.forEach { (rawKey, rawValue) ->
654+ require(rawKey is String ) { " Unsupported TOML key type: ${rawKey?.javaClass} " }
655+ when (rawValue) {
656+ is Map <* , * > -> flattenTable(path.child(rawKey), rawValue, tables)
657+ else -> scalars[rawKey] = rawValue.normalizedTomlValue()
658+ }
659+ }
660+ tables[path] = scalars
661+ }
662+
663+ private fun String.child (name : String ): String =
664+ if (isBlank()) name else " $this .$name "
665+
666+ private fun Any?.normalizedTomlValue (): Any? =
667+ when (this ) {
668+ null -> null
669+ is String ,
670+ is Boolean ,
671+ is Int ,
672+ is Long ,
673+ is Float ,
674+ is Double ,
675+ is BigDecimal ,
676+ is Date ,
677+ -> this
678+ is Iterable <* > -> map { it.normalizedTomlValue() }
679+ is Map <* , * > -> entries.associate { (key, value) ->
680+ require(key is String ) { " Unsupported TOML key type: ${key?.javaClass} " }
681+ key to value.normalizedTomlValue()
682+ }
683+ else -> this .toString()
684+ }
685+ }
0 commit comments