-
-
Save Daenyth/fff7b3029da2ab52266760c52bc3a057 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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//> 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