Skip to content

Commit af0e222

Browse files
authored
Merge pull request #562 from AVSystem/config-utils
Hocon config companions
2 parents 4519050 + 0ab4592 commit af0e222

File tree

5 files changed

+179
-6
lines changed

5 files changed

+179
-6
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package com.avsystem.commons
2+
package hocon
3+
4+
import com.avsystem.commons.meta.MacroInstances
5+
import com.avsystem.commons.serialization.GenCodec.ReadFailure
6+
import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec, GenObjectCodec}
7+
import com.typesafe.config.{Config, ConfigFactory, ConfigObject, ConfigRenderOptions}
8+
9+
import java.time.{Period, Duration as JDuration}
10+
import scala.concurrent.duration.*
11+
import scala.jdk.javaapi.DurationConverters
12+
13+
trait HoconGenCodecs {
14+
implicit def configCodec: GenCodec[Config] = HoconGenCodecs.ConfigCodec
15+
implicit def finiteDurationCodec: GenCodec[FiniteDuration] = HoconGenCodecs.FiniteDurationCodec
16+
implicit def jDurationCodec: GenCodec[JDuration] = HoconGenCodecs.JavaDurationCodec
17+
implicit def periodCodec: GenCodec[Period] = HoconGenCodecs.PeriodCodec
18+
implicit def sizeInBytesCodec: GenCodec[SizeInBytes] = HoconGenCodecs.SizeInBytesCodec
19+
implicit def classKeyCodec: GenKeyCodec[Class[?]] = HoconGenCodecs.ClassKeyCodec
20+
implicit def classCodec: GenCodec[Class[?]] = HoconGenCodecs.ClassCodec
21+
}
22+
object HoconGenCodecs {
23+
implicit final val ConfigCodec: GenCodec[Config] = GenCodec.nullable(
24+
input =>
25+
input.readCustom(ConfigValueMarker).fold(ConfigFactory.parseString(input.readSimple().readString())) {
26+
case obj: ConfigObject => obj.toConfig
27+
case v => throw new ReadFailure(s"expected a config OBJECT, got ${v.valueType}")
28+
},
29+
(output, value) =>
30+
if (!output.writeCustom(ConfigValueMarker, value.root)) {
31+
val renderOptions = ConfigRenderOptions.defaults().setOriginComments(false)
32+
output.writeSimple().writeString(value.root.render(renderOptions))
33+
},
34+
)
35+
36+
implicit final val FiniteDurationCodec: GenCodec[FiniteDuration] = GenCodec.nullable(
37+
input => input.readCustom(DurationMarker).map(DurationConverters.toScala).getOrElse(input.readSimple().readLong().millis),
38+
(output, value) => if (!output.writeCustom(DurationMarker, DurationConverters.toJava(value))) output.writeSimple().writeLong(value.toMillis),
39+
)
40+
41+
implicit final val JavaDurationCodec: GenCodec[JDuration] = GenCodec.nullable(
42+
input => input.readCustom(DurationMarker).getOrElse(JDuration.ofMillis(input.readSimple().readLong())),
43+
(output, value) => if (!output.writeCustom(DurationMarker, value)) output.writeSimple().writeLong(value.toMillis),
44+
)
45+
46+
implicit final val PeriodCodec: GenCodec[Period] = GenCodec.nullable(
47+
input => input.readCustom(PeriodMarker).getOrElse(Period.parse(input.readSimple().readString())),
48+
(output, value) => if (!output.writeCustom(PeriodMarker, value)) output.writeSimple().writeString(value.toString),
49+
)
50+
51+
implicit final val SizeInBytesCodec: GenCodec[SizeInBytes] = GenCodec.nonNull(
52+
input => SizeInBytes(input.readCustom(SizeInBytesMarker).getOrElse(input.readSimple().readLong())),
53+
(output, value) => if (!output.writeCustom(SizeInBytesMarker, value.bytes)) output.writeSimple().writeLong(value.bytes),
54+
)
55+
56+
implicit final val ClassKeyCodec: GenKeyCodec[Class[?]] =
57+
GenKeyCodec.create(Class.forName, _.getName)
58+
59+
implicit final val ClassCodec: GenCodec[Class[?]] =
60+
GenCodec.nullableString(Class.forName, _.getName)
61+
}
62+
63+
object DefaultHoconGenCodecs extends HoconGenCodecs
64+
65+
trait ConfigObjectCodec[T] {
66+
def objectCodec: GenObjectCodec[T]
67+
}
68+
69+
abstract class AbstractConfigCompanion[Implicits <: HoconGenCodecs, T](
70+
implicits: Implicits
71+
)(implicit instances: MacroInstances[Implicits, ConfigObjectCodec[T]]
72+
) {
73+
implicit lazy val codec: GenCodec[T] = instances(implicits, this).objectCodec
74+
75+
final def read(config: Config): T = HoconInput.read[T](config)
76+
}
77+
78+
/**
79+
* Base class for companion objects of configuration case classes and sealed traits
80+
* (typically deserialized from HOCON files).
81+
*
82+
* [[DefaultConfigCompanion]] is equivalent to [[com.avsystem.commons.serialization.HasGenCodec HasGenCodec]]
83+
* except that it automatically imports codecs from [[HoconGenCodecs]] - codecs for third party types often used
84+
* in configuration.
85+
*/
86+
abstract class DefaultConfigCompanion[T](implicit macroCodec: MacroInstances[HoconGenCodecs, ConfigObjectCodec[T]])
87+
extends AbstractConfigCompanion[HoconGenCodecs, T](DefaultHoconGenCodecs)

hocon/src/main/scala/com/avsystem/commons/hocon/HoconOutput.scala

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package com.avsystem.commons
22
package hocon
33

44
import com.avsystem.commons.annotation.explicitGenerics
5-
import com.avsystem.commons.serialization._
5+
import com.avsystem.commons.serialization.*
6+
import com.avsystem.commons.serialization.cbor.RawCbor
7+
import com.avsystem.commons.serialization.json.RawJson
68
import com.typesafe.config.{ConfigValue, ConfigValueFactory}
79

810
object HoconOutput {
@@ -31,7 +33,14 @@ class HoconOutput(consumer: ConfigValue => Unit) extends OutputAndSimpleOutput {
3133
def writeList(): HoconListOutput = new HoconListOutput(consumer)
3234
def writeObject(): HoconObjectOutput = new HoconObjectOutput(consumer)
3335

34-
//TODO: writeCustom
36+
// TODO: handle other markers in writeCustom? Unfortunately typesafe config does not provide a methods to write
37+
// duration to nice string representation etc.
38+
override def writeCustom[T](typeMarker: TypeMarker[T], value: T): Boolean =
39+
typeMarker match {
40+
case ConfigValueMarker => consumer(value); true
41+
case PeriodMarker => anyRef(value.toString.toLowerCase.stripPrefix("p")); true
42+
case _ => false
43+
}
3544
}
3645

3746
class HoconListOutput(consumer: ConfigValue => Unit) extends ListOutput {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.avsystem.commons
2+
package hocon
3+
4+
/**
5+
* To read the size in bytes represented in [[https://github.com/lightbend/config/blob/master/HOCON.md#size-in-bytes-format HOCON format]],
6+
* use this type together with [[com.avsystem.commons.hocon.SizeInBytesMarker]] when deserializing data from HOCON.
7+
*
8+
* @see [[com.avsystem.commons.hocon.HoconGenCodecs.SizeInBytesCodec]]
9+
*/
10+
final case class SizeInBytes(bytes: Long)
11+
object SizeInBytes {
12+
final val Zero = SizeInBytes(0)
13+
final val `1KiB` = SizeInBytes(1024L)
14+
final val `1MiB` = SizeInBytes(1024 * 1024L)
15+
}

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package com.avsystem.commons
22
package hocon
33

4+
import com.avsystem.commons.hocon.HoconInputTest.CustomCodecsClass
45
import com.avsystem.commons.serialization.{GenCodecRoundtripTest, Input, Output}
5-
import com.typesafe.config.ConfigValue
6+
import com.typesafe.config.{ConfigFactory, ConfigValue}
7+
8+
import java.time.{Duration, Period}
9+
import scala.concurrent.duration.*
610

711
class HoconGenCodecRoundtripTest extends GenCodecRoundtripTest {
812
type Raw = ConfigValue
@@ -15,4 +19,17 @@ class HoconGenCodecRoundtripTest extends GenCodecRoundtripTest {
1519

1620
def createInput(raw: ConfigValue): Input =
1721
new HoconInput(raw)
22+
23+
test("custom codes class") {
24+
val value = CustomCodecsClass(
25+
duration = 1.minute,
26+
jDuration = Duration.ofMinutes(5),
27+
fileSize = SizeInBytes.`1KiB`,
28+
embeddedConfig = ConfigFactory.parseMap(JMap("something" -> "abc")),
29+
period = Period.ofWeeks(2),
30+
clazz = classOf[HoconGenCodecRoundtripTest],
31+
clazzMap = Map(classOf[HoconGenCodecRoundtripTest] -> "abc"),
32+
)
33+
testRoundtrip(value)
34+
}
1835
}

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

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,30 @@
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+
jDuration: Duration,
15+
fileSize: SizeInBytes,
16+
embeddedConfig: Config,
17+
period: Period,
18+
clazz: Class[?],
19+
clazzMap: Map[Class[?], String],
20+
)
21+
object CustomCodecsClass extends DefaultConfigCompanion[CustomCodecsClass]
22+
}
923

1024
class HoconInputTest extends GenCodecRoundtripTest {
25+
26+
import HoconInputTest.*
27+
1128
type Raw = ConfigValue
1229

1330
def writeToOutput(write: Output => Unit): ConfigValue = {
@@ -56,4 +73,32 @@ class HoconInputTest extends GenCodecRoundtripTest {
5673
test("number reading") {
5774
assert(rawInput(42.0).readNumber().doubleValue == 42.0)
5875
}
76+
77+
test("class reading") {
78+
val config = ConfigFactory.parseString(
79+
"""{
80+
| duration = 1m
81+
| jDuration = 5m
82+
| fileSize = 1KiB
83+
| embeddedConfig {
84+
| something = "abc"
85+
| }
86+
| period = "7d"
87+
| clazz = "com.avsystem.commons.hocon.HoconInputTest"
88+
| clazzMap {
89+
| "com.avsystem.commons.hocon.HoconInputTest" = "abc"
90+
| }
91+
|}""".stripMargin
92+
)
93+
val expected = CustomCodecsClass(
94+
duration = 1.minute,
95+
jDuration = Duration.ofMinutes(5),
96+
fileSize = SizeInBytes.`1KiB`,
97+
embeddedConfig = ConfigFactory.parseMap(JMap("something" -> "abc")),
98+
period = Period.ofDays(7),
99+
clazz = classOf[HoconInputTest],
100+
clazzMap = Map(classOf[HoconInputTest] -> "abc"),
101+
)
102+
assert(CustomCodecsClass.read(config) == expected)
103+
}
59104
}

0 commit comments

Comments
 (0)