A boilerplate-free Kotlin config library for loading configuration files as data classes

Overview

Hoplite

Hoplite is a Kotlin library for loading configuration files into typesafe classes in a boilerplate-free way. Define your config using Kotlin data classes, and at startup Hoplite will read from one or more config files, mapping the values in those files into your config classes. Any missing values, or values that cannot be converted into the required type will cause the config to fail with detailed error messages.

master

Features

  • Multiple formats: Write your configuration in several formats: Yaml, JSON, Toml, Hocon, or Java .properties files or even mix and match formats in the same system.
  • Property Sources: Per-system overrides are possible from JVM system properties, environment variables, JDNI or a per-user local config file.
  • Batteries included: Support for many standard types such as primitives, enums, dates, collection types, inline classes, uuids, nullable types, as well as popular Kotlin third party library types such as NonEmptyList, Option and TupleX from Arrow.
  • Custom Data Types: The Decoder interface makes it easy to add support for your custom domain types or standard library types not covered out of the box.
  • Cascading: Config files can be stacked. Start with a default file and then layer new configurations on top. When resolving config, lookup of values falls through to the first file that contains a definition. Can be used to have a default config file and then an environment specific file.
  • Beautiful errors: Fail fast when the config objects are built, with detailed and beautiful errors showing exactly what went wrong and where.

Changelog

See the list of changes in each release here.

Getting Started

Add Hoplite to your build:

implementation 'com.sksamuel.hoplite:hoplite-core:<version>'

You will also need to include a module for the format(s) you to use.

Next define the data classes that are going to contain the config. You should create a top level class which can be named simply Config, or ProjectNameConfig. This class then defines a field for each config value you need. It can include nested data classes for grouping together related configs.

For example, if we had a project that needed database config, config for an embedded HTTP server, and a field which contained which environment we were running in (staging, QA, production etc), then we may define our classes like this:

data class Database(val host: String, val port: Int, val user: String, val pass: String)
data class Server(val port: Int, val redirectUrl: String)
data class Config(val env: String, val database: Database, val server: Server)

For our staging environment, we may create a YAML (or Json, etc) file called application-staging.yaml:

env: staging

database:
  host: staging.wibble.com
  port: 3306
  user: theboss
  pass: 0123abcd

server:
  port: 8080
  redirectUrl: /404.html

Finally, to build an instance of Config from this file, and assuming the config file was on the classpath, we can simply execute:

val config = ConfigLoader().loadConfigOrThrow<Config>("/application-staging.yaml")

If the values in the config file are compatible, then an instance of Config will be returned. Otherwise an exception will be thrown containing details of the errors.

Config Loader

As you have seen from the getting started guide, ConfigLoader is the entry point to using Hoplite. We can create an instance of this loader directly for simple cases, or use the ConfigLoader.Builder if we need to customize how the loader works. Then we can load config into data classes from resources on the classpath, java.io.File, java.nio.Path, or URLS.

There are two ways to retrieve a populated data class from config. The first is to throw an exception if the config could not be resolved via the loadConfigOrThrow<T> function. Another is to return a ConfigResult via the loadConfig<T> function.

For most cases, when you are resolving config at application startup, the exception based approach is better. This is because you typically want any errors in config to abort application bootstrapping, dumping errors to the console.

Beautiful Errors

When an error does occur, if you choose to throw an exception, the errors will be formatted in a human readable way along with as much location information as possible. No more trying to track down a NumberFormatException in a 400 line config file.

Here is an example of the error formatting for a test file used by the unit tests.

Error loading config because:

    - Could not instantiate 'com.sksamuel.hoplite.json.Foo' because:

        - 'bar': Required type Boolean could not be decoded from a Long (/error1.json:2:19)

        - 'baz': Missing from config

        - 'hostname': Type defined as not-null but null was loaded from config (/error1.json:6:18)

        - 'season': Required a value for the Enum type com.sksamuel.hoplite.json.Season but given value was Fun (/error1.json:8:18)

        - 'users': Defined as a List but a Boolean cannot be converted to a collection (/error1.json:3:19)

        - 'interval': Required type java.time.Duration could not be decoded from a String (/error1.json:7:26)

        - 'nested': - Could not instantiate 'com.sksamuel.hoplite.json.Wibble' because:

            - 'a': Required type java.time.LocalDateTime could not be decoded from a String (/error1.json:10:17)

            - 'b': Unable to locate a decoder for java.time.LocalTime

Supported Formats

Hoplite supports config files in several formats. You can mix and match formats if you really want to. For each format you wish to use, you must include the appropriate hoplite module on your classpath. The format that hoplite uses to parse a file is determined by the file extension.

Format Module File Extensions
Json hoplite-json .json
Yaml hoplite-yaml .yml, .yaml
Toml hoplite-toml .toml
Hocon hoplite-hocon .conf
Java Properties files built-in .props, .properties

If you wish to add another format you can extend Parser and provide an instance of that implementation to the ConfigLoader.Builder via withFileExtensionMapping.

That same function can be used to map non-default file extensions to an existing parser. For example, if you wish to have your config in files called application.data but in yaml format, then you can register .data with the Yaml parser like this:

ConfigLoader.Builder().addFileExtensionMapping("data", YamlParser).build()

Property Sources

The PropertySource interface is how Hoplite reads configuration values.

Hoplite supports several built in property source implementations, and you can write your own if required.

The EnvironmentVariablesPropertySource, SystemPropertiesPropertySource and UserSettingsPropertySource sources are automatically registered, with precedence in that order. Other property sources can be passed to the config loader builder.

EnvironmentVariablesPropertySource

The EnvironmentVariablesPropertySource reads config from environment variables. It does not map cases so HOSTNAME does not provide a value for a field with the name hostname.

For nested config, use a period to seperate keys, for example topic.name would override name located in a topic parent. Alternatively, in some environments a . is not supported in ENV names, so you can also use double underscore __. Eg topic__name would override name in a Topic object.

Optionally you can also create a EnvironmentVariablesPropertySource with allowUppercaseNames set to true to allows for uppercase-only names.

SystemPropertiesPropertySource

The SystemPropertiesPropertySource provides config through system properties that are prefixed with config.override.. For example, starting your JVM with -Dconfig.override.database.name would override a config key of database.name residing in a file.

UserSettingsPropertySource

The UserSettingsPropertySource provides config through a config file defined at ~/.userconfig.[ext] where ext is one of the supported formats.

InputStreamPropertySource

The InputStreamPropertySource provides config from an input stream. This source requires a parameter that indicates what the format is. For example, InputStreamPropertySource(input, "yml")

ConfigFilePropertySource

Config from files or resources are retrieved via instances of ConfigFilePropertySource. This property source is added automatically when we pass strings, Files or Paths to the loadConfigOrThrow or loadConfig functions.

There are convenience methods on PropertySource to construct ConfigFilePropertySources from resources on the classpath or files.

For example, the following are equivalent:

ConfigLoader().loadConfig<MyConfig>("config.json")

and

ConfigLoader.Builder()
   .addSource(PropertySource.resource("/config.json"))
   .build()
   .loadConfig<MyConfig>()

The advantage of the second approach is that we can specify a file can be optional, for example:

ConfigLoader.Builder()
  .addSource(PropertySource.resource("/missing.yml", optional = true))
  .addSource(PropertySource.resource("/config.json"))
  .build()
  .loadConfig<MyConfig>()

JsonPropertySource

To use a JSON string as a property source, we can use the JsonPropertySource implementation. For example,

ConfigLoader.Builder()
   .addSource(JsonPropertySource(""" { "database": "localhost", "port": 1234 } """))
   .build()
   .loadConfig<MyConfig>()

YamlPropertySource

To use a Yaml string as a property source, we can use the YamlPropertySource implementation.

ConfigLoader.Builder()
   .addSource(YamlPropertySource(
     """
        database: "localhost"
        port: 1234
     """))
   .build()
   .loadConfig<MyConfig>()

TomlPropertySource

To use a Toml string as a property source, we can use the TomlPropertySource implementation.

ConfigLoader.Builder()
  .addSource(TomlPropertySource(
    """
        database = "localhost"
        port = 1234
     """))
  .build()
  .loadConfig<MyConfig>()

Cascading Config

Hoplite has the concept of cascading or layered or fallback config. This means you can pass more than one config file to the ConfigLoader. When the config is resolved into Kotlin classes, a lookup will cascade or fall through one file to another in the order they were passed to the loader, until the first file that defines that key.

For example, if you had the following two files in yaml:

application.yaml:

elasticsearch:
  port: 9200
  clusterName: product-search

application-prod.yaml:

elasticsearch:
  host: production-elasticsearch.mycompany.internal
  port: 9202

And both were passed to the ConfigLoader like this: ConfigLoader().loadConfigOrThrow<Config>("/application-prod.yaml", "/application.yaml"), then lookups will be attempted in the order the files were declared. So in this case, the config would be resolved like this:

elasticsearch.port = 9202 // the value in application-prod.yaml takes priority over the value in application.yaml
elasticsearch.host = production-elasticsearch.mycompany.internal
elasitcsearch.clusterName = product-search // not defined in application-prod.yaml so falls through to application.yaml

Let's see a more complicated example. In JSON this time.

default.json

{
  "a": "alice",
  "b": {
    "c": true,
    "d": 123
  },
  "e": [
    {
      "x": 1,
      "y": true
    },
    {
      "x": 2,
      "y": false
    }
  ],
  "f": "Fall"
}

prod.json

{
  "a": "bob",
  "b": {
    "d": 999
  },
  "e": [
    {
      "y": true
    }
  ]
}

And we will parse the above config files into these data classes:

enum class Season { Fall, Winter, Spring, Summer }
data class Foo(val c: Boolean, val d: Int)
data class Bar(val x: Int?, val y: Boolean)
data class Config(val a: String, val b: Foo, val e: List<Bar>, val f: Season)
val config = ConfigLoader.load("prod.json", "default.json")
println(config)

The resolution rules are as follows:

  • "a" is present in both files and so is resolved from the first file - which was "prod.json"
  • "b" is present in both files and therefore resolved from the file file as well
  • "c" is a nested value of "b" and is not present in the first file so is resolved from the second file "default.json"
  • "d" is a nested value of "b" present in both files and therefore resolved from the first file
  • "e" is present in both files and so the entire list is resolved from the first file. This means that the list only contains a single element, and x is null despite being present in the list in the first file. List's cannot be merged.
  • "f" is only present in the second file and so is resolved from the second file.

Strict Mode

Hoplite can be configured to throw an error if a config value is not used. This is useful to detect stale configs.

To enable this setting, use .strict() on the config builder. For example:

ConfigLoader.Builder()
  .addSource(PropertySource.resource("/config-prd.yml", true))
  .addSource(PropertySource.resource("/config.yml"))
  .strict()
  .build()
  .loadConfig<MyConfig>()

Aliases

If you wish to refactor your config classes and rename a field, but you don't want to have to update all your config files, you can add a migration path by allowing a field to use more than one name. To do this we use the @ConfigAlias annotation.

For example, with this config file:

database:
  host: String

We can marshall this into the following data classes.

data class Database(val host: String)
data class MyConfig(val database: Database)

or

data class Database(@ConfigAlias("host") val hostname: String)
data class MyConfig(val database: Database)

Param Mappers

Hoplite provides an interface ParameterMapper which allows the parameter name to be modified before it is looked up inside a config source. This allows hoplite to find config keys which don't match the exact name. The main use case for this is to allow snake_case or kebab-case names to be used as config keys.

For example, given the following config class:

data class Database(val instanceHostName: String)

Then we can of course define our config file (using YML as an example):

database:
    instanceHostName: server1.prd

But because Hoplite registers KebabCaseParamMapper and SnakeCaseParamMapper automatically, we can just as easily use:

database:
  instance-host-name: server1.prd

Decoders

Hoplite converts the raw value in config files to JDK types using instances of the Decoder interface. There are built in decoders for all the standard day to day types, such as primitives, dates, lists, sets, maps, enums, arrow types and so on. The full list is below:

Basic JDK Types Conversion Notes
String
Long
Int
Short
Byte
Boolean Creates a Boolean from the following values: "true", "t", "1", "yes" map to true and "false", "f", "0", "no" map to false
Double
Float
Enums Java and Kotlin enums are both supported. An instance of the defined Enum class will be created with the constant value given in config.
BigDecimal Converts from a String, Long, Int, Double, or Float into a BigDecimal
BigInteger Converts from a String, Long or Int into a BigInteger.
UUID Creates a java.util.UUID from a String
java.time types
LocalDateTime
LocalDate
LocalTime
Duration Converts a String into a Duration, where the string uses a value and unit such as "10 seconds" or "5m". The set of units supported is the same as here. Also supports a long value which will be interpreted as a Duration of milliseconds.
Instant Creates an instance of Instant from an offset from the unix epoc in milliseconds.
Year Creates an instance of Year from a String in the format 2007
YearMonth Creates an instance of YearMonth from a String in the format 2007-12
MonthDay Creates an instance of MonthDay from a String in the format 08-18
java.util.Date
java.net types
URI
URL
InetAddress
JDK IO types
File Creates a java.io.File from a String path
Path Creates a java.nio.Path from a String path
Kotlin stdlib types
Pair<A,B> Converts from an array of three two into an instance of Pair<A,B>. Will fail if the array does not have exactly two elements.
Triple<A,B,C> Converts from an array of three elements into an instance of Triple<A,B,C>. Will fail if the array does not have exactly three elements.
kotlin.text.Regex Creates a kotlin.text.Regex from a regex compatible string
Collections
List<A> Creates a List from either an array or a string delimited by commas.
Set<A> Creates a Set from either an array or a string delimited by commas.
SortedSet<A> Creates a SortedSet from either an array or a string delimited by commas.
Map<K,V>
LinkedHashMap<K,V> A Map that mains the order defined in config
hoplite types
Masked Wraps a String in a Masked object that redacts toString()
SizeInBytes Returns a SizeInBytes object which parses values like 12Mib or 9KB
javax.security.auth
X500Principal Creates an instance of X500Principal for String values
KerberosPrincipal Creates an instance of KerberosPrincipal for String values
JMXPrincipal Creates an instance of JMXPrincipal for String values
Principal Creates an instance of BasicPrincipal for String values
Arrow Requires hoplite-arrow module
arrow.data.NonEmptyList<A> Converts arrays into a NonEmptyList<A> if the array is non empty. If the array is empty then an error is raised.
arrow.core.Option<A> A None is used for null or undefined values, and present values are converted to a Some<A>.
arrow.core.Tuple2<A,B> Converts an array of two elements into an instance of Tuple2<A,B>. Will fail if the array does not have exactly two elements.
arrow.core.Tuple3<A,B,C> Converts an array of three elements into an instance of Tuple3<A,B,C>. Will fail if the array does not have exactly three elements.
arrow.core.Tuple4<A,B,C,D> Converts an array of four elements into an instance of Tuple4<A,B,C,D>. Will fail if the array does not have exactly four elements.
arrow.core.Tuple5<A,B,C,D,E> Converts an array of five elements into an instance of Tuple5<A,B,C,D,E>. Will fail if the array does not have exactly five elements.
Hikari Connection Pool Requires hoplite-arrow module
HikariDataSource Converts nested config into a HikariDataSource. Any keys nested under the field name will be passed through to the HikariConfig object as the datasource is created. Requires hoplite-hikaricp module
Hadoop Types Requires hoplite-hdfs module
org.apache.hadoop.fs.Path Returns instances of HDFS Path objects
CronUtils types Requires hoplite-cronutils module
com.cronutils.model.Cron Returns parsed instance of a cron expression
kotlinx datetime Types Requires hoplite-datetime module
kotlinx.datetime.LocalDateTime
kotlinx.datetime.LocalDate
kotlinx.datetime.Instant
AWS SDK types Requires hoplite-aws module
com.amazonaws.regions.Region

Preprocessors

Hoplite supports what it calls preprocessors. These are just functions that are applied to every value as they are read from the underlying config file. The preprocessor is able to transform the value (or return the input - aka identity function) depending on the logic of that preprocessor.

For example, a preprocessor may choose to perform environment variable substitution, configure default values, perform database lookups, or whatever other custom action you need when the config is being resolved.

You can add custom pre-processors in addition to the built in ones, by using the function withPreprocessor on the ConfigLoader class, and passing in an instance of the Preprocessor interface. A typical use case of a custom preprocessor is to lookup some values in a database, or from a third party secrets store such as Vault or Amazon Parameter Store.

One way this can be implemented is to have a prefix, and then use a preprocessor to look for the prefix in strings, and if the prefix is present, use the rest of the string as a key to the service. The PrefixProcessor abstract class implements this by handling the node traversal, while leaving the specific processing as an exercise for the reader.

For example

database:
  user: root
  password: vault:/my/key/path

Built-in Preprocessors

These built-in preprocessors are registered automatically.

Preprocessor Function
EnvVar Preprocessor Replaces any strings of the form ${VAR} with the environment variable $VAR if defined. These replacement strings can occur between other strings.

For example foo: hello ${USERNAME}! would result in foo being assigned the value hello Sam! assuming the env var USERNAME was set to SAM. Also the expressions can have default values using the usual bash expression style syntax foo: hello ${USERNAME:-fallback}!
System Property Preprocessor Replaces any strings of the form ${VAR} with the system property $VAR if defined. These replacement strings can occur between other strings.

For example debug: ${DEBUG} would result in debug being assigned the value true assuming the application had been started with -Ddebug=true
Random Preprocessor Inserts random strings into the config. See the section on Random Preprocessor for syntax.
Props File Preprocessor Replaces any strings of the form ${key} with the value of the key in a provided java.util.Properties file. The file can be specified by a Path or a resource on the classpath.
Lookup Preprocessor Replaces any strings of the form {{key}} with the value of that node in the already parsed config. In other words, this allow substitution from config in one place to another place (even across files).

Optional Preprocessors

These preprocessors must be added to the ConfigBuilder before they take effect, and require extra modules to be added to the build.

Preprocessor Function
ParameterStorePreprocessor Replaces strings of the form ${ssm:key} by looking up the value of 'key' from the AWS Systems Manager Parameter Store.

This preprocessor requires the hoplite-aws module to be added to the classpath.
AwsSecretsManagerPreprocessor Replaces strings of the form ${awssecret:key} by looking up the value of 'key' from the AWS Secrets Manager.

This preprocessor requires the hoplite-aws module to be added to the classpath.
ConsulConfigPreprocessor Replaces strings of the form ${consul:key} by looking up the value of 'key' from a Consul server.

This preprocessor requires the hoplite-consul module to be added to the classpath.

Random Preprocessor

The random preprocessor replaces placeholder strings with random values.

Placeholder Generated random value
${random.int} A random int
${random.int(k)} A positive random int between 0 and k
${random.int(k, j)} A random int between k and j
${random.double} A random double
${random.boolean A random boolean
${random.string(k)} A random alphanumeric string of length k
${random.uuid} A randomly generated type 4 UUID

For example:

my.number=${random.int}
my.bignumber=${random.long}
my.uuid=${random.uuid}
my.number.less.than.ten=${random.int(10)}
my.number.in.range=${random.int[1024,65536]}

Masked values

It is quite common to output the resolved config at startup for reference when debugging. In this case, the default toString generated by Kotlin's data classes is very useful. However configuration typically includes sensitive information such as passwords or keys which normally you would not want to appear in logs.

To avoid sensitive fields appearing in the log output, Hoplite provides a built in type called Masked which is a wrapper around a String. By declaring a field to have this type, the value will still be loaded from configuration files, but will not be included in the generated toString.

For example, you may define a config class like this:

data class Database(val host: String, val user: String, val password: Masked)

And corresponding json config:

{
  "host": "localhost",
  "user": "root",
  "password": "letmein"
}

And then the output of the Database config class via toString would be Database(host=localhost, user=root, password=****)

Note: The masking effect only happens if you use toString. If you marshall your config to a String using a reflection based tool like Jackson, it will still be able to see the underlying value. In these cases, you would need to register a custom serializer. For the Jackson project, a HopliteModule object is available in the hoplite-json module. Register this with your Jackson mapper, like mapper.registerModule(HopliteModule) and then Masked values will be ouputted into Json as "****"

Inline Classes

Some developers, this writer included, like to have strong types wrapping simple values. For example, a Port object rather than an Int. This helps to alleviate Stringy typed development. Kotlin has support for what it calls inline classes which fulfil this need.

Hoplite directly supports inline classes. When using inline classes, you don't need to nest config keys.

For example, given the following config classes:

inline class Port(val value: Int)
inline class Hostname(val value: String)
data class Database(val port: Port, val host: Hostname)

And then this config file:

port: 9200
host: localhost

We can parse directly:

val config = ConfigLoader().loadConfigOrThrow<Database>("config.file")
println(config.port) // Port(9200)
println(config.host) // Hostname("localhost")

Sealed Classes

Hoplite will support sealed classes where it is able to match up the available config keys with the parameters of one of the implementations. For example, lets create a config hierarchy as implementations of a sealed class.

sealed class Database {
  data class Elasticsearch(val host: String, val port: Int, val index: String) : Database()
  data class Postgres(val host: String, val port: Int, val schema: String, val table: String) : Database()
}

data class TestConfig(val databases: List<Database>)

For the above definition, if hoplite encountered a host, port, and index then it would be clear that it should instantiate an Elasticsearch instance. Similarly, if the config keys were host, port, schema, and table, then the Postgres implementation should be used. If the keys don't match an implementation, the config loader would fail. If keys match multiple implementations then the first match is taken.

For example, the following yaml config file could be used:

databases:
  - host: localhost
    port: 9200
    index: foo
  - host: localhost
    port: 9300
    index: bar
  - host: localhost
    port: 5234
    schema: public
    table: faz

And the output would be:

TestConfig(
  databases=[
    Elasticsearch(host=localhost, port=9200, index=foo),
    Elasticsearch(host=localhost, port=9300, index=bar),
    Postgres(host=localhost, port=5234, schema=public, table=faz)
  ]
)

Objects in sealed classes

Hoplite additionally supports using objects in sealed classes. For example, lets expand the database definition to include an Embedded object subclass:

sealed class Database {
  data class Elasticsearch(val host: String, val port: Int, val index: String) : Database()
  data class Postgres(val host: String, val port: Int, val schema: String, val table: String) : Database()
  object Embedded : Database()
}

data class TestConfig(val databases: List<Database>)

We can indicate to Hoplite to use the Embedded option in two ways. The first is by referencing the type name:

For example, in yaml:

database: Embedded

Or in Json:

{
  "database": "Embedded"
}

This also works for lists, and we can mix and match:

Yaml:

databases:
  - "Embedded"
  - host: localhost
    port: 9300
    index: bar

Json:

{
  "databases": ["Embedded", { "host": "localhost", "port": 9200, "index": "foo" }]
}

The second method is only for Json by specifying an empty object:

{
  "database": { }
}

When using the second option, there must be only a single object instance in the hierarchy, otherwise a disambiguation error is thrown. If you want to support multiple object instances, then refer to the type by name.

Add on Modules

Hoplite makes available several other modules that add functionality outside of the main core module. They are in seperate modules because they bring in dependencies from those projects and so the modules are optional.

Module Function
hoplite-arrow Provides decoders for common arrow types
hoplite-aws Provides decoder for aws Region and a preprocessor for Amazon's parameter store
hoplite-consul Provides a preprocessor for retreiving values from a Consul server
hoplite-datetime Provides decoders for kotlinx datetime. Requires Kotlin 1.4.x
hoplite-hdfs Provides decoder for hadoop Path
hoplite-hikaricp Provides decoder for HikariDataSource
hoplite-javax Provides decoders for Principals
hoplite-vavr Provides decoders for vavr

License

This software is licensed under the Apache 2 license, quoted below.

Copyright 2019 Stephen Samuel

Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy of
the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations under
the License.
Comments
  • AWS Parameter Store get-parameters-by-path support

    AWS Parameter Store get-parameters-by-path support

    AWS Parameter Store allows you to query for all params by a path prefix. As an example, here's the aws-cli usage:

    aws ssm get-parameters-by-path --recursive --path /test --with-decryption
    

    It would be nice if hoplite supported this. I'm not sure if it should be passed into the ParameterStorePreprocessor or decoratively via yaml (application.yml). Ideally, the user would be able to choose the path-prefix dynamically. E.g., choosing the path by ENV_NAME of the running app. /$env/$appName

    For reference, Spring Boot's cloud config has this functionality as well.

    opened by chrsblck 22
  • Upgrading from 1.4.9 to more recent versions throws NoSuchElementException

    Upgrading from 1.4.9 to more recent versions throws NoSuchElementException

    I've tried upgrading from 1.4.9 to 1.4.10 and above, but I get the following stacktrace:

    java.util.NoSuchElementException: List is empty.
            at kotlin.collections.CollectionsKt___CollectionsKt.first(_Collections.kt:212)
            at com.sksamuel.hoplite.decoder.DataClassDecoder.safeDecode(DataClassDecoder.kt:116)
            at com.sksamuel.hoplite.decoder.NullHandlingDecoder$DefaultImpls.decode(Decoder.kt:85)
            at com.sksamuel.hoplite.decoder.DataClassDecoder.decode(DataClassDecoder.kt:28)
            at com.sksamuel.hoplite.ConfigLoader.decode(ConfigLoader.kt:400)
            at com.sksamuel.hoplite.ConfigLoader.loadConfig(ConfigLoader.kt:386)
    ...
    

    I haven't narrowed what in our config is causing this as yet, but if I do, I'll update the issue with the details.

    opened by chris-ryan-square 21
  • Using strict() throws an error loading config listing unused Environment Variables.

    Using strict() throws an error loading config listing unused Environment Variables.

    Using the following example:

    application.yaml

    foo:
       enabled: true
    

    and this code

    import com.sksamuel.hoplite.ConfigLoader
    import com.sksamuel.hoplite.PropertySource
    
    object Application {
    
        @JvmStatic
        fun main(args: Array<String>) {
            ConfigLoader.Builder()
                .addSource(PropertySource.resource("/application.yaml"))
                .strict()
                .build()
                .loadConfigOrThrow<Config>()
        }
    }
    
    data class Config(val foo: Foo)
    data class Foo(val enabled: Boolean)
    

    fails, with an error similar to:

    Exception in thread "main" com.sksamuel.hoplite.ConfigException: Error loading config because:
    
        Config values were not used: PATH, ...snip...
    	at com.sksamuel.hoplite.ConfigLoader$returnOrThrow$1.invoke(ConfigLoader.kt:335)
    	at com.sksamuel.hoplite.ConfigLoader$returnOrThrow$1.invoke(ConfigLoader.kt:20)
    	at com.sksamuel.hoplite.fp.ValidatedKt.getOrElse(Validated.kt:93)
    	at com.sksamuel.hoplite.ConfigLoader.returnOrThrow(ConfigLoader.kt:332)
    ...
    

    Can the Environment, System and UserSettings property sources be optional for strict?

    Perhaps change strict() to accept override flags (with defaults matching current usage):

    strict(
      includeEnvironmentVariables = true, 
      includeSystemProperties = true,
      includeUserSettings = true
    )
    
    enhancement pinned 
    opened by chris-ryan-square 21
  • ClasspathPropertySource?

    ClasspathPropertySource?

    I'm planning on using this library in one of my projects. I'm planning to put the base configuration in my classpath / inside the jar and let the user override it with a local file. Would that be possible?
    Oh, and would something like setting a default value on a class work?

    opened by H4xX0r1337 19
  • NoSuchFieldError: Companion

    NoSuchFieldError: Companion

    upgrade from v1.0.8 to v1.1.2 and ran into this error: NoSuchFieldError: Companion

    • also cpu at 100%

    downgraded back to v1.0.8 and error disappeared.

    config file:

    env: production
    
    redis:
      url: "redis://localhost:6379/"
      requestChannel: "foo"
      responseChannel: "foo"
    
    odb:
      home: "db"
      configFile: "config/production.conf"
      dbFile: "engine.db" 
      server: false
      # dbFile ='abc
    

    config classes:

    data class EngineConfig(
            val env: String,
            val redis: RedisConfig,
            val odb: ObjectDbConfig)
    
    data class RedisConfig(
            val url: String,
            val requestChannel: String,
            val responseChannel: String)
    
    data class OdbConfig(
            val home: String,
            val configFile: String,
            val dbFile: String,
            val server: Boolean)
    
    bug wontfix 
    opened by davidmoshal 18
  • Cannot find YamlParser or DataClassDecoder at runtime

    Cannot find YamlParser or DataClassDecoder at runtime

    I am using Kotlin 1.6.21 with Intellij IDEA 2022.1 and Hoplite 2.0.4. My Gradle is version 7.4.2, and I am compiling into a fat jar via the gradle shadow jar plugin version 7.1.2.

    I have been working on a Minecraft plugin that loads its configurations from a YAML file via Hoplite. I bundle all dependencies via shadow because including them as a separate JAR would be inconvenient for somebody downloading the plugin. In my code, I load my configuration as follows:

    val config = ConfigLoaderBuilder.default()
                    .addDecoder(CommonDecoder())
                    .addResourceSource(path.toString())
                    .build()
                    .loadConfigOrThrow<Config>()
    

    However, during runtime, I encounter the following error:

    [00:31:36 INFO]: [Gateway] Configuration failed to load: Error loading config because:
    
        Could not detect parser for file extension '.yaml' - available parsers are {}
    
        Could not find config file plugins\Gateway\config.yaml
    

    I have both hoplite-core and hoplite-yaml including in my build.gradle.kts. I am aware of both #94 and #204, however, merging service files does not appear to be solving the issue for me. This error will occur, in some form, even when I add the YamlParser manually. In that case, it instead complains about being unable to find the DataClassDecoder, and recommends merging services if using a fat jar. This, though, is already being done. In my build.gradle.kts, I am doing this:

    shadowJar {
            archiveBaseName.set(rootProject.name)
            archiveClassifier.set("")
            archiveVersion.set(rootProject.version.toString())
            mergeServiceFiles()
        }
    

    Regardless of the inclusion, the error still occurs during runtime, even when conducting a clean build. My resultant jar always looks like this, even when merging service files: image

    I've tried every variation of merging services I can think of, but to no avail. Any ideas would be greatly appreciated. Thank you for your time. I apologize if the fix is something particularly obvious that I may have missed; my familiarity with the construction of JAR files is limited.

    opened by nicholasgrose 15
  • Cannot override hocon configuration with env variables

    Cannot override hocon configuration with env variables

    Hello,

    It seems that when I try to override my HOCON configuration with env variables like this:

    mongo {
      hostname = "localhost"
      hostname = ${?MONGO_HOSTNAME}
    }
    

    and load configuration with:

    val config = ConfigLoader().loadConfigOrThrow<Conf>("/application.conf")

    hoplite throws the following error:

    com.typesafe.config.ConfigException$NotResolved: called valueType() on value with unresolved substitutions, need to Config#resolve() first, see API docs at com.typesafe.config.impl.ConfigDelayedMerge.valueType(ConfigDelayedMerge.java:46) at com.sksamuel.hoplite.hocon.ValueProduction.invoke(HoconParser.kt:51) at com.sksamuel.hoplite.hocon.MapProduction.invoke(HoconParser.kt:41) at com.sksamuel.hoplite.hocon.ValueProduction.invoke(HoconParser.kt:52) at com.sksamuel.hoplite.hocon.MapProduction.invoke(HoconParser.kt:41) at com.sksamuel.hoplite.hocon.HoconParser.load(HoconParser.kt:27) at com.sksamuel.hoplite.ConfigFilePropertySource$node$1.invoke(PropertySource.kt:92) at com.sksamuel.hoplite.ConfigFilePropertySource$node$1.invoke(PropertySource.kt:86) at com.sksamuel.hoplite.arrow.ValidationsKt.ap(validations.kt:23) at com.sksamuel.hoplite.ConfigFilePropertySource.node(PropertySource.kt:91) at com.sksamuel.hoplite.ConfigLoader.loadNode(ConfigLoader.kt:161) at com.sksamuel.hoplite.ConfigLoader.loadConfig(ConfigLoader.kt:149)

    This is the default HOCON way of overriding with env variables, is there an alternative syntax to use?

    bug 
    opened by psilos 12
  • Allow parsing empty files without throwing error

    Allow parsing empty files without throwing error

    I'd like to be able to pass in empty files and not have the yml parser throw, such as:

    Caused by: java.lang.IllegalArgumentException: Expected document start at  in 'reader', line 1, column 1:
        
        ^
    	at com.sksamuel.hoplite.yaml.YamlParser.load(YamlParser.kt:38)
    	at com.sksamuel.hoplite.sources.ConfigFilePropertySource$node$1.invoke(ConfigFilePropertySource.kt:36)
    	at com.sksamuel.hoplite.sources.ConfigFilePropertySource$node$1.invoke(ConfigFilePropertySource.kt:36)
    	at com.sksamuel.hoplite.fp.Validated$Companion.ap(Validated.kt:70)
    	at com.sksamuel.hoplite.sources.ConfigFilePropertySource.node(ConfigFilePropertySource.kt:36)
    	at com.sksamuel.hoplite.NodeParser.parseNode(NodeParser.kt:29)
    	at com.sksamuel.hoplite.ConfigLoader.loadConfig(ConfigLoader.kt:136)
    	at com.smartnews.cg.configuration.BuildConfigKt.<clinit>(BuildConfig.kt:169)
    	... 3 more
    

    What do you think?

    opened by andrew-smartnews 11
  • Empty or missing config fails even if config class has defaults

    Empty or missing config fails even if config class has defaults

    This is with version 2.0.2

    This test should pass, with the config returned the default MyConfig().

    import com.sksamuel.hoplite.ConfigLoader
    import io.kotest.core.spec.style.FreeSpec
    
    class FooTest : FreeSpec({
      "x" {
        val config = ConfigLoader().loadConfigOrThrow<MyConfig>()
        println("My config is: $config")
      }
    })
    
    data class MyConfig(
      val name: String = "",
      val nested: NestedConfig = NestedConfig(),
    )
    
    data class NestedConfig(
      val nums: List<Int> = emptyList(),
    )
    

    It fails with:

    Error loading config because:

    Missing from config
    

    com.sksamuel.hoplite.ConfigException: Error loading config because:

    Missing from config
    

    at app//com.sksamuel.hoplite.ConfigLoader$returnOrThrow$1.invoke(ConfigLoader.kt:147) at app//com.sksamuel.hoplite.ConfigLoader$returnOrThrow$1.invoke(ConfigLoader.kt:144) at app//com.sksamuel.hoplite.fp.ValidatedKt.getOrElse(Validated.kt:93) at app//com.sksamuel.hoplite.ConfigLoader.returnOrThrow(ConfigLoader.kt:144) at app//com.squareup.cash.lease.FooTest$1$1.invokeSuspend(FooTest.kt:34) at app//com.squareup.cash.lease.FooTest$1$1.invoke(FooTest.kt) at app//com.squareup.cash.lease.FooTest$1$1.invoke(FooTest.kt)

    opened by chris-ryan-square 11
  • KotlinReflectionInternalError when a kotlin.time.Duration parameter has a default value

    KotlinReflectionInternalError when a kotlin.time.Duration parameter has a default value

    Cause: Duration is an inline class (a value class annotated with @JvmInline). There is a known bug KT-27598, causing the error when a constructor with inline class parameters is called via reflection.

    Versions used:

    version.com.sksamuel.hoplite..hoplite-hocon=1.4.14
    version.kotlin=1.5.31 and 1.6.0
    version.kotlinx.datetime=0.3.1
    

    Source:

    import com.sksamuel.hoplite.ConfigLoader
    import com.sksamuel.hoplite.PropertySource
    import kotlin.time.Duration
    import kotlin.time.ExperimentalTime
    
    @OptIn(ExperimentalTime::class)
    data class Config constructor(
        val medium: Duration = Duration.days(1),
        val high: Duration = Duration.days(10)
    )
    
    fun main() {
        val config = ConfigLoader.Builder()
            .addPropertySource(
                PropertySource.string(
                    """
                        medium = 4 hours
                    """.trimIndent(), "props"
                )
            )
            .build()
            .loadConfigOrThrow<Config>()
    
        println("$config")
    }
    

    Build script modifications:

    tasks.withType<KotlinCompile>() {
        kotlinOptions {
            jvmTarget = "11"
            freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn")
        }
    }
    

    Exception produced:

    Exception in thread "main" kotlin.reflect.jvm.internal.KotlinReflectionInternalError: This callable does not support a default call: public constructor Config(medium: kotlin.time.Duration = ..., high: kotlin.time.Duration = ...) defined in Config[DeserializedClassConstructorDescriptor@22bac7bc]
    	at kotlin.reflect.jvm.internal.KCallableImpl.callDefaultMethod$kotlin_reflection(KCallableImpl.kt:164)
    	at kotlin.reflect.jvm.internal.KCallableImpl.callBy(KCallableImpl.kt:112)
    	at com.sksamuel.hoplite.decoder.DataClassDecoder.construct(DataClassDecoder.kt:128)
    	at com.sksamuel.hoplite.decoder.DataClassDecoder.safeDecode(DataClassDecoder.kt:116)
    	at com.sksamuel.hoplite.decoder.NullHandlingDecoder$DefaultImpls.decode(Decoder.kt:85)
    	at com.sksamuel.hoplite.decoder.DataClassDecoder.decode(DataClassDecoder.kt:28)
    	at com.sksamuel.hoplite.ConfigLoader.decode(ConfigLoader.kt:400)
    	at com.sksamuel.hoplite.ConfigLoader.loadConfig(ConfigLoader.kt:386)
    	at MainKt.main(Main.kt:26)
    	at MainKt.main(Main.kt)
    
    pinned 
    opened by OliverO2 11
  • Added order to defaults (Sources, Preprocessors and ParamMappers)

    Added order to defaults (Sources, Preprocessors and ParamMappers)

    The current implementation allows specifying, for instance, any kind of sources, while still adding the default ones. However, it is always being added before the specified ones.

    I had to add it manually in my personal project, like this:

    val configLoaderBuilder = ConfigLoader.Builder()
            .addMapSource(myConfigs)
            .addSources(defaultPropertySources())
            .withDefaultSources(false)
    

    So I think it's best to have it specified.

    opened by LegendL3n 10
  • Overriding values of a sealed class with `EnvironmentVariablesPropertySource` is always case-sensitive

    Overriding values of a sealed class with `EnvironmentVariablesPropertySource` is always case-sensitive

    I'm using ConfigLoaderBuilder.addEnvironmentSource() which defaults to allowUppercaseNames: Boolean = true (side remark, that property should probably just be named ignoreNameCase).

    However, I realized that overriding a property from a sealed class via an upper-cased environment variable name does not work. It seems that the sealed class name is being looked for in its exact casing as part of the environment variable name.

    For example, in this test postgresStorage is a sealed class and I can override a property with ort.scanner.provenanceStorage.postgresStorage.connection.password, but if I change the "path" e.g. to ORT.scanner.provenanceStorage.postgresStorage.connection.password it stops working. But the same works if the "path" does not come across a seal class instance, e.g. ORT.scanner.storages.postgres.connection.password works to override the password.

    opened by sschuberth 9
  • Configuration file cannot be found in Graalvm native image

    Configuration file cannot be found in Graalvm native image

    Hi,

    Thanks for this great library,

    I'm integrating it into my Ktor project which will run as a native Graalvm image.

    I'm using theses dependencies:

    implementation("com.sksamuel.hoplite:hoplite-core:$hoplite_core_version")
    implementation("com.sksamuel.hoplite:hoplite-yaml:$hoplite_core_version")
    

    I have the following application.yml

    image

    I have integrated the following configuration in order to make the yml files available in the binary

      buildArgs.add("-H:+ReportExceptionStackTraces")
      buildArgs.add("-H:IncludeResources='.*(yml|xml)$'")
      buildArgs.add("-H:Log=registerResource:5")
      imageName.set("mfa_graalvm_service")
    

    At the time of starting the microservice and instantiating the configuration Koin module, the application crashes because it cannot find the application.yml file

    val configModule = module {
        single<AppConfig> {
            ConfigLoaderBuilder.default()
                .addResourceSource("/application.yml")
                .strict()
                .build()
                .loadConfigOrThrow()
        }
        single { get<AppConfig>().mfa }
        single { get<AppConfig>().redis }
        single { get<AppConfig>().auth }
    }
    

    Could you tell me if I have to apply any additional configuration so that the library finds the configuration file?

    Thank so much, Greetings

    opened by sergio11 1
Releases(v2.7.0)
Owner
Sam Sam
Bit of this, bit of that.
Sam Sam
A .NET Watch Run Configuration (dotnet-watch) that can be used in RiderA .

A .NET Watch Run Configuration (dotnet-watch) that can be used in RiderA .NET Watch Run Configuration (dotnet-watch) that can be used in Rider

Maarten Balliauw 19 Dec 10, 2022
I was fed up with writing Java classes to mirror json models. So I wrote this Java app to automate the process.

Json2Java I was fed up with writing Java classes to mirror json models. So I wrote this Java app to automate the process. What this tool can do right

Jon F Hancock 303 Oct 8, 2022
Android library that manages your app's cached data with ease.

Teller Android library that makes your apps faster. Teller facilitates the downloading, saving, and reading of the cached data of your app. Keep your

Levi Bostian 14 Apr 2, 2022
A comprehensive tutorial for Android Data Binding

精通 Android Data Binding 更多干货可移步至个人主页 QQ 交流群:324112728 ,或者点击链接加入QQ群 官方虽然已经给出了教程 - Data Binding Guide (中文版 - Data Binding(数据绑定)用户指南) ,但是实践之后发现槽点实在太多,于是就

Fei Liang 2.6k Dec 6, 2022
****. Use the native and support library variants instead - https://developer.android.com/guide/topics/ui/look-and-feel/fonts-in-xml.html. An android library that makes it easy to add custom fonts to edittexts and textviews

Add to your project Add this line to your dependencies in build.gradle compile 'in.workarounds.typography:typography:0.0.8' Using the views There are

Workarounds 43 Nov 6, 2021
Starter-Android-Library - Starter Android Library is an Android Project with Modular Architecture.

Starter-Android-Library - Starter Android Library is an Android Project with Modular Architecture.

OpenBytes 1 Feb 18, 2022
Editframe Kotlin Client library

Editframe Kotlin Client library Installing Add the project to your gradle dependencies.

editframe 2 Apr 7, 2022
ZXing ("Zebra Crossing") barcode scanning library for Java, Android

Project in Maintenance Mode Only The project is in maintenance mode, meaning, changes are driven by contributed patches. Only bug fixes and minor enha

ZXing Project 30.5k Dec 27, 2022
AboutLibraries is a library to offer some information of libraries.

AboutLibraries .. allows you to easily create an used open source libraries fragment/activity within your app. All the library information is automati

Mike Penz 3.1k Jan 3, 2023
An android library for displaying fps from the choreographer and percentage of time with two or more frames dropped

DEPRECATED TinyDancer is deprecated. No more development will be taking place. Check out the Google Android developer documentation for UI performance

Friendly Robot 1.9k Jan 3, 2023
Android Library to help you with your runtime Permissions.

PermissionHelper Android Library to help you with your runtime Permissions. Demo Android M Watch it in action. Pre M Watch it in action. Nexus 6 (M) N

Kosh Sergani 1.2k Dec 14, 2022
Android validation library which helps developer boil down the tedious work to three easy steps.

AwesomeValidation Introduction Implement validation for Android within only 3 steps. Developers should focus on their awesome code, and let the librar

Jing Li 1.1k Dec 17, 2022
A plug and play ;) android library for displaying a "rate this app" dialog

Easy Rating Dialog This lib provides a simple way to display an alert dialog for rating app. Default conditions to show: User opened the app more than

Fernando Martínez 111 Dec 30, 2022
Tutorial For openJDK 11 and AGP 7.0.0+ | Tutorial Multi Library Android in 1 Project | Groovy

jitpack-library-guide For openJDK 11 and AGP 7.0.0 + Please read every single note for detail Tutorial Click Here Kotlin DSL Click Here Repository for

Faisal Amir 7 Dec 10, 2022
Makes Google play in app purchase library (BillingClient) into a flowable that can easily be used in compose world

Billy the android Our goal is to make a modern api of BillingClient so that it is easier to use in compose world. This library is still early beta and

Stefan Wärting 16 Dec 14, 2022
Simple Artificial Neural Network java library

SANN Simple Artificial Neural Network java library WIP Creating neural network Variant 1 //create new net (input, hidden, default len, output) Net net

Korpusov Maxim 14 Oct 11, 2022
Using Resilience4J, this is a Demo project which uses a Library as background and manages all its requests.

Using Resilience4J, this is a Demo project which uses a Library as background and manages all its requests. This library can rent books online and the usage rate is extremely high. There is a reactive way to access, which is the one normally used, but sometimes, the system needs a bit of help from an old machine running a non-reactive MVC Spring application using JPA. Let's make this in Kotlin and find the best solution!

João Filipe Sabino Esperancinha 2 Jan 10, 2022
A library to display pokemon with Shakespearean descriptions

Pokemon Shakespeare SDK A library to display pokemon with Shakespearean descriptions Dependencies Java 11 Retrofit Picasso Material Appcompat Usage Cr

Keenen Charles 0 Nov 12, 2021
ArduinoJava - A project that uses the JSSC library

ArduinoJava este es un proyecto que utiliza la libreria JSSC para connectarse al

null 0 Jan 24, 2022