From 00c88ac368047bdf3e46b2f5bf3f84c85bd690cc Mon Sep 17 00:00:00 2001 From: Sebastian Haracz Date: Thu, 11 Apr 2024 12:22:30 +0200 Subject: [PATCH 1/6] Hocon config companions --- .../commons/hocon/ConfigCompanion.scala | 69 +++++++++++++++++++ .../avsystem/commons/hocon/SizeInBytes.scala | 13 ++++ .../commons/hocon/HoconInputTest.scala | 40 ++++++++++- 3 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 hocon/src/main/scala/com/avsystem/commons/hocon/ConfigCompanion.scala create mode 100644 hocon/src/main/scala/com/avsystem/commons/hocon/SizeInBytes.scala diff --git a/hocon/src/main/scala/com/avsystem/commons/hocon/ConfigCompanion.scala b/hocon/src/main/scala/com/avsystem/commons/hocon/ConfigCompanion.scala new file mode 100644 index 000000000..cbbd258d5 --- /dev/null +++ b/hocon/src/main/scala/com/avsystem/commons/hocon/ConfigCompanion.scala @@ -0,0 +1,69 @@ +package com.avsystem.commons +package hocon + +import com.avsystem.commons.meta.MacroInstances +import com.avsystem.commons.misc.ValueOf +import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec, GenObjectCodec, HasGenObjectCodecWithDeps} +import com.avsystem.commons.serialization.GenCodec.ReadFailure +import com.typesafe.config.{Config, ConfigFactory, ConfigObject} + +import scala.concurrent.duration.* + +trait CommonsHoconCodecs { + implicit final val configCodec: GenCodec[Config] = GenCodec.nullable( + input => + input.readCustom(ConfigValueMarker).map { + case obj: ConfigObject => obj.toConfig + case v => throw new ReadFailure(s"expected a config OBJECT, got ${v.valueType}") + }.getOrElse { + ConfigFactory.parseString(input.readSimple().readString()) + }, + (output, value) => + if (!output.writeCustom(ConfigValueMarker, value.root)) { + output.writeSimple().writeString(value.root.render) + }, + ) + + implicit final val finiteDurationCodec: GenCodec[FiniteDuration] = GenCodec.nullable( + input => input.readCustom(DurationMarker).fold(input.readSimple().readLong())(_.toMillis).millis, + (output, value) => output.writeSimple().writeLong(value.toMillis), + ) + + implicit final val sizeInBytesCodec: GenCodec[SizeInBytes] = GenCodec.nonNull( + input => SizeInBytes(input.readCustom(SizeInBytesMarker).getOrElse(input.readSimple().readLong())), + (output, value) => output.writeSimple().writeLong(value.bytes), + ) + + implicit final val classKeyCodec: GenKeyCodec[Class[?]] = + GenKeyCodec.create(Class.forName, _.getName) + + implicit final val classCodec: GenCodec[Class[?]] = + GenCodec.nullableString(Class.forName, _.getName) +} +object CommonsHoconCodecs extends CommonsHoconCodecs + +/** + * Base class for companion objects of configuration case classes and sealed traits + * (typically deserialized from HOCON files). + * + * [[ConfigCompanion]] is equivalent to [[com.avsystem.commons.serialization.HasGenCodec HasGenCodec]] + * except that it automatically imports codecs from [[CommonsHoconCodecs]] - codecs for third party types often used + * in configuration. + */ +abstract class ConfigCompanion[T](implicit + macroCodec: MacroInstances[CommonsHoconCodecs.type, () => GenObjectCodec[T]], +) extends HasGenObjectCodecWithDeps[CommonsHoconCodecs.type, T] { + final def read(config: Config): T = HoconInput.read[T](config) +} + +/** + * A version of [[ConfigCompanion]] which injects additional implicits into macro materialization. + * Implicits are imported from an object specified with type parameter `D`. + * It must be a singleton object type, i.e. `SomeObject.type`. + */ +abstract class ConfigCompanionWithDeps[T, D <: CommonsHoconCodecs](implicit + deps: ValueOf[D], + macroCodec: MacroInstances[D, () => GenObjectCodec[T]], +) extends HasGenObjectCodecWithDeps[D, T] { + final def read(config: Config): T = HoconInput.read[T](config) +} diff --git a/hocon/src/main/scala/com/avsystem/commons/hocon/SizeInBytes.scala b/hocon/src/main/scala/com/avsystem/commons/hocon/SizeInBytes.scala new file mode 100644 index 000000000..999635946 --- /dev/null +++ b/hocon/src/main/scala/com/avsystem/commons/hocon/SizeInBytes.scala @@ -0,0 +1,13 @@ +package com.avsystem.commons +package hocon + +/** + * Use this type in data deserialized from HOCON to in order to read size in bytes represented with + * [[https://github.com/lightbend/config/blob/master/HOCON.md#size-in-bytes-format HOCON's nice representation]]. + */ +final case class SizeInBytes(bytes: Long) +object SizeInBytes { + final val Zero = SizeInBytes(0) + final val `1KiB` = SizeInBytes(1024L) + final val `1MiB` = SizeInBytes(1024 * 1024L) +} diff --git a/hocon/src/test/scala/com/avsystem/commons/hocon/HoconInputTest.scala b/hocon/src/test/scala/com/avsystem/commons/hocon/HoconInputTest.scala index 6d176c7fe..18192d57d 100644 --- a/hocon/src/test/scala/com/avsystem/commons/hocon/HoconInputTest.scala +++ b/hocon/src/test/scala/com/avsystem/commons/hocon/HoconInputTest.scala @@ -1,13 +1,27 @@ package com.avsystem.commons package hocon -import java.time.{Duration, Period} - import com.avsystem.commons.serialization.json.JsonStringOutput import com.avsystem.commons.serialization.{GenCodecRoundtripTest, Input, Output} -import com.typesafe.config.{ConfigFactory, ConfigMemorySize, ConfigValue, ConfigValueFactory, ConfigValueType} +import com.typesafe.config.* + +import java.time.{Duration, Period} +import scala.concurrent.duration.* + +object HoconInputTest { + case class CustomCodecsClass( + duration: FiniteDuration, + fileSize: SizeInBytes, + embeddedConfig: Config, + clazz: Class[?], + ) + object CustomCodecsClass extends ConfigCompanion[CustomCodecsClass] +} class HoconInputTest extends GenCodecRoundtripTest { + + import HoconInputTest.* + type Raw = ConfigValue def writeToOutput(write: Output => Unit): ConfigValue = { @@ -56,4 +70,24 @@ class HoconInputTest extends GenCodecRoundtripTest { test("number reading") { assert(rawInput(42.0).readNumber().doubleValue == 42.0) } + + test("class reading") { + val config = ConfigFactory.parseString( + """{ + | duration = 1m + | fileSize = 1KiB + | embeddedConfig { + | something = "abc" + | } + | clazz = "com.avsystem.commons.hocon.HoconInputTest" + |}""".stripMargin + ) + val expected = CustomCodecsClass( + duration = 1.minute, + fileSize = SizeInBytes.`1KiB`, + embeddedConfig = ConfigFactory.parseMap(JMap("something" -> "abc")), + clazz = classOf[HoconInputTest], + ) + assert(CustomCodecsClass.read(config) == expected) + } } From 24c80dc54e7ccda535f97f1f076d9cb8714f5fd5 Mon Sep 17 00:00:00 2001 From: Sebastian Haracz Date: Mon, 23 Sep 2024 20:41:27 +0200 Subject: [PATCH 2/6] ConfigCompanion - move codecs to val in object, renames --- .../commons/hocon/ConfigCompanion.scala | 65 ++++++++++--------- .../commons/hocon/HoconInputTest.scala | 9 ++- 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/hocon/src/main/scala/com/avsystem/commons/hocon/ConfigCompanion.scala b/hocon/src/main/scala/com/avsystem/commons/hocon/ConfigCompanion.scala index cbbd258d5..e448e97b8 100644 --- a/hocon/src/main/scala/com/avsystem/commons/hocon/ConfigCompanion.scala +++ b/hocon/src/main/scala/com/avsystem/commons/hocon/ConfigCompanion.scala @@ -2,21 +2,25 @@ package com.avsystem.commons package hocon import com.avsystem.commons.meta.MacroInstances -import com.avsystem.commons.misc.ValueOf -import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec, GenObjectCodec, HasGenObjectCodecWithDeps} import com.avsystem.commons.serialization.GenCodec.ReadFailure +import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec, GenObjectCodec} import com.typesafe.config.{Config, ConfigFactory, ConfigObject} import scala.concurrent.duration.* -trait CommonsHoconCodecs { - implicit final val configCodec: GenCodec[Config] = GenCodec.nullable( +trait HoconGenCodecs { + implicit def configCodec: GenCodec[Config] = HoconGenCodecs.ConfigCodec + implicit def finiteDurationCodec: GenCodec[FiniteDuration] = HoconGenCodecs.FiniteDurationCodec + implicit def sizeInBytesCodec: GenCodec[SizeInBytes] = HoconGenCodecs.SizeInBytesCodec + implicit def classKeyCodec: GenKeyCodec[Class[?]] = HoconGenCodecs.ClassKeyCodec + implicit def classCodec: GenCodec[Class[?]] = HoconGenCodecs.ClassCodec +} +object HoconGenCodecs { + implicit final val ConfigCodec: GenCodec[Config] = GenCodec.nullable( input => - input.readCustom(ConfigValueMarker).map { + input.readCustom(ConfigValueMarker).fold(ConfigFactory.parseString(input.readSimple().readString())) { case obj: ConfigObject => obj.toConfig case v => throw new ReadFailure(s"expected a config OBJECT, got ${v.valueType}") - }.getOrElse { - ConfigFactory.parseString(input.readSimple().readString()) }, (output, value) => if (!output.writeCustom(ConfigValueMarker, value.root)) { @@ -24,46 +28,45 @@ trait CommonsHoconCodecs { }, ) - implicit final val finiteDurationCodec: GenCodec[FiniteDuration] = GenCodec.nullable( + implicit final val FiniteDurationCodec: GenCodec[FiniteDuration] = GenCodec.nullable( input => input.readCustom(DurationMarker).fold(input.readSimple().readLong())(_.toMillis).millis, (output, value) => output.writeSimple().writeLong(value.toMillis), ) - implicit final val sizeInBytesCodec: GenCodec[SizeInBytes] = GenCodec.nonNull( + implicit final val SizeInBytesCodec: GenCodec[SizeInBytes] = GenCodec.nonNull( input => SizeInBytes(input.readCustom(SizeInBytesMarker).getOrElse(input.readSimple().readLong())), (output, value) => output.writeSimple().writeLong(value.bytes), ) - implicit final val classKeyCodec: GenKeyCodec[Class[?]] = + implicit final val ClassKeyCodec: GenKeyCodec[Class[?]] = GenKeyCodec.create(Class.forName, _.getName) - implicit final val classCodec: GenCodec[Class[?]] = + implicit final val ClassCodec: GenCodec[Class[?]] = GenCodec.nullableString(Class.forName, _.getName) } -object CommonsHoconCodecs extends CommonsHoconCodecs + +object DefaultHoconGenCodecs extends HoconGenCodecs + +trait ConfigObjectCodec[T] { + def objectCodec: GenObjectCodec[T] +} + +abstract class AbstractConfigCompanion[Implicits <: HoconGenCodecs, T]( + implicits: Implicits +)(implicit instances: MacroInstances[Implicits, ConfigObjectCodec[T]] +) { + implicit lazy val codec: GenCodec[T] = instances(implicits, this).objectCodec + + final def read(config: Config): T = HoconInput.read[T](config) +} /** * Base class for companion objects of configuration case classes and sealed traits * (typically deserialized from HOCON files). * - * [[ConfigCompanion]] is equivalent to [[com.avsystem.commons.serialization.HasGenCodec HasGenCodec]] - * except that it automatically imports codecs from [[CommonsHoconCodecs]] - codecs for third party types often used + * [[DefaultConfigCompanion]] is equivalent to [[com.avsystem.commons.serialization.HasGenCodec HasGenCodec]] + * except that it automatically imports codecs from [[HoconGenCodecs]] - codecs for third party types often used * in configuration. */ -abstract class ConfigCompanion[T](implicit - macroCodec: MacroInstances[CommonsHoconCodecs.type, () => GenObjectCodec[T]], -) extends HasGenObjectCodecWithDeps[CommonsHoconCodecs.type, T] { - final def read(config: Config): T = HoconInput.read[T](config) -} - -/** - * A version of [[ConfigCompanion]] which injects additional implicits into macro materialization. - * Implicits are imported from an object specified with type parameter `D`. - * It must be a singleton object type, i.e. `SomeObject.type`. - */ -abstract class ConfigCompanionWithDeps[T, D <: CommonsHoconCodecs](implicit - deps: ValueOf[D], - macroCodec: MacroInstances[D, () => GenObjectCodec[T]], -) extends HasGenObjectCodecWithDeps[D, T] { - final def read(config: Config): T = HoconInput.read[T](config) -} +abstract class DefaultConfigCompanion[T](implicit macroCodec: MacroInstances[HoconGenCodecs, ConfigObjectCodec[T]]) + extends AbstractConfigCompanion[HoconGenCodecs, T](DefaultHoconGenCodecs) diff --git a/hocon/src/test/scala/com/avsystem/commons/hocon/HoconInputTest.scala b/hocon/src/test/scala/com/avsystem/commons/hocon/HoconInputTest.scala index 18192d57d..15cc76673 100644 --- a/hocon/src/test/scala/com/avsystem/commons/hocon/HoconInputTest.scala +++ b/hocon/src/test/scala/com/avsystem/commons/hocon/HoconInputTest.scala @@ -14,8 +14,9 @@ object HoconInputTest { fileSize: SizeInBytes, embeddedConfig: Config, clazz: Class[?], + clazzMap: Map[Class[?], String], ) - object CustomCodecsClass extends ConfigCompanion[CustomCodecsClass] + object CustomCodecsClass extends DefaultConfigCompanion[CustomCodecsClass] } class HoconInputTest extends GenCodecRoundtripTest { @@ -79,7 +80,10 @@ class HoconInputTest extends GenCodecRoundtripTest { | embeddedConfig { | something = "abc" | } - | clazz = "com.avsystem.commons.hocon.HoconInputTest" + | clazz = "com.avsystem.commons.hocon.HoconInputTest", + | clazzMap { + | "com.avsystem.commons.hocon.HoconInputTest" = "abc" + | } |}""".stripMargin ) val expected = CustomCodecsClass( @@ -87,6 +91,7 @@ class HoconInputTest extends GenCodecRoundtripTest { fileSize = SizeInBytes.`1KiB`, embeddedConfig = ConfigFactory.parseMap(JMap("something" -> "abc")), clazz = classOf[HoconInputTest], + clazzMap = Map(classOf[HoconInputTest] -> "abc") ) assert(CustomCodecsClass.read(config) == expected) } From b60fe6bc23b14dbb20995daa97911a34efcd054c Mon Sep 17 00:00:00 2001 From: Sebastian Haracz Date: Mon, 23 Sep 2024 20:49:07 +0200 Subject: [PATCH 3/6] ConfigCompanion - add java.time config codecs --- .../avsystem/commons/hocon/ConfigCompanion.scala | 13 +++++++++++++ .../com/avsystem/commons/hocon/HoconInputTest.scala | 10 ++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/hocon/src/main/scala/com/avsystem/commons/hocon/ConfigCompanion.scala b/hocon/src/main/scala/com/avsystem/commons/hocon/ConfigCompanion.scala index e448e97b8..20e67a0f6 100644 --- a/hocon/src/main/scala/com/avsystem/commons/hocon/ConfigCompanion.scala +++ b/hocon/src/main/scala/com/avsystem/commons/hocon/ConfigCompanion.scala @@ -6,11 +6,14 @@ import com.avsystem.commons.serialization.GenCodec.ReadFailure import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec, GenObjectCodec} import com.typesafe.config.{Config, ConfigFactory, ConfigObject} +import java.time.{Period, Duration as JDuration} import scala.concurrent.duration.* trait HoconGenCodecs { implicit def configCodec: GenCodec[Config] = HoconGenCodecs.ConfigCodec implicit def finiteDurationCodec: GenCodec[FiniteDuration] = HoconGenCodecs.FiniteDurationCodec + implicit def jDurationCodec: GenCodec[JDuration] = HoconGenCodecs.JavaDurationCodec + implicit def periodCodec: GenCodec[Period] = HoconGenCodecs.PeriodCodec implicit def sizeInBytesCodec: GenCodec[SizeInBytes] = HoconGenCodecs.SizeInBytesCodec implicit def classKeyCodec: GenKeyCodec[Class[?]] = HoconGenCodecs.ClassKeyCodec implicit def classCodec: GenCodec[Class[?]] = HoconGenCodecs.ClassCodec @@ -33,6 +36,16 @@ object HoconGenCodecs { (output, value) => output.writeSimple().writeLong(value.toMillis), ) + implicit val JavaDurationCodec: GenCodec[JDuration] = GenCodec.nullable( + input => input.readCustom(DurationMarker).getOrElse(JDuration.ofMillis(input.readSimple().readLong())), + (output, value) => output.writeSimple().writeLong(value.toMillis), + ) + + implicit final val PeriodCodec: GenCodec[Period] = GenCodec.nullable( + input => input.readCustom(PeriodMarker).getOrElse(Period.parse(input.readSimple().readString())), + (output, value) => output.writeSimple().writeString(value.toString), + ) + implicit final val SizeInBytesCodec: GenCodec[SizeInBytes] = GenCodec.nonNull( input => SizeInBytes(input.readCustom(SizeInBytesMarker).getOrElse(input.readSimple().readLong())), (output, value) => output.writeSimple().writeLong(value.bytes), diff --git a/hocon/src/test/scala/com/avsystem/commons/hocon/HoconInputTest.scala b/hocon/src/test/scala/com/avsystem/commons/hocon/HoconInputTest.scala index 15cc76673..8673bc38b 100644 --- a/hocon/src/test/scala/com/avsystem/commons/hocon/HoconInputTest.scala +++ b/hocon/src/test/scala/com/avsystem/commons/hocon/HoconInputTest.scala @@ -11,8 +11,10 @@ import scala.concurrent.duration.* object HoconInputTest { case class CustomCodecsClass( duration: FiniteDuration, + jDuration: Duration, fileSize: SizeInBytes, embeddedConfig: Config, + period: Period, clazz: Class[?], clazzMap: Map[Class[?], String], ) @@ -76,11 +78,13 @@ class HoconInputTest extends GenCodecRoundtripTest { val config = ConfigFactory.parseString( """{ | duration = 1m + | jDuration = 5m | fileSize = 1KiB | embeddedConfig { | something = "abc" | } - | clazz = "com.avsystem.commons.hocon.HoconInputTest", + | period = "7d" + | clazz = "com.avsystem.commons.hocon.HoconInputTest" | clazzMap { | "com.avsystem.commons.hocon.HoconInputTest" = "abc" | } @@ -88,10 +92,12 @@ class HoconInputTest extends GenCodecRoundtripTest { ) val expected = CustomCodecsClass( duration = 1.minute, + jDuration = Duration.ofMinutes(5), fileSize = SizeInBytes.`1KiB`, embeddedConfig = ConfigFactory.parseMap(JMap("something" -> "abc")), + period = Period.ofDays(7), clazz = classOf[HoconInputTest], - clazzMap = Map(classOf[HoconInputTest] -> "abc") + clazzMap = Map(classOf[HoconInputTest] -> "abc"), ) assert(CustomCodecsClass.read(config) == expected) } From 31369b4140aafc6f860c2cd0fb87ee6e2ab61956 Mon Sep 17 00:00:00 2001 From: Sebastian Haracz Date: Thu, 26 Sep 2024 18:53:49 +0200 Subject: [PATCH 4/6] Improve SizeInBytes doc --- .../avsystem/commons/hocon/ConfigCompanion.scala | 13 +++++++------ .../com/avsystem/commons/hocon/SizeInBytes.scala | 6 ++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/hocon/src/main/scala/com/avsystem/commons/hocon/ConfigCompanion.scala b/hocon/src/main/scala/com/avsystem/commons/hocon/ConfigCompanion.scala index 20e67a0f6..2a08ea80d 100644 --- a/hocon/src/main/scala/com/avsystem/commons/hocon/ConfigCompanion.scala +++ b/hocon/src/main/scala/com/avsystem/commons/hocon/ConfigCompanion.scala @@ -8,6 +8,7 @@ import com.typesafe.config.{Config, ConfigFactory, ConfigObject} import java.time.{Period, Duration as JDuration} import scala.concurrent.duration.* +import scala.jdk.javaapi.DurationConverters trait HoconGenCodecs { implicit def configCodec: GenCodec[Config] = HoconGenCodecs.ConfigCodec @@ -32,23 +33,23 @@ object HoconGenCodecs { ) implicit final val FiniteDurationCodec: GenCodec[FiniteDuration] = GenCodec.nullable( - input => input.readCustom(DurationMarker).fold(input.readSimple().readLong())(_.toMillis).millis, - (output, value) => output.writeSimple().writeLong(value.toMillis), + input => input.readCustom(DurationMarker).map(DurationConverters.toScala).getOrElse(input.readSimple().readLong().millis), + (output, value) => if (!output.writeCustom(DurationMarker, DurationConverters.toJava(value))) output.writeSimple().writeLong(value.toMillis), ) - implicit val JavaDurationCodec: GenCodec[JDuration] = GenCodec.nullable( + implicit final val JavaDurationCodec: GenCodec[JDuration] = GenCodec.nullable( input => input.readCustom(DurationMarker).getOrElse(JDuration.ofMillis(input.readSimple().readLong())), - (output, value) => output.writeSimple().writeLong(value.toMillis), + (output, value) => if (!output.writeCustom(DurationMarker, value)) output.writeSimple().writeLong(value.toMillis), ) implicit final val PeriodCodec: GenCodec[Period] = GenCodec.nullable( input => input.readCustom(PeriodMarker).getOrElse(Period.parse(input.readSimple().readString())), - (output, value) => output.writeSimple().writeString(value.toString), + (output, value) => if (!output.writeCustom(PeriodMarker, value)) output.writeSimple().writeString(value.toString), ) implicit final val SizeInBytesCodec: GenCodec[SizeInBytes] = GenCodec.nonNull( input => SizeInBytes(input.readCustom(SizeInBytesMarker).getOrElse(input.readSimple().readLong())), - (output, value) => output.writeSimple().writeLong(value.bytes), + (output, value) => if (!output.writeCustom(SizeInBytesMarker, value.bytes)) output.writeSimple().writeLong(value.bytes), ) implicit final val ClassKeyCodec: GenKeyCodec[Class[?]] = diff --git a/hocon/src/main/scala/com/avsystem/commons/hocon/SizeInBytes.scala b/hocon/src/main/scala/com/avsystem/commons/hocon/SizeInBytes.scala index 999635946..5e3023dbc 100644 --- a/hocon/src/main/scala/com/avsystem/commons/hocon/SizeInBytes.scala +++ b/hocon/src/main/scala/com/avsystem/commons/hocon/SizeInBytes.scala @@ -2,8 +2,10 @@ package com.avsystem.commons package hocon /** - * Use this type in data deserialized from HOCON to in order to read size in bytes represented with - * [[https://github.com/lightbend/config/blob/master/HOCON.md#size-in-bytes-format HOCON's nice representation]]. + * To read the size in bytes represented in [[https://github.com/lightbend/config/blob/master/HOCON.md#size-in-bytes-format HOCON format]], + * use this type together with [[com.avsystem.commons.hocon.SizeInBytesMarker]] when deserializing data from HOCON. + * + * @see [[com.avsystem.commons.hocon.HoconGenCodecs.SizeInBytesCodec]] */ final case class SizeInBytes(bytes: Long) object SizeInBytes { From 65c5f75565559d68d8145beafd724a8f55416539 Mon Sep 17 00:00:00 2001 From: Sebastian Haracz Date: Sat, 28 Sep 2024 14:41:41 +0200 Subject: [PATCH 5/6] Skip rendering origin comments in ConfigCodec --- .../scala/com/avsystem/commons/hocon/ConfigCompanion.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hocon/src/main/scala/com/avsystem/commons/hocon/ConfigCompanion.scala b/hocon/src/main/scala/com/avsystem/commons/hocon/ConfigCompanion.scala index 2a08ea80d..ec540f4f0 100644 --- a/hocon/src/main/scala/com/avsystem/commons/hocon/ConfigCompanion.scala +++ b/hocon/src/main/scala/com/avsystem/commons/hocon/ConfigCompanion.scala @@ -4,7 +4,7 @@ package hocon import com.avsystem.commons.meta.MacroInstances import com.avsystem.commons.serialization.GenCodec.ReadFailure import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec, GenObjectCodec} -import com.typesafe.config.{Config, ConfigFactory, ConfigObject} +import com.typesafe.config.{Config, ConfigFactory, ConfigObject, ConfigRenderOptions} import java.time.{Period, Duration as JDuration} import scala.concurrent.duration.* @@ -28,7 +28,8 @@ object HoconGenCodecs { }, (output, value) => if (!output.writeCustom(ConfigValueMarker, value.root)) { - output.writeSimple().writeString(value.root.render) + val renderOptions = ConfigRenderOptions.defaults().setOriginComments(false) + output.writeSimple().writeString(value.root.render(renderOptions)) }, ) From 0ab45922c2c91a6c5c6ba5efbc1a7f6f67df29b1 Mon Sep 17 00:00:00 2001 From: Sebastian Haracz Date: Sat, 28 Sep 2024 14:55:25 +0200 Subject: [PATCH 6/6] Add roundtrip test for custom Hocon codecs --- .../avsystem/commons/hocon/HoconOutput.scala | 13 +++++++++++-- .../hocon/HoconGenCodecRoundtripTest.scala | 19 ++++++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/hocon/src/main/scala/com/avsystem/commons/hocon/HoconOutput.scala b/hocon/src/main/scala/com/avsystem/commons/hocon/HoconOutput.scala index 511a2e8fe..d35a3ca64 100644 --- a/hocon/src/main/scala/com/avsystem/commons/hocon/HoconOutput.scala +++ b/hocon/src/main/scala/com/avsystem/commons/hocon/HoconOutput.scala @@ -2,7 +2,9 @@ package com.avsystem.commons package hocon import com.avsystem.commons.annotation.explicitGenerics -import com.avsystem.commons.serialization._ +import com.avsystem.commons.serialization.* +import com.avsystem.commons.serialization.cbor.RawCbor +import com.avsystem.commons.serialization.json.RawJson import com.typesafe.config.{ConfigValue, ConfigValueFactory} object HoconOutput { @@ -31,7 +33,14 @@ class HoconOutput(consumer: ConfigValue => Unit) extends OutputAndSimpleOutput { def writeList(): HoconListOutput = new HoconListOutput(consumer) def writeObject(): HoconObjectOutput = new HoconObjectOutput(consumer) - //TODO: writeCustom + // TODO: handle other markers in writeCustom? Unfortunately typesafe config does not provide a methods to write + // duration to nice string representation etc. + override def writeCustom[T](typeMarker: TypeMarker[T], value: T): Boolean = + typeMarker match { + case ConfigValueMarker => consumer(value); true + case PeriodMarker => anyRef(value.toString.toLowerCase.stripPrefix("p")); true + case _ => false + } } class HoconListOutput(consumer: ConfigValue => Unit) extends ListOutput { diff --git a/hocon/src/test/scala/com/avsystem/commons/hocon/HoconGenCodecRoundtripTest.scala b/hocon/src/test/scala/com/avsystem/commons/hocon/HoconGenCodecRoundtripTest.scala index 9d015347c..f79ad3b5b 100644 --- a/hocon/src/test/scala/com/avsystem/commons/hocon/HoconGenCodecRoundtripTest.scala +++ b/hocon/src/test/scala/com/avsystem/commons/hocon/HoconGenCodecRoundtripTest.scala @@ -1,8 +1,12 @@ package com.avsystem.commons package hocon +import com.avsystem.commons.hocon.HoconInputTest.CustomCodecsClass import com.avsystem.commons.serialization.{GenCodecRoundtripTest, Input, Output} -import com.typesafe.config.ConfigValue +import com.typesafe.config.{ConfigFactory, ConfigValue} + +import java.time.{Duration, Period} +import scala.concurrent.duration.* class HoconGenCodecRoundtripTest extends GenCodecRoundtripTest { type Raw = ConfigValue @@ -15,4 +19,17 @@ class HoconGenCodecRoundtripTest extends GenCodecRoundtripTest { def createInput(raw: ConfigValue): Input = new HoconInput(raw) + + test("custom codes class") { + val value = CustomCodecsClass( + duration = 1.minute, + jDuration = Duration.ofMinutes(5), + fileSize = SizeInBytes.`1KiB`, + embeddedConfig = ConfigFactory.parseMap(JMap("something" -> "abc")), + period = Period.ofWeeks(2), + clazz = classOf[HoconGenCodecRoundtripTest], + clazzMap = Map(classOf[HoconGenCodecRoundtripTest] -> "abc"), + ) + testRoundtrip(value) + } }