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..ec540f4f0 --- /dev/null +++ b/hocon/src/main/scala/com/avsystem/commons/hocon/ConfigCompanion.scala @@ -0,0 +1,87 @@ +package com.avsystem.commons +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, ConfigRenderOptions} + +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 + 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 +} +object HoconGenCodecs { + implicit final val ConfigCodec: GenCodec[Config] = GenCodec.nullable( + input => + 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}") + }, + (output, value) => + if (!output.writeCustom(ConfigValueMarker, value.root)) { + val renderOptions = ConfigRenderOptions.defaults().setOriginComments(false) + output.writeSimple().writeString(value.root.render(renderOptions)) + }, + ) + + implicit final val FiniteDurationCodec: GenCodec[FiniteDuration] = GenCodec.nullable( + 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 final val JavaDurationCodec: GenCodec[JDuration] = GenCodec.nullable( + input => input.readCustom(DurationMarker).getOrElse(JDuration.ofMillis(input.readSimple().readLong())), + (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) => 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) => if (!output.writeCustom(SizeInBytesMarker, value.bytes)) 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 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). + * + * [[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 DefaultConfigCompanion[T](implicit macroCodec: MacroInstances[HoconGenCodecs, ConfigObjectCodec[T]]) + extends AbstractConfigCompanion[HoconGenCodecs, T](DefaultHoconGenCodecs) 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/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..5e3023dbc --- /dev/null +++ b/hocon/src/main/scala/com/avsystem/commons/hocon/SizeInBytes.scala @@ -0,0 +1,15 @@ +package com.avsystem.commons +package hocon + +/** + * 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 { + 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/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) + } } 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..8673bc38b 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,30 @@ 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, + jDuration: Duration, + fileSize: SizeInBytes, + embeddedConfig: Config, + period: Period, + clazz: Class[?], + clazzMap: Map[Class[?], String], + ) + object CustomCodecsClass extends DefaultConfigCompanion[CustomCodecsClass] +} class HoconInputTest extends GenCodecRoundtripTest { + + import HoconInputTest.* + type Raw = ConfigValue def writeToOutput(write: Output => Unit): ConfigValue = { @@ -56,4 +73,32 @@ class HoconInputTest extends GenCodecRoundtripTest { test("number reading") { assert(rawInput(42.0).readNumber().doubleValue == 42.0) } + + test("class reading") { + val config = ConfigFactory.parseString( + """{ + | duration = 1m + | jDuration = 5m + | fileSize = 1KiB + | embeddedConfig { + | something = "abc" + | } + | period = "7d" + | clazz = "com.avsystem.commons.hocon.HoconInputTest" + | clazzMap { + | "com.avsystem.commons.hocon.HoconInputTest" = "abc" + | } + |}""".stripMargin + ) + 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"), + ) + assert(CustomCodecsClass.read(config) == expected) + } }