Skip to content

Commit

Permalink
Introduce SmallRyeConfig.subset
Browse files Browse the repository at this point in the history
- Fixes smallrye#981
  • Loading branch information
gastaldi committed Aug 24, 2023
1 parent 6bd3669 commit 3e9f521
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 52 deletions.
5 changes: 5 additions & 0 deletions implementation/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@
<groupId>io.smallrye.testing</groupId>
<artifactId>smallrye-testing-utilities</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
97 changes: 63 additions & 34 deletions implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@
*/
package io.smallrye.config;

import static io.smallrye.config.ConfigSourceInterceptor.EMPTY;
import static io.smallrye.config.common.utils.StringUtil.replaceNonAlphanumericByUnderscores;
import static io.smallrye.config.common.utils.StringUtil.toLowerCaseAndDotted;
import io.smallrye.common.annotation.Experimental;
import io.smallrye.config.SmallRyeConfigBuilder.InterceptorWithPriority;
import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigProvider;
import org.eclipse.microprofile.config.spi.ConfigSource;
import org.eclipse.microprofile.config.spi.ConfigSourceProvider;
import org.eclipse.microprofile.config.spi.Converter;

import java.io.ObjectStreamException;
import java.io.Serializable;
Expand All @@ -39,14 +43,9 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.IntFunction;

import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigProvider;
import org.eclipse.microprofile.config.spi.ConfigSource;
import org.eclipse.microprofile.config.spi.ConfigSourceProvider;
import org.eclipse.microprofile.config.spi.Converter;

import io.smallrye.common.annotation.Experimental;
import io.smallrye.config.SmallRyeConfigBuilder.InterceptorWithPriority;
import static io.smallrye.config.ConfigSourceInterceptor.EMPTY;
import static io.smallrye.config.common.utils.StringUtil.replaceNonAlphanumericByUnderscores;
import static io.smallrye.config.common.utils.StringUtil.toLowerCaseAndDotted;

