refactor: extend TOML parser for nested tables, inline tables, and JSON arrays
- Add support for nested table headers ([tools.shell], [tools.file_read], etc.)
- Add support for inline tables (capabilities = { General = 1.0, Coding = 0.7 })
- Add support for JSON-style arrays (shell_allowed_executables = ["bash", "sh"])
- Maintain backward compatibility with old CSV formats (deprecated, logged as warnings)
- Refactor buildConfig() to handle new value types (Map, List, Boolean, Number)
- Update docs/sample-config.toml to use clean TOML shapes
- Add comprehensive tests for inline tables, nested sections, JSON arrays, and fallback parsing
- Remove ugly workarounds: capabilities no longer a CSV string, tool flags now in proper nested sections
This commit is contained in:
@@ -33,34 +33,158 @@ object ConfigLoader {
|
||||
private fun parseToml(content: String): CorrexConfig {
|
||||
val lines = content.trim().split("\n")
|
||||
var currentSection = ""
|
||||
val sections = mutableMapOf<String, MutableMap<String, String>>()
|
||||
val sections = mutableMapOf<String, MutableMap<String, Any>>()
|
||||
val providers = mutableListOf<MutableMap<String, Any>>()
|
||||
var currentProvider: MutableMap<String, Any>? = null
|
||||
|
||||
for (line in lines) {
|
||||
for ((lineNum, line) in lines.withIndex()) {
|
||||
val trimmed = line.trim()
|
||||
|
||||
when {
|
||||
trimmed.isEmpty() || trimmed.startsWith("#") -> {
|
||||
// Skip empty lines and comments
|
||||
}
|
||||
trimmed.startsWith("[") && trimmed.endsWith("]") -> {
|
||||
// Parse section headers like [server]
|
||||
trimmed == "[[providers]]" -> {
|
||||
// Start a new provider entry
|
||||
if (currentProvider != null) {
|
||||
providers.add(currentProvider)
|
||||
}
|
||||
currentProvider = mutableMapOf()
|
||||
currentSection = ""
|
||||
}
|
||||
trimmed.startsWith("[") && !trimmed.startsWith("[[") && trimmed.endsWith("]") -> {
|
||||
// Parse section headers like [server] or [tools.shell]
|
||||
if (currentProvider != null) {
|
||||
providers.add(currentProvider)
|
||||
currentProvider = null
|
||||
}
|
||||
currentSection = trimmed.substring(1, trimmed.length - 1).trim()
|
||||
sections.putIfAbsent(currentSection, mutableMapOf())
|
||||
}
|
||||
else -> {
|
||||
// Parse key=value pairs
|
||||
val eqIndex = trimmed.indexOf("=")
|
||||
if (eqIndex > 0 && currentSection.isNotEmpty()) {
|
||||
if (eqIndex > 0) {
|
||||
val key = trimmed.substring(0, eqIndex).trim()
|
||||
val value = trimmed.substring(eqIndex + 1).trim()
|
||||
val cleanedValue = stripQuotes(value)
|
||||
sections[currentSection]?.put(key, cleanedValue)
|
||||
val valueStr = trimmed.substring(eqIndex + 1).trim()
|
||||
val parsedValue = parseValue(valueStr, lineNum + 1)
|
||||
|
||||
if (currentProvider != null) {
|
||||
currentProvider[key] = parsedValue
|
||||
} else if (currentSection.isNotEmpty()) {
|
||||
sections[currentSection]?.put(key, parsedValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buildConfig(sections)
|
||||
// Don't forget the last provider if file ends with [[providers]] block
|
||||
if (currentProvider != null) {
|
||||
providers.add(currentProvider)
|
||||
}
|
||||
|
||||
return buildConfig(sections, providers)
|
||||
}
|
||||
|
||||
private fun parseValue(valueStr: String, lineNum: Int): Any {
|
||||
return when {
|
||||
// Inline table: { key = value, key2 = value2 }
|
||||
valueStr.startsWith("{") && valueStr.endsWith("}") -> {
|
||||
parseInlineTable(valueStr, lineNum)
|
||||
}
|
||||
// JSON-style array: ["item1", "item2"]
|
||||
valueStr.startsWith("[") && valueStr.endsWith("]") -> {
|
||||
parseArray(valueStr, lineNum)
|
||||
}
|
||||
// CSV fallback for backward compat (detect by "," and "=" pattern without brackets)
|
||||
valueStr.contains(",") && valueStr.contains("=") && !valueStr.startsWith("\"") -> {
|
||||
parseCSVCapabilities(valueStr)
|
||||
}
|
||||
// Boolean
|
||||
valueStr == "true" || valueStr == "false" -> valueStr.toBoolean()
|
||||
// Number
|
||||
valueStr.toDoubleOrNull() != null -> {
|
||||
if (valueStr.contains(".")) valueStr.toDouble() else valueStr.toLong()
|
||||
}
|
||||
// String (with or without quotes)
|
||||
else -> stripQuotes(valueStr)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseInlineTable(tableStr: String, lineNum: Int): Map<String, Any> {
|
||||
val result = mutableMapOf<String, Any>()
|
||||
val content = tableStr.substring(1, tableStr.length - 1).trim()
|
||||
if (content.isEmpty()) return result
|
||||
|
||||
val pairs = splitTopLevel(content, ',')
|
||||
for (pair in pairs) {
|
||||
val eqIdx = pair.indexOf("=")
|
||||
if (eqIdx > 0) {
|
||||
val key = pair.substring(0, eqIdx).trim()
|
||||
val valStr = pair.substring(eqIdx + 1).trim()
|
||||
result[key] = parseValue(valStr, lineNum)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun parseArray(arrayStr: String, lineNum: Int): List<String> {
|
||||
val result = mutableListOf<String>()
|
||||
val content = arrayStr.substring(1, arrayStr.length - 1).trim()
|
||||
if (content.isEmpty()) return result
|
||||
|
||||
val items = splitTopLevel(content, ',')
|
||||
for (item in items) {
|
||||
val trimmed = item.trim()
|
||||
result.add(stripQuotes(trimmed))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun splitTopLevel(text: String, delimiter: Char): List<String> {
|
||||
val result = mutableListOf<String>()
|
||||
var current = StringBuilder()
|
||||
var inQuotes = false
|
||||
var quoteChar = ' '
|
||||
|
||||
for (ch in text) {
|
||||
when {
|
||||
(ch == '"' || ch == '\'') && !inQuotes -> {
|
||||
inQuotes = true
|
||||
quoteChar = ch
|
||||
current.append(ch)
|
||||
}
|
||||
ch == quoteChar && inQuotes -> {
|
||||
inQuotes = false
|
||||
current.append(ch)
|
||||
}
|
||||
ch == delimiter && !inQuotes -> {
|
||||
result.add(current.toString().trim())
|
||||
current = StringBuilder()
|
||||
}
|
||||
else -> current.append(ch)
|
||||
}
|
||||
}
|
||||
if (current.isNotEmpty()) {
|
||||
result.add(current.toString().trim())
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun parseCSVCapabilities(capsStr: String): Map<String, Double> {
|
||||
val result = mutableMapOf<String, Double>()
|
||||
capsStr.split(",").forEach { pair ->
|
||||
val parts = pair.trim().split("=")
|
||||
if (parts.size == 2) {
|
||||
val cap = parts[0].trim()
|
||||
val score = parts[1].trim().toDoubleOrNull()
|
||||
if (score != null && score >= 0.0 && score <= 1.0) {
|
||||
result[cap] = score
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun stripQuotes(value: String): String {
|
||||
@@ -73,47 +197,211 @@ object ConfigLoader {
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildConfig(sections: Map<String, Map<String, String>>): CorrexConfig {
|
||||
private fun buildConfig(
|
||||
sections: Map<String, Map<String, Any>>,
|
||||
providersList: List<Map<String, Any>> = emptyList(),
|
||||
): CorrexConfig {
|
||||
val serverSection = sections["server"] ?: emptyMap()
|
||||
val tuiSection = sections["tui"] ?: emptyMap()
|
||||
val cliSection = sections["cli"] ?: emptyMap()
|
||||
val approvalSection = sections["approval"] ?: emptyMap()
|
||||
val toolsSection = sections["tools"] ?: emptyMap()
|
||||
val toolsShellSection = sections["tools.shell"] ?: emptyMap()
|
||||
val toolsFileReadSection = sections["tools.file_read"] ?: emptyMap()
|
||||
val toolsFileWriteSection = sections["tools.file_write"] ?: emptyMap()
|
||||
val toolsFileEditSection = sections["tools.file_edit"] ?: emptyMap()
|
||||
|
||||
val server = ServerConfig(
|
||||
host = serverSection["host"] ?: "localhost",
|
||||
port = serverSection["port"]?.toIntOrNull() ?: 8080,
|
||||
host = asString(serverSection["host"], "localhost"),
|
||||
port = asInt(serverSection["port"], 8080),
|
||||
)
|
||||
|
||||
val tui = TuiConfig(
|
||||
theme = tuiSection["theme"] ?: "dark",
|
||||
sessionListLimit = tuiSection["session_list_limit"]?.toIntOrNull() ?: 5,
|
||||
theme = asString(tuiSection["theme"], "dark"),
|
||||
sessionListLimit = asInt(tuiSection["session_list_limit"], 5),
|
||||
)
|
||||
|
||||
val cli = CliConfig(
|
||||
defaultOutput = cliSection["default_output"] ?: "human",
|
||||
defaultOutput = asString(cliSection["default_output"], "human"),
|
||||
)
|
||||
|
||||
val approval = ApprovalConfig(
|
||||
timeoutMs = approvalSection["timeout_ms"]?.toLongOrNull() ?: 300_000L,
|
||||
timeoutMs = asLong(approvalSection["timeout_ms"], 300_000L),
|
||||
)
|
||||
|
||||
// Resolve tool enable flags: prefer nested [tools.shell], [tools.file_read], etc.
|
||||
// Fall back to flat [tools] section for backward compat
|
||||
val shellEnabled = when {
|
||||
toolsShellSection.containsKey("enabled") -> asBoolean(toolsShellSection["enabled"], true)
|
||||
toolsSection.containsKey("shell_enabled") -> {
|
||||
System.err.println("Warning: 'shell_enabled' in [tools] is deprecated, use [tools.shell] enabled instead")
|
||||
asBoolean(toolsSection["shell_enabled"], true)
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
|
||||
val fileReadEnabled = when {
|
||||
toolsFileReadSection.containsKey("enabled") -> asBoolean(toolsFileReadSection["enabled"], true)
|
||||
toolsSection.containsKey("file_read_enabled") -> {
|
||||
System.err.println("Warning: 'file_read_enabled' in [tools] is deprecated, use [tools.file_read] enabled instead")
|
||||
asBoolean(toolsSection["file_read_enabled"], true)
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
|
||||
val fileWriteEnabled = when {
|
||||
toolsFileWriteSection.containsKey("enabled") -> asBoolean(toolsFileWriteSection["enabled"], true)
|
||||
toolsSection.containsKey("file_write_enabled") -> {
|
||||
System.err.println("Warning: 'file_write_enabled' in [tools] is deprecated, use [tools.file_write] enabled instead")
|
||||
asBoolean(toolsSection["file_write_enabled"], true)
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
|
||||
val fileEditEnabled = when {
|
||||
toolsFileEditSection.containsKey("enabled") -> asBoolean(toolsFileEditSection["enabled"], true)
|
||||
toolsSection.containsKey("file_edit_enabled") -> {
|
||||
System.err.println("Warning: 'file_edit_enabled' in [tools] is deprecated, use [tools.file_edit] enabled instead")
|
||||
asBoolean(toolsSection["file_edit_enabled"], true)
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
|
||||
val shellAllowedExecutables = when {
|
||||
toolsShellSection.containsKey("allowed_executables") -> {
|
||||
asStringList(toolsShellSection["allowed_executables"])
|
||||
}
|
||||
toolsSection.containsKey("shell_allowed_executables") -> {
|
||||
val val1 = toolsSection["shell_allowed_executables"]
|
||||
when {
|
||||
val1 is List<*> -> val1.filterIsInstance<String>()
|
||||
val1 is String -> {
|
||||
// Backward compat: check if it's CSV or already a single item
|
||||
if (val1.contains(",")) {
|
||||
System.err.println("Warning: CSV format 'shell_allowed_executables' is deprecated, use JSON array instead")
|
||||
val1.split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
} else {
|
||||
listOf(val1)
|
||||
}
|
||||
}
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
else -> emptyList()
|
||||
}
|
||||
|
||||
val tools = ToolsConfig(
|
||||
sandboxRoot = toolsSection["sandbox_root"] ?: "",
|
||||
workingDir = toolsSection["working_dir"] ?: "",
|
||||
shellAllowedExecutables = toolsSection["shell_allowed_executables"]
|
||||
?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
|
||||
?: emptyList(),
|
||||
defaultSystemPromptPath = toolsSection["default_system_prompt_path"]
|
||||
?: "~/.config/correx/prompts/default_system.md",
|
||||
sandboxRoot = asString(toolsSection["sandbox_root"], ""),
|
||||
workingDir = asString(toolsSection["working_dir"], ""),
|
||||
shellAllowedExecutables = shellAllowedExecutables,
|
||||
defaultSystemPromptPath = asString(
|
||||
toolsSection["default_system_prompt_path"],
|
||||
"~/.config/correx/prompts/default_system.md"
|
||||
),
|
||||
shellEnabled = shellEnabled,
|
||||
fileReadEnabled = fileReadEnabled,
|
||||
fileWriteEnabled = fileWriteEnabled,
|
||||
fileEditEnabled = fileEditEnabled,
|
||||
)
|
||||
|
||||
val providers = providersList.mapNotNull { providerMap ->
|
||||
val id = asString(providerMap["id"]) ?: return@mapNotNull null
|
||||
val type = asString(providerMap["type"]) ?: return@mapNotNull null
|
||||
val modelId = asString(providerMap["model_id"]) ?: return@mapNotNull null
|
||||
val modelPath = asString(providerMap["model_path"], "")
|
||||
val url = asString(providerMap["url"], "http://127.0.0.1:10000")
|
||||
val capabilities = parseCapabilities(providerMap["capabilities"])
|
||||
|
||||
ProviderConfig(
|
||||
id = id,
|
||||
type = type,
|
||||
modelId = modelId,
|
||||
modelPath = modelPath,
|
||||
url = url,
|
||||
capabilities = capabilities,
|
||||
)
|
||||
}
|
||||
|
||||
return CorrexConfig(
|
||||
server = server,
|
||||
tui = tui,
|
||||
cli = cli,
|
||||
approval = approval,
|
||||
tools = tools,
|
||||
providers = providers,
|
||||
)
|
||||
}
|
||||
|
||||
private fun asString(value: Any?, default: String = ""): String {
|
||||
return when (value) {
|
||||
is String -> value
|
||||
else -> default
|
||||
}
|
||||
}
|
||||
|
||||
private fun asStringOrNull(value: Any?): String? {
|
||||
return when (value) {
|
||||
is String -> value
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun asStringList(value: Any?): List<String> {
|
||||
return when (value) {
|
||||
is List<*> -> value.filterIsInstance<String>()
|
||||
is String -> if (value.contains(",")) {
|
||||
value.split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
} else {
|
||||
listOf(value)
|
||||
}
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun asInt(value: Any?, default: Int = 0): Int {
|
||||
return when (value) {
|
||||
is Int -> value
|
||||
is Long -> value.toInt()
|
||||
is String -> value.toIntOrNull() ?: default
|
||||
else -> default
|
||||
}
|
||||
}
|
||||
|
||||
private fun asLong(value: Any?, default: Long = 0L): Long {
|
||||
return when (value) {
|
||||
is Long -> value
|
||||
is Int -> value.toLong()
|
||||
is String -> value.toLongOrNull() ?: default
|
||||
else -> default
|
||||
}
|
||||
}
|
||||
|
||||
private fun asBoolean(value: Any?, default: Boolean = false): Boolean {
|
||||
return when (value) {
|
||||
is Boolean -> value
|
||||
is String -> value.lowercase() == "true"
|
||||
else -> default
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseCapabilities(value: Any?): Map<String, Double> {
|
||||
return when (value) {
|
||||
is Map<*, *> -> {
|
||||
// Inline table parsed earlier
|
||||
value.filterKeys { it is String }
|
||||
.mapKeys { it.key as String }
|
||||
.mapValues { (_, v) ->
|
||||
when (v) {
|
||||
is Double -> v
|
||||
is Number -> v.toDouble()
|
||||
is String -> v.toDoubleOrNull() ?: 0.0
|
||||
else -> 0.0
|
||||
}
|
||||
}
|
||||
.filter { it.value in 0.0..1.0 }
|
||||
}
|
||||
is String -> parseCSVCapabilities(value)
|
||||
else -> emptyMap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,4 +73,148 @@ class ConfigLoaderTest {
|
||||
// Verify it returns a non-null Path object
|
||||
assertEquals("config.toml", configPath.fileName.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parseToml parses providers with inline table capabilities`() {
|
||||
val toml = """
|
||||
[server]
|
||||
host = "localhost"
|
||||
|
||||
[[providers]]
|
||||
id = "local-llama"
|
||||
type = "llamacpp"
|
||||
model_id = "mistral-7b"
|
||||
model_path = "/path/to/model.gguf"
|
||||
url = "http://127.0.0.1:10000"
|
||||
capabilities = { General = 1.0, Coding = 0.7, Reasoning = 0.6 }
|
||||
|
||||
[[providers]]
|
||||
id = "alt-llama"
|
||||
type = "llamacpp"
|
||||
model_id = "neural-chat-7b"
|
||||
model_path = "/path/to/model2.gguf"
|
||||
url = "http://127.0.0.1:10001"
|
||||
capabilities = { General = 0.9, Coding = 0.8 }
|
||||
""".trimIndent()
|
||||
|
||||
val loader = ConfigLoader::class.java
|
||||
val parseTomlMethod = loader.getDeclaredMethod("parseToml", String::class.java)
|
||||
parseTomlMethod.isAccessible = true
|
||||
val result = parseTomlMethod.invoke(ConfigLoader, toml) as CorrexConfig
|
||||
|
||||
assertEquals(2, result.providers.size)
|
||||
assertEquals("local-llama", result.providers[0].id)
|
||||
assertEquals("llamacpp", result.providers[0].type)
|
||||
assertEquals("mistral-7b", result.providers[0].modelId)
|
||||
assertEquals("/path/to/model.gguf", result.providers[0].modelPath)
|
||||
assertEquals("http://127.0.0.1:10000", result.providers[0].url)
|
||||
assertEquals(3, result.providers[0].capabilities.size)
|
||||
assertEquals(1.0, result.providers[0].capabilities["General"])
|
||||
assertEquals(0.7, result.providers[0].capabilities["Coding"])
|
||||
assertEquals(0.6, result.providers[0].capabilities["Reasoning"])
|
||||
|
||||
assertEquals("alt-llama", result.providers[1].id)
|
||||
assertEquals("neural-chat-7b", result.providers[1].modelId)
|
||||
assertEquals(2, result.providers[1].capabilities.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parseToml parses nested tool sections with enable flags`() {
|
||||
val toml = """
|
||||
[tools]
|
||||
sandbox_root = "/tmp/correx"
|
||||
|
||||
[tools.shell]
|
||||
enabled = false
|
||||
allowed_executables = ["bash", "sh", "python3"]
|
||||
|
||||
[tools.file_read]
|
||||
enabled = true
|
||||
|
||||
[tools.file_write]
|
||||
enabled = false
|
||||
|
||||
[tools.file_edit]
|
||||
enabled = true
|
||||
""".trimIndent()
|
||||
|
||||
val loader = ConfigLoader::class.java
|
||||
val parseTomlMethod = loader.getDeclaredMethod("parseToml", String::class.java)
|
||||
parseTomlMethod.isAccessible = true
|
||||
val result = parseTomlMethod.invoke(ConfigLoader, toml) as CorrexConfig
|
||||
|
||||
assertEquals(false, result.tools.shellEnabled)
|
||||
assertEquals(true, result.tools.fileReadEnabled)
|
||||
assertEquals(false, result.tools.fileWriteEnabled)
|
||||
assertEquals(true, result.tools.fileEditEnabled)
|
||||
assertEquals(3, result.tools.shellAllowedExecutables.size)
|
||||
assertEquals("bash", result.tools.shellAllowedExecutables[0])
|
||||
assertEquals("sh", result.tools.shellAllowedExecutables[1])
|
||||
assertEquals("python3", result.tools.shellAllowedExecutables[2])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parseToml parses JSON array shell_allowed_executables`() {
|
||||
val toml = """
|
||||
[tools]
|
||||
shell_allowed_executables = ["bash", "sh", "node", "python3"]
|
||||
""".trimIndent()
|
||||
|
||||
val loader = ConfigLoader::class.java
|
||||
val parseTomlMethod = loader.getDeclaredMethod("parseToml", String::class.java)
|
||||
parseTomlMethod.isAccessible = true
|
||||
val result = parseTomlMethod.invoke(ConfigLoader, toml) as CorrexConfig
|
||||
|
||||
assertEquals(4, result.tools.shellAllowedExecutables.size)
|
||||
assertEquals("bash", result.tools.shellAllowedExecutables[0])
|
||||
assertEquals("sh", result.tools.shellAllowedExecutables[1])
|
||||
assertEquals("node", result.tools.shellAllowedExecutables[2])
|
||||
assertEquals("python3", result.tools.shellAllowedExecutables[3])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parseToml parses old flat tool enable flags for backward compat`() {
|
||||
val toml = """
|
||||
[tools]
|
||||
shell_enabled = false
|
||||
file_read_enabled = true
|
||||
file_write_enabled = false
|
||||
file_edit_enabled = true
|
||||
shell_allowed_executables = "bash,sh,python3"
|
||||
""".trimIndent()
|
||||
|
||||
val loader = ConfigLoader::class.java
|
||||
val parseTomlMethod = loader.getDeclaredMethod("parseToml", String::class.java)
|
||||
parseTomlMethod.isAccessible = true
|
||||
val result = parseTomlMethod.invoke(ConfigLoader, toml) as CorrexConfig
|
||||
|
||||
assertEquals(false, result.tools.shellEnabled)
|
||||
assertEquals(true, result.tools.fileReadEnabled)
|
||||
assertEquals(false, result.tools.fileWriteEnabled)
|
||||
assertEquals(true, result.tools.fileEditEnabled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parseToml parses capabilities as CSV string for backward compat`() {
|
||||
val toml = """
|
||||
[server]
|
||||
host = "localhost"
|
||||
|
||||
[[providers]]
|
||||
id = "legacy-llama"
|
||||
type = "llamacpp"
|
||||
model_id = "mistral-7b"
|
||||
capabilities = "General=1.0,Coding=0.7,Reasoning=0.6"
|
||||
""".trimIndent()
|
||||
|
||||
val loader = ConfigLoader::class.java
|
||||
val parseTomlMethod = loader.getDeclaredMethod("parseToml", String::class.java)
|
||||
parseTomlMethod.isAccessible = true
|
||||
val result = parseTomlMethod.invoke(ConfigLoader, toml) as CorrexConfig
|
||||
|
||||
assertEquals(1, result.providers.size)
|
||||
assertEquals(3, result.providers[0].capabilities.size)
|
||||
assertEquals(1.0, result.providers[0].capabilities["General"])
|
||||
assertEquals(0.7, result.providers[0].capabilities["Coding"])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user