Skip to content

Commit

Permalink
Merge pull request #562 from AVSystem/config-utils
Browse files Browse the repository at this point in the history
Hocon config companions
  • Loading branch information
ddworak authored Sep 30, 2024
2 parents 4519050 + 0ab4592 commit af0e222
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -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)
13 changes: 11 additions & 2 deletions hocon/src/main/scala/com/avsystem/commons/hocon/HoconOutput.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
15 changes: 15 additions & 0 deletions hocon/src/main/scala/com/avsystem/commons/hocon/SizeInBytes.scala
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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)
}
}

0 comments on commit af0e222

Please sign in to comment.