Skip to content

Instantly share code, notes, and snippets.

@Baccata
Last active August 28, 2024 08:41
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.

Revisions

  1. Baccata revised this gist Aug 28, 2024. 1 changed file with 27 additions and 24 deletions.
    51 changes: 27 additions & 24 deletions hoconLeftovers.scala
    Original file line number Diff line number Diff line change
    @@ -60,17 +60,16 @@ object ConfigLeftovers {
    * 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) = {
    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)
    trackingConfig.toConfig()
    (collapseUnused(trackingConfig), result)
    (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. This
    /// 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)
    @@ -156,25 +155,29 @@ object ConfigLeftovers {
    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.forall(v => v.valueType() != ConfigValueType.OBJECT)) None
    else Some(ConfigValueFactory.fromIterable(values.asJava, cl.origin().description()))
    case other => Some(other)
    }
    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)
    }

    }
  2. Baccata revised this gist Aug 27, 2024. 1 changed file with 7 additions and 3 deletions.
    10 changes: 7 additions & 3 deletions hoconLeftovers.scala
    Original file line number Diff line number Diff line change
    @@ -20,7 +20,7 @@ object Main {
    implicit val reader: ConfigReader[C] = deriveReader
    }

    case class ABC(a: Int, b: String, c: List[C])
    case class ABC(a: Int, b: String, c: List[C], d: List[String])
    object ABC {
    implicit val reader: ConfigReader[ABC] = deriveReader
    }
    @@ -37,6 +37,8 @@ object Main {
    | value = 1
    | }
    | ]
    | d = ["foo", "bar"]
    | e = ["foo", "bar"]
    | unused2 = 33
    |}
    |""".stripMargin)
    @@ -61,13 +63,14 @@ object ConfigLeftovers {
    def using[A](config: Config)(f: ConfigValue => A): (Option[ConfigValue], A) = {
    val trackingConfig = transformObject(config.root())
    val result = f(trackingConfig)
    trackingConfig.toConfig()
    (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.
    /// Whenever a key is accessed, we add it to the list of keys that were queried. This
    override def get(key: Object): ConfigValue = {
    usedKeys += key.asInstanceOf[String]
    map.get(key)
    @@ -169,7 +172,8 @@ object ConfigLeftovers {
    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()))
    if (values.forall(v => v.valueType() != ConfigValueType.OBJECT)) None
    else Some(ConfigValueFactory.fromIterable(values.asJava, cl.origin().description()))
    case other => Some(other)
    }

  3. Baccata revised this gist Jun 6, 2024. 1 changed file with 0 additions and 1 deletion.
    1 change: 0 additions & 1 deletion hoconLeftovers.scala
    Original file line number Diff line number Diff line change
    @@ -61,7 +61,6 @@ object ConfigLeftovers {
    def using[A](config: Config)(f: ConfigValue => A): (Option[ConfigValue], A) = {
    val trackingConfig = transformObject(config.root())
    val result = f(trackingConfig)
    trackingConfig.toConfig()
    (collapseUnused(trackingConfig), result)
    }

  4. Baccata revised this gist Jun 6, 2024. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion hoconLeftovers.scala
    Original file line number Diff line number Diff line change
    @@ -68,7 +68,7 @@ object ConfigLeftovers {
    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. This
    /// 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)
  5. Baccata created this gist Jun 6, 2024.
    177 changes: 177 additions & 0 deletions hoconLeftovers.scala
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,177 @@
    //> 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)
    trackingConfig.toConfig()
    (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. This
    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)
    }

    }