mirror of
https://github.com/lightbend/config.git
synced 2025-01-15 23:01:05 +08:00
missing, optional substitutions ${?foo} become undefined not null
This is more useful behavior because it permits an optional override for example.
This commit is contained in:
parent
4dddfdaf19
commit
6ec2681055
21
README.md
21
README.md
@ -193,6 +193,27 @@ You can take advantage of this for "inheritance":
|
||||
Using `include` statements you could split this across multiple
|
||||
files, too.
|
||||
|
||||
#### Optional system or env variable overrides
|
||||
|
||||
In default uses of the library, exact-match system properties
|
||||
already override the corresponding config properties. However,
|
||||
you can add your own overrides, or allow environment variables to
|
||||
override, using the `${?foo}` substitution syntax.
|
||||
|
||||
basedir = "/whatever/whatever"
|
||||
basedir = ${?FORCED_BASEDIR}
|
||||
|
||||
Here, the override field `basedir = ${?FORCED_BASEDIR}` simply
|
||||
vanishes if there's no value for `FORCED_BASEDIR`, but if you set
|
||||
an environment variable `FORCED_BASEDIR` for example, it would be
|
||||
used.
|
||||
|
||||
Object fields and array elements with a `${?foo}` substitution
|
||||
value just disappear if the substitution is not found.
|
||||
|
||||
// this array could have one or two elements
|
||||
path = [ "a", ${?OPTIONAL_A} ]
|
||||
|
||||
## Future Directions
|
||||
|
||||
Here are some features that might be nice to add.
|
||||
|
@ -200,6 +200,8 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements
|
||||
Map<String, AbstractConfigValue> changes = null;
|
||||
for (String k : keySet()) {
|
||||
AbstractConfigValue v = peek(k);
|
||||
// "modified" may be null, which means remove the child;
|
||||
// to do that we put null in the "changes" map.
|
||||
AbstractConfigValue modified = modifier.modifyChild(v);
|
||||
if (modified != v) {
|
||||
if (changes == null)
|
||||
@ -213,7 +215,12 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements
|
||||
Map<String, AbstractConfigValue> modified = new HashMap<String, AbstractConfigValue>();
|
||||
for (String k : keySet()) {
|
||||
if (changes.containsKey(k)) {
|
||||
modified.put(k, changes.get(k));
|
||||
AbstractConfigValue newValue = changes.get(k);
|
||||
if (newValue != null) {
|
||||
modified.put(k, newValue);
|
||||
} else {
|
||||
// remove this child; don't put it in the new map.
|
||||
}
|
||||
} else {
|
||||
modified.put(k, peek(k));
|
||||
}
|
||||
|
@ -77,10 +77,12 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements
|
||||
AbstractConfigValue merged = null;
|
||||
for (AbstractConfigValue v : stack) {
|
||||
AbstractConfigValue resolved = resolver.resolve(v, depth, options);
|
||||
if (merged == null)
|
||||
merged = resolved;
|
||||
else
|
||||
merged = merged.withFallback(resolved);
|
||||
if (resolved != null) {
|
||||
if (merged == null)
|
||||
merged = resolved;
|
||||
else
|
||||
merged = merged.withFallback(resolved);
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
|
@ -191,14 +191,8 @@ final class ConfigSubstitution extends AbstractConfigValue implements
|
||||
"ConfigSubstitution should never contain a single String piece");
|
||||
SubstitutionExpression exp = (SubstitutionExpression) pieces.get(0);
|
||||
ConfigValue v = resolve(resolver, exp, depth, options);
|
||||
if (v == null) {
|
||||
if (exp.optional()) {
|
||||
// FIXME want to delete the field or array element here
|
||||
// instead of this
|
||||
v = new ConfigNull(origin());
|
||||
} else {
|
||||
throw new ConfigException.UnresolvedSubstitution(origin(), exp.toString());
|
||||
}
|
||||
if (v == null && !exp.optional()) {
|
||||
throw new ConfigException.UnresolvedSubstitution(origin(), exp.toString());
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
|
||||
import com.typesafe.config.ConfigException;
|
||||
import com.typesafe.config.ConfigList;
|
||||
import com.typesafe.config.ConfigOrigin;
|
||||
import com.typesafe.config.ConfigResolveOptions;
|
||||
@ -68,8 +67,9 @@ final class SimpleConfigList extends AbstractConfigValue implements ConfigList {
|
||||
}
|
||||
|
||||
// once the new list is created, all elements
|
||||
// have to go in it.
|
||||
if (changed != null) {
|
||||
// have to go in it. if modifyChild returned
|
||||
// null, we drop that element.
|
||||
if (changed != null && modified != null) {
|
||||
changed.add(modified);
|
||||
}
|
||||
|
||||
@ -77,9 +77,6 @@ final class SimpleConfigList extends AbstractConfigValue implements ConfigList {
|
||||
}
|
||||
|
||||
if (changed != null) {
|
||||
if (changed.size() != value.size())
|
||||
throw new ConfigException.BugOrBroken(
|
||||
"substituted list's size doesn't match");
|
||||
return new SimpleConfigList(origin(), changed, newResolveStatus);
|
||||
} else {
|
||||
return this;
|
||||
|
@ -16,6 +16,8 @@ import com.typesafe.config.ConfigResolveOptions;
|
||||
*/
|
||||
final class SubstitutionResolver {
|
||||
final private AbstractConfigObject root;
|
||||
// note that we can resolve things to undefined (represented as Java null,
|
||||
// rather than ConfigNull) so this map can have null values.
|
||||
final private Map<AbstractConfigValue, AbstractConfigValue> memos;
|
||||
|
||||
SubstitutionResolver(AbstractConfigObject root) {
|
||||
@ -31,9 +33,11 @@ final class SubstitutionResolver {
|
||||
} else {
|
||||
AbstractConfigValue resolved = original.resolveSubstitutions(this,
|
||||
depth, options);
|
||||
if (resolved.resolveStatus() != ResolveStatus.RESOLVED)
|
||||
throw new ConfigException.BugOrBroken(
|
||||
"resolveSubstitutions() did not give us a resolved object");
|
||||
if (resolved != null) {
|
||||
if (resolved.resolveStatus() != ResolveStatus.RESOLVED)
|
||||
throw new ConfigException.BugOrBroken(
|
||||
"resolveSubstitutions() did not give us a resolved object");
|
||||
}
|
||||
memos.put(original, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
@ -1,6 +1,3 @@
|
||||
{
|
||||
"a" : null,
|
||||
"b" : null,
|
||||
"c" : null
|
||||
}
|
||||
|
||||
|
@ -127,6 +127,11 @@ class ConfigSubstitutionTest extends TestUtils {
|
||||
val v = resolveWithoutFallbacks(s, simpleObject)
|
||||
// absent object becomes empty string
|
||||
assertEquals(stringValue("start<>end"), v)
|
||||
|
||||
intercept[ConfigException.UnresolvedSubstitution] {
|
||||
val s2 = substInString("bar.missing", false /* optional */ )
|
||||
resolveWithoutFallbacks(s2, simpleObject)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -150,6 +155,32 @@ class ConfigSubstitutionTest extends TestUtils {
|
||||
assertEquals(stringValue("start<3.14>end"), v)
|
||||
}
|
||||
|
||||
@Test
|
||||
def missingInArray() {
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
val obj = parseObject("""
|
||||
a : [ ${?missing}, ${?also.missing} ]
|
||||
""")
|
||||
|
||||
val resolved = resolve(obj)
|
||||
|
||||
assertEquals(Seq(), resolved.getList("a").asScala)
|
||||
}
|
||||
|
||||
@Test
|
||||
def missingInObject() {
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
val obj = parseObject("""
|
||||
a : ${?missing}, b : ${?also.missing}, c : ${?b}, d : ${?c}
|
||||
""")
|
||||
|
||||
val resolved = resolve(obj)
|
||||
|
||||
assertTrue(resolved.isEmpty())
|
||||
}
|
||||
|
||||
private val substChainObject = {
|
||||
parseObject("""
|
||||
{
|
||||
@ -298,7 +329,7 @@ class ConfigSubstitutionTest extends TestUtils {
|
||||
existed += 1
|
||||
assertEquals(e, resolved.getString(k))
|
||||
} else {
|
||||
assertEquals(nullValue, resolved.root.get(k))
|
||||
assertNull(resolved.root.get(k))
|
||||
}
|
||||
}
|
||||
if (existed == 0) {
|
||||
@ -345,7 +376,7 @@ class ConfigSubstitutionTest extends TestUtils {
|
||||
existed += 1
|
||||
assertEquals(e, resolved.getConfig("a").getString(k))
|
||||
} else {
|
||||
assertEquals(nullValue, resolved.getObject("a").get(k))
|
||||
assertNull(resolved.getObject("a").get(k))
|
||||
}
|
||||
}
|
||||
if (existed == 0) {
|
||||
|
@ -37,10 +37,10 @@ class PublicApiTest extends TestUtils {
|
||||
val conf = ConfigFactory.load("test01", ConfigParseOptions.defaults(),
|
||||
ConfigResolveOptions.noSystem())
|
||||
|
||||
intercept[ConfigException.Null] {
|
||||
intercept[ConfigException.Missing] {
|
||||
conf.getString("system.home")
|
||||
}
|
||||
intercept[ConfigException.Null] {
|
||||
intercept[ConfigException.Missing] {
|
||||
conf.getString("system.javaversion")
|
||||
}
|
||||
}
|
||||
|
@ -149,6 +149,8 @@ abstract trait TestUtils {
|
||||
"[$]", // '$' by itself
|
||||
"[$ ]", // '$' by itself with spaces after
|
||||
"[${}]", // empty substitution (no path)
|
||||
"[${?}]", // no path with ? substitution
|
||||
ParseTest(false, true, "[${ ?foo}]"), // space before ? not allowed
|
||||
"""{ "a" : [1,2], "b" : y${a}z }""", // trying to interpolate an array in a string
|
||||
"""{ "a" : { "c" : 2 }, "b" : y${a}z }""", // trying to interpolate an object in a string
|
||||
"""{ "a" : ${a} }""", // simple cycle
|
||||
@ -249,6 +251,7 @@ abstract trait TestUtils {
|
||||
"{ include \"foo\", \"a\" : \"b\" }", // valid include followed by comma and field
|
||||
"{ foo include : 42 }", // valid to have a key not starting with include
|
||||
"[ ${foo} ]",
|
||||
"[ ${?foo} ]",
|
||||
"[ ${\"foo\"} ]",
|
||||
"[ ${foo.bar} ]",
|
||||
"[ abc xyz ${foo.bar} qrs tuv ]", // value concatenation
|
||||
|
Loading…
Reference in New Issue
Block a user