kiwi API 0.1.0
User Guide
Kiwi: JDK 21+ micro configuration frameworkA non-opinionated Java bootstrapping configuration library that allows recursive chain loading of configuration from key values.
Contents
Description
A non-opinionated Java bootstrapping configuration library that allows recursive chain loading of configuration from key values.
Key values are everywhere (also known as associative arrays, list of tuples, or name-value pairs)!
Environment variables, system properties, cloud metadata, vault, HTTP FORM post, URI queries, command line arguments, and even most forms of JSON, HOCON, TOML, YAML, and XML can all be represented as simple "key value" pairs.
Thus, it is the perfect common denominator for providing applications with initial configuration. We call this "bootstrapping configuration" and it is usually gathered even before logging.
To do this, Kiwi loads streams of key values (KeyValue
) from resources. What is unique about it is that certain key values will load more key values to the current stream, which is what we call chaining.
URI -> KeyValues* -> URI* -> KeyValues -> ...
Streams are useful because they can be filtered and transformed, which matters because keys and values from various sources often need transformation. For example, environment variable names often need to be converted to lower case, and some prefix removed.
In short, it is a micro configuration framework that itself can be configured with key values.
Why and Use Case
Kiwi allows "users" to decide where their configuration comes from, instead of developers.
A simple command-line application use case might be like ripgrep, where a single environment variable dictates the location of configuration. Kiwi easily supports this style and also allows the user to load additional configuration from other places using key values in the configuration file. Kiwi can even enable configuration to come from other environment variables, which is something not typically possible with tools like ripgrep or similar utilities.
Another use case might be to simulate Spring Boot's configuration loading but without the rest of Spring Boot. Kiwi can achieve this declaratively through key values. In fact, a user could configure the previously mentioned ripgrep environment variable to perform a Spring Boot-like load of configuration.
Adding Kiwi to Your Project
To use Kiwi in your project, add the following dependency to your build configuration:
Maven
<dependency>
<groupId>io.jstach.kiwi</groupId>
<artifactId>kiwi-kvs</artifactId>
<version>0.1.0</version>
</dependency>
Gradle
implementation 'io.jstach.kiwi:kiwi-kvs:0.1.0'
Example Usage
A simple example using java.util.Properties
files that could be parsed to KeyValues
:
var kvs = KeyValuesSystem.defaults()
.loader()
.variables(Variables::ofSystemProperties) // Use system properties as base variables for interpolation
.add("classpath:/start.properties")
.add("system:///") // add system properties to override
.add("env:///") // add environment variables to override
.add("cmd:///?_filter_sed=s/^-D//") // add command line for final override using the prefix of -D
.load();
// Give the key values to some other config framework like Spring:
var map = kvs.toMap();
ConfigurableEnvironment env = applicationContext.getEnvironment();
env.getPropertySources().addFirst(new MapPropertySource("start", map));
_load_foo=file:/./myapp.properties?_flag=optional
Project Information
- Source Control
- https://github.com/jstachio/kiwi
- Team
-
- Adam Gent (agentgt) - lead
- Issues
- https://github.com/jstachio/kiwi/issues
- Community
- https://github.com/jstachio/kiwi/discussions
- User Guide
- This document
- Javadoc
- This document (modules listing at bottom)
Requirements
- Java 21 or greater
- A build system that supports running the Java compiler annotation processor
java.base
Architecture
Kiwi's two major concepts are:
KeyValues
- a stream of key valuesKeyValuesResource
- a URI with associated key-value metadata
Resources are used to load key values, and key values can be used to specify and find more resources (to load more key values). Kiwi is recursive.
For the rest of the explanation of architecture, we will go bottom-up.
KeyValue
A KeyValue
object in Kiwi, unlike a Map.Entry<String, String>
, has more information than just the simple tuple of key
and value
.
- Immutable
- Have interpolated value as well as the original pre-interpolated value
- Have source information
- Whether or not it should be used for interpolation
- Whether or not it should be printed out ever (e.g., a password or other sensitive information)
Kiwi provides ergonomics for working with streams of key values to filter, collect, parse, and format them.
Notice that Kiwi is like a list of key values and thus:
- Order can be important
- There can be duplicate "keys" (that may or may not override in the final result)
Finally, a KeyValue
can be a special key that can reference another resource to load.
These keys are usually prefixed with _
to avoid collision and maximize compatibility. The most important one is _load_name
, where name
is the name you assign to the resource and the value is a URI. This mini DSL syntax will be configurable in the future, so you can pick different key name patterns.
Interpolation
Kiwi can perform Bash-like interpolation on a stream of KeyValues
(e.g., ${somevar:-${othervar}}
). It does this by using the key values themselves and Variables
. Variables
are simply Function<String, String>
.
This allows you to interpolate on key values with data you do not want in the final result (KeyValues
). For example, it is common to use System.getProperties()
as variables, but often you do not want all the system properties to end up in the KeyValues
.
Interpolation can be disabled with the resource flag no_interpolation
.
Furthermore, you can load a resource as variables instead of KeyValues
with the resource flag no_add
.
# first loaded properties
_load_system=system:///
_flags_system=no_add
_load_app=classpath:/${app.name:-app}.properties
If app.name
is defined in system properties, it will be used in the _load_app
URI. Otherwise, app.properties
will be used.
Often, interpolation will create a new stream of KeyValues
where the value part of the key is replaced with the interpolated result; however, the original value is always retained.
KeyValuesResource
A KeyValuesResource
has a URI
and a symbolic name (used to find configuration).
It is backed by a key value with additional metadata on how to load that resource.
URIs are designed to point at resources, and the additional metadata in a KeyValuesResource
is,
as you might guess, more KeyValues
.
The additional metadata is used to determine how to load the key values and what metadata should be associated with each key value.
Some examples of metadata use cases include:
- The key values from the resource are sensitive and should not be easily printed out.
- The key values should not be interpolated because the data is raw.
- The loaded key values should or should not load other key values.
- The key values need their names transformed or some key values ignored.
This is all configurable through key values (and URIs), particularly using the _flags_name
key.
Resource Key Value Configuration
Resource loading configuration can be done with special key values.
These can either be specified in the URI of the resource or as key values within the resource where the _load_[name]
is specified.
Resource Configuration in Resource
The default key-value pattern to specify resources is:
_load_[name]=URI
_mediaType_[name]=Content Type or file extension to resolve format of resource for parsing
_flags_[name]=CSV of flag names
_param_[name]_[key]=String
_filter_[name]=String expression for filter
The [name]
part should be replaced with a name of your choosing, where only case-sensitive alphanumeric characters are allowed.
This becomes the symbolic name of the resource. Don't worry—those special keys will be filtered out.
The _load_[name]
is the most important key as it dictates the name of the resource and the URI of the resource.
Resource Configuration in URI
The URI of the _load_[name]
can also contribute to resource keys using the following format:
_load_custom=file://./something?_mediaType=properties&_flags=optional&_filter_sed=s/myapp_//&_param_custom=something
Notice: The resource name does not need to be specified with URI parameters, as it is deduced.
Resource Flags
Resource flags can be set with _flags_[name]
in the resource or on the URI with _flags
.
These can be repeatable parameters, and their values are combined.
This is currently a subset of the flags:
- "NO_REQUIRE" / "OPTIONAL" - Resources are usually required to exist; otherwise, an error occurs. This flag makes them not required.
- "SENSITIVE" -
The key values loaded from the resource will be marked as sensitive and will not be output in
toString
or similar methods. - "NO_ADD" -
The key values will not be added to the final result but will instead be used as
variables
. - "NO_LOAD_CHILDREN" -
The resource is not allowed to chain additional resources (e.g., it cannot use
_load_
keys).
Resource Media Type
Kiwi will try to automatically determine the media type of a resource based on the URI file extension.
However, in some cases, that is not sufficient. The key _mediaType_[name]
allows you to explicitly specify the media type.
This is particularly useful for URIs that can load a key containing key-value data.
A real-world example of this is the
Spring Boot environment variable SPRING_APPLICATION_JSON
.
An example of emulating that behavior in Kiwi:
_load_springJson=env:///SPRING_APPLICATION_JSON?_mediaType=json
Note: JSON support is not provided out-of-the-box (OOB) and will be available as a separate module.
Resource Filters
Filters can be applied to a resource to alter the key values after they are loaded but before being added to the final results. The ordering of filters matters, so it is generally recommended to use the URI notation, as the order is guaranteed.
The current filters provided out-of-the-box (OOB) are:
sed
- Similar to the Unixsed
utility but only supporting thes
(substitute) andd
(delete) commands.grep
- Filters keys matching a regular expression.
Here is an example of using both filters:
_load_env=env:///?_filter_grep=^MY_APP_&_filter_sed=s/^MY_APP/myapp./
The above configuration will only load environment variables prefixed with MY_APP_
and will replace
MY_APP_
with myapp.
.
The original key name is always preserved for key tracking, so regardless of how keys are renamed, users can still identify where the key-value originally came from.
Note:
To add new filters, implement KeyValuesServiceProvider.KeyValuesFilter
.
This interface allows you to provide custom filters.
Resource Loading
A KeyValuesLoader
usually takes a KeyValuesResource
and turns it into KeyValues
.
It is an extension point
that, in simple terms, takes a URI and loads KeyValues
, usually
based on the schema of the URI. For example:
classpath
- Uses the JDK classloader mechanism.file
- Usesjava.io
/java.nio
file loading.
This part of the library is extendable, and custom loaders can be manually wired or the service loader can be used.
Out of the box, Kiwi supports the following URI schemas:
classpath
file
system
- System propertiesenv
- Environment variablescmd
- Command line argument pairs separated by=
stdin
- Allows Unix piping of key values, often useful for passwordsprofile.[schema]
- Loads multiple resources based on a CSV of profiles where the profile name replaces part of the URI
Other URI schemas will be added in the future, usually as separate modules, such as dotenv, HOCON, Terraform/Tofu tfvars.json
format, etc.
Note: If no schema is provided, Kiwi assumes it is a file path, which is treated as a URI.
We won’t cover file
and classpath
here, as they are straightforward and self-explanatory.
URI schema: env
, system
, cmd
env:///
, system:///
, and cmd:///
resources have features that differ from file
or classpath
.
Each of these URI schemas can take a path that fetches a key value, where the value is used as the source of the key values. If no path is provided, all key values of the resource will be loaded.
For example, if we have an environment variable whose value is JSON, we can load it like this:
_load_springJson=env:///SPRING_APPLICATION_JSON?_mediaType=json
URI schema: stdin
stdin:///
without a path assumes that the entire contents of stdin are in java.util.Properties
key-value format.
If this is not the case, it is advisable to specify _mediaType=
explicitly with stdin.
stdin:///
can also bind the input content, parsed as a UTF-8 string, to the key provided in the URI path.
_load_stdin=stdin:///db.password?_flag=sensitive,optional
The above example is particularly useful for passwords, similar to how Docker handles passwords from stdin.
URI schema: profile.
Note: This schema may be renamed to profiles
in the future.
The profile
schema is one of the more complex KeyValuesLoader
implementations.
It allows loading multiple resources based on a CSV list of profile names. Here's an example:
PROFILES=profile1,profile2 # this could come from an environment variable
_load_profiles=profile.classpath:/app-__PROFILE__.properties
_param_profiles_profile=${PROFILES}
_flags_profiles=no_require
The above configuration will attempt to load the following resources:
classpath:/app-profile1.properties
classpath:/app-profile2.properties
It will not fail if those resources are not found, as the flag no_require
is set.
The logic behind this schema is not special or internal. The profile
loader simply generates the following key values:
_load_profiles1=classpath:/app-profile1.properties
_flags_profiles1=no_require
_load_profiles2=classpath:/app-profile2.properties
_flags_profiles2=no_require
This means users can create a similar implementation themselves if desired.
KeyValuesMedia
Some KeyValuesLoader
implementations can directly parse the URI
into KeyValues
.
However, many will rely on a parser to handle the data.
Kiwi provides a framework to parse and format key values from or to byte streams or strings, based on media type (also known as "Content Type" or MIME) or file extension.
This part of the library is extendable. Custom media types can be manually wired or loaded using the Java ServiceLoader
.
Out of the box, Kiwi supports:
java.util.Properties
format- URL Query percent encoding format
Additional formats will be added as separate modules, such as dotenv, HOCON, and Terraform/Tofu tfvars.json
format.
Note: To add new media types, implement KeyValuesServiceProvider.KeyValuesMediaFinder
.
This interface allows you to provide custom parsing logic for new formats.
KeyValuesEnvironment
Kiwi provides a facade to access system properties, environment variables, class resource loaders, stdin, command-line arguments, and logging. This is particularly useful for unit testing, but the most valuable feature is the logger.
The logger in Kiwi does nothing by default, but users can plug in their own implementations to track resource loading.
KeyValuesSystem
This is the entry point into Kiwi and is used to load the initial part of the chain of resources. The bootstrapping part of your application will call it first and will often convert or pass the loaded key values to another system.
io.jstach.kiwi.kvs
module provides the core library for Kiwi, a non-opinionated
Java configuration system that supports recursive chain loading of configuration from key-value
pairs.