Skip to content
/ konf Public
forked from uchuhimo/konf

A type-safe cascading configuration library for Kotlin/Java, supporting most configuration formats

License

Notifications You must be signed in to change notification settings

feature/konf

 
 

Repository files navigation

Konf

Java 8+ Maven metadata URL Bintray JitPack Build Status codecov codebeat badge Awesome Kotlin Badge

A type-safe cascading configuration library for Kotlin/Java, supporting most configuration formats.

Features

  • Type-safe. Get/set value in config with type-safe APIs.
  • Thread-safe. All APIs for config is thread-safe.
  • Batteries included. Support sources from JSON, XML, YAML, HOCON, TOML, properties, map, command line and system environment out of box.
  • Cascading. Config can fork from another config by adding a new layer on it. Each layer of config can be updated independently. This feature is powerful enough to support complicated situation such as configs with different values share common fallback config, which is automatically updated when configuration file changes.
  • Self-documenting. Document config item with type, default value and description when declaring.
  • Extensible. Easy to customize new sources for config or expose items in config.

Contents

Prerequisites

  • JDK 1.8 or higher

Use in your projects

This library has been published to Maven Central, JCenter and JitPack.

Maven

<dependency>
  <groupId>com.uchuhimo</groupId>
  <artifactId>konf</artifactId>
  <version>0.13.1</version>
</dependency>

Gradle

compile 'com.uchuhimo:konf:0.13.1'

Gradle Kotlin DSL

compile(group = "com.uchuhimo", name = "konf", version = "0.13.1")

Maven (master snapshot)

Add JitPack repository to <repositories> section:

<repository>
    <id>jitpack.io</id>
    <url>https://jitpack.io</url>
</repository>

Add dependencies:

<dependency>
    <groupId>com.github.uchuhimo</groupId>
    <artifactId>konf</artifactId>
    <version>master-SNAPSHOT</version>
</dependency>

Gradle (master snapshot)

Add JitPack repository:

repositories {
    maven { url 'https://jitpack.io' }
}

Add dependencies:

compile 'com.github.uchuhimo:konf:master-SNAPSHOT'

Quick start

  1. Define items in config spec:

    object ServerSpec : ConfigSpec() {
        val host by optional("0.0.0.0")
        val port by required<Int>()
    }
  2. Construct config with items in config spec and values from multiple sources:

    val config = Config { addSpec(ServerSpec) }
            .from.yaml.file("/path/to/server.yml")
            .from.json.resource("server.json")
            .from.env()
            .from.systemProperties()

    This config contains all items defined in ServerSpec, and load values from 4 different sources. Values in resource file server.json will override those in file /path/to/server.yml, values from system environment will override those in server.json, and values from system properties will override those from system environment.

    If you want to watch file /path/to/server.yml and reload values when file content is changed, you can use watchFile instead of file:

    val config = Config { addSpec(ServerSpec) }
            .from.yaml.watchFile("/path/to/server.yml")
            .from.json.resource("server.json")
            .from.env()
            .from.systemProperties()
  3. Define values in source. You can define in any of these sources:

    • in /path/to/server.yml:
      server:
          host: 0.0.0.0
          port: 8080
    • in server.json:
      {
          "server": {
              "host": "0.0.0.0",
              "port": 8080
          }
      }
    • in system environment:
      SERVER_HOST=0.0.0.0
      SERVER_PORT=8080
    • in command line for system properties:
      -Dserver.host=0.0.0.0 -Dserver.port=8080
  4. Retrieve values from config with type-safe APIs:

    val server = Server(config[server.host], config[server.port])
    server.start()

Define items

Config items is declared in config spec, added to config by Config#addSpec. All items in same config spec have same prefix. Define a config spec with prefix local.server:

object ServerSpec : ConfigSpec("server") {
}

If the config spec is binding with single class, you can declare config spec as companion object of the class:

class Server {
    companion object : ConfigSpec("server") {
        val host by optional("0.0.0.0")
        val port by required<Int>()
    }
}

The config spec prefix can be automatically inferred from the class name, leading to further simplification like:

object ServerSpec : ConfigSpec() {
}

or

class Server {
    companion object : ConfigSpec() {
    }
}

Here are some examples showing the inference convention: Uppercase to uppercase, lowercase to lowercase, SuffixSpec to suffix, TCPService to tcpService.

The config spec can also be nested. For example, the path of Service.Backend.Login.user in the following example will be inferred as "service.backend.login.user":

object Service : ConfigSpec() {
    object Backend : ConfigSpec() {
        object Login : ConfigSpec() {
            val user by optional("admin")
        }
    }
}

There are three kinds of item:

  • Required item. Required item doesn't have default value, thus must be set with value before retrieved in config. Define a required item with description:
    val port by required<Int>(description = "port of server")
    Or omit the description:
    val port by required<Int>()
  • Optional item. Optional item has default value, thus can be safely retrieved before setting. Define an optional item:
    val host by optional("0.0.0.0", description = "host IP of server")
    Description can be omitted.
  • Lazy item. Lazy item also has default value, however, the default value is not a constant, it is evaluated from thunk every time when retrieved. Define a lazy item:
    val nextPort by lazy { config -> config[port] + 1 }

You can also define config spec in Java, with a more vorbose API (compared to Kotlin version in "quick start"):

public class ServerSpec {
  public static final ConfigSpec spec = new ConfigSpec("server");

  public static final OptionalItem<String> host =
      new OptionalItem<String>(spec, "host", "0.0.0.0") {};

  public static final RequiredItem<Integer> port = new RequiredItem<Integer>(spec, "port") {};
}

Notice that the {} part in item declaration is necessary to avoid type erasure of item's type information.

Use config

Create config

Create an empty new config:

val config = Config()

Or an new config with some initial actions:

val config = Config { addSpec(Server) }

Add config spec

Add multiple config specs into config:

config.addSpec(Server)
config.addSpec(Client)

Retrieve value from config

Retrieve associated value with item (type-safe API):

val host = config[Server.host]

Retrieve associated value with item name (unsafe API):

val host = config.get<String>("server.host")

or:

val host = config<String>("server.host")

Check whether value exists in config or not

Check whether value exists in config or not with item:

config.contains(Server.host)

Check whether value exists in config or not with item name:

config.contains("server.host")

Modify value in config

Associate item with value (type-safe API):

config[Server.port] = 80

Find item with specified name, and associate it with value (unsafe API):

config["server.port"] = 80

Discard associated value of item:

config.unset(Server.port)

Discard associated value of item with specified name:

config.unset("server.port")

Associate item with lazy thunk (type-safe API):

config.lazySet(Server.port) { it[basePort] + 1 }

Find item with specified name, and associate it with lazy thunk (unsafe API):

config.lazySet("server.port") { it[basePort] + 1 }

Export value in config as property

Export a read-write property from value in config:

var port by config.property(Server.port)
port = 9090
check(port == 9090)

Export a read-only property from value in config:

val port by config.property(Server.port)
check(port == 9090)

Fork from another config

val config = Config { addSpec(Server) }
config[Server.port] = 1000
// fork from parent config
val childConfig = config.withLayer("child")
// child config inherit values from parent config
check(childConfig[Server.port] == 1000)
// modifications in parent config affect values in child config
config[Server.port] = 2000
check(config[Server.port] == 2000)
check(childConfig[Server.port] == 2000)
// modifications in child config don't affect values in parent config
childConfig[Server.port] = 3000
check(config[Server.port] == 2000)
check(childConfig[Server.port] == 3000)

Load values from source

Use from to load values from source doesn't affect values in config, it will return a new child config by loading all values into new layer in child config:

val config = Config { addSpec(Server) }
// values in source is loaded into new layer in child config
val childConfig = config.from.env()
check(childConfig.parent === config)

All out-of-box supported sources are declared in DefaultLoaders, shown below (the corresponding config spec for these samples is ConfigForLoad):

Type Sample
HOCON source.conf
JSON source.json
properties source.properties
TOML source.toml
XML source.xml
YAML source.yaml
hierarchical map MapSourceLoadSpec
map in key-value format KVSourceSpec
map in flat format FlatSourceLoadSpec
system properties -
system environment -

Format of system properties source is same with that of properties source. System environment source follows the same mapping convention with properties source, but with the following name convention:

  • All letters in name are in uppercase
  • . in name is replaced with _

HOCON/JSON/properties/TOML/XML/YAML source can be loaded from a variety of input format. Use properties source as example:

  • From file: config.from.properties.file("/path/to/file")
  • From watched file: config.from.properties.watchFile("/path/to/file", 100, TimeUnit.MILLISECONDS)
  • From string: config.from.properties.string("server.port = 8080")
  • From URL: config.from.properties.url("http://localhost:8080/source.properties")
  • From watched URL: config.from.properties.watchUrl("http://localhost:8080/source.properties", 1, TimeUnit.MINUTES)
  • From Git repository: config.from.properties.git("https://github.com/uchuhimo/konf.git", "/path/to/source.properties", branch = "dev")
  • From watched Git repository: config.from.properties.watchGit("https://github.com/uchuhimo/konf.git", "/path/to/source.properties", period = 1, unit = TimeUnit.MINUTES)
  • From resource: config.from.properties.resource("source.properties")
  • From reader: config.from.properties.reader(reader)
  • From input stream: config.from.properties.inputStream(inputStream)
  • From byte array: config.from.properties.bytes(bytes)
  • From portion of byte array: config.from.properties.bytes(bytes, 1, 12)

If source is from file, file extension can be auto detected. Thus, you can use config.from.file("/path/to/source.json") instead of config.from.json.file("/path/to/source.json"), or use config.from.watchFile("/path/to/source.json") instead of config.from.json.watchFile("/path/to/source.json"). Source from URL also support auto-detected extension (use config.from.url or config.from.watchUrl). The following file extensions can be supported:

Type Extension
HOCON conf
JSON json
Properties properties
TOML toml
XML xml
YAML yml, yaml

You can also implement Source to customize your new source, which can be loaded into config by config.withSource(source).

Strict parsing when loading

By default, Konf extracts desired paths from sources and ignores other unknown paths in sources. If you want Konf to throws exception when unknown paths are found, you can enable FAIL_ON_UNKNOWN_PATH feature:

config.enable(Feature.FAIL_ON_UNKNOWN_PATH)
    .from.properties.file("server.properties")
    .from.json.resource("server.json")

Then config will validate paths from both the properties file and the JSON resource. Furthermore, If you want to validate the properties file only, you can use:

config.from.enable(Feature.FAIL_ON_UNKNOWN_PATH).properties.file("/path/to/file")
    .from.json.resource("server.json")

Export/Reload values in config

Export all values in config to map in key-value format:

val map = config.toMap()

Export all values in facade layer of config to map:

val map = config.layer.toMap()

Export all values in config to hierarchical map:

val map = config.toHierarchicalMap()

Export all values in config to map in flat format:

val map = config.toFlatMap()

Export all values in config to JSON:

val file = createTempFile(suffix = ".json")
config.toJson.toFile(file)

Reload values from JSON:

val newConfig = Config {
    addSpec(Server)
}.from.json.file(file)
check(config == newConfig)

Config can be saved to a variety of output format in HOCON/JSON/properties/TOML/XML/YAML. Use JSON as example:

  • To file: config.toJson.toFile("/path/to/file")
  • To string: config.toJson.toText()
  • To writer: config.toJson.toWriter(writer)
  • To output stream: config.toJson.toOutputStream(outputStream)
  • To byte array: config.toJson.toBytes()

You can also implement Writer to customize your new writer (see JsonWriter for how to integrate your writer with config).

Supported item types

Supported item types include:

  • All primitive types
  • All primitive array types
  • BigInteger
  • BigDecimal
  • String
  • Date and Time
    • java.util.Date
    • OffsetTime
    • OffsetDateTime
    • ZonedDateTime
    • LocalDate
    • LocalTime
    • LocalDateTime
    • Year
    • YearMonth
    • Instant
    • Duration
  • SizeInBytes
  • Enum
  • Array
  • Collection
    • List
    • Set
    • SortedSet
    • Map
    • SortedMap
  • Kotlin Built-in classes
    • Pair
    • Triple
    • IntRange
    • CharRange
    • LongRange
  • Data classes
  • POJOs supported by Jackson core modules

Konf supports size in bytes format described in HOCON document with class SizeInBytes.

Konf supports both ISO-8601 duration format and HOCON duration format for Duration.

Konf uses Jackson to support Kotlin Built-in classes, Data classes and POJOs. You can use config.mapper to access ObjectMapper instance used by config, and configure it to support more types from third-party Jackson modules. Default modules registered by Konf include:

Optional features

There are some optional features that you can enable/disable in the config scope or the source scope by Config#enable(Feature)/Config#disable(Feature) or Source#enabled(Feature)/Source#disable(Feature). You can use Config#isEnabled() or Source#isEnabled() to check whether a feature is enabled.

These features include:

  • FAIL_ON_UNKNOWN_PATH: feature that determines what happens when unknown paths appear in the source. If enabled, an exception is thrown when loading from the source to indicate it contains unknown paths. This feature is disabled by default.
  • LOAD_KEYS_CASE_INSENSITIVELY: feature that determines whether loading keys from sources case-insensitively. This feature is disabled by default except for system environment.

Build from source

Build library with Gradle using the following command:

gradlew clean assemble

Test library with Gradle using the following command:

gradlew clean test

Since Gradle has excellent incremental build support, you can usually omit executing the clean task.

Install library in a local Maven repository for consumption in other projects via the following command:

gradlew clean install

License

© uchuhimo, 2017-2018. Licensed under an Apache 2.0 license.

About

A type-safe cascading configuration library for Kotlin/Java, supporting most configuration formats

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Kotlin 98.1%
  • Java 1.9%