io.jstach.kiwi.kvs.Variables Maven / Gradle / Ivy
package io.jstach.kiwi.kvs;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Properties;
import java.util.SequencedCollection;
import java.util.function.BiConsumer;
import java.util.function.Function;
import org.jspecify.annotations.Nullable;
import io.jstach.kiwi.kvs.Variables.Parameters;
/**
* Represents a mapping function that associates a key to a value, typically used for
* variable resolution during interpolation.
*
* The {@code Variables} interface allows lookup of values based on keys and is used in
* contexts where key-to-value mappings are required for interpolation. Unlike
* {@link KeyValues}, there are no duplicate keys in {@code Variables}. Variables
* unlike KeyValues may not end up in the final config when loaded and that is why there
* is a distinction.. This distinction for example allows you to use environment
* variables for lookup but not have the final config contain all the environment
* variables.
*/
@FunctionalInterface
public interface Variables extends Function {
/**
* Retrieves the value mapped to the specified key.
* @param key the key whose value is to be retrieved
* @return the value associated with the key, or {@code null} if not found
*/
public @Nullable String getValue(String key);
/**
* Applies functional composition to rename key while retrieving value this variables.
* @param keyFunc function to rename key.
* @return composed variables instance.
*/
default Variables renameKey(Function keyFunc) {
return k -> this.getValue(keyFunc.apply(k));
}
@Override
default @Nullable String apply(String t) {
return getValue(t);
}
/**
* Find a variable key-value tuple based on vararg keys. This method provides
* ergonomics for searching for fallback keys.
* @param name names to String. Note if an array passed in is null an NPE will be
* thrown but contents of the array (keys) maybe null and are skipped if they are.
* @return optional entry where the key is the first matching key and the value is the
* value associated with that key.
*/
default Optional> findEntry(String... name) {
for (String k : name) {
if (k == null)
continue;
String prop = getValue(k);
if (prop != null)
return Optional.of(Map.entry(k, prop));
}
return Optional.empty();
}
/**
* Represents a specialized {@code Variables} interface that can list all keys it
* knows about. The data in {@code Parameters} is guaranteed to be immutable and
* copied when created.
*
*
* Unlike {@link KeyValues}, there are no duplicate keys in {@code Parameters}.
*/
public sealed interface Parameters extends Variables {
/**
* Returns an iterable over all keys that this {@code Parameters} instance knows
* about.
* @return an iterable of keys
*/
public Iterable keys();
/**
* Iterates over all key-value pairs in this {@code Parameters} instance and
* applies them to a provided consumer.
* @param consumer the consumer to apply each key-value pair
*/
default void forKeyValues(BiConsumer consumer) {
for (var k : keys()) {
String value = getValue(k);
if (value == null) {
continue;
}
consumer.accept(k, value);
}
}
/**
* Creates a {@code Parameters} instance from a provided map.
* @param map the map to create the parameters from
* @return a new {@code Parameters} instance
*/
public static Parameters of(Map map) {
return new MapParameters(map);
}
}
/**
* Returns an empty {@code Parameters} instance.
* @return an empty {@code Parameters} instance
*/
public static Parameters empty() {
return EmptyVariables.EMPTY_VARIABLES;
}
/**
* Creates a new {@code Variables} builder for constructing a composite
* {@code Variables} instance.
* @return a new {@link Builder} instance
*/
public static Variables.Builder builder() {
return new Builder();
}
/**
* A builder class for creating composite {@link Variables} instances by combining
* multiple sources of key-to-value mappings. NOTE: Variables resolution
* order is the opposite of KeyValues. Primacy takes precedence! .
*/
public final static class Builder {
private @Nullable Map properties = null;
private List suppliers = new ArrayList<>();
private Builder() {
}
/**
* Adds a key-value mapping to this builder.
* @param key the key to add
* @param value the value associated with the key
* @return this builder instance
*/
public Builder add(String key, String value) {
Map _properties = properties;
if (_properties == null) {
_properties = properties = new LinkedHashMap<>();
}
_properties.put(key, value);
return this;
}
/**
* Adds a {@link Variables} supplier to this builder.
* @param supplier the variables supplier to add
* @return this builder instance
*/
public Builder add(@Nullable Variables supplier) {
if (supplier != null) {
suppliers.add(supplier);
}
return this;
}
/**
* Adds a map of key-value mappings as a {@link Variables} supplier.
* @param m the map of key-value mappings to add
* @return this builder instance
*/
public Builder add(Map m) {
suppliers.add(m::get);
return this;
}
/**
* Adds a set of properties as a {@link Variables} supplier.
* @param properties the properties to add
* @return this builder instance
*/
public Builder add(Properties properties) {
suppliers.add(Variables.create(properties));
return this;
}
/**
* Builds a {@link Variables} instance from the current state of this builder,
* combining all added sources into a chain.
* @return a new {@link Variables} instance
*/
public Variables build() {
List candidates = new ArrayList<>(suppliers.size() + 1);
Map p = properties;
if (p != null && !p.isEmpty()) {
candidates.add(p::get);
}
candidates.addAll(suppliers);
return copyOf(candidates);
}
/**
* A {@link Variables} implementation that chains multiple {@code Variables}
* sources together, checking each one in sequence for the value of a given key.
*/
private static class ChainedVariables implements Variables {
private Iterable extends Variables> variables;
/**
* Constructs a {@code ChainedVariables} instance that checks each
* {@code Variables} in sequence for a key's value.
* @param variables an iterable of {@code Variables} to chain
*/
public ChainedVariables(Iterable extends Variables> variables) {
super();
this.variables = variables;
}
@Override
public @Nullable String getValue(String name) {
for (Variables p : variables) {
String v = p.getValue(name);
if (v != null)
return v;
}
return null;
}
@Override
public String toString() {
return "ChainedVariables[" + variables + "]";
}
}
}
/**
* Creates variables from system properties.
* @param env environment facade.
* @return variables.
*/
public static Variables ofSystemProperties(KeyValuesEnvironment env) {
return k -> env.getSystemProperties().getProperty(k);
}
/**
* Creates variables from system environment variables.
* @param env environment facade.
* @return variables.
*/
public static Variables ofSystemEnv(KeyValuesEnvironment env) {
return k -> env.getSystemEnv().get(k);
}
/**
* Creates a {@code Variables} instance from the provided properties.
* @param properties the properties to use as a variable mapping
* @return a new {@code Variables} instance
*/
public static Variables create(Properties properties) {
return properties::getProperty;
}
/**
* Creates a {@code Variables} instance that chains together multiple
* {@link Variables} sources.
* @param variables an iterable of {@link Variables} to chain
* @return a new {@code Variables} instance
*/
static Variables create(final SequencedCollection extends Variables> variables) {
return new Builder.ChainedVariables(variables);
}
/**
* Creates a {@code Variables} instance that chains together multiple
* {@link Variables} sources but copies and filters the collection.
* @param variables an iterable of {@link Variables} to chain
* @return a new {@code Variables} instance
*/
public static Variables copyOf(final SequencedCollection extends Variables> variables) {
List list = new ArrayList<>();
for (var v : variables) {
if (v != empty()) {
list.add(v);
}
}
if (list.isEmpty()) {
return empty();
}
if (list.size() == 1) {
return list.get(0);
}
return Variables.create(list);
}
}
enum EmptyVariables implements Parameters {
EMPTY_VARIABLES;
@Override
public @Nullable String getValue(String key) {
return null;
}
@Override
public Iterable keys() {
return List.of();
}
}
record MapParameters(Map map) implements Parameters {
MapParameters {
map = new LinkedHashMap<>(map);
}
@Override
public @Nullable String getValue(String key) {
return map.get(key);
}
@Override
@SuppressWarnings("return")
public Iterable keys() {
return map.keySet();
}
}