diff --git a/src/main/scala/net/ceedubs/ficus/readers/EitherReader.scala b/src/main/scala/net/ceedubs/ficus/readers/EitherReader.scala new file mode 100644 index 0000000..82b3e27 --- /dev/null +++ b/src/main/scala/net/ceedubs/ficus/readers/EitherReader.scala @@ -0,0 +1,19 @@ +package net.ceedubs.ficus.readers +import com.typesafe.config.{Config, ConfigException} + +trait EitherReader { + implicit def eitherReader[L,R]( implicit lReader : ValueReader[L], rReader : ValueReader[R]) : ValueReader[Either[L,R]] = + new ValueReader[Either[L,R]]{ + /** Reads the value at the path `path` in the Config */ + override def read(config: Config, path: String): Either[L, R] = { + TryReader.tryValueReader(rReader).read( config, path ) + .map( Right(_) ) + .recover{ + case _ : ConfigException => Left( lReader.read(config, path)) + } + .get + } + } +} + +object EitherReader extends EitherReader \ No newline at end of file diff --git a/src/test/scala/net/ceedubs/ficus/ConfigSerializer.scala b/src/test/scala/net/ceedubs/ficus/ConfigSerializer.scala index 8b41b5b..d8a3d38 100644 --- a/src/test/scala/net/ceedubs/ficus/ConfigSerializer.scala +++ b/src/test/scala/net/ceedubs/ficus/ConfigSerializer.scala @@ -1,6 +1,7 @@ package net.ceedubs.ficus -import com.typesafe.config.ConfigUtil +import com.typesafe.config.{ConfigFactory, ConfigUtil, ConfigValue} + import scala.language.implicitConversions trait ConfigSerializer[A] { @@ -45,6 +46,7 @@ object ConfigSerializer { final case class ConfigSerializerOps[A](a: A, serializer: ConfigSerializer[A]) { def asConfigValue: String = serializer.serialize(a) + def toConfigValue : ConfigValue = ConfigFactory.parseString( s"dummy=$asConfigValue").root().get("dummy") } object ConfigSerializerOps { diff --git a/src/test/scala/net/ceedubs/ficus/readers/EitherReadersSpec.scala b/src/test/scala/net/ceedubs/ficus/readers/EitherReadersSpec.scala new file mode 100644 index 0000000..8f91ec9 --- /dev/null +++ b/src/test/scala/net/ceedubs/ficus/readers/EitherReadersSpec.scala @@ -0,0 +1,63 @@ +package net.ceedubs.ficus.readers + +import com.typesafe.config.ConfigFactory +import net.ceedubs.ficus.{ConfigSerializer, Spec} +import net.ceedubs.ficus.ConfigSerializerOps._ +import org.scalacheck.Arbitrary + +import scala.util.{Failure, Try} +import scala.collection.JavaConverters._ + +class EitherReadersSpec extends Spec with EitherReader with OptionReader with AnyValReaders with StringReader with TryReader with CollectionReaders{ + def is=s2""" + An Either value reader should + should read right side when possible $readRightSideString + fallback to left side when key is missing $fallbackToLeftSideOnMissingKey + fallback to left when failing to read right $fallbackToLeftSideOnBadRightValue + fail when both sides fail $rightAndLeftFailure + handle a Try on the right side $rightSideTry + handle a Try on the left side $leftSideTry + handle complex types $handleComplexTypes + """ + + + def readRightSideString = prop{ a : String => + val cfg = a.toConfigValue.atKey("x") + eitherReader[String,String].read(cfg, "x") must beEqualTo(Right(a)) + } + + def fallbackToLeftSideOnMissingKey = prop{ a : String => + eitherReader[Option[String], String].read( ConfigFactory.empty(), "x" ) must beEqualTo( Left(None) ) + } + + def fallbackToLeftSideOnBadRightValue = prop{ a : Int => + val badVal = a.toString + "xx" + eitherReader[String, Int].read( badVal.toConfigValue.atKey("x"), "x" ) must beEqualTo( Left(badVal) ) + } + + def rightAndLeftFailure = prop{ a : Int => + val badVal = a.toString + "xx" + tryValueReader(eitherReader[Int, Int]).read( badVal.toConfigValue.atKey("x"), "x" ) must beAnInstanceOf[Failure[Int]] + } + + def rightSideTry = prop{ a : Int => + val badVal = a.toString + "xx" + eitherReader[Int, Try[Int]].read( a.toConfigValue.atKey("x"), "x" ) must beRight( a ) + eitherReader[Int, Try[Int]].read( badVal.toConfigValue.atKey("x"), "x" ) must beRight( beFailedTry[Int] ) + } + + def leftSideTry = prop{ a : Int => + val badVal = a.toString + "xx" + eitherReader[Try[String], Int].read( badVal.toConfigValue.atKey("x"), "x" ) must beLeft( beSuccessfulTry( badVal) ) + eitherReader[Try[Int], Int].read( badVal.toConfigValue.atKey("x"), "x" ) must beLeft( beFailedTry[Int] ) + } + + def handleComplexTypes = prop{ (a : Int, b : Int ) => + val iMap = Map( "a" -> a, "b" -> b ) + val sMap = Map( "a" -> s"${a}xx", "b" -> s"${b}xx") + + eitherReader[Map[String,String], Map[String,String]].read( sMap.toConfigValue.atKey("a"), "a" ) must beRight(sMap) + eitherReader[Map[String,String], Map[String,Int]].read( iMap.toConfigValue.atKey("a"), "a" ) must beRight(iMap) + eitherReader[Map[String,String], Map[String,Int]].read( sMap.toConfigValue.atKey("a"), "a" ) must beLeft(sMap) + } +}