From b696dbee38ca6ffa11d41e5b44c4df6826594dff Mon Sep 17 00:00:00 2001 From: Havoc Pennington Date: Thu, 5 Apr 2012 14:16:42 -0400 Subject: [PATCH] Fix semantics of references from _inside_ a field value to that field vs. the value itself being a reference to the field. So this looks "backward": a : ${a} vs. this does not: a : { b : ${a} } --- .../config/impl/AbstractConfigObject.java | 18 +--- .../config/impl/AbstractConfigValue.java | 98 ++++++++++++++---- .../config/impl/ConfigConcatenation.java | 37 ------- .../config/impl/ConfigDelayedMerge.java | 59 +++++------ .../config/impl/ConfigDelayedMergeObject.java | 76 ++++++++------ .../typesafe/config/impl/ConfigReference.java | 41 -------- .../typesafe/config/impl/ResolveContext.java | 1 - .../config/impl/SimpleConfigObject.java | 3 +- .../config/impl/ConfigSubstitutionTest.scala | 99 +++++++++++++++++++ .../com/typesafe/config/impl/ConfigTest.scala | 17 +++- 10 files changed, 263 insertions(+), 186 deletions(-) diff --git a/config/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java b/config/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java index 54e2c09b..b2dcd4ce 100644 --- a/config/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java +++ b/config/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java @@ -83,7 +83,7 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements Confi * Looks up the path with no transformation, type conversion, or exceptions * (just returns null if path not found). Does however resolve the path, if * resolver != null. - * + * * @throws NotPossibleToResolve * if context is not null and resolution fails */ @@ -156,19 +156,9 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements Confi } @Override - protected final AbstractConfigObject mergedWithTheUnmergeable(Unmergeable fallback) { - if (ignoresFallbacks()) - throw new ConfigException.BugOrBroken("should not be reached"); - - List stack = new ArrayList(); - if (this instanceof Unmergeable) { - stack.addAll(((Unmergeable) this).unmergedValues()); - } else { - stack.add(this); - } - stack.addAll(fallback.unmergedValues()); - return new ConfigDelayedMergeObject(mergeOrigins(stack), stack, - ((AbstractConfigValue) fallback).ignoresFallbacks()); + protected AbstractConfigObject constructDelayedMerge(ConfigOrigin origin, + List stack) { + return new ConfigDelayedMergeObject(origin, stack); } @Override diff --git a/config/src/main/java/com/typesafe/config/impl/AbstractConfigValue.java b/config/src/main/java/com/typesafe/config/impl/AbstractConfigValue.java index 703ad100..629a60ea 100644 --- a/config/src/main/java/com/typesafe/config/impl/AbstractConfigValue.java +++ b/config/src/main/java/com/typesafe/config/impl/AbstractConfigValue.java @@ -4,6 +4,10 @@ package com.typesafe.config.impl; import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigMergeable; @@ -124,32 +128,91 @@ abstract class AbstractConfigValue implements ConfigValue, MergeableValue, Seria // really need to store the boolean, and they may be able to pack it // with another boolean to save space. protected boolean ignoresFallbacks() { - return true; + // if we are not resolved, then somewhere in this value there's + // a substitution that may need to look at the fallbacks. + return resolveStatus() == ResolveStatus.RESOLVED; } - private ConfigException badMergeException() { + // the withFallback() implementation is supposed to avoid calling + // mergedWith* if we're ignoring fallbacks. + protected final void requireNotIgnoringFallbacks() { if (ignoresFallbacks()) throw new ConfigException.BugOrBroken( - "method should not have been called with ignoresFallbacks=true" + "method should not have been called with ignoresFallbacks=true " + getClass().getSimpleName()); - else - throw new ConfigException.BugOrBroken("should override this in " - + getClass().getSimpleName()); + } + + protected AbstractConfigValue constructDelayedMerge(ConfigOrigin origin, + List stack) { + return new ConfigDelayedMerge(origin, stack); + } + + protected final AbstractConfigValue mergedWithTheUnmergeable( + Collection stack, Unmergeable fallback) { + requireNotIgnoringFallbacks(); + + // if we turn out to be an object, and the fallback also does, + // then a merge may be required; delay until we resolve. + List newStack = new ArrayList(); + newStack.addAll(stack); + newStack.addAll(fallback.unmergedValues()); + return constructDelayedMerge(AbstractConfigObject.mergeOrigins(newStack), newStack); + } + + private final AbstractConfigValue delayMerge(Collection stack, + AbstractConfigValue fallback) { + // if we turn out to be an object, and the fallback also does, + // then a merge may be required. + // if we contain a substitution, resolving it may need to look + // back to the fallback. + List newStack = new ArrayList(); + newStack.addAll(stack); + newStack.add(fallback); + return constructDelayedMerge(AbstractConfigObject.mergeOrigins(newStack), newStack); + } + + protected final AbstractConfigValue mergedWithObject(Collection stack, + AbstractConfigObject fallback) { + requireNotIgnoringFallbacks(); + + if (this instanceof AbstractConfigObject) + throw new ConfigException.BugOrBroken("Objects must reimplement mergedWithObject"); + + return mergedWithNonObject(stack, fallback); + } + + protected final AbstractConfigValue mergedWithNonObject(Collection stack, + AbstractConfigValue fallback) { + requireNotIgnoringFallbacks(); + + if (resolveStatus() == ResolveStatus.RESOLVED) { + // falling back to a non-object doesn't merge anything, and also + // prohibits merging any objects that we fall back to later. + // so we have to switch to ignoresFallbacks mode. + return newCopy(true /* ignoresFallbacks */, origin); + } else { + // if unresolved, we may have to look back to fallbacks as part of + // the resolution process, so always delay + return delayMerge(stack, fallback); + } } protected AbstractConfigValue mergedWithTheUnmergeable(Unmergeable fallback) { - throw badMergeException(); + requireNotIgnoringFallbacks(); + + return mergedWithTheUnmergeable(Collections.singletonList(this), fallback); } protected AbstractConfigValue mergedWithObject(AbstractConfigObject fallback) { - throw badMergeException(); + requireNotIgnoringFallbacks(); + + return mergedWithObject(Collections.singletonList(this), fallback); } protected AbstractConfigValue mergedWithNonObject(AbstractConfigValue fallback) { - // falling back to a non-object doesn't merge anything, and also - // prohibits merging any objects that we fall back to later. - // so we have to switch to ignoresFallbacks mode. - return newCopy(true /* ignoresFallbacks */, origin); + requireNotIgnoringFallbacks(); + + return mergedWithNonObject(Collections.singletonList(this), fallback); } public AbstractConfigValue withOrigin(ConfigOrigin origin) { @@ -159,6 +222,7 @@ abstract class AbstractConfigValue implements ConfigValue, MergeableValue, Seria return newCopy(ignoresFallbacks(), origin); } + // this is only overridden to change the return type @Override public AbstractConfigValue withFallback(ConfigMergeable mergeable) { if (ignoresFallbacks()) { @@ -169,15 +233,7 @@ abstract class AbstractConfigValue implements ConfigValue, MergeableValue, Seria if (other instanceof Unmergeable) { return mergedWithTheUnmergeable((Unmergeable) other); } else if (other instanceof AbstractConfigObject) { - AbstractConfigObject fallback = (AbstractConfigObject) other; - if (fallback.resolveStatus() == ResolveStatus.RESOLVED && fallback.isEmpty()) { - if (fallback.ignoresFallbacks()) - return newCopy(true /* ignoresFallbacks */, origin); - else - return this; - } else { - return mergedWithObject((AbstractConfigObject) other); - } + return mergedWithObject((AbstractConfigObject) other); } else { return mergedWithNonObject((AbstractConfigValue) other); } diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigConcatenation.java b/config/src/main/java/com/typesafe/config/impl/ConfigConcatenation.java index 4ae3f24e..1a561def 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigConcatenation.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigConcatenation.java @@ -60,43 +60,6 @@ final class ConfigConcatenation extends AbstractConfigValue implements Unmergeab return false; } - @Override - protected AbstractConfigValue mergedWithTheUnmergeable(Unmergeable fallback) { - // if we turn out to be an object, and the fallback also does, - // then a merge may be required; delay until we resolve. - List newStack = new ArrayList(); - newStack.add(this); - newStack.addAll(fallback.unmergedValues()); - return new ConfigDelayedMerge(AbstractConfigObject.mergeOrigins(newStack), newStack, - ((AbstractConfigValue) fallback).ignoresFallbacks()); - } - - protected AbstractConfigValue mergedLater(AbstractConfigValue fallback) { - List newStack = new ArrayList(); - newStack.add(this); - newStack.add(fallback); - return new ConfigDelayedMerge(AbstractConfigObject.mergeOrigins(newStack), newStack, - fallback.ignoresFallbacks()); - } - - @Override - protected AbstractConfigValue mergedWithObject(AbstractConfigObject fallback) { - // if we turn out to be an object, and the fallback also does, - // then a merge may be required; delay until we resolve. - return mergedLater(fallback); - } - - @Override - protected AbstractConfigValue mergedWithNonObject(AbstractConfigValue fallback) { - // We may need the fallback if we contain a self-referential - // ConfigReference. - // - // we can't easily detect the self-referential case since the cycle - // may involve more than one step, so we have to wait and - // merge later when resolving. - return mergedLater(fallback); - } - @Override public Collection unmergedValues() { return Collections.singleton(this); diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigDelayedMerge.java b/config/src/main/java/com/typesafe/config/impl/ConfigDelayedMerge.java index 41344821..5f62eda8 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigDelayedMerge.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigDelayedMerge.java @@ -28,13 +28,15 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements Unmergeabl // earlier items in the stack win final private List stack; - final private boolean ignoresFallbacks; + // this is just here for serialization compat; whether we ignore is purely + // a function of the bottom of the merge stack + @SuppressWarnings("unused") + @Deprecated + final private boolean ignoresFallbacks = false; - ConfigDelayedMerge(ConfigOrigin origin, List stack, - boolean ignoresFallbacks) { + ConfigDelayedMerge(ConfigOrigin origin, List stack) { super(origin); this.stack = stack; - this.ignoresFallbacks = ignoresFallbacks; if (stack.isEmpty()) throw new ConfigException.BugOrBroken( "creating empty delayed merge value"); @@ -46,10 +48,6 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements Unmergeabl } } - ConfigDelayedMerge(ConfigOrigin origin, List stack) { - this(origin, stack, false /* ignoresFallbacks */); - } - @Override public ConfigValueType valueType() { throw new ConfigException.NotResolved( @@ -80,10 +78,9 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements Unmergeabl AbstractConfigValue merged = null; for (AbstractConfigValue v : stack) { boolean replaced = false; - // checking for RESOLVED already is just an optimization - // to avoid creating the replacer when it can't possibly - // be needed. - if (v.resolveStatus() != ResolveStatus.RESOLVED) { + // we only replace if we have a substitution, or + // value-concatenation containing one + if (v instanceof Unmergeable) { // If, while resolving 'v' we come back to the same // merge stack, we only want to look _below_ 'v' // in the stack. So we arrange to replace the @@ -158,50 +155,38 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements Unmergeabl for (AbstractConfigValue o : stack) { newStack.add(o.relativized(prefix)); } - return new ConfigDelayedMerge(origin(), newStack, ignoresFallbacks); + return new ConfigDelayedMerge(origin(), newStack); + } + + // static utility shared with ConfigDelayedMergeObject + static boolean stackIgnoresFallbacks(List stack) { + AbstractConfigValue last = stack.get(stack.size() - 1); + return last.ignoresFallbacks(); } @Override protected boolean ignoresFallbacks() { - return ignoresFallbacks; + return stackIgnoresFallbacks(stack); } @Override protected AbstractConfigValue newCopy(boolean newIgnoresFallbacks, ConfigOrigin newOrigin) { - return new ConfigDelayedMerge(newOrigin, stack, newIgnoresFallbacks); + return new ConfigDelayedMerge(newOrigin, stack); } @Override protected final ConfigDelayedMerge mergedWithTheUnmergeable(Unmergeable fallback) { - if (ignoresFallbacks) - throw new ConfigException.BugOrBroken("should not be reached"); - - // if we turn out to be an object, and the fallback also does, - // then a merge may be required; delay until we resolve. - List newStack = new ArrayList(); - newStack.addAll(stack); - newStack.addAll(fallback.unmergedValues()); - return new ConfigDelayedMerge(AbstractConfigObject.mergeOrigins(newStack), newStack, - ((AbstractConfigValue) fallback).ignoresFallbacks()); + return (ConfigDelayedMerge) mergedWithTheUnmergeable(stack, fallback); } @Override protected final ConfigDelayedMerge mergedWithObject(AbstractConfigObject fallback) { - return mergedWithNonObject(fallback); + return (ConfigDelayedMerge) mergedWithObject(stack, fallback); } @Override protected ConfigDelayedMerge mergedWithNonObject(AbstractConfigValue fallback) { - if (ignoresFallbacks) - throw new ConfigException.BugOrBroken("should not be reached"); - - // if we turn out to be an object, and the fallback also does, - // then a merge may be required; delay until we resolve. - List newStack = new ArrayList(); - newStack.addAll(stack); - newStack.add(fallback); - return new ConfigDelayedMerge(AbstractConfigObject.mergeOrigins(newStack), newStack, - fallback.ignoresFallbacks()); + return (ConfigDelayedMerge) mergedWithNonObject(stack, fallback); } @Override @@ -300,6 +285,6 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements Unmergeabl private Object writeReplace() throws ObjectStreamException { // switch to LinkedList return new ConfigDelayedMerge(origin(), - new java.util.LinkedList(stack), ignoresFallbacks); + new java.util.LinkedList(stack)); } } diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigDelayedMergeObject.java b/config/src/main/java/com/typesafe/config/impl/ConfigDelayedMergeObject.java index 580ed5ba..8021f019 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigDelayedMergeObject.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigDelayedMergeObject.java @@ -11,6 +11,7 @@ import java.util.Map; import java.util.Set; import com.typesafe.config.ConfigException; +import com.typesafe.config.ConfigList; import com.typesafe.config.ConfigMergeable; import com.typesafe.config.ConfigOrigin; import com.typesafe.config.ConfigValue; @@ -23,18 +24,16 @@ final class ConfigDelayedMergeObject extends AbstractConfigObject implements Unm private static final long serialVersionUID = 1L; final private List stack; - final private boolean ignoresFallbacks; - ConfigDelayedMergeObject(ConfigOrigin origin, - List stack) { - this(origin, stack, false /* ignoresFallbacks */); - } + // this is just here for serialization compat; whether we ignore is purely + // a function of the bottom of the merge stack + @SuppressWarnings("unused") + @Deprecated + final private boolean ignoresFallbacks = false; - ConfigDelayedMergeObject(ConfigOrigin origin, List stack, - boolean ignoresFallbacks) { + ConfigDelayedMergeObject(ConfigOrigin origin, List stack) { super(origin); this.stack = stack; - this.ignoresFallbacks = ignoresFallbacks; if (stack.isEmpty()) throw new ConfigException.BugOrBroken( @@ -56,7 +55,7 @@ final class ConfigDelayedMergeObject extends AbstractConfigObject implements Unm if (status != resolveStatus()) throw new ConfigException.BugOrBroken( "attempt to create resolved ConfigDelayedMergeObject"); - return new ConfigDelayedMergeObject(origin, stack, ignoresFallbacks); + return new ConfigDelayedMergeObject(origin, stack); } @Override @@ -93,30 +92,31 @@ final class ConfigDelayedMergeObject extends AbstractConfigObject implements Unm for (AbstractConfigValue o : stack) { newStack.add(o.relativized(prefix)); } - return new ConfigDelayedMergeObject(origin(), newStack, - ignoresFallbacks); + return new ConfigDelayedMergeObject(origin(), newStack); } @Override protected boolean ignoresFallbacks() { - return ignoresFallbacks; + return ConfigDelayedMerge.stackIgnoresFallbacks(stack); } @Override - protected ConfigDelayedMergeObject mergedWithObject(AbstractConfigObject fallback) { + protected final ConfigDelayedMergeObject mergedWithTheUnmergeable(Unmergeable fallback) { + requireNotIgnoringFallbacks(); + + return (ConfigDelayedMergeObject) mergedWithTheUnmergeable(stack, fallback); + } + + @Override + protected final ConfigDelayedMergeObject mergedWithObject(AbstractConfigObject fallback) { return mergedWithNonObject(fallback); } @Override - protected ConfigDelayedMergeObject mergedWithNonObject(AbstractConfigValue fallback) { - if (ignoresFallbacks) - throw new ConfigException.BugOrBroken("should not be reached"); + protected final ConfigDelayedMergeObject mergedWithNonObject(AbstractConfigValue fallback) { + requireNotIgnoringFallbacks(); - List newStack = new ArrayList(); - newStack.addAll(stack); - newStack.add(fallback); - return new ConfigDelayedMergeObject(AbstractConfigObject.mergeOrigins(newStack), newStack, - fallback.ignoresFallbacks()); + return (ConfigDelayedMergeObject) mergedWithNonObject(stack, fallback); } @Override @@ -249,10 +249,6 @@ final class ConfigDelayedMergeObject extends AbstractConfigObject implements Unm // to touch the exact key that isn't resolved, so this is in that // spirit. - // this function should never return null; if we know a value doesn't - // exist, then there would be no reason for the merge to be delayed - // (i.e. as long as some stuff is unmerged, the value may be non-null). - // we'll be able to return a key if we have a value that ignores // fallbacks, prior to any unmergeable values. for (AbstractConfigValue layer : stack) { @@ -289,17 +285,33 @@ final class ConfigDelayedMergeObject extends AbstractConfigObject implements Unm throw new ConfigException.NotResolved("Key '" + key + "' is not available at '" + origin().description() + "' because value at '" + layer.origin().description() - + "' has not been resolved and may turn out to contain '" + key + "'." + + "' has not been resolved and may turn out to contain or hide '" + key + + "'." + " Be sure to Config#resolve() before using a config object."); + } else if (layer.resolveStatus() == ResolveStatus.UNRESOLVED) { + // if the layer is not an object, and not a substitution or + // merge, + // then it's something that's unresolved because it _contains_ + // an unresolved object... i.e. it's an array + if (!(layer instanceof ConfigList)) + throw new ConfigException.BugOrBroken("Expecting a list here, not " + layer); + // all later objects will be hidden so we can say we won't find + // the key + return null; } else { - // non-object, but not unresolved, like an integer or something. + // non-object, but resolved, like an integer or something. // has no children so the one we're after won't be in it. - // this should always be overridden by an object though so - // ideally we never build a stack that would have this in it. - continue; + // we would only have this in the stack in case something + // else "looks back" to it due to a cycle. + // anyway at this point we know we can't find the key anymore. + if (!layer.ignoresFallbacks()) { + throw new ConfigException.BugOrBroken( + "resolved non-object should ignore fallbacks"); + } + return null; } } - // If we get here, then we never found an unmergeable which means + // If we get here, then we never found anything unresolved which means // the ConfigDelayedMergeObject should not have existed. some // invariant was violated. throw new ConfigException.BugOrBroken( @@ -314,6 +326,6 @@ final class ConfigDelayedMergeObject extends AbstractConfigObject implements Unm private Object writeReplace() throws ObjectStreamException { // switch to LinkedList return new ConfigDelayedMergeObject(origin(), - new java.util.LinkedList(stack), ignoresFallbacks); + new java.util.LinkedList(stack)); } } diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigReference.java b/config/src/main/java/com/typesafe/config/impl/ConfigReference.java index b2718a20..b57be5cd 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigReference.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigReference.java @@ -1,9 +1,7 @@ package com.typesafe.config.impl; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.List; import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigOrigin; @@ -59,45 +57,6 @@ final class ConfigReference extends AbstractConfigValue implements Unmergeable { return false; } - @Override - protected AbstractConfigValue mergedWithTheUnmergeable(Unmergeable fallback) { - // if we turn out to be an object, and the fallback also does, - // then a merge may be required; delay until we resolve. - List newStack = new ArrayList(); - newStack.add(this); - newStack.addAll(fallback.unmergedValues()); - return new ConfigDelayedMerge(AbstractConfigObject.mergeOrigins(newStack), newStack, - ((AbstractConfigValue) fallback).ignoresFallbacks()); - } - - protected AbstractConfigValue mergedLater(AbstractConfigValue fallback) { - List newStack = new ArrayList(); - newStack.add(this); - newStack.add(fallback); - return new ConfigDelayedMerge(AbstractConfigObject.mergeOrigins(newStack), newStack, - fallback.ignoresFallbacks()); - } - - @Override - protected AbstractConfigValue mergedWithObject(AbstractConfigObject fallback) { - // if we turn out to be an object, and the fallback also does, - // then a merge may be required; delay until we resolve. - return mergedLater(fallback); - } - - @Override - protected AbstractConfigValue mergedWithNonObject(AbstractConfigValue fallback) { - // We may need the fallback for two reasons: - // 1. if an optional substitution ends up getting deleted - // because it is not defined - // 2. if the substitution is self-referential - // - // we can't easily detect the self-referential case since the cycle - // may involve more than one step, so we have to wait and - // merge later when resolving. - return mergedLater(fallback); - } - @Override public Collection unmergedValues() { return Collections.singleton(this); diff --git a/config/src/main/java/com/typesafe/config/impl/ResolveContext.java b/config/src/main/java/com/typesafe/config/impl/ResolveContext.java index b2529ba9..a6858618 100644 --- a/config/src/main/java/com/typesafe/config/impl/ResolveContext.java +++ b/config/src/main/java/com/typesafe/config/impl/ResolveContext.java @@ -93,7 +93,6 @@ final class ResolveContext { } AbstractConfigValue resolve(AbstractConfigValue original) throws NotPossibleToResolve { - // a fully-resolved (no restrictToChild) object can satisfy a // request for a restricted object, so always check that first. final MemoKey fullKey = new MemoKey(original, null); diff --git a/config/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java b/config/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java index 8d23bb02..ee7073a1 100644 --- a/config/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java +++ b/config/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java @@ -159,8 +159,7 @@ final class SimpleConfigObject extends AbstractConfigObject { @Override protected SimpleConfigObject mergedWithObject(AbstractConfigObject abstractFallback) { - if (ignoresFallbacks()) - throw new ConfigException.BugOrBroken("should not be reached"); + requireNotIgnoringFallbacks(); if (!(abstractFallback instanceof SimpleConfigObject)) { throw new ConfigException.BugOrBroken( diff --git a/config/src/test/scala/com/typesafe/config/impl/ConfigSubstitutionTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConfigSubstitutionTest.scala index 17da465f..026c0338 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConfigSubstitutionTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConfigSubstitutionTest.scala @@ -1039,6 +1039,20 @@ class ConfigSubstitutionTest extends TestUtils { assertEquals("optional self reference disappears", 0, resolved.root.size) } + @Test + def substSelfReferenceAlongPath() { + val obj = parseObject("""a.b=1, a.b=${a.b}""") + val resolved = resolve(obj) + assertEquals(1, resolved.getInt("a.b")) + } + + @Test + def substSelfReferenceAlongLongerPath() { + val obj = parseObject("""a.b.c=1, a.b.c=${a.b.c}""") + val resolved = resolve(obj) + assertEquals(1, resolved.getInt("a.b.c")) + } + @Test def substSelfReferenceIndirect() { val obj = parseObject("""a=1, b=${a}, a=${b}""") @@ -1077,6 +1091,13 @@ class ConfigSubstitutionTest extends TestUtils { assertEquals(5, resolved.getInt("a.b")) } + @Test + def substSelfReferenceObjectAlongPath() { + val obj = parseObject("""a.b={c=5}, a.b=${a.b}""") + val resolved = resolve(obj) + assertEquals(5, resolved.getInt("a.b.c")) + } + @Test def substSelfReferenceInConcat() { val obj = parseObject("""a=1, a=${a}foo""") @@ -1174,6 +1195,15 @@ class ConfigSubstitutionTest extends TestUtils { assertEquals(5, resolved.getInt("a.c")) } + @Test + def substSelfReferenceAlongAPathInsideObject() { + // if the ${a.b} is _inside_ a field value instead of + // _being_ the field value, it does not look backward. + val obj = parseObject("""a={b={c=5}}, a={ x : ${a.b} }, a={b=2}""") + val resolved = resolve(obj) + assertEquals(2, resolved.getInt("a.x")) + } + @Test def substInChildFieldNotASelfReference1() { // here, ${bar.foo} is not a self reference because @@ -1220,6 +1250,37 @@ class ConfigSubstitutionTest extends TestUtils { assertEquals(42, resolved.getInt("bar.foo")) } + @Test + def substInChildFieldNotASelfReference4() { + // checking that having bar set to non-object earlier + // doesn't break the behavior. + val obj = parseObject(""" + bar : 101 + bar : { foo : 42, + baz : ${bar.foo} + } + """) + val resolved = resolve(obj) + assertEquals(42, resolved.getInt("bar.baz")) + assertEquals(42, resolved.getInt("bar.foo")) + } + + @Test + def substInChildFieldNotASelfReference5() { + // checking that having bar set to unresolved array earlier + // doesn't break the behavior. + val obj = parseObject(""" + x : 0 + bar : [ ${x}, 1, 2, 3 ] + bar : { foo : 42, + baz : ${bar.foo} + } + """) + val resolved = resolve(obj) + assertEquals(42, resolved.getInt("bar.baz")) + assertEquals(42, resolved.getInt("bar.foo")) + } + @Test def mutuallyReferringNotASelfReference() { val obj = parseObject(""" @@ -1248,4 +1309,42 @@ class ConfigSubstitutionTest extends TestUtils { val resolved = resolve(obj) assertEquals("1xyz", resolved.getString("a")) } + + @Test + def substSelfReferenceInArray() { + // never "look back" from "inside" an array + val obj = parseObject("""a=1,a=[${a}, 2]""") + val e = intercept[ConfigException.UnresolvedSubstitution] { + resolve(obj) + } + assertTrue("wrong exception: " + e.getMessage, + e.getMessage.contains("cycle") && e.getMessage.contains("${a}")) + } + + @Test + def substSelfReferenceInObject() { + // never "look back" from "inside" an object + val obj = parseObject("""a=1,a={ x : ${a} }""") + val e = intercept[ConfigException.UnresolvedSubstitution] { + resolve(obj) + } + assertTrue("wrong exception: " + e.getMessage, + e.getMessage.contains("cycle") && e.getMessage.contains("${a}")) + } + + @Test + def selfReferentialObjectNotAffectedByOverriding() { + // this is testing that we can still refer to another + // field in the same object, even though we are overriding + // an earlier object. + val obj = parseObject("""a={ x : 42, y : ${a.x} }""") + val resolved = resolve(obj) + assertEquals(parseObject("{ x : 42, y : 42 }"), resolved.getConfig("a").root) + + // this is expected because if adding "a=1" here affects the outcome, + // it would be flat-out bizarre. + val obj2 = parseObject("""a=1, a={ x : 42, y : ${a.x} }""") + val resolved2 = resolve(obj2) + assertEquals(parseObject("{ x : 42, y : 42 }"), resolved2.getConfig("a").root) + } } diff --git a/config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala index 064747db..259800c7 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala @@ -79,7 +79,8 @@ class ConfigTest extends TestUtils { if (!trees(0).equals(tree)) throw new AssertionError("Merge was not associative, " + "verify that it should not be, then don't use associativeMerge " + - "for this one.\none: " + trees(0) + "\ntwo: " + tree) + "for this one. two results were: \none: " + trees(0) + "\ntwo: " + + tree + "\noriginal list: " + allObjects) } for (tree <- trees) { @@ -503,6 +504,20 @@ class ConfigTest extends TestUtils { testIgnoredMergesDoNothing(conf) } + @Test + def testNoMergeAcrossArray() { + val conf = parseConfig("a: {b:1}, a: [2,3], a:{c:4}") + assertFalse("a.b found in: " + conf, conf.hasPath("a.b")) + assertTrue("a.c not found in: " + conf, conf.hasPath("a.c")) + } + + @Test + def testNoMergeAcrossUnresolvedArray() { + val conf = parseConfig("a: {b:1}, a: [2,${x}], a:{c:4}, x: 42") + assertFalse("a.b found in: " + conf, conf.hasPath("a.b")) + assertTrue("a.c not found in: " + conf, conf.hasPath("a.c")) + } + @Test def integerRangeChecks() { val conf = parseConfig("{ tooNegative: " + (Integer.MIN_VALUE - 1L) + ", tooPositive: " + (Integer.MAX_VALUE + 1L) + "}")