/**
* @author <a href="http://jmesnil.net/">Jeff Mesnil</a> (c) 2017 Red Hat inc.
Expand Down Expand Up @@ -112,7 +111,7 @@ public <T, C extends Collection<T>> C getValues(String name, Converter<T> conver
}

public <T, C extends Collection<T>> C getIndexedValues(String name, Converter<T> converter,
IntFunction<C> collectionFactory) {
IntFunction<C> collectionFactory) {
List<String> indexedProperties = getIndexedProperties(name);
if (indexedProperties.isEmpty()) {
throw new NoSuchElementException(ConfigMessages.msg.propertyNotFound(name));
Expand Down Expand Up @@ -142,7 +141,7 @@ public List<Integer> getIndexedPropertiesIndexes(final String property) {
if (propertyName.startsWith(property) && propertyName.length() > property.length()) {
int index = property.length();
if (propertyName.charAt(index) == '[') {
for (;;) {
for (; ; ) {
if (propertyName.charAt(index) == ']') {
try {
indexes.add(Integer.parseInt(propertyName.substring(property.length() + 1, index)));
Expand Down Expand Up @@ -172,14 +171,14 @@ public <T> T getValue(String name, Class<T> aClass) {
/**
* Return the content of the direct sub properties as the requested type of Map.
*
* @param name The configuration property name
* @param name The configuration property name
* @param kClass the type into which the keys should be converted
* @param vClass the type into which the values should be converted
* @param <K> the key type
* @param <V> the value type
* @param <K> the key type
* @param <V> the value type
* @return the resolved property value as an instance of the requested Map (not {@code null})
* @throws IllegalArgumentException if a key or a value cannot be converted to the specified types
* @throws NoSuchElementException if no direct sub properties could be found.
* @throws NoSuchElementException if no direct sub properties could be found.
*/
public <K, V> Map<K, V> getValues(String name, Class<K> kClass, Class<V> vClass) {
final Map<K, V> result = getValuesAsMap(name, requireConverter(kClass), requireConverter(vClass));
Expand All @@ -192,11 +191,11 @@ public <K, V> Map<K, V> getValues(String name, Class<K> kClass, Class<V> vClass)
/**
* Return the content of the direct sub properties as the requested type of Map.
*
* @param name The configuration property name
* @param keyConverter The converter to use for the keys.
* @param name The configuration property name
* @param keyConverter The converter to use for the keys.
* @param valueConverter The converter to use for the values.
* @param <K> The type of the keys.
* @param <V> The type of the values.
* @param <K> The type of the keys.
* @param <V> The type of the values.
* @return the resolved property value as an instance of the requested Map or {@code null} if it could not be found.
* @throws IllegalArgumentException if a key or a value cannot be converted to the specified types
*/
Expand All @@ -223,7 +222,6 @@ public <K, V> Map<K, V> getValuesAsMap(String name, Converter<K> keyConverter, C
}

/**
*
* This method handles calls from both {@link Config#getValue} and {@link Config#getOptionalValue}.<br>
*/
@SuppressWarnings("unchecked")
Expand All @@ -246,17 +244,17 @@ public <T> T getValue(String name, Converter<T> converter) {
/**
* This method handles converting values for both CDI injections and programatical calls.<br>
* <br>
*
* <p>
* Calls for converting non-optional values ({@link Config#getValue} and "Injecting Native Values")
* should throw an {@link Exception} for each of the following:<br>
*
* <p>
* 1. {@link IllegalArgumentException} - if the property cannot be converted by the {@link Converter} to the specified type
* <br>
* 2. {@link NoSuchElementException} - if the property is not defined <br>
* 3. {@link NoSuchElementException} - if the property is defined as an empty string <br>
* 4. {@link NoSuchElementException} - if the {@link Converter} returns {@code null} <br>
* <br>
*
* <p>
* Calls for converting optional values ({@link Config#getOptionalValue} and "Injecting Optional Values")
* should only throw an {@link Exception} for #1 ({@link IllegalArgumentException} when the property cannot be converted to
* the specified type).
Expand Down Expand Up @@ -312,7 +310,7 @@ public <T> T convertValue(ConfigValue configValue, Converter<T> converter) {
* Determine whether the <em>raw value</em> of a configuration property is exactly equal to the expected given
* value.
*
* @param name the property name (must not be {@code null})
* @param name the property name (must not be {@code null})
* @param expected the expected value (may be {@code null})
* @return {@code true} if the values are equal, {@code false} otherwise
*/
Expand Down Expand Up @@ -344,11 +342,11 @@ public <T> Optional<T> getOptionalValue(String name, Class<T> aClass) {
/**
* Return the content of the direct sub properties as the requested type of Map.
*
* @param name The configuration property name
* @param name The configuration property name
* @param kClass the type into which the keys should be converted
* @param vClass the type into which the values should be converted
* @param <K> the key type
* @param <V> the value type
* @param <K> the key type
* @param <V> the value type
* @return the resolved property value as an instance of the requested Map (not {@code null})
* @throws IllegalArgumentException if a key or a value cannot be converted to the specified types
*/
Expand All @@ -365,12 +363,12 @@ public <T> Optional<List<T>> getOptionalValues(final String propertyName, final
}

public <T, C extends Collection<T>> Optional<C> getOptionalValues(String name, Class<T> itemClass,
IntFunction<C> collectionFactory) {
IntFunction<C> collectionFactory) {
return getOptionalValues(name, requireConverter(itemClass), collectionFactory);
}

public <T, C extends Collection<T>> Optional<C> getOptionalValues(String name, Converter<T> converter,
IntFunction<C> collectionFactory) {
IntFunction<C> collectionFactory) {
final Optional<C> optionalValue = getOptionalValue(name,
Converters.newCollectionConverter(converter, collectionFactory));
if (optionalValue.isPresent()) {
Expand All @@ -381,7 +379,7 @@ public <T, C extends Collection<T>> Optional<C> getOptionalValues(String name, C
}

public <T, C extends Collection<T>> Optional<C> getIndexedOptionalValues(String name, Converter<T> converter,
IntFunction<C> collectionFactory) {
IntFunction<C> collectionFactory) {
List<String> indexedProperties = getIndexedProperties(name);
if (indexedProperties.isEmpty()) {
return Optional.empty();
Expand Down Expand Up @@ -459,11 +457,42 @@ public Optional<ConfigSource> getConfigSource(final String name) {
return Optional.empty();
}

/**
* Return a {@link Config} containing every key from the current {@link Config} that starts with the specified
* prefix. The prefix is removed from the keys in the subset. For example, if the configuration contains the following
* properties:
*
* <pre>
* prefix.number = 1
* prefix.string = Hello
* prefixed.foo = bar
* prefix = World
* </pre>
* <p>
* the Configuration returned by {@code subset("prefix")} will contain the properties:
*
* <pre>
* number = 1
* string = Hello
* = World
* </pre>
* <p>
* (The key for the value "World" is an empty string)
* <p>
*
* @param prefix The prefix used to select the properties.
* @return a subset configuration
*/
@Experimental("Return a subset of the configuration")
public Config subset(final String prefix) {
return new SmallRyeSubsetConfig(prefix, this);
}

public <T> T convert(String value, Class<T> asType) {
return value != null ? requireConverter(asType).convert(value) : null;
}

@SuppressWarnings({ "unchecked", "rawtypes" })
@SuppressWarnings({"unchecked", "rawtypes"})
private <T> Converter<Optional<T>> getOptionalConverter(Class<T> asType) {
return optionalConverters.computeIfAbsent(asType,
clazz -> Converters.newOptionalConverter(requireConverter((Class) clazz)));
Expand Down Expand Up @@ -733,7 +762,7 @@ private static List<ConfigurableConfigSource> getConfigurableSources(final List<
* If <code>FOO_BAR</code> is present a property <code>foo.bar</code> is required.
*/
private static Set<String> generateDottedProperties(final List<ConfigSource> sources,
final SmallRyeConfigSourceInterceptorContext current) {
final SmallRyeConfigSourceInterceptorContext current) {
// Collect all known properties
Set<String> properties = new HashSet<>();
Iterator<String> iterateNames = current.iterateNames();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package io.smallrye.config;

import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigValue;
import org.eclipse.microprofile.config.spi.ConfigSource;
import org.eclipse.microprofile.config.spi.Converter;

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

/**
* @author George Gastaldi
*/
public class SmallRyeSubsetConfig implements Config {

private final String prefix;

private final Config delegate;

public SmallRyeSubsetConfig(String prefix, Config delegate) {
this.prefix = prefix;
this.delegate = delegate;
}

@Override
public <T> T getValue(String propertyName, Class<T> propertyType) {
return delegate.getValue(toSubsetPropertyName(propertyName), propertyType);
}

@Override
public ConfigValue getConfigValue(String propertyName) {
return delegate.getConfigValue(toSubsetPropertyName(propertyName));
}

@Override
public <T> List<T> getValues(String propertyName, Class<T> propertyType) {
return delegate.getValues(toSubsetPropertyName(propertyName), propertyType);
}

@Override
public <T> Optional<T> getOptionalValue(String propertyName, Class<T> propertyType) {
return delegate.getOptionalValue(toSubsetPropertyName(propertyName), propertyType);
}

@Override
public <T> Optional<List<T>> getOptionalValues(String propertyName, Class<T> propertyType) {
return delegate.getOptionalValues(toSubsetPropertyName(propertyName), propertyType);
}

@Override
public Iterable<String> getPropertyNames() {
return StreamSupport.stream(delegate.getPropertyNames().spliterator(), false)
.map(this::chopSubsetPropertyName)
.collect(Collectors.toSet());
}

@Override
public Iterable<ConfigSource> getConfigSources() {
return delegate.getConfigSources();
}

@Override
public <T> Optional<Converter<T>> getConverter(Class<T> forType) {
return delegate.getConverter(forType);
}

@Override
public <T> T unwrap(Class<T> type) {
return delegate.unwrap(type);
}

private String toSubsetPropertyName(String propertyName) {
if (propertyName.isBlank()) {
return prefix;
} else {
return prefix + "." + propertyName;
}
}

private String chopSubsetPropertyName(String propertyName) {
if (propertyName.equalsIgnoreCase(prefix)) {
return "";
} else if (propertyName.startsWith(prefix)) {
return propertyName.substring(prefix.length() + 1);
} else {
return propertyName;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
package io.smallrye.config;

import static io.smallrye.config.Converters.STRING_CONVERTER;
import static io.smallrye.config.KeyValuesConfigSource.config;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static java.util.stream.Collectors.toSet;
import static java.util.stream.StreamSupport.stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import io.smallrye.config.common.AbstractConfigSource;
import io.smallrye.config.common.MapBackedConfigSource;
import org.assertj.core.api.Assertions;
import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.spi.ConfigSource;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.Arrays;
Expand All @@ -23,12 +17,14 @@
import java.util.Optional;
import java.util.Set;

import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.spi.ConfigSource;
import org.junit.jupiter.api.Test;

import io.smallrye.config.common.AbstractConfigSource;
import io.smallrye.config.common.MapBackedConfigSource;
import static io.smallrye.config.Converters.STRING_CONVERTER;
import static io.smallrye.config.KeyValuesConfigSource.config;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static java.util.stream.Collectors.toSet;
import static java.util.stream.StreamSupport.stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

class SmallRyeConfigTest {
@Test
Expand Down Expand Up @@ -405,4 +401,19 @@ void emptyPropertyNames() {

assertEquals("value", config.getRawValue(""));
}

@Test
void subset() {
SmallRyeConfig config = new SmallRyeConfigBuilder()
.withSources(config(
"app.foo", "bar",
"app.foo.user", "guest",
"app.foo.password", "apassword"))
.build();
Config subset = config.subset("app.foo");
assertEquals("bar", subset.getValue("", String.class));
assertEquals("guest", subset.getValue("user", String.class));
assertEquals("apassword", subset.getValue("password", String.class));
assertThat(subset.getPropertyNames()).containsAnyOf("", "user", "password");
}
}

0 comments on commit 3e9f521

Please sign in to comment.