Skip to content

Commit 00c88ac

Browse files
committed
Hocon config companions
1 parent ca514b8 commit 00c88ac

File tree

3 files changed

+119
-3
lines changed

3 files changed

+119
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package com.avsystem.commons
2+
package hocon
3+
4+
import com.avsystem.commons.meta.MacroInstances
5+
import com.avsystem.commons.misc.ValueOf
6+
import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec, GenObjectCodec, HasGenObjectCodecWithDeps}
7+
import com.avsystem.commons.serialization.GenCodec.ReadFailure
8+
import com.typesafe.config.{Config, ConfigFactory, ConfigObject}
9+
10+
import scala.concurrent.duration.*
11+
12+
trait CommonsHoconCodecs {
13+
implicit final val configCodec: GenCodec[Config] = GenCodec.nullable(
14+
input =>
15+
input.readCustom(ConfigValueMarker).map {
16+
case obj: ConfigObject => obj.toConfig
17+
case v => throw new ReadFailure(s"expected a config OBJECT, got ${v.valueType}")
18+
}.getOrElse {
19+
ConfigFactory.parseString(input.readSimple().readString())
20+
},
21+
(output, value) =>
22+
if (!output.writeCustom(ConfigValueMarker, value.root)) {
23+
output.writeSimple().writeString(value.root.render)
24+
},
25+
)
26+
27+
implicit final val finiteDurationCodec: GenCodec[FiniteDuration] = GenCodec.nullable(
28+
input => input.readCustom(DurationMarker).fold(input.readSimple().readLong())(_.toMillis).millis,
29+
(output, value) => output.writeSimple().writeLong(value.toMillis),
30+
)
31+
32+
implicit final val sizeInBytesCodec: GenCodec[SizeInBytes] = GenCodec.nonNull(
33+
input => SizeInBytes(input.readCustom(SizeInBytesMarker).getOrElse(input.readSimple().readLong())),
34+
(output, value) => output.writeSimple().writeLong(value.bytes),
35+
)
36+
37+
implicit final val classKeyCodec: GenKeyCodec[Class[?]] =
38+
GenKeyCodec.create(Class.forName, _.getName)
39+
40+
implicit final val classCodec: GenCodec[Class[?]] =
41+
GenCodec.nullableString(Class.forName, _.getName)
42+
}
43+
object CommonsHoconCodecs extends CommonsHoconCodecs
44+
45+
/**
46+
* Base class for companion objects of configuration case classes and sealed traits
47+
* (typically deserialized from HOCON files).
48+
*
49+
* [[ConfigCompanion]] is equivalent to [[com.avsystem.commons.serialization.HasGenCodec HasGenCodec]]
50+
* except that it automatically imports codecs from [[CommonsHoconCodecs]] - codecs for third party types often used
51+
* in configuration.
52+
*/
53+
abstract class ConfigCompanion[T](implicit
54+
macroCodec: MacroInstances[CommonsHoconCodecs.type, () => GenObjectCodec[T]],
55+
) extends HasGenObjectCodecWithDeps[CommonsHoconCodecs.type, T] {
56+
final def read(config: Config): T = HoconInput.read[T](config)
57+
}
58+
59+
/**
60+
* A version of [[ConfigCompanion]] which injects additional implicits into macro materialization.
61+
* Implicits are imported from an object specified with type parameter `D`.
62+
* It must be a singleton object type, i.e. `SomeObject.type`.
63+
*/
64+
abstract class ConfigCompanionWithDeps[T, D <: CommonsHoconCodecs](implicit
65+
deps: ValueOf[D],
66+
macroCodec: MacroInstances[D, () => GenObjectCodec[T]],
67+
) extends HasGenObjectCodecWithDeps[D, T] {
68+
final def read(config: Config): T = HoconInput.read[T](config)
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.avsystem.commons
2+
package hocon
3+
4+
/**
5+
* Use this type in data deserialized from HOCON to in order to read size in bytes represented with
6+
* [[https://github.com/lightbend/config/blob/master/HOCON.md#size-in-bytes-format HOCON's nice representation]].
7+
*/
8+
final case class SizeInBytes(bytes: Long)
9+
object SizeInBytes {
10+
final val Zero = SizeInBytes(0)
11+
final val `1KiB` = SizeInBytes(1024L)
12+
final val `1MiB` = SizeInBytes(1024 * 1024L)
13+
}

hocon/src/test/scala/com/avsystem/commons/hocon/HoconInputTest.scala

+37-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,27 @@
11
package com.avsystem.commons
22
package hocon
33

4-
import java.time.{Duration, Period}
5-
64
import com.avsystem.commons.serialization.json.JsonStringOutput
75
import com.avsystem.commons.serialization.{GenCodecRoundtripTest, Input, Output}
8-
import com.typesafe.config.{ConfigFactory, ConfigMemorySize, ConfigValue, ConfigValueFactory, ConfigValueType}
6+
import com.typesafe.config.*
7+
8+
import java.time.{Duration, Period}
9+
import scala.concurrent.duration.*
10+
11+
object HoconInputTest {
12+
case class CustomCodecsClass(
13+
duration: FiniteDuration,
14+
fileSize: SizeInBytes,
15+
embeddedConfig: Config,
16+
clazz: Class[?],
17+
)
18+
object CustomCodecsClass extends ConfigCompanion[CustomCodecsClass]
19+
}
920

1021
class HoconInputTest extends GenCodecRoundtripTest {
22+
23+
import HoconInputTest.*
24+
1125
type Raw = ConfigValue
1226

1327
def writeToOutput(write: Output => Unit): ConfigValue = {
@@ -56,4 +70,24 @@ class HoconInputTest extends GenCodecRoundtripTest {
5670
test("number reading") {
5771
assert(rawInput(42.0).readNumber().doubleValue == 42.0)
5872
}
73+
74+
test("class reading") {
75+
val config = ConfigFactory.parseString(
76+
"""{
77+
| duration = 1m
78+
| fileSize = 1KiB
79+
| embeddedConfig {
80+
| something = "abc"
81+
| }
82+
| clazz = "com.avsystem.commons.hocon.HoconInputTest"
83+
|}""".stripMargin
84+
)
85+
val expected = CustomCodecsClass(
86+
duration = 1.minute,
87+
fileSize = SizeInBytes.`1KiB`,
88+
embeddedConfig = ConfigFactory.parseMap(JMap("something" -> "abc")),
89+
clazz = classOf[HoconInputTest],
90+
)
91+
assert(CustomCodecsClass.read(config) == expected)
92+
}
5993
}

0 commit comments

Comments
 (0)