circe / circe-config

Yet another Typesafe config Scala wrapper powered by circe
https://circe.github.io/circe-config/io/circe/config/index.html
Apache License 2.0
122 stars 21 forks source link

circe-config will fail on Boolean values written as "on/true/false/off" #12

Open seigert opened 6 years ago

seigert commented 6 years ago

It may be intended, but right now circe-config is inconsistent with original config for Boolean values written as strings:

scala> import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigFactory

scala> import io.circe.config.parser
import io.circe.config.parser

scala> val config = ConfigFactory.parseString("server { host = localhost, port = 8080 }")
config: com.typesafe.config.Config = Config(SimpleConfigObject({"server":{"host":"localhost","port":8080}}))

scala> val config = ConfigFactory.parseString("server { host = localhost, port = 8080, enabled = on }")
config: com.typesafe.config.Config = Config(SimpleConfigObject({"server":{"enabled":"on","host":"localhost","port":8080}}))

scala> val json: Either[io.circe.ParsingFailure, io.circe.Json] = parser.parse(config)

json: Either[io.circe.ParsingFailure,io.circe.Json] =
Right({
  "server" : {
    "enabled" : "on",
    "port" : 8080,
    "host" : "localhost"
  }
})

scala> config.getBoolean("server.enabled")
res0: Boolean = true

It is possible to work around it, but it is not very convenient when passing config values via ENV_VARIABLES in K8s -- it disallows unquoted strings. :(

jonas commented 6 years ago

The problem is that com.typesafe.config.Config is converted to io.circe.Json without knowing what will be the final type of the "on" string. As far as I know it is not possible to introspect the structure of the type when it is being decoded.

The only solution I can think of is to create a wrapper "boolean' type which allows to use an implicit convertion to translate from string to boolean as Typesafe config does.

import scala.language.implicitConversions
import org.scalatest.{ FlatSpec, Matchers }
import com.typesafe.config.ConfigFactory
import io.circe._
import io.circe.generic.auto._
import io.circe.config.syntax._

object ConfigBooleanSpec {
  case class ConfigBoolean(value: Boolean)
  object ConfigBoolean {
    implicit def toBoolean(b: ConfigBoolean): Boolean = b.value
  }

  implicit val configBooleanDecoder: Decoder[ConfigBoolean] = {
    val truthful = Set("true", "yes", "on")
    Decoder.decodeString.map(s => ConfigBoolean(truthful(s)))
  }

  case class ServerConfig(host: String, port: Int, enabled: ConfigBoolean)
}

class ConfigBooleanSpec extends FlatSpec with Matchers {
  import ConfigBooleanSpec._

  "custom boolean" should "parse and decode from string" in {
    val config = ConfigFactory.parseString(
      """
        host = localhost
        port = 8080
        enabled = on
      """)
    val Right(ServerConfig(_, _, enabled)) = config.as[ServerConfig]
    assert(enabled.value)
    assert(enabled)
  }

}
jonas commented 6 years ago

It would be good to document this though so if you are up for making a PR that would be great else let's keep this issue open until that gets fixed.

seigert commented 6 years ago

Sorry for the late answer, ConfigBoolean is a possible solution, but it will not work with external/legacy entities with Boolean attributes or providing their own Decoder instance. Or if you just dont want to leak circe-config details in your domain model.

I think better solution would be to delay conversion to Json as much as possible or even do not convert at all, but it would require some kind of JsonDelegate instance that does nothing but delegates fold(..) and asNull|asBoolean|.. methods to it's underlying value. And I don't think it's possible with current circe as Json is sealed.

Maybe something could be done via ConfigCursor version of HCursor for Decoder to use?

jonas commented 6 years ago

I agree that would be optimal but not sure it is possible to do this sort of lazy conversion.