Skip to content

Commit

Permalink
Feat/config container (#155)
Browse files Browse the repository at this point in the history
* feat: configContainers.
  • Loading branch information
credmond-git authored Feb 29, 2024
1 parent 44cb24d commit ec49956
Show file tree
Hide file tree
Showing 10 changed files with 465 additions and 2 deletions.
53 changes: 51 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,7 @@ To register your own default ConfigLoaders add them to the builder, or add it to
| Boolean | Boolean and boolean |
| Byte | Byte and byte |
| Char | Char and char |
| ConfigContainer | A container that caches your config value, and updates it when there is a configuration change. |
| Date | takes a DateTimeFormatter as a parameter, by default it uses DateTimeFormatter.ISO_DATE_TIME |
| Double | Double and double |
| Duration | Decodes a duration from either a number or an ISO 8601 standardized string format like "PT42S". |
Expand Down Expand Up @@ -961,7 +962,7 @@ val dbService1: DBService1 = myApp.koin.get()
```

# Use Cases
## Dynamic overriding config values with command line arguments
## Overriding config values with command line arguments

Often you may wish to override a configuration value with a value provided on the command line.
One way to do this is to add a `SystemPropertiesConfigSource` as the last source in Gestalt. This way it will have the highest priority and override all previous sources.
Expand Down Expand Up @@ -999,7 +1000,7 @@ However, we override with a command line parameter of: `-Dhttp.pool.maxTotal=200

In the end we should get the value 200 based on the overridden command line parameter.

## Dynamic overriding config values with Environment Variables (Env Var)
## Overriding config values with Environment Variables (Env Var)

In a similar vein as overriding with command line variables, you can override with an Environment Variable.
There is two ways of doing this. You can use string substitution but an alternative is to use the `EnvironmentConfigSource`.
Expand Down Expand Up @@ -1082,6 +1083,53 @@ There are several configuration options on the `EnvironmentConfigSource`,
| removePrefix | false | If we should remove the prefix and the following "_" or"." from the imported configuration |


## Dynamically updating config values
Typically, when you get a configuration from Gestalt, you maintain a reference to the value in your class. You typically dont want to call Gestalt each time you want to check the value of the configuration. Although Gestalt has a cache, there is overhead in calling Gestalt each time.
However, when you cache locally if the configuration in Gestalt change via a reload, you will still have a reference to the old value.

So, instead of getting your specific configuration you could request a ConfigContainer.
```java
var myConfigValue = gestalt.getConfig("some.value", new TypeCapture<ConfigContainer<String>>() {});
```
The ConfigContainer will hold your configuration value with several options to get it.
```java
var myValue = configContainer.orElseThrow();
var myOptionalValue = configContainer.getOptional();
```

Then, when there is a reload, the ConfigContainer will get and cache the new configuration. Ensuring you always have the most recent value.

The following example shows a simple use case for ConfigContainer.
```java
Map<String, String> configs = new HashMap<>();
configs.put("some.value", "value1");

var manualReload = new ManualConfigReloadStrategy();

GestaltBuilder builder = new GestaltBuilder();
Gestalt gestalt = builder
.addSource(MapConfigSourceBuilder.builder()
.setCustomConfig(configs)
.addConfigReloadStrategy(manualReload)
.build())
.build();

gestalt.loadConfigs();

var configContainer = gestalt.getConfig("some.value", new TypeCapture<ConfigContainer<String>>() {});

Assertions.assertEquals("value1", configContainer.orElseThrow());

// Change the values in the config map
configs.put("some.value", "value2");

// let gestalt know the values have changed so it can update the config tree.
manualReload.reload();

// The config container is automatically updated.
Assertions.assertEquals("value2", configContainer.orElseThrow());
```

# Example code
For more examples of how to use gestalt see the [gestalt-sample](https://github.com/gestalt-config/gestalt/tree/main/gestalt-examples/gestalt-sample/src/test) or for Java 17 + samples [gestalt-sample-java-latest](https://github.com/gestalt-config/gestalt/tree/main/gestalt-examples/gestalt-sample-java-latest/src/test)

Expand Down Expand Up @@ -1256,3 +1304,4 @@ To register your own default Transformer, add it to a file in META-INF\services\

the annotation @ConfigPriority(100), specifies the descending priority order to check your transformer when a substitution has been made without specifying the source ${key}


Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.github.gestalt.config.decoder;

import org.github.gestalt.config.entity.ConfigContainer;
import org.github.gestalt.config.node.ConfigNode;
import org.github.gestalt.config.reflect.TypeCapture;
import org.github.gestalt.config.tag.Tags;
import org.github.gestalt.config.utils.GResultOf;

/**
* Decodes a generic optional type.
*
* @author <a href="mailto:colin.redmond@outlook.com"> Colin Redmond </a> (c) 2024.
*/
public final class ConfigDecoder implements Decoder<ConfigContainer<?>> {

@Override
public Priority priority() {
return Priority.MEDIUM;
}

@Override
public String name() {
return "ConfigContainer";
}

@Override
public boolean canDecode(String path, Tags tags, ConfigNode node, TypeCapture<?> type) {
return ConfigContainer.class.isAssignableFrom(type.getRawType());
}

@SuppressWarnings({"unchecked", "rawtypes"})
@Override
public GResultOf<ConfigContainer<?>> decode(String path, Tags tags, ConfigNode node, TypeCapture<?> type,
DecoderContext decoderContext) {
// decode the generic type of the optional. Then we will wrap the result into an Optional
GResultOf<?> configValue = decoderContext.getDecoderService()
.decodeNode(path, tags, node, type.getFirstParameterType(), decoderContext);

return configValue.mapWithError((result) -> new ConfigContainer(path, tags, decoderContext, result, type));
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package org.github.gestalt.config.entity;

import org.github.gestalt.config.decoder.DecoderContext;
import org.github.gestalt.config.exceptions.GestaltException;
import org.github.gestalt.config.reflect.TypeCapture;
import org.github.gestalt.config.reload.CoreReloadListener;
import org.github.gestalt.config.tag.Tags;

import java.util.Optional;

/**
* A Container for a configuration value that supports dynamic config reloading.
* Allows users to get the config value.
*
* @param <T> Type of the Config Container
*/
public class ConfigContainer<T> implements CoreReloadListener {

private static final System.Logger logger = System.getLogger(ConfigContainer.class.getName());

// Path used to get the config on reload.
protected final String path;
// Tags used to get the config on reload.
protected final Tags tags;
// Decoder Context hold gestalt to get the config on reload.
protected final DecoderContext decoderContext;
// Type of the Configuration value.
protected final TypeCapture<ConfigContainer<T>> klass;

protected final TypeCapture<T> configContainerType;

@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
Optional<T> value;

@SuppressWarnings("unchecked")
public ConfigContainer(String path, Tags tags, DecoderContext decoderContext, T value, TypeCapture<ConfigContainer<T>> klass) {
this.path = path;
this.tags = tags;
this.decoderContext = decoderContext;
this.klass = klass;
this.value = Optional.ofNullable(value);

configContainerType = (TypeCapture<T>) klass.getFirstParameterType();

decoderContext.getGestalt().registerListener(this);
}

public boolean isPresent() {
return value.isPresent();
}

public T orElseThrow() throws GestaltException {
return value.orElseThrow(() -> new GestaltException("No results for config path: " + path + ", tags: " + tags +
", and class: " + klass.getName()));
}

public Optional<T> getOptional() {
return value;
}

@Override
public void reload() {
value = decoderContext.getGestalt().getConfigOptional(path, configContainerType, tags);

if (value.isEmpty()) {
logger.log(System.Logger.Level.WARNING, "On Reload, no results for config path: " + path + ", tags: " + tags +
", and class: " + klass.getName());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public List<WeakReference<CoreReloadListener>> getListeners() {
* called when the core has reloaded.
*/
public void reload() {
cleanup();
listeners.forEach((it) -> {
var weakRef = it.get();
if (weakRef != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,12 @@ public boolean equals(Object o) {
public int hashCode() {
return Objects.hash(key, value);
}

@Override
public String toString() {
return "Tag{" +
"key='" + key + '\'' +
", value='" + value + '\'' +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,11 @@ public boolean equals(Object o) {
public int hashCode() {
return Objects.hash(internalTags);
}

@Override
public String toString() {
return "Tags{" +
"internalTags=" + internalTags +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ org.github.gestalt.config.decoder.BigIntegerDecoder
org.github.gestalt.config.decoder.BooleanDecoder
org.github.gestalt.config.decoder.ByteDecoder
org.github.gestalt.config.decoder.CharDecoder
org.github.gestalt.config.decoder.ConfigDecoder
org.github.gestalt.config.decoder.DateDecoder
org.github.gestalt.config.decoder.DoubleDecoder
org.github.gestalt.config.decoder.DurationDecoder
Expand Down
Loading

0 comments on commit ec49956

Please sign in to comment.