简体   繁体   中英

Representing Either in pureconfig

I have a HOCON config like this:

[
    {
        name = 1
        url = "http://example.com"
    },
    {
        name = 2
        url = "http://example2.com"
    },
    {
        name = 3
        url = {
            A = "http://example3.com"
            B = "http://example4.com"
        }
    }
]

I want to parse it with pureconfig. How can I represent that the URL can be either a string or a map of multiple urls, each having a key?

I have tried this:

import pureconfig.ConfigSource
import pureconfig.generic.auto.exportReader

case class Site(name: Int, url: Either[String, Map[String, String]])
case class Config(sites: List[Site])
ConfigSource.default.loadOrThrow[Config]

But it resulted in "Expected type OBJECT. Found STRING instead."

I know pureconfig supports Option . I have found no mention of supporting Either , does it mean it can be replaced with something else?

As you can see Either in not on list oftypes supported out of the box .

However Either falls under sealed family , so:

@ ConfigSource.string("""{ type: left, value: "test" }""").load[Either[String, String]]
res15: ConfigReader.Result[Either[String, String]] = Right(Left("test"))

@ ConfigSource.string("""{ type: right, value: "test" }""").load[Either[String, String]]
res16: ConfigReader.Result[Either[String, String]] = Right(Right("test"))

works. If you have a sealed hierarchy, what pureconfig will do is require an object which has a field type - this field will be used to dispatch parsing to a specific subtype. All the other fields will be passed as fields to parse into that subtype.

If that doesn't work for you, you might try to implement the codec yourself:

// just an example
implicit def eitherReader[A: ConfigReader, B: ConfigReader] =
  new ConfigReader[Either[A, B]] {
    def from(cur: ConfigCursor) =
      // try left, if fail try right
      ConfigReader[A].from(cur).map(Left(_)) orElse ConfigReader[B].from(cur).map(Right(_))
  }

which now will not require discrimination value:

@ ConfigSource.string("""{ test: "test" }""").load[Map[String, Either[String, String]]]
res26: ConfigReader.Result[Map[String, Either[String, String]]] = Right(Map("test" -> Left("test")))

This is not provided by default because you would have to answer a few things yourself:

  • how do you decide if you should go with Left or Right decoding?
  • does Left fallback Right or Right fallback Left make sense?
  • how about Either[X, X] ?

If you have an idea what is expected behavior you can implement your own codec and use it in derivation.

There are might be several ways of doing it, but I don't like using Either as a config representation. Thus, I would suggest to use ADT approach with sealed trait:

  sealed trait NameUrl {
    val name: Int
  }

  case class Name(
    name: Int,
    url: String
  ) extends NameUrl

  case class NameUrlObj(
    name: Int,
    url: Map[String, String]
  ) extends NameUrl

Sorry for my naming here. This would be a representation of of your config. We need to modify a bit our config to parse easily the config with you ADT. In order to support generic types you should add your spefici type name for each subtype. I'm going to put here full example so that you can run it on your machine:

import com.typesafe.config.ConfigFactory
import pureconfig.generic.auto._
import pureconfig.ConfigSource

object TstObj extends App {

  sealed trait NameUrl {
    val name: Int
  }

  case class Name(
    name: Int,
    url: String
  ) extends NameUrl

  case class NameUrlObj(
    name: Int,
    url: Map[String, String]
  ) extends NameUrl

  val cfgStr = ConfigFactory.parseString(
    """
      |abc: [
      |  {
      |    type: name,
      |    name = 1
      |    url = "http://example.com"
      |  },
      |  {
      |    type: name,
      |    name = 1
      |    url = "http://example.com"
      |  },
      |  {
      |    type: name-url-obj,
      |    name = 3
      |    url = {
      |      "A": "http://example3.com"
      |      "B": "http://example4.com"
      |    }
      |  }
      |]
      |""".stripMargin
  )


  case class RootA(abc: List[NameUrl])
  println(ConfigSource.fromConfig(cfgStr).loadOrThrow[RootA])

}

You can read more here about Sealed Families here

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM