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}