diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5320795 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.idea/ +/project +/target +/.bsp/ diff --git a/README.md b/README.md index c8896d9..3318c11 100644 --- a/README.md +++ b/README.md @@ -47,16 +47,23 @@ val config = ConfigFactory.parseString(""" | elements = 2 | burst-duration = 100 millis | check-interval = 2 weeks + | values = [ first, second ] |} """.stripMargin) -case class Rate(elements: Int, burstDuration: FiniteDuration, checkInterval: Period) +case class Rate( + elements: Int, + burstDuration: FiniteDuration, + checkInterval: Period, + values: List[String] +) val hocon = hoconAt(config)("rate") ( hocon("elements").as[Int], hocon("burst-duration").as[FiniteDuration], - hocon("check-interval").as[Period] + hocon("check-interval").as[Period], + hocon("values").as[List[String]] ).parMapN(Rate.apply).load[IO].map { rate => assertEquals(rate.burstDuration, 100.millis) assertEquals(rate.checkInterval, Period.ofWeeks(2)) diff --git a/src/main/scala/Hocon.scala b/src/main/scala/Hocon.scala index 4240c01..1413d15 100644 --- a/src/main/scala/Hocon.scala +++ b/src/main/scala/Hocon.scala @@ -16,6 +16,7 @@ package lt.dvim.ciris +import scala.jdk.CollectionConverters._ import scala.util.Try import ciris._ @@ -48,6 +49,18 @@ trait HoconConfigDecoders { implicit val stringHoconDecoder: ConfigDecoder[HoconConfigValue, String] = ConfigDecoder[HoconConfigValue].map(_.atKey("t").getString("t")) + implicit def listHoconDecoder[T](implicit + decoder: ConfigDecoder[HoconConfigValue, T] + ): ConfigDecoder[HoconConfigValue, List[T]] = + ConfigDecoder[HoconConfigValue] + .map(_.atKey("t").getList("t").asScala.toList) + .mapEither { (key, list) => + list.map(decoder.decode(key, _)).partitionMap(identity) match { + case (Nil, rights) => Right(rights) + case (firstLeft :: _, _) => Left(firstLeft) + } + } + implicit val javaTimeDurationHoconDecoder: ConfigDecoder[HoconConfigValue, java.time.Duration] = ConfigDecoder[HoconConfigValue].map(_.atKey("t").getDuration("t")) diff --git a/src/test/scala/HoconSpec.scala b/src/test/scala/HoconSpec.scala index 52ae1ef..dd70b4c 100644 --- a/src/test/scala/HoconSpec.scala +++ b/src/test/scala/HoconSpec.scala @@ -31,6 +31,12 @@ class HoconSpec extends CatsEffectSuite { | dur = 10 ms | bool = true | per = 2 weeks + | listInt = [ 1, 2, 3, 4 ] + | listString = [ a, b, c, d ] + | listBool = [ true, false, true ] + | listDouble = [ 1.12, 2.34, 2.33 ] + | listDur = [ 10 ms, 15 ms, 1 s ] + | invalidList = [ 1, a, true ] | } |} |subst { @@ -59,6 +65,43 @@ class HoconSpec extends CatsEffectSuite { test("parse Period") { nested("per").as[java.time.Period].load[IO] assertEquals java.time.Period.ofWeeks(2) } + test("parse List[Int]") { + nested("listInt").as[List[Int]].load[IO] assertEquals List(1, 2, 3, 4) + } + test("parse List[Long]") { + nested("listInt").as[List[Long]].load[IO] assertEquals List(1L, 2, 3, 4) + } + test("parse List[String]") { + nested("listString").as[List[String]].load[IO] assertEquals List("a", "b", "c", "d") + } + test("parse List[Bool]") { + nested("listBool").as[List[Boolean]].load[IO] assertEquals List(true, false, true) + } + test("parse List[Double]") { + nested("listDouble").as[List[Double]].load[IO] assertEquals List(1.12, 2.34, 2.33) + } + test("parse List[java Duration]") { + nested("listDur").as[List[java.time.Duration]].load[IO] assertEquals List( + java.time.Duration.ofMillis(10), + java.time.Duration.ofMillis(15), + java.time.Duration.ofSeconds(1) + ) + } + test("parse List[scala Duration]") { + nested("listDur").as[List[FiniteDuration]].load[IO] assertEquals List(10.millis, 15.millis, 1.second) + } + test("handle decode error for invalid list") { + nested("invalidList") + .as[List[Int]] + .attempt[IO] + .map { + case Left(error) => error.messages.toList.head + case Right(_) => "config loaded" + } + .assertEquals( + "Nested.config.invalidList with value a cannot be converted to Int" + ) + } test("handle missing") { nested("missing") .as[Int]