//> 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], d: List[String]) object ABC { implicit val reader: ConfigReader[ABC] = deriveReader } val config = ConfigFactory.parseString("""|{ | a = 1 | b = "b" | c = [ | { | value = 3 | unused1 = {} | }, | { | value = 1 | } | ] | d = ["foo", "bar"] | e = ["foo", "bar"] | 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, ignoreKeys: Set[String] = Set.empty)(f: ConfigValue => A): (Option[ConfigValue], A) = { val trackingConfig = transformObject(config.root()) val result = f(trackingConfig) (collapseUnused(trackingConfig, ignoreKeys), 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, ignore: Set[String] = Set.empty): 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)) if !(ignore(key)) => (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.forall(v => v.valueType() != ConfigValueType.OBJECT)) None else Some(ConfigValueFactory.fromIterable(values.asJava, cl.origin().description())) case other => Some(other) } }