Skip to content

Instantly share code, notes, and snippets.

@Baccata
Last active June 6, 2024 13:38
Show Gist options
  • Save Baccata/a990c526374784b70535b307b43ce88c to your computer and use it in GitHub Desktop.
Save Baccata/a990c526374784b70535b307b43ce88c to your computer and use it in GitHub Desktop.
Poor man's solution for getting the "remainder" of a hocon config after its decoding, in order to increase confidence when moving/deleting config values
//> using dep "com.typesafe:config:1.4.3"
//> using dep "com.github.pureconfig::pureconfig:0.17.6"
//> using scala "2.13.14"
import com.typesafe.config._
import pureconfig.generic.semiauto._
import pureconfig.ConfigReader
import pureconfig.ConfigSource
import java.{util => ju}
import java.util.Collection
import java.util.Map.Entry
import scala.jdk.CollectionConverters._
object Main {
def main(args: Array[String]): Unit = {
case class C(value: Int)
object C {
implicit val reader: ConfigReader[C] = deriveReader
}
case class ABC(a: Int, b: String, c: List[C])
object ABC {
implicit val reader: ConfigReader[ABC] = deriveReader
}
val config = ConfigFactory.parseString("""|{
| a = 1
| b = "b"
| c = [
| {
| value = 3
| unused1 = {}
| },
| {
| value = 1
| }
| ]
| unused2 = 33
|}
|""".stripMargin)
val (maybeRemainder, result) =
ConfigLeftovers.using(config)(ABC.reader.from(_))
println(maybeRemainder.map(_.render(ConfigRenderOptions.defaults())))
}
}
// scalafmt: {maxColumn = 120}
object ConfigLeftovers {
/** Provided a Config and a function that produces a value from a ConfigValue, this will attempt to return a
* ConfigValue stripped of all the configuration keys that will have been queried during the execution of the
* function.
*
* This is a poor man's solution, in the absence of any known library that would do this in a rather principled
* fashion.
*/
def using[A](config: Config)(f: ConfigValue => A): (Option[ConfigValue], A) = {
val trackingConfig = transformObject(config.root())
val result = f(trackingConfig)
(collapseUnused(trackingConfig), result)
}
private class ConfigObjectWrapper(map: ju.Map[String, ConfigValue], configOrigin: ConfigOrigin) extends ConfigObject {
val usedKeys: scala.collection.mutable.Set[String] = scala.collection.mutable.Set.empty[String]
/// Whenever a key is accessed, we add it to the list of keys that were queried.
override def get(key: Object): ConfigValue = {
usedKeys += key.asInstanceOf[String]
map.get(key)
}
override def origin(): ConfigOrigin = configOrigin
override def valueType(): ConfigValueType = ConfigValueType.OBJECT
override def render(): String =
ConfigValueFactory.fromMap(map, configOrigin.description()).render()
override def render(options: ConfigRenderOptions): String =
ConfigValueFactory.fromMap(map, configOrigin.description()).render(options)
override def containsKey(key: Object): Boolean = map.containsKey(key)
override def size(): Int = map.size()
override def isEmpty(): Boolean = map.isEmpty()
override def unwrapped(): ju.Map[String, Object] = map.asInstanceOf[ju.Map[String, Object]]
override def containsValue(value: Object): Boolean = map.containsValue(value)
override def keySet(): ju.Set[String] = map.keySet()
override def values(): Collection[ConfigValue] = map.values()
override def entrySet(): ju.Set[Entry[String, ConfigValue]] = map.entrySet()
override def toConfig(): Config = ConfigFactory.parseMap(map)
// SHOULD NOT USE DURING DECODING
override def withFallback(other: ConfigMergeable): ConfigObject = ???
override def put(key: String, value: ConfigValue): ConfigValue = ???
override def remove(key: Object): ConfigValue = ???
override def putAll(m: ju.Map[_ <: String, _ <: ConfigValue]): Unit = ???
override def clear(): Unit = ???
override def withOnlyKey(key: String): ConfigObject = ???
override def withoutKey(key: String): ConfigObject = ???
override def withValue(key: String, value: ConfigValue): ConfigObject = ???
override def withOrigin(origin: ConfigOrigin): ConfigObject = ???
override def atPath(path: String): Config = ???
override def atKey(key: String): Config = ???
}
private class ConfigListWrapper(list: ju.List[ConfigValue], configOrigin: ConfigOrigin) extends ConfigList {
def unwrapped(): ju.List[Object] = list.asInstanceOf[ju.List[Object]]
def size(): Int = list.size()
def isEmpty(): Boolean = list.isEmpty()
def contains(o: Object): Boolean = list.contains(o)
def iterator(): ju.Iterator[ConfigValue] = list.iterator()
def toArray(): Array[Object] = list.toArray()
def toArray[T <: Object](x: Array[T with Object]): Array[T with Object] = list.toArray[T](x)
def containsAll(c: Collection[_ <: Object]): Boolean = list.containsAll(c)
def get(index: Int): ConfigValue = list.get(index)
def indexOf(o: Object): Int = list.indexOf(o)
def lastIndexOf(o: Object): Int = list.lastIndexOf(o)
def listIterator(): ju.ListIterator[ConfigValue] = list.listIterator()
def listIterator(x: Int): ju.ListIterator[ConfigValue] = list.listIterator(x)
def origin(): ConfigOrigin = configOrigin
def valueType(): ConfigValueType = ConfigValueType.LIST
def render(): String = render(ConfigRenderOptions.defaults())
def render(options: ConfigRenderOptions): String =
ConfigValueFactory.fromIterable(list, configOrigin.description()).render(options)
// SHOULD NOT USE DURING DECODING
def subList(fromIndex: Int, toIndex: Int): ju.List[ConfigValue] = ???
def withFallback(other: ConfigMergeable): ConfigValue = ???
def add(x: ConfigValue): Boolean = ???
def remove(x: Object): Boolean = ???
def addAll(x: Collection[_ <: ConfigValue]): Boolean = ???
def addAll(x: Int, col: Collection[_ <: ConfigValue]): Boolean = ???
def set(index: Int, element: ConfigValue): ConfigValue = ???
def add(x: Int, cv: ConfigValue): Unit = ???
def remove(x: Int): ConfigValue = ???
def removeAll(c: Collection[_ <: Object]): Boolean = ???
def retainAll(c: Collection[_ <: Object]): Boolean = ???
def atPath(path: String): Config = ???
def clear(): Unit = ???
def atKey(key: String): Config = ???
def withOrigin(origin: ConfigOrigin): ConfigList = ???
}
private def transform(configValue: ConfigValue): ConfigValue =
configValue match {
case co: ConfigObject => transformObject(co)
case cl: ConfigList => transformList(cl)
case other => other
}
private def transformObject(co: ConfigObject): ConfigObjectWrapper =
new ConfigObjectWrapper(co.asScala.view.mapValues(transform).toMap.asJava, co.origin())
private def transformList(cl: ConfigList): ConfigListWrapper =
new ConfigListWrapper(cl.asScala.map(transform).asJava, cl.origin())
private def collapseUnused(configValue: ConfigValue): Option[ConfigValue] = configValue match {
case co: ConfigObjectWrapper =>
val map = co.asScala.view
.map { case (key, value) =>
if (co.usedKeys(key)) {
val valueType = value.valueType()
if (valueType == ConfigValueType.OBJECT || valueType == ConfigValueType.LIST)
(key, collapseUnused(value))
else (key, None)
} else (key, Some(value))
}
.collect { case (key, Some(value)) => (key, value) }
.toMap
if (map.isEmpty) None else Some(ConfigValueFactory.fromMap(map.asJava, co.origin().description()))
case cl: ConfigList =>
val values = cl.asScala.toList.map(collapseUnused).collect { case Some(value) => value }
if (values.isEmpty) None else Some(ConfigValueFactory.fromIterable(values.asJava, cl.origin().description()))
case other => Some(other)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment