Reading and Processing HOCON in Kotlin
Asked Answered
L

2

5

I would like to read the following configuration from a HOCON (Typesafe Config) file into Kotlin.

tablename: {  
  columns: [
    { item: { type: integer, key: true, null: false } }
    { desc: { type: varchar, length: 64 } }
    { quantity: { type: integer, null: false } }
    { price: { type: decimal, precision: 14, scale: 3 } }
  ]
}

In fact I would like to extract the key column(s). I have tried the following so far.

val metadata = ConfigFactory.parseFile(metafile)
val keys = metadata.getObjectList("${tablename.toLowerCase()}.columns")
                   .filter { it.unwrapped().values.first().get("key") == true }

But it fails with the following error.

Unresolved reference. None of the following candidates is applicable because of receiver type mismatch:
@kotlin.internal.InlineOnly public operator inline fun <@kotlin.internal.OnlyInputTypes K, V> kotlin.collections.Map<out kotlin.String, ???>.get(key: kotlin.String): ??? defined in kotlin.collections

It is clear that Kotlin is not able to understand the data type of the "value" field in the Map. How do I declare it or let Kotlin know?

Also not that there are different types and optional keys in this Map.

PS: I know that there are couple of wrappers available for Kotlin such as Konfig and Klutter. I was hoping that if this is easy to write I could avoid another library.

UPDATE 1:

I have tried the following.

it.unwrapped().values.first().get<String, Boolean>("key")

to get the following compiler error.

Unresolved reference. None of the following candidates is applicable because of receiver type mismatch:
@kotlin.internal.InlineOnly public operator inline fun <@kotlin.internal.OnlyInputTypes K, V> kotlin.collections.Map<out kotlin.String, kotlin.Boolean>.get(key: kotlin.String): kotlin.Boolean? defined in kotlin.collections

And this

it.unwrapped().values.first().get<String, Boolean?>("key")

with output

Unresolved reference. None of the following candidates is applicable because of receiver type mismatch:
@kotlin.internal.InlineOnly public operator inline fun <@kotlin.internal.OnlyInputTypes K, V> kotlin.collections.Map<out kotlin.String, kotlin.Boolean?>.get(key: kotlin.String): kotlin.Boolean? defined in kotlin.collections

UPDATE 2:

Looking at the way it is handled elsewhere, I guess I probably need to use reflection. Trying it out with my limited exposure. No luck so far.

Louvain answered 7/5, 2016 at 19:28 Comment(1)
I probably don't need to unwrap the config object. But dealing with it as is did not yield any results and this was the closest I could bring it to "print" something.Louvain
Y
8

Consider your code, deconstructed below:

val keys = metadata.getObjectList("tablename.columns")
        .filter {
            val item:ConfigObject = it
            val unwrapped:Map<String,Any?> = item.unwrapped()
            val values:Collection<Any?> = unwrapped.values
            val firstValue:Any? = values.first()
            firstValue.get("key") == true // does not compile
        }

From the above the problem should be obvious. You need to aid the compiler with the information that firstValue holds a Map like so:

val firstValueMap = firstValue as Map<String,Any?>
firstValueMap["key"] == true
Yolande answered 8/5, 2016 at 9:41 Comment(1)
I had to suppress an UNCHECKED_CAST warning too.Louvain
R
2

Even though you are not using Klutter, I created an update for it to make ConfigObject and Config act uniformly the same. From Klutter version 1.17.1 onwards (pushing to Maven central today) you can do what is represented in the following unit test based on your question.

The function finding the key columns:

fun findKeyColumns(cfg: Config, tableName: String): Map<String, ConfigObject> {
    return cfg.nested(tableName).value("columns").asObjectList()
            .map { it.keys.single() to it.value(it.keys.single()).asObject() }
            .filter {
                it.second.value("key").asBoolean(false)
            }
            .toMap()
}

Here is full unit test for this:

// from https://mcmap.net/q/1946645/-reading-and-processing-hocon-in-kotlin
@Test fun testFromSo37092808() {
    // === mocked configuration file

    val cfg = loadConfig(StringAsConfig("""
            products: {
              columns: [
                { item: { type: integer, key: true, null: false } }
                { desc: { type: varchar, length: 64 } }
                { quantity: { type: integer, null: false } }
                { price: { type: decimal, precision: 14, scale: 3 } }
              ]
            }
          """))

    // === function to find which columns are key columns

    fun findKeyColumns(cfg: Config, tableName: String): Map<String, ConfigObject> {
        return cfg.nested(tableName).value("columns").asObjectList()
                .map { it.keys.single() to it.value(it.keys.single()).asObject() }
                .filter {
                    it.second.value("key").asBoolean(false)
                }
                .toMap()
    }

    // === sample usage

    val productKeys = findKeyColumns(cfg, "products")

    // we only have 1 in the test data, so grab the name and the values
    val onlyColumnName = productKeys.entries.first().key
    val onlyColumnObj = productKeys.entries.first().value

    assertEquals ("item", onlyColumnName)
    assertEquals (true, onlyColumnObj.value("key").asBoolean())
    assertEquals ("integer", onlyColumnObj.value("type").asString())
    assertEquals (false, onlyColumnObj.value("null").asBoolean())
}

You could return a Map as above, or a list of Pair for the column name to settings mapping since the column name is not inside of its settings.

The design of the configuration file could also be changed to make the processing of the configuration more simple (i.e. the name of the table inside its configuration object, rather than as a left-side key. Same for column names, add into the object and not as a left-side key.)

Readjustment answered 9/5, 2016 at 18:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.