Hoplite

repository·master·Indexed 21 days ago

https://github.com/sksamuel/hoplite

A Kotlin library for loading configuration files into typesafe data classes. It supports multiple formats including JSON, YAML, TOML, HOCON, and Java Properties, featuring cascading configurations, relaxed binding for environment variables, and detailed error messages. Hoplite provides built-in decoders for standard JDK and Kotlin types, with extended support for Arrow, HikariCP, AWS SDK, and others via dedicated modules. It also includes support for GraalVM native images.

Tokens
12.6K
Snippets
48
Records
63
Agent score
76%

What's inside Hoplite

  1. Use EnvironmentVariablesPropertySource with relaxed binding

    master

    The EnvironmentVariablesPropertySource maps environment variables to configuration properties using idiomatic conventions similar to Spring Boot's relaxed binding.

    Binding Rules:

    • Dots to Underscores: TOPIC_NAME $\rightarrow$ topic.name
    • Case-Insensitivity: SPRING_MAIN_LOGSTARTUPINFO $\rightarrow$ spring.main.logStartupInfo (matches camelCase fields)
    • Dashes: Dashes are removed (e.g., LOGSTARTUPINFO $\rightarrow$ log-startup-info via @ConfigAlias)
    • List/Array Indices: Surrounded by underscores (e.g., ITEMS_0 $\rightarrow$ items[0])
    • Nested Objects in Lists: SERVICE_0_OTHER $\rightarrow$ service[0].other
    • Map Keys: The trailing path segment names the key. Note: Unlike Spring, Hoplite preserves the case of the environment variable for map keys (e.g., LABELS_ENV=v produces {"ENV": "v"}).

    If an optional prefix is provided to the source, only environment variables starting with that prefix are considered, and the prefix is stripped before processing.

  2. Use Preprocessors to transform configuration values

    master

    Preprocessors are functions applied to every value as it is read from the configuration source. They can transform values (e.g., performing environment variable substitution, database lookups, or secret retrieval) or act as an identity function.

    To add a custom preprocessor, use the withPreprocessor method on the ConfigLoader class and provide an instance of the Preprocessor interface.

    If you need one preprocessor to resolve a value that then requires another preprocessor to resolve, you can enable looped application by setting withPreprocessingIterations on the ConfigLoaderBuilder to a value greater than 1.

    val config = ConfigLoaderBuilder.default()
      .withPreprocessingIterations(2)
      .withPreprocessor(MyCustomPreprocessor())
      .build()
      .loadConfigOrThrow<MyConfig>()
  3. Use ParameterMappers for snake_case or kebab-case

    master

    Hoplite uses ParameterMapper implementations to transform parameter names before looking them up in a source. By default, KebabCaseParamMapper and SnakeCaseParamMapper are registered, allowing you to use idiomatic config keys that don't match your Kotlin field names exactly.

    // Kotlin field
    data class Database(val instanceHostName: String)
    
    // Works with kebab-case in YAML
    database:
      instance-host-name: server1.prd
    
    // Works with snake_case in YAML
    database:
      instance_host_name: server1.prd
  4. Understand Hoplite Decoders and supported types

    master

    Hoplite uses the Decoder interface to convert raw configuration values into JDK or Kotlin types. It provides built-in support for a wide range of standard types, including primitives, collections, and specialized domain types.

    Built-in Support

    • Primitives & Basics: String, Long, Int, Short, Byte, Double, Float, Boolean (supports "true", "t", "1", "yes" for true; "false", "f", "0", "no" for false), BigDecimal, BigInteger, UUID, Locale, and Enums.
    • Date & Time:
      • java.time types: LocalDateTime, LocalDate, LocalTime, Duration (from duration format or milliseconds), Instant (from unix epoch milliseconds), Year, YearMonth, MonthDay, and java.util.Date.
      • Kotlin types: kotlin.time.Duration and kotlin.ByteArray.
    • Networking & IO: URI, URL, InetAddress, java.io.File, and java.nio.Path.
    • Collections: List<A>, Set<A>, SortedSet<A>, Map<K,V>, and LinkedHashMap<K,V> (preserves config order).
    • Kotlin Stdlib: Pair<A,B>, Triple<A,B,C>, and kotlin.text.Regex.

    Specialized Hoplite Types

    • Masked: Wraps a String to redact its toString() output.
    • SizeInBytes: Parses values like 12MiB or 9KB.
    • Seconds / Minutes: Wraps an integer; use the .duration() extension method to convert to a duration.
    • Base64: Wraps a ByteBuffer from a valid base64 encoded string.
  5. Bind configuration prefixes to independent types

    master

    For modular configuration (e.g., plugins or independent modules), you can bind specific sub-trees of your configuration to independent data classes using a ConfigBinder. This allows you to parse the configuration sources only once and then extract specific sections using a prefix.

    Example: If your YAML contains:

    module1:
      foo: bar
    module2:
      baz: qux

    You can bind them like this:

    val configBinder = ConfigLoaderBuilder.default()
      .addResourceSource("/application-prod.yml")
      .build()
      .configBinder()
    
    val module1Config = configBinder.bindOrThrow<Module1Config>("module1")
    val module2Config = configBinder.bindOrThrow<Module2Config>("module2")

    A prefix can also be passed directly to loadConfig variants if only one prefix is needed. Prefixes can be nested (e.g., foo.bar).

    val configBinder = ConfigLoaderBuilder.default()
      .addResourceSource("/application-prod.yml")
      .build()
      .configBinder()
    
    val module1Config = configBinder.bindOrThrow<Module1Config>("module1")
  6. Understand PropertySources in Hoplite

    master

    A PropertySource is the mechanism Hoplite uses to read configuration values. Hoplite provides several built-in implementations that are automatically registered in a specific precedence order:

    1. EnvironmentVariablesPropertySource
    2. SystemPropertiesPropertySource
    3. UserSettingsPropertySource
    4. XdgConfigPropertySource

    You can add custom property sources or additional built-in ones to the ConfigLoaderBuilder as needed.

  7. Configure Duration formats in Hoplite

    master

    Hoplite supports parsing duration types from strings using lower-case unit names. An optional space is allowed between the value and the unit. Supported units are:

    • Nanoseconds: ns, nano, nanos, nanosecond, nanoseconds
    • Microseconds: us, micro, micros, microsecond, microseconds
    • Milliseconds: ms, milli, millis, millisecond, milliseconds
    • Seconds: s, second, seconds
    • Minutes: m, minute, minutes
    • Hours: h, hour, hours
    • Days: d, day, days

    Examples: 10s, 3 days, or 12 hours.

  8. Implement Cascading (Layered) Configuration

    master

    Hoplite supports cascading configuration by passing multiple sources to the loader. When resolving keys, Hoplite searches through the sources in the order they were provided. The first source that defines a key wins.

    Important Rule for Lists: Lists cannot be merged. If a key pointing to a list is found in a higher-priority source, the entire list from that source is used, and values from lower-priority sources are ignored.

    // 'prod.json' values will take priority over 'default.json' values
    val config = ConfigLoader.load("prod.json", "default.json")
  9. Use inline classes for strong typing

    master

    Hoplite supports Kotlin inline classes, allowing you to use strong types (e.g., Port instead of Int) without requiring nested configuration keys. The loader will map the flat configuration value directly to the inline class constructor.

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

    // yaml config: // port: 9200 // host: localhost

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

  10. Map configuration to sealed classes

    master

    Hoplite can instantiate specific implementations of a sealed class by matching the available keys in the configuration to the parameters of the subclasses.

    • If keys match a specific implementation, that instance is created.
    • If keys match multiple implementations, the first match is taken.
    • If no keys match, the loader fails.

    Handling Objects in Sealed Classes: If a subclass is an object (singleton), you can trigger its use in two ways:

    1. By Name: Reference the type name as a string in YAML or JSON (e.g., database: Embedded or "database": "Embedded").
    2. By Empty Object (JSON only): Use an empty object (e.g., "database": { }). Note that this only works if there is a single object instance in the hierarchy to avoid disambiguation errors; otherwise, use the name-based method.
    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>)

    // YAML example with mixed types: // databases: // - "Embedded" // - host: localhost // port: 9300 // index: bar

  11. Mask sensitive values with the `Masked` type

    master

    To prevent sensitive information like passwords or API keys from appearing in logs when using Kotlin's toString() on configuration data classes, use the Masked type. A field declared as Masked will be loaded normally but will be represented as **** in the generated toString() output.

    Note on JSON Serialization: The masking effect only applies to toString(). If you use a reflection-based tool like Jackson to marshal your config to a String, the underlying value will still be visible. To mask values in JSON output, register the HopliteModule (available in the hoplite-json module) with your Jackson ObjectMapper.

    data class Database(val host: String, val user: String, val password: Masked)
    {
      "host": "localhost",
      "user": "root",
      "password": "letmein"
    }

    // Output via toString(): // Database(host=localhost, user=root, password=****)

  12. Quickstart: Load configuration into data classes

    master

    Hoplite maps configuration files to Kotlin data classes. Define your configuration structure using nested data classes, then use ConfigLoaderBuilder to load the values from a resource on the classpath.

    1. Define Data Classes:
    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)
    1. Create Configuration File (e.g., application-staging.yaml):
    env: staging
    database:
      host: staging.wibble.com
      port: 3306
      user: theboss
      pass: 0123abcd
    server:
      port: 8080
      redirectUrl: /404.html
    1. Load the Config:
    val config = ConfigLoaderBuilder.default()
                   .addResourceSource("/application-staging.yml")
                   .build()
                   .loadConfigOrThrow<Config>()