001package io.jstach.ezkv.kvs; 002 003import java.util.ArrayList; 004import java.util.LinkedHashMap; 005import java.util.List; 006import java.util.Map; 007import java.util.Objects; 008import java.util.Map.Entry; 009import java.util.Optional; 010import java.util.Properties; 011import java.util.SequencedCollection; 012import java.util.function.BiConsumer; 013import java.util.function.Function; 014 015import org.jspecify.annotations.Nullable; 016 017import io.jstach.ezkv.kvs.Variables.Parameters; 018 019/** 020 * Represents a mapping function that associates a key to a value, typically used for 021 * variable resolution during interpolation. 022 * <p> 023 * The {@code Variables} interface allows lookup of values based on keys and is used in 024 * contexts where key-to-value mappings are required for interpolation. Unlike 025 * {@link KeyValues}, there are no duplicate keys in {@code Variables}. <strong> Variables 026 * unlike KeyValues may not end up in the final config when loaded and that is why there 027 * is a distinction.</strong>. This distinction for example allows you to use environment 028 * variables for lookup but not have the final config contain all the environment 029 * variables. 030 */ 031@FunctionalInterface 032public interface Variables extends Function<String, @Nullable String> { 033 034 /** 035 * Retrieves the value mapped to the specified key. 036 * @param key the key whose value is to be retrieved 037 * @return the value associated with the key, or {@code null} if not found 038 */ 039 public @Nullable String getValue(String key); 040 041 /** 042 * Applies functional composition to rename key while retrieving value this variables. 043 * @param keyFunc function to rename key. 044 * @return composed variables instance. 045 */ 046 default Variables renameKey(Function<String, String> keyFunc) { 047 return k -> this.getValue(keyFunc.apply(k)); 048 } 049 050 @Override 051 default @Nullable String apply(String t) { 052 return getValue(t); 053 } 054 055 /** 056 * Find a variable key-value tuple based on vararg keys. This method provides 057 * ergonomics for searching for fallback keys. 058 * @param name names to String. Note if an array passed in is null an NPE will be 059 * thrown but contents of the array (keys) maybe null and are skipped if they are. 060 * @return optional entry where the key is the first matching key and the value is the 061 * value associated with that key. 062 */ 063 default Optional<Entry<String, String>> findEntry(String... name) { 064 for (String k : name) { 065 if (k == null) 066 continue; 067 String prop = getValue(k); 068 if (prop != null) 069 return Optional.of(Map.entry(k, prop)); 070 } 071 return Optional.empty(); 072 } 073 074 /** 075 * Find a variable key-value tuple based on vararg keys. This method provides 076 * ergonomics for searching for fallback keys. 077 * @param names to use for keys. 078 * @return optional entry where the key is the first matching key and the value is the 079 * value associated with that key. 080 */ 081 default Optional<Entry<String, String>> findEntry(SequencedCollection<String> names) { 082 for (String k : names) { 083 String prop = getValue(k); 084 if (prop != null) 085 return Optional.of(Map.entry(k, prop)); 086 } 087 return Optional.empty(); 088 } 089 090 /** 091 * A mixin for enums or similar representing parameters that provides some validation 092 * on retrieving variables. 093 */ 094 public interface Parameter { 095 096 /** 097 * Keys of the parameter. 098 * @return keys to try. 099 */ 100 List<String> keys(); 101 102 /** 103 * Require a value and transform or throw exception if missing or invalid. 104 * @param <T> transformed type. 105 * @param variables variables to retrieve value from. 106 * @param compute function to transform value. 107 * @return transformed value. 108 * @throws IllegalArgumentException if not found or compute function is invalid. 109 */ 110 default <T> T require(Variables variables, Function<String, T> compute) throws IllegalArgumentException { 111 var entry = entry(variables); 112 try { 113 return compute.apply(entry.getValue()); 114 } 115 catch (RuntimeException e) { 116 String message = message(entry, e); 117 throw new IllegalArgumentException(message, e); 118 } 119 } 120 121 /** 122 * Require a value and transform or throw exception if missing or invalid. 123 * @param <T> transformed type. 124 * @param variables variables to retrieve value from. 125 * @param compute function to transform value. 126 * @param fallback returned if missing. 127 * @return transformed value. 128 * @throws IllegalArgumentException if not found or compute function is invalid. 129 */ 130 default <T> T requireElse(Variables variables, Function<String, T> compute, T fallback) 131 throws IllegalArgumentException { 132 var entry = variables.findEntry(keys()).orElse(null); 133 if (entry == null) { 134 return fallback; 135 } 136 try { 137 return compute.apply(entry.getValue()); 138 } 139 catch (RuntimeException e) { 140 String message = message(entry, e); 141 throw new IllegalArgumentException(message, e); 142 } 143 } 144 145 private static String message(Entry<String, String> entry, RuntimeException e) { 146 String message = e.getMessage(); 147 message = message == null ? "" : " " + message; 148 message = "Parameter is invalid. key='%s', value='%s.%s'".formatted(entry.getKey(), entry.getValue(), 149 message); 150 return message; 151 } 152 153 /** 154 * Require a value. 155 * @param variables to retrieve value from. 156 * @return value. 157 * @throws IllegalArgumentException if value is missing. 158 */ 159 default String require(Variables variables) throws IllegalArgumentException { 160 return entry(variables).getValue(); 161 } 162 163 /** 164 * Require a value. 165 * @param variables to retrieve value from. 166 * @param fallback if value is missing from variables return fallback. 167 * @return value. 168 */ 169 default String requireElse(Variables variables, String fallback) { 170 var v = variables.findEntry(keys()).map(e -> e.getValue()).orElse(null); 171 return Objects.requireNonNullElse(v, fallback); 172 } 173 174 /** 175 * Requires the parameter. 176 * @param variables variables. 177 * @return key and value. 178 */ 179 default Entry<String, String> entry(Variables variables) { 180 var ks = keys(); 181 Entry<String, String> e = variables.findEntry(ks).orElseThrow(() -> { 182 String k = ks.size() == 1 ? ks.get(0) : ks.toString(); 183 throw new IllegalArgumentException("Parameter is missing. key(s) tried = '" + k + "'"); 184 }); 185 return e; 186 } 187 188 } 189 190 /** 191 * Represents a specialized {@code Variables} interface that can list all keys it 192 * knows about. The data in {@code Parameters} is guaranteed to be immutable and 193 * copied when created. 194 * 195 * <p> 196 * Unlike {@link KeyValues}, there are no duplicate keys in {@code Parameters}. 197 */ 198 public sealed interface Parameters extends Variables { 199 200 /** 201 * Returns an iterable over all keys that this {@code Parameters} instance knows 202 * about. 203 * @return an iterable of keys 204 */ 205 public Iterable<String> keys(); 206 207 /** 208 * Iterates over all key-value pairs in this {@code Parameters} instance and 209 * applies them to a provided consumer. 210 * @param consumer the consumer to apply each key-value pair 211 */ 212 default void forKeyValues(BiConsumer<String, String> consumer) { 213 for (var k : keys()) { 214 String value = getValue(k); 215 if (value == null) { 216 continue; 217 } 218 consumer.accept(k, value); 219 } 220 } 221 222 /** 223 * Creates a {@code Parameters} instance from a provided map. 224 * @param map the map to create the parameters from 225 * @return a new {@code Parameters} instance 226 */ 227 public static Parameters of(Map<String, String> map) { 228 return new MapParameters(map); 229 } 230 231 } 232 233 /** 234 * Returns an empty {@code Parameters} instance. 235 * @return an empty {@code Parameters} instance 236 */ 237 public static Parameters empty() { 238 return EmptyVariables.EMPTY_VARIABLES; 239 } 240 241 /** 242 * Creates a new {@code Variables} builder for constructing a composite 243 * {@code Variables} instance. 244 * @return a new {@link Builder} instance 245 */ 246 public static Variables.Builder builder() { 247 return new Builder(); 248 } 249 250 /** 251 * A builder class for creating composite {@link Variables} instances by combining 252 * multiple sources of key-to-value mappings. <strong> NOTE: Variables resolution 253 * order is the opposite of KeyValues. Primacy takes precedence! </strong>. 254 */ 255 public final static class Builder implements BiConsumer<String, String> { 256 257 private @Nullable Map<String, String> properties = null; 258 259 private List<Variables> suppliers = new ArrayList<>(); 260 261 private Builder() { 262 } 263 264 /** 265 * Adds a key-value mapping to this builder. 266 * @param key the key to add 267 * @param value the value associated with the key 268 * @return this builder instance 269 */ 270 public Builder add(String key, String value) { 271 Map<String, String> _properties = properties; 272 if (_properties == null) { 273 _properties = properties = new LinkedHashMap<>(); 274 } 275 _properties.put(key, value); 276 return this; 277 } 278 279 @Override 280 public void accept(String t, String u) { 281 add(t, u); 282 } 283 284 /** 285 * Adds a {@link Variables} supplier to this builder. 286 * @param supplier the variables supplier to add 287 * @return this builder instance 288 */ 289 public Builder add(@Nullable Variables supplier) { 290 if (supplier != null) { 291 suppliers.add(supplier); 292 } 293 return this; 294 } 295 296 /** 297 * Adds a map of key-value mappings as a {@link Variables} supplier. 298 * @param m the map of key-value mappings to add 299 * @return this builder instance 300 */ 301 public Builder add(Map<String, String> m) { 302 suppliers.add(m::get); 303 return this; 304 } 305 306 /** 307 * Adds a set of properties as a {@link Variables} supplier. 308 * @param properties the properties to add 309 * @return this builder instance 310 */ 311 public Builder add(Properties properties) { 312 suppliers.add(Variables.create(properties)); 313 return this; 314 } 315 316 /** 317 * Builds a {@link Variables} instance from the current state of this builder, 318 * combining all added sources into a chain. 319 * @return a new {@link Variables} instance 320 */ 321 public Variables build() { 322 List<Variables> candidates = new ArrayList<>(suppliers.size() + 1); 323 Map<String, String> p = properties; 324 if (p != null && !p.isEmpty()) { 325 candidates.add(p::get); 326 } 327 candidates.addAll(suppliers); 328 return copyOf(candidates); 329 } 330 331 /** 332 * A {@link Variables} implementation that chains multiple {@code Variables} 333 * sources together, checking each one in sequence for the value of a given key. 334 */ 335 private static class ChainedVariables implements Variables { 336 337 private Iterable<? extends Variables> variables; 338 339 /** 340 * Constructs a {@code ChainedVariables} instance that checks each 341 * {@code Variables} in sequence for a key's value. 342 * @param variables an iterable of {@code Variables} to chain 343 */ 344 public ChainedVariables(Iterable<? extends Variables> variables) { 345 super(); 346 this.variables = variables; 347 } 348 349 @Override 350 public @Nullable String getValue(String name) { 351 for (Variables p : variables) { 352 String v = p.getValue(name); 353 if (v != null) 354 return v; 355 } 356 return null; 357 } 358 359 @Override 360 public String toString() { 361 return "ChainedVariables[" + variables + "]"; 362 } 363 364 } 365 366 } 367 368 /** 369 * Creates variables from system properties. 370 * @param env environment facade. 371 * @return variables. 372 */ 373 public static Variables ofSystemProperties(KeyValuesEnvironment env) { 374 return k -> env.getSystemProperties().getProperty(k); 375 } 376 377 /** 378 * Creates variables from system environment variables. 379 * @param env environment facade. 380 * @return variables. 381 */ 382 public static Variables ofSystemEnv(KeyValuesEnvironment env) { 383 return k -> env.getSystemEnv().get(k); 384 } 385 386 /** 387 * Creates a {@code Variables} instance from the provided properties. 388 * @param properties the properties to use as a variable mapping 389 * @return a new {@code Variables} instance 390 */ 391 public static Variables create(Properties properties) { 392 return properties::getProperty; 393 } 394 395 /** 396 * Creates a {@code Variables} instance that chains together multiple 397 * {@link Variables} sources. 398 * @param variables an iterable of {@link Variables} to chain 399 * @return a new {@code Variables} instance 400 */ 401 static Variables create(final SequencedCollection<? extends Variables> variables) { 402 return new Builder.ChainedVariables(variables); 403 } 404 405 /** 406 * Creates a {@code Variables} instance that chains together multiple 407 * {@link Variables} sources but copies and filters the collection. 408 * @param variables an iterable of {@link Variables} to chain 409 * @return a new {@code Variables} instance 410 */ 411 public static Variables copyOf(final SequencedCollection<? extends Variables> variables) { 412 List<Variables> list = new ArrayList<>(); 413 for (var v : variables) { 414 if (v != empty()) { 415 list.add(v); 416 } 417 } 418 if (list.isEmpty()) { 419 return empty(); 420 } 421 if (list.size() == 1) { 422 return list.get(0); 423 } 424 return Variables.create(list); 425 } 426 427} 428 429enum EmptyVariables implements Parameters { 430 431 EMPTY_VARIABLES; 432 433 @Override 434 public @Nullable String getValue(String key) { 435 return null; 436 } 437 438 @Override 439 public Iterable<String> keys() { 440 return List.of(); 441 } 442 443} 444 445record MapParameters(Map<String, String> map) implements Parameters { 446 MapParameters { 447 map = new LinkedHashMap<>(map); 448 } 449 450 @Override 451 public @Nullable String getValue(String key) { 452 return map.get(key); 453 } 454 455 @Override 456 @SuppressWarnings("return") 457 public Iterable<String> keys() { 458 return map.keySet(); 459 } 460}