sksamuel/hoplite

Is there a way to load only a certain prefix?

Closed this issue · 7 comments

If I have a config file as follows

plugin1:
  value1: "a value"
  value2: "another value"
plugin2:
  username: "a username"

is it possible to load just the plugin1 settings directly into a data class of the following form somehow

data class Plugin1Settings(val value1: String, val value2: String)

or is there no way to load the data without having a "wrapper" object like this

data class Settings(val plugin1: Plugin1Settings)

As you can probably tell from the example, the use-case is for people adding 3rd party plugins where

  • You want the configuration to just go in the main configuration file with everything else
  • The plugin can't modify the "main" settings class used by the application (since it is a 3rd party plugin)
  • It therefore needs to only load its own configuration but from a config file containing other stuff - using a prefix to identify which plugin the configuration relates to

Obviously, the workaround is to create a wrapper class called settings which contains the actual Plugin1Settings with the right property name, but it's a slightly awkward way of doing things. I was hoping there might be some way of doing something along the lines of

ConfigLoader.builder.addResourceSource("application.yaml").loadConfigOrThrow<Plugin1Settings>(prefix = "plugin1")

i.e. load just the data with the given prefix into a class of this type, thus removing the need for the wrapper.

So if you could specify the prefix when you add the file source, would be that sufficient ?

That would probably work. My opinion is that it would be nicer if it was done at the loadConfigOrThrow stage (as that means you could have a set of sources containing configuration and load a subset of the data from across all of them without duplicating the prefix every time you added the source), but if that is a much harder implementation, then setting it per file would also work.

It's basically useful if you want to extract some information out of someone else's config file without needing to write matching data classes for every single element. Instead you'd just write data classes for the bits you wanted and then provide the "path" to the start element if that makes any sense.

Have a similar use case, migrating from Spring Boot where @ConfigurationProperties are bound by prefix.

This is really a separation of concerns - loading (once) of underlying properties (property sources, etc), and then - separately - binding properties to objects, based on a prefix.

In Spring the former is the "Environment" - all the resolved properties, with a PropertyResolver interface for consumers to access them; and way-overly-complex Binder objects that allow for something like this (my code, much more direct than the magical incantations of @ConfigurationProperties):

public inline fun <reified T : Any> Environment.bindConfigurationProperties(prefix: String): T {
    return Binder.get(this).bindOrCreate(prefix, Bindable.of(T::class.java))
}

Ideally we'd be able to do something like:

    val config = ConfigLoaderBuilder.default()
        .addResourceSource("/application-staging.props", optional = true, allowEmpty = true)
        .withExplicitSealedTypes()
        .build()

// above exists today,  sort of: ideally we could get a "Configuration" or "Environment" object back that can then be used as below (in addition to existing uses)

config.bind<MyConfigObject>(prefix = "abc")  // this would have the same semantics as config.loadConfigOrThrow<MyConfigObject>(), except that the config would already be loaded

Selective binding supports a wide range of use cases - one of the more important ones is allowing different components to have their own configuration (and possibly multiple, separately configured instances), without coupling everything into a global config.

I like the concept suggested by @tomhillgreyridge.

In addition (I'm not sure if this is part of the original suggestion), I would like to have the possibility to load "leaf" nodes, for example:

val plugin1Value2 = configLoader.loadConfigOrThrow<String>(prefix = "plugin1.value2")

I would also like to have the possibility to deal with nullable values, for example:

val plugin1Value2 = configLoader.loadConfigOrThrow<String?>(prefix = "plugin1.value2")

Looking for this capability as well. I'm building a system (a simplified no-annotation Spring Boot) in which configurations are modular, and the possible prefixes are not known by the configuration system in advance.

Just in case anybody wants it, I just built this in a project of mine:

class PrefixPreprocessor(val prefix: String) : Preprocessor {
  override fun process(
    node: Node,
    context: DecoderContext,
  ): ConfigResult<Node> {
    return node.atPath(prefix).valid()
  }
}

To be used like this:

inline fun <reified T> loadConfig(prefix: String? = null): T {
  val loader =
    ConfigLoaderBuilder.default()
      .addResourceSource("/config.yaml")
      .also {
        if (prefix != null) {
          it.addPreprocessor(PrefixPreprocessor(prefix))
        }
      }

  return loader.build().loadConfigOrThrow()
}

I won't find time to make this into a PR anytime soon, but maybe it helps others already like this (or someone else makes it into a PR) :-)

// above exists today, sort of: ideally we could get a "Configuration" or "Environment" object back that can then be used as below (in addition to existing uses)

config.bind(prefix = "abc") // this would have the same semantics as config.loadConfigOrThrow(), except that the config would already be loaded


Selective binding supports a wide range of use cases - one of the more important ones is allowing different components to have their own configuration (and possibly multiple, separately configured instances), without coupling everything into a global config.

Agree with this from @cloudshiftchris . At the moment, if one uses a preprocessor approach like @lenalebt above (thank you!), things work as expected, but loading of the property sources happens multiple times. This also results in some suboptimal behavior like a report printing per prefix.