Title: Allow collections with multiple types in Configuration Properties
It's not uncommon for libraries or applications that accept configuration properties to
need to extend their model. For example, here is a configuration
for zalando/logbook, where
the logbook.obfuscate.headers
property is a list of strings, and is mapped to
a List<String>
in a @ConfigurationProperties
-annotated class:
logbook:
obfuscate:
headers:
- Authorization
- Cookie
The problem is, if we decide that these strings aren't enough, and decide to also accept
regexes or prefix matches without a breaking change in the library, we have to resort to
either using an alternate property, or using magic strings like regex->.*secret.*
.
A better pattern would be to accept either strings or objects in the list of headers, so that clients can specify header names if that's all they need, or specify more complex objects if they need particular behaviour. This satisfies both readability and extensibility:
logbook:
obfuscate:
headers:
- Authorization
- Cookie
- type: pattern
value: .*secret.*
- type: startsWith
value: x-secret
With the introduction of sealed interfaces, it would be possible to bind these properties in a type-safe manner. There are a several ways of implementing this, but one would be to expose interfaces for primitives, and to let clients specify how one type or the other should be selected for binding:
import lombok.Getter;
// Hypothetical Spring Boot API
// Extending this would let users bind Strings to objects, and
// there could be corresponding IntegerProperty, BooleanProperty, etc.
public interface StringProperty {
String getProp();
}
// start non-boot library code
sealed interface HeaderMatcher permits SimpleHeaderMatcher, PatternHeaderMatcher {
}
@Getter
@RequiredArgsConstructor
final class SimpleHeaderMatcher implements StringProperty, HeaderMatcher {
private final String prop;
}
// Here, `context` would be
@DiscriminatedBy("#{context.type == 'pattern'}")
record PatternHeaderMatcher(String value) implements HeaderMatcher {
}
@ConfigurationProperties("logbook.obfuscate.headers")
record LogbookProperties(List<HeaderMatcher> headers) {
}
Note: I'm not a zalando/logbook maintainer or contributor, it just made for a good example.