From 241609dbca50183d147b3c133996ea20f1122f44 Mon Sep 17 00:00:00 2001 From: a-khakimov Date: Sun, 12 Feb 2023 14:06:11 +0500 Subject: [PATCH 1/2] Add support of List[_] type --- .gitignore | 4 ++ README.md | 11 +++++- src/main/scala/Hocon.scala | 68 ++++++++++++++++++++++++++++++++-- src/test/scala/HoconSpec.scala | 44 ++++++++++++++++++++++ 4 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 .gitignore 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..0389b70 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.list("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..34dbac1 100644 --- a/src/main/scala/Hocon.scala +++ b/src/main/scala/Hocon.scala @@ -16,8 +16,10 @@ package lt.dvim.ciris +import scala.concurrent.duration.FiniteDuration import scala.util.Try +import cats.Show import ciris._ import com.typesafe.config.{Config, ConfigException, ConfigFactory, ConfigValue => HoconConfigValue} @@ -26,13 +28,21 @@ object Hocon extends HoconConfigDecoders { final class HoconAt(config: Config, path: String) { def apply(name: String): ConfigValue[Effect, HoconConfigValue] = Try(config.getValue(fullPath(name))).fold( - { - case _: ConfigException.Missing => ConfigValue.missing(key(name)) - case ex => ConfigValue.failed(ConfigError(ex.getMessage)) - }, + errHandler(name), ConfigValue.loaded(key(name), _) ) + def list(name: String): ConfigValue[Effect, HoconConfigValue] = + Try(config.getList(fullPath(name))).fold( + errHandler(name), + ConfigValue.loaded(key(name), _) + ) + + private def errHandler(name: String): Throwable => ConfigValue[Effect, HoconConfigValue] = { + case _: ConfigException.Missing => ConfigValue.missing(key(name)) + case ex => ConfigValue.failed(ConfigError(ex.getMessage)) + } + private def key(name: String) = ConfigKey(fullPath(name)) private def fullPath(name: String) = s"$path.$name" } @@ -48,6 +58,49 @@ trait HoconConfigDecoders { implicit val stringHoconDecoder: ConfigDecoder[HoconConfigValue, String] = ConfigDecoder[HoconConfigValue].map(_.atKey("t").getString("t")) + private implicit val show: Show[HoconConfigValue] = new Show[HoconConfigValue]() { + def show(t: HoconConfigValue): String = t.toString + } + + implicit val listStringHoconDecoder: ConfigDecoder[HoconConfigValue, List[String]] = + ConfigDecoder[HoconConfigValue].mapOption("List[String]") { c => + Try(asScalaList(c.atKey("t").getStringList("t"))).toOption + } + + implicit val listIntHoconDecoder: ConfigDecoder[HoconConfigValue, List[Int]] = + ConfigDecoder[HoconConfigValue].mapOption("List[Int]") { c => + Try(asScalaList(c.atKey("t").getIntList("t")).map(_.intValue())).toOption + } + + implicit val listLongHoconDecoder: ConfigDecoder[HoconConfigValue, List[Long]] = + ConfigDecoder[HoconConfigValue].mapOption("List[Long]") { c => + Try(asScalaList(c.atKey("t").getLongList("t")).map(_.longValue())).toOption + } + + implicit val listBooleanHoconDecoder: ConfigDecoder[HoconConfigValue, List[Boolean]] = + ConfigDecoder[HoconConfigValue].mapOption("List[Boolean]") { c => + Try(asScalaList(c.atKey("t").getBooleanList("t")).map(_.booleanValue())).toOption + } + + implicit val listDoubleHoconDecoder: ConfigDecoder[HoconConfigValue, List[Double]] = + ConfigDecoder[HoconConfigValue].mapOption("List[Double]") { c => + Try(asScalaList(c.atKey("t").getDoubleList("t")).map(_.doubleValue())).toOption + } + + implicit val listJavaDurationHoconDecoder: ConfigDecoder[HoconConfigValue, List[java.time.Duration]] = + ConfigDecoder[HoconConfigValue].mapOption("List[java.time.Duration]") { c => + Try(asScalaList(c.atKey("t").getDurationList("t"))).toOption + } + + implicit val listDurationHoconDecoder: ConfigDecoder[HoconConfigValue, List[FiniteDuration]] = + ConfigDecoder[HoconConfigValue].mapOption("List[FiniteDuration]") { c => + Try { + asScalaList(c.atKey("t").getDurationList("t")) + .map(_.toNanos) + .map(scala.concurrent.duration.Duration.fromNanos) + }.toOption + } + implicit val javaTimeDurationHoconDecoder: ConfigDecoder[HoconConfigValue, java.time.Duration] = ConfigDecoder[HoconConfigValue].map(_.atKey("t").getDuration("t")) @@ -56,4 +109,11 @@ trait HoconConfigDecoders { implicit def throughStringHoconDecoder[T](implicit d: ConfigDecoder[String, T]): ConfigDecoder[HoconConfigValue, T] = stringHoconDecoder.as[T] + + private def asScalaList[T](collection: java.util.Collection[T]): List[T] = { + val builder = List.newBuilder[T] + val it = collection.iterator() + while (it.hasNext) builder += it.next() + builder.result() + } } diff --git a/src/test/scala/HoconSpec.scala b/src/test/scala/HoconSpec.scala index 52ae1ef..a1c6db5 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,44 @@ 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.list("listInt").as[List[Int]].load[IO] assertEquals List(1, 2, 3, 4) + } + test("parse List[Long]") { + nested.list("listInt").as[List[Long]].load[IO] assertEquals List(1L, 2, 3, 4) + } + test("parse List[String]") { + nested.list("listString").as[List[String]].load[IO] assertEquals List("a", "b", "c", "d") + } + test("parse List[Bool]") { + nested.list("listBool").as[List[Boolean]].load[IO] assertEquals List(true, false, true) + } + test("parse List[Double]") { + nested.list("listDouble").as[List[Double]].load[IO] assertEquals List(1.12, 2.34, 2.33) + } + test("parse List[java Duration]") { + nested.list("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.list("listDur").as[List[FiniteDuration]].load[IO] assertEquals List(10.millis, 15.millis, 1.second) + } + test("handle decode error for invalid list") { + nested + .list("invalidList") + .as[List[Int]] + .attempt[IO] + .map { + case Left(error) => error.messages.toList.head + case Right(_) => "config loaded" + } + .assertEquals( + "Nested.config.invalidList with value SimpleConfigList([1,\"a\",true]) cannot be converted to List[Int]" + ) + } test("handle missing") { nested("missing") .as[Int] From 547f9f741fc01f42453e897c8542b7d91821d041 Mon Sep 17 00:00:00 2001 From: a-khakimov Date: Tue, 14 Feb 2023 15:45:40 +0500 Subject: [PATCH 2/2] Fixes after review --- README.md | 2 +- src/main/scala/Hocon.scala | 79 +++++++--------------------------- src/test/scala/HoconSpec.scala | 19 ++++---- 3 files changed, 26 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index 0389b70..3318c11 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ val hocon = hoconAt(config)("rate") hocon("elements").as[Int], hocon("burst-duration").as[FiniteDuration], hocon("check-interval").as[Period], - hocon.list("values").as[List[String]] + 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 34dbac1..1413d15 100644 --- a/src/main/scala/Hocon.scala +++ b/src/main/scala/Hocon.scala @@ -16,10 +16,9 @@ package lt.dvim.ciris -import scala.concurrent.duration.FiniteDuration +import scala.jdk.CollectionConverters._ import scala.util.Try -import cats.Show import ciris._ import com.typesafe.config.{Config, ConfigException, ConfigFactory, ConfigValue => HoconConfigValue} @@ -28,21 +27,13 @@ object Hocon extends HoconConfigDecoders { final class HoconAt(config: Config, path: String) { def apply(name: String): ConfigValue[Effect, HoconConfigValue] = Try(config.getValue(fullPath(name))).fold( - errHandler(name), + { + case _: ConfigException.Missing => ConfigValue.missing(key(name)) + case ex => ConfigValue.failed(ConfigError(ex.getMessage)) + }, ConfigValue.loaded(key(name), _) ) - def list(name: String): ConfigValue[Effect, HoconConfigValue] = - Try(config.getList(fullPath(name))).fold( - errHandler(name), - ConfigValue.loaded(key(name), _) - ) - - private def errHandler(name: String): Throwable => ConfigValue[Effect, HoconConfigValue] = { - case _: ConfigException.Missing => ConfigValue.missing(key(name)) - case ex => ConfigValue.failed(ConfigError(ex.getMessage)) - } - private def key(name: String) = ConfigKey(fullPath(name)) private def fullPath(name: String) = s"$path.$name" } @@ -58,48 +49,17 @@ trait HoconConfigDecoders { implicit val stringHoconDecoder: ConfigDecoder[HoconConfigValue, String] = ConfigDecoder[HoconConfigValue].map(_.atKey("t").getString("t")) - private implicit val show: Show[HoconConfigValue] = new Show[HoconConfigValue]() { - def show(t: HoconConfigValue): String = t.toString - } - - implicit val listStringHoconDecoder: ConfigDecoder[HoconConfigValue, List[String]] = - ConfigDecoder[HoconConfigValue].mapOption("List[String]") { c => - Try(asScalaList(c.atKey("t").getStringList("t"))).toOption - } - - implicit val listIntHoconDecoder: ConfigDecoder[HoconConfigValue, List[Int]] = - ConfigDecoder[HoconConfigValue].mapOption("List[Int]") { c => - Try(asScalaList(c.atKey("t").getIntList("t")).map(_.intValue())).toOption - } - - implicit val listLongHoconDecoder: ConfigDecoder[HoconConfigValue, List[Long]] = - ConfigDecoder[HoconConfigValue].mapOption("List[Long]") { c => - Try(asScalaList(c.atKey("t").getLongList("t")).map(_.longValue())).toOption - } - - implicit val listBooleanHoconDecoder: ConfigDecoder[HoconConfigValue, List[Boolean]] = - ConfigDecoder[HoconConfigValue].mapOption("List[Boolean]") { c => - Try(asScalaList(c.atKey("t").getBooleanList("t")).map(_.booleanValue())).toOption - } - - implicit val listDoubleHoconDecoder: ConfigDecoder[HoconConfigValue, List[Double]] = - ConfigDecoder[HoconConfigValue].mapOption("List[Double]") { c => - Try(asScalaList(c.atKey("t").getDoubleList("t")).map(_.doubleValue())).toOption - } - - implicit val listJavaDurationHoconDecoder: ConfigDecoder[HoconConfigValue, List[java.time.Duration]] = - ConfigDecoder[HoconConfigValue].mapOption("List[java.time.Duration]") { c => - Try(asScalaList(c.atKey("t").getDurationList("t"))).toOption - } - - implicit val listDurationHoconDecoder: ConfigDecoder[HoconConfigValue, List[FiniteDuration]] = - ConfigDecoder[HoconConfigValue].mapOption("List[FiniteDuration]") { c => - Try { - asScalaList(c.atKey("t").getDurationList("t")) - .map(_.toNanos) - .map(scala.concurrent.duration.Duration.fromNanos) - }.toOption - } + 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")) @@ -109,11 +69,4 @@ trait HoconConfigDecoders { implicit def throughStringHoconDecoder[T](implicit d: ConfigDecoder[String, T]): ConfigDecoder[HoconConfigValue, T] = stringHoconDecoder.as[T] - - private def asScalaList[T](collection: java.util.Collection[T]): List[T] = { - val builder = List.newBuilder[T] - val it = collection.iterator() - while (it.hasNext) builder += it.next() - builder.result() - } } diff --git a/src/test/scala/HoconSpec.scala b/src/test/scala/HoconSpec.scala index a1c6db5..dd70b4c 100644 --- a/src/test/scala/HoconSpec.scala +++ b/src/test/scala/HoconSpec.scala @@ -66,33 +66,32 @@ class HoconSpec extends CatsEffectSuite { nested("per").as[java.time.Period].load[IO] assertEquals java.time.Period.ofWeeks(2) } test("parse List[Int]") { - nested.list("listInt").as[List[Int]].load[IO] assertEquals List(1, 2, 3, 4) + nested("listInt").as[List[Int]].load[IO] assertEquals List(1, 2, 3, 4) } test("parse List[Long]") { - nested.list("listInt").as[List[Long]].load[IO] assertEquals List(1L, 2, 3, 4) + nested("listInt").as[List[Long]].load[IO] assertEquals List(1L, 2, 3, 4) } test("parse List[String]") { - nested.list("listString").as[List[String]].load[IO] assertEquals List("a", "b", "c", "d") + nested("listString").as[List[String]].load[IO] assertEquals List("a", "b", "c", "d") } test("parse List[Bool]") { - nested.list("listBool").as[List[Boolean]].load[IO] assertEquals List(true, false, true) + nested("listBool").as[List[Boolean]].load[IO] assertEquals List(true, false, true) } test("parse List[Double]") { - nested.list("listDouble").as[List[Double]].load[IO] assertEquals List(1.12, 2.34, 2.33) + nested("listDouble").as[List[Double]].load[IO] assertEquals List(1.12, 2.34, 2.33) } test("parse List[java Duration]") { - nested.list("listDur").as[List[java.time.Duration]].load[IO] assertEquals List( + 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.list("listDur").as[List[FiniteDuration]].load[IO] assertEquals List(10.millis, 15.millis, 1.second) + nested("listDur").as[List[FiniteDuration]].load[IO] assertEquals List(10.millis, 15.millis, 1.second) } test("handle decode error for invalid list") { - nested - .list("invalidList") + nested("invalidList") .as[List[Int]] .attempt[IO] .map { @@ -100,7 +99,7 @@ class HoconSpec extends CatsEffectSuite { case Right(_) => "config loaded" } .assertEquals( - "Nested.config.invalidList with value SimpleConfigList([1,\"a\",true]) cannot be converted to List[Int]" + "Nested.config.invalidList with value a cannot be converted to Int" ) } test("handle missing") {