Skip to content

Commit

Permalink
Hocon config companions
Browse files Browse the repository at this point in the history
  • Loading branch information
sebaciv committed Apr 11, 2024
1 parent ca514b8 commit 00c88ac
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -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)
}
13 changes: 13 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,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)
}
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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)
}
}

0 comments on commit 00c88ac

Please sign in to comment.