Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cannot cast a spec to data class (in general issue in casting) #46

Closed
Animeshz opened this issue Jan 8, 2020 · 4 comments
Closed

Cannot cast a spec to data class (in general issue in casting) #46

Animeshz opened this issue Jan 8, 2020 · 4 comments

Comments

@Animeshz
Copy link

Animeshz commented Jan 8, 2020

1. To reproduce the error run the following code.:
Main.kt

val config = Config { addSpec(ConfigurationSpec) }.enable(Feature.OPTIONAL_SOURCE_BY_DEFAULT)
	.from.json.file("config.json")
	.from.properties.file("config.prop")
	.from.properties.file("config.properties")
	.from.hocon.file("config.conf")
	.validateRequired()
	.toValue<Configuration>()

ConfigurationSpec.kt:

package io.github.xyz.xyz

import com.uchuhimo.konf.*
import io.github.animeshz.air_bent.*
import java.io.*
import java.nio.file.*

object ConfigurationSpec : ConfigSpec("configuration") {
	object Server : ConfigSpec("server") {
		val port by optional(80)
		val dbFile by optional("${Paths.get("").toAbsolutePath().toString().trimEnd(File.separatorChar) + File.separator}data.db", "database file")
	}

	object Hardware : ConfigSpec("hardware") {
		val A0 by required<Configuration.SerialAddressConfiguration>()
		val A1 by required<Configuration.SerialAddressConfiguration>()

		val rShunt by required<Double>("resistor shunt")
		val maxExpectedCurrent by optional(10.0, "max expected current")
	}
}

Confguration.kt:

package io.github.xyz.xyz

data class Configuration (val server: Server, val hardware: Hardware){
	enum class SerialAddressConfiguration(val value: Int) {
		GND(0),
		VS(1),
		SDA(2),
		SCL(3)
	}

	data class Server(val port: Int, val dbFile: String)
	data class Hardware(val A0: SerialAddressConfiguration, val A1: SerialAddressConfiguration, val rShunt: Double, val maxExpectedCurrent: Double)
}

Sample hocon file:

configuration = {
  server = {
    port = 80
    database file = "data.db"
  }

  hardware = {
    resistor shunt = 0.005
    max expected current = 10.0

    A0 = "GND"
    A1 = "GND"
  }
}

2. Error occurred:

Exception in thread "main" java.lang.ExceptionInInitializerError
Caused by: com.uchuhimo.konf.NameConflictException: item root cannot be added
	at com.uchuhimo.konf.BaseConfig.addItem(BaseConfig.kt:530)
	at com.uchuhimo.konf.RequiredConfigProperty.provideDelegate(Config.kt:399)
	at io.github.xyz.xyz.MainKt.<clinit>(Main.kt:53)

3. Expected behavior:
Should cast the spec to the Configuration data class. I did not cast to data class directly because configuration hocon file has spaces in between the keys which cannot be casted to standard kotlin as variables with spaces donot exist :(

@uchuhimo
Copy link
Owner

uchuhimo commented Jan 8, 2020

@Animeshz You cannot cast a spec to a data class and you don't have to.

Spec is used as a schema for your HOCON source. We cannot use two schema to explain the same source. When you use config.toValue<Configuration>(), konf will implictly add a new spec, which will conflict with ConfigurationSpec. Thus cast a config with an existed spec to a data class will fail.

Fortunately, there is a much simpler solution for your case. Konf use Jackson to support data class, thus you can use Jackson annotation to cast your key names. Add @JsonProperty annotation to Configuration:

import com.fasterxml.jackson.annotation.JsonProperty

data class Configuration (val server: Server, val hardware: Hardware){
    enum class SerialAddressConfiguration(val value: Int) {
        GND(0),
        VS(1),
        SDA(2),
        SCL(3)
    }

    data class Server(val port: Int, @JsonProperty("database file") val dbFile: String)
    data class Hardware(
        val A0: SerialAddressConfiguration, 
        val A1: SerialAddressConfiguration, 
        @JsonProperty("resistor shunt") val rShunt: Double, 
        @JsonProperty("max expected current") val maxExpectedCurrent: Double)
}

And then you can cast the config to the data class directly:

val configuration = config.at("configuration").toValue<Configuration>()

@uchuhimo uchuhimo closed this as completed Jan 8, 2020
@P1NG2WIN
Copy link

P1NG2WIN commented Jun 19, 2020

Omg, why this moment is not in readme ... I spent several hours trying to understand why my config cause error. Please mark that you must write 'at' and why

@P1NG2WIN
Copy link

@uchuhimo also what i should write in 'at' if my config is this and i want to cast it to ApiConfig data class
image
image

@P1NG2WIN
Copy link

P1NG2WIN commented Jun 20, 2020

@uchuhimo For some reason i also get error item root cannot be added

    val apiConfig =
        Config {
            addSpec(ApiConfigSpec)
        }
            .from.json.watchFile("/home/config.json")
            .validateRequired()
            .at("")
            .toValue<ApiConfig>()

object ApiConfigSpec : ConfigSpec("") {
    val updatePass by required<String>("update_pass")
    val androidConfig by required<AndroidConfig>("android_config")
    val remoteUpdateConf by required<RemoteUpdateConf>("remote_update_config")

//    object AndroidConfigSpec : ConfigSpec("android_config") {
//        val suspendApp by required<Boolean>("suspend_app")
//    }
//
//    object RemoteUpdateConfSpec : ConfigSpec("remote_update_config") {
//        val remoteUpdateEnabled by required<Boolean>("remote_update_enabled")
//        val newAppVersion by required<String>("new_app_version")
//        val newAppStoreUrl by required<String>("new_app_store_url")
//        val requestClearData by required<Boolean>("request_clear_data")
//        val testMode by required<Boolean>("test_mode")
//        val testTokes by required<List<String>>("test_tokens")
//    }
}

data class ApiConfig(
    @SerializedName("update_pass")
    @JsonProperty("update_pass")
    val updatePass: String = "",
    @SerializedName("android_config")
    @JsonProperty("android_config")
    val androidConfig: AndroidConfig,
    @SerializedName("remote_update_config")
    @JsonProperty("remote_update_config")
    val remoteUpdateConf: RemoteUpdateConf
)

data class AndroidConfig(
    @SerializedName("suspend_app")
    @JsonProperty("suspend_app")
    val suspendApp: Boolean = false
)

data class RemoteUpdateConf(
    @SerializedName("remote_update_enabled")
    @JsonProperty("remote_update_enabled")
    val remoteUpdateEnabled: Boolean = true,
    @SerializedName("new_app_version")
    @JsonProperty("new_app_version")
    val newAppVersion: String = "",
    @SerializedName("new_app_store_url")
    @JsonProperty("new_app_store_url")
    val newAppStoreUrl: String = "",
    @SerializedName("request_clear_data")
    @JsonProperty("request_clear_data")
    val requestClearData: Boolean = false,
    @SerializedName("test_mode")
    @JsonProperty("test_mode")
    val testMode: Boolean = false,
    @SerializedName("test_tokens")
    @JsonProperty("test_tokens")
    val testTokes: List<String> = emptyList()
)

config.json:

{
    
    "update_pass": "",

    "android_config": {
        "suspend_app": false
    },

    "remote_update_config": {
        "remote_update_enabled":true,

        "new_app_version":"1.2553",
        "new_app_store_url":"",
        "request_clear_data":true,

        "test_mode": false,
        "test_tokens": [
            "asd",
            "asd",
            "asd"
        ]
    }

}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants