Merge pull request #1 from twitter-forks/substitutions-in-includes

Substitutions in includes
This commit is contained in:
Ryan O'Neill 2015-08-06 13:45:11 -07:00
commit 48623a5c44
24 changed files with 76 additions and 91 deletions

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
.classpath .classpath
.project .project
.cache .cache
.DS_Store
.settings .settings
.idea .idea
.idea_modules .idea_modules

View File

@ -56,4 +56,4 @@ welcome!):
release) release)
16. push the "vX.Y.Z" tag 16. push the "vX.Y.Z" tag
17. announce release, possibly wait for maven central to sync 17. announce release, possibly wait for maven central to sync
first first

View File

@ -1025,6 +1025,19 @@ Then the `${x}` in "foo.conf", which has been fixed up to
`${a.x}`, would evaluate to `42` rather than to `10`. `${a.x}`, would evaluate to `42` rather than to `10`.
Substitution happens _after_ parsing the whole configuration. Substitution happens _after_ parsing the whole configuration.
Includes inside of lists use the list index as a path segment when
fixing up substitutions.
Example:
{
a : [ { include "foo.conf" } ]
a.0.x : 42
}
Since the `${x}` in "foo.conf" would be fixed up to `${a.0.x}' we
get our expected value of 42.
However, there are plenty of cases where the included file might However, there are plenty of cases where the included file might
intend to refer to the application's root config. For example, to intend to refer to the application's root config. For example, to
get a value from a system property or from the reference get a value from a system property or from the reference

View File

@ -382,4 +382,4 @@ Thank you to contributors with commits since v1.2.1 tag:
`${foo}` syntax) without getting confused; just about anything `${foo}` syntax) without getting confused; just about anything
that makes conceptual sense should now work. Only inherently that makes conceptual sense should now work. Only inherently
circular config files should fail. circular config files should fail.
- some minor documentation fixes. - some minor documentation fixes.

View File

@ -832,4 +832,4 @@ format.
#### Python port #### Python port
* pyhocon https://github.com/chimpler/pyhocon * pyhocon https://github.com/chimpler/pyhocon

View File

@ -252,4 +252,4 @@ public class ConfigBeanImpl {
return false; return false;
} }
} }

View File

@ -339,4 +339,4 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements Unmergeabl
sb.append("# ) end of unresolved merge\n"); sb.append("# ) end of unresolved merge\n");
} }
} }
} }

View File

@ -233,4 +233,4 @@ final public class ConfigImplUtil {
} }
return nameBuilder.toString(); return nameBuilder.toString();
} }
} }

View File

@ -278,4 +278,4 @@ final class ConfigNodeObject extends ConfigNodeComplexValue {
Path path = PathParser.parsePathNode(desiredPath, flavor).value(); Path path = PathParser.parsePathNode(desiredPath, flavor).value();
return changeValueOnPath(path, null, flavor); return changeValueOnPath(path, null, flavor);
} }
} }

View File

@ -34,11 +34,6 @@ final class ConfigParser {
final private ConfigOrigin baseOrigin; final private ConfigOrigin baseOrigin;
final private LinkedList<Path> pathStack; final private LinkedList<Path> pathStack;
// the number of lists we are inside; this is used to detect the "cannot
// generate a reference to a list element" problem, and once we fix that
// problem we should be able to get rid of this variable.
int arrayCount;
ParseContext(ConfigSyntax flavor, ConfigOrigin origin, ConfigNodeRoot document, ParseContext(ConfigSyntax flavor, ConfigOrigin origin, ConfigNodeRoot document,
FullIncluder includer, ConfigIncludeContext includeContext) { FullIncluder includer, ConfigIncludeContext includeContext) {
lineNumber = 1; lineNumber = 1;
@ -48,7 +43,6 @@ final class ConfigParser {
this.includer = includer; this.includer = includer;
this.includeContext = includeContext; this.includeContext = includeContext;
this.pathStack = new LinkedList<Path>(); this.pathStack = new LinkedList<Path>();
this.arrayCount = 0;
} }
// merge a bunch of adjacent values into one // merge a bunch of adjacent values into one
@ -95,8 +89,6 @@ final class ConfigParser {
private AbstractConfigValue parseValue(AbstractConfigNodeValue n, List<String> comments) { private AbstractConfigValue parseValue(AbstractConfigNodeValue n, List<String> comments) {
AbstractConfigValue v; AbstractConfigValue v;
int startingArrayCount = arrayCount;
if (n instanceof ConfigNodeSimpleValue) { if (n instanceof ConfigNodeSimpleValue) {
v = ((ConfigNodeSimpleValue) n).value(); v = ((ConfigNodeSimpleValue) n).value();
} else if (n instanceof ConfigNodeObject) { } else if (n instanceof ConfigNodeObject) {
@ -114,9 +106,6 @@ final class ConfigParser {
comments.clear(); comments.clear();
} }
if (arrayCount != startingArrayCount)
throw new ConfigException.BugOrBroken("Bug in config parser: unbalanced array count");
return v; return v;
} }
@ -187,14 +176,6 @@ final class ConfigParser {
throw new ConfigException.BugOrBroken("should not be reached"); throw new ConfigException.BugOrBroken("should not be reached");
} }
// we really should make this work, but for now throwing an
// exception is better than producing an incorrect result.
// See https://github.com/typesafehub/config/issues/160
if (arrayCount > 0 && obj.resolveStatus() != ResolveStatus.RESOLVED)
throw parseError("Due to current limitations of the config parser, when an include statement is nested inside a list value, "
+ "${} substitutions inside the included file cannot be resolved correctly. Either move the include outside of the list value or "
+ "remove the ${} statements from the included file.");
if (!pathStack.isEmpty()) { if (!pathStack.isEmpty()) {
Path prefix = fullCurrentPath(); Path prefix = fullCurrentPath();
obj = obj.relativized(prefix); obj = obj.relativized(prefix);
@ -240,21 +221,6 @@ final class ConfigParser {
// path must be on-stack while we parse the value // path must be on-stack while we parse the value
pathStack.push(path); pathStack.push(path);
if (((ConfigNodeField) node).separator() == Tokens.PLUS_EQUALS) {
// we really should make this work, but for now throwing
// an exception is better than producing an incorrect
// result. See
// https://github.com/typesafehub/config/issues/160
if (arrayCount > 0)
throw parseError("Due to current limitations of the config parser, += does not work nested inside a list. "
+ "+= expands to a ${} substitution and the path in ${} cannot currently refer to list elements. "
+ "You might be able to move the += outside of the list and then refer to it from inside the list with ${}.");
// because we will put it in an array after the fact so
// we want this to be incremented during the parseValue
// below in order to throw the above exception.
arrayCount += 1;
}
AbstractConfigNodeValue valueNode; AbstractConfigNodeValue valueNode;
AbstractConfigValue newValue; AbstractConfigValue newValue;
@ -265,7 +231,6 @@ final class ConfigParser {
newValue = parseValue(valueNode, comments); newValue = parseValue(valueNode, comments);
if (((ConfigNodeField) node).separator() == Tokens.PLUS_EQUALS) { if (((ConfigNodeField) node).separator() == Tokens.PLUS_EQUALS) {
arrayCount -= 1;
List<AbstractConfigValue> concat = new ArrayList<AbstractConfigValue>(2); List<AbstractConfigValue> concat = new ArrayList<AbstractConfigValue>(2);
AbstractConfigValue previousRef = new ConfigReference(newValue.origin(), AbstractConfigValue previousRef = new ConfigReference(newValue.origin(),
@ -346,7 +311,6 @@ final class ConfigParser {
} }
private SimpleConfigList parseArray(ConfigNodeArray n) { private SimpleConfigList parseArray(ConfigNodeArray n) {
arrayCount += 1;
SimpleConfigOrigin arrayOrigin = lineOrigin(); SimpleConfigOrigin arrayOrigin = lineOrigin();
List<AbstractConfigValue> values = new ArrayList<AbstractConfigValue>(); List<AbstractConfigValue> values = new ArrayList<AbstractConfigValue>();
@ -356,6 +320,8 @@ final class ConfigParser {
AbstractConfigValue v = null; AbstractConfigValue v = null;
int index = 0;
for (AbstractConfigNode node : n.children()) { for (AbstractConfigNode node : n.children()) {
if (node instanceof ConfigNodeComment) { if (node instanceof ConfigNodeComment) {
comments.add(((ConfigNodeComment) node).commentText()); comments.add(((ConfigNodeComment) node).commentText());
@ -376,14 +342,16 @@ final class ConfigParser {
values.add(v.withOrigin(v.origin().appendComments(new ArrayList<String>(comments)))); values.add(v.withOrigin(v.origin().appendComments(new ArrayList<String>(comments))));
comments.clear(); comments.clear();
} }
pathStack.push(new Path(Integer.toString(index)));
index ++;
v = parseValue((AbstractConfigNodeValue)node, comments); v = parseValue((AbstractConfigNodeValue)node, comments);
pathStack.pop();
} }
} }
// There shouldn't be any comments at this point, but add them just in case // There shouldn't be any comments at this point, but add them just in case
if (v != null) { if (v != null) {
values.add(v.withOrigin(v.origin().appendComments(new ArrayList<String>(comments)))); values.add(v.withOrigin(v.origin().appendComments(new ArrayList<String>(comments))));
} }
arrayCount -= 1;
return new SimpleConfigList(arrayOrigin, values); return new SimpleConfigList(arrayOrigin, values);
} }

View File

@ -55,7 +55,7 @@ final class ResolveSource {
} }
} }
static private ValueWithPath findInObject(AbstractConfigObject obj, Path path) { static private ValueWithPath findInObject(AbstractConfigValue obj, Path path) {
try { try {
// we'll fail if anything along the path can't // we'll fail if anything along the path can't
// be looked at without resolving. // be looked at without resolving.
@ -65,19 +65,34 @@ final class ResolveSource {
} }
} }
static private ValueWithPath findInObject(AbstractConfigObject obj, Path path, Node<Container> parents) { static private ValueWithPath findInObject(AbstractConfigValue obj, Path path, Node<Container> parents) {
String key = path.first(); String key = path.first();
Path next = path.remainder(); Path next = path.remainder();
if (ConfigImpl.traceSubstitutionsEnabled()) if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace("*** looking up '" + key + "' in " + obj); ConfigImpl.trace("*** looking up '" + key + "' in " + obj);
AbstractConfigValue v = obj.attemptPeekWithPartialResolve(key);
Node<Container> newParents = parents == null ? new Node<Container>(obj) : parents.prepend(obj); AbstractConfigValue v = null;
Node<Container> newParents = null;
if (obj instanceof AbstractConfigObject) {
v = ((AbstractConfigObject) obj).attemptPeekWithPartialResolve(key);
newParents = parents == null ? new Node<Container>((AbstractConfigObject) obj) : parents.prepend((AbstractConfigObject) obj);
} else if (obj instanceof SimpleConfigList) {
int ix;
try {
ix = Integer.parseInt(key);
} catch(Exception e) {
throw new ConfigException.BadPath(key, "path for list must be numeric index");
}
v = ((SimpleConfigList) obj).get(ix);
newParents = parents == null ? new Node<Container>((SimpleConfigList) obj) : parents.prepend((SimpleConfigList) obj);
}
if (next == null) { if (next == null) {
return new ValueWithPath(v, newParents); return new ValueWithPath(v, newParents);
} else { } else {
if (v instanceof AbstractConfigObject) { if (v instanceof AbstractConfigObject || v instanceof SimpleConfigList) {
return findInObject((AbstractConfigObject) v, next, newParents); return findInObject(v, next, newParents);
} else { } else {
return new ValueWithPath(null, newParents); return new ValueWithPath(null, newParents);
} }

View File

@ -60,4 +60,4 @@ final class SimpleConfigDocument implements ConfigDocument {
public int hashCode() { public int hashCode() {
return render().hashCode(); return render().hashCode();
} }
} }

View File

@ -692,4 +692,4 @@ final class Tokenizer {
"Does not make sense to remove items from token stream"); "Does not make sense to remove items from token stream");
} }
} }
} }

View File

@ -136,4 +136,4 @@ public class ArraysConfig {
public void setOfStringBean(List<StringsConfig> ofStringBean) { public void setOfStringBean(List<StringsConfig> ofStringBean) {
this.ofStringBean = ofStringBean; this.ofStringBean = ofStringBean;
} }
} }

View File

@ -42,4 +42,4 @@ public class StringsConfig {
public String toString() { public String toString() {
return "StringsConfig(" + abcd + "," + yes + ")"; return "StringsConfig(" + abcd + "," + yes + ")";
} }
} }

View File

@ -52,17 +52,7 @@
"ofConfigObject" : [${numbers}, ${booleans}, ${strings}], "ofConfigObject" : [${numbers}, ${booleans}, ${strings}],
"ofConfigValue" : [1, 2, "a"], "ofConfigValue" : [1, 2, "a"],
"ofDuration" : [1, 2h, 3 days], "ofDuration" : [1, 2h, 3 days],
"ofMemorySize" : [1024, 1M, 1G], "ofMemorySize" : [1024, 1M, 1G]
"ofStringBean" : [
{
abcd : "testAbcdOne"
yes : "testYesOne"
},
{
abcd : "testAbcdTwo"
yes : "testYesTwo"
}
]
}, },
"bytes" : { "bytes" : {
"kilobyte" : "1kB", "kilobyte" : "1kB",

View File

@ -1,5 +1,10 @@
// The {} inside the [] is needed because // The {} inside the [] is needed because
// just [ include ] means an array with the // just [ include ] means an array with the
// string "include" in it. // string "include" in it.
a = [ { include "test01.conf" } ] a = [
{
include "test12.conf"
replace-key: "replaced"
}
]

View File

@ -0,0 +1,3 @@
// This file is included by include-from-list.conf to verify that includes with substitutions work in arrays
replace-me: ${replace-key}

View File

@ -345,26 +345,19 @@ class ConcatenationTest extends TestUtils {
assertEquals(Seq(1, 2, 3), conf.getObjectList("x.a").asScala.toList.map(_.toConfig.getInt("b"))) assertEquals(Seq(1, 2, 3), conf.getObjectList("x.a").asScala.toList.map(_.toConfig.getInt("b")))
} }
// We would ideally make this case NOT throw an exception but we need to do some work
// to get there, see https://github.com/typesafehub/config/issues/160
@Test @Test
def plusEqualsMultipleTimesNestedInArray() { def plusEqualsMultipleTimesNestedInArray() {
val e = intercept[ConfigException.Parse] { val conf = parseConfig("""x = [ { a += 1, a += 2, a += 3 } ] """).resolve()
val conf = parseConfig("""x = [ { a += 1, a += 2, a += 3 } ] """).resolve() assertEquals(Seq(1, 2, 3), conf.getObjectList("x").asScala.toVector(0).toConfig.getIntList("a").asScala.toList)
assertEquals(Seq(1, 2, 3), conf.getObjectList("x").asScala.toVector(0).toConfig.getIntList("a").asScala.toList)
}
assertTrue(e.getMessage.contains("limitation"))
} }
// We would ideally make this case NOT throw an exception but we need to do some work
// to get there, see https://github.com/typesafehub/config/issues/160
@Test @Test
def plusEqualsMultipleTimesNestedInPlusEquals() { def plusEqualsMultipleTimesNestedInPlusEquals() {
val e = intercept[ConfigException.Parse] { val e = intercept[ConfigException.BugOrBroken] {
val conf = parseConfig("""x += { a += 1, a += 2, a += 3 } """).resolve() val conf = parseConfig("""x += { a += 1, a += 2, a += 3 } """).resolve()
assertEquals(Seq(1, 2, 3), conf.getObjectList("x").asScala.toVector(0).toConfig.getIntList("a").asScala.toList) assertEquals(Seq(1, 2, 3), conf.getObjectList("x").asScala.toVector(0).toConfig.getIntList("a").asScala.toList)
} }
assertTrue(e.getMessage.contains("limitation")) assertTrue(e.getMessage.contains("did not find"))
} }
// from https://github.com/typesafehub/config/issues/177 // from https://github.com/typesafehub/config/issues/177

View File

@ -801,6 +801,14 @@ class ConfParserTest extends TestUtils {
assertTrue("including basename URL doesn't load anything", conf.isEmpty()) assertTrue("including basename URL doesn't load anything", conf.isEmpty())
} }
@Test
def includeWithSubstitutionsFromList() {
val conf = ConfigFactory.parseString("include file(" + jsonQuotedResourceFile("include-from-list") + ")")
val resolved = conf.resolve()
assertEquals(resolved.getConfigList("a").get(0).getString("replace-me"), "replaced")
}
@Test @Test
def acceptBOMStartingFile() { def acceptBOMStartingFile() {
// BOM at start of file should be ignored // BOM at start of file should be ignored

View File

@ -209,4 +209,4 @@ class ConfigBeanFactoryTest extends TestUtils {
} }
} }
} }

View File

@ -455,4 +455,4 @@ class ConfigDocumentTest extends TestUtils {
assertEquals("{ a : {\n \"a\" : 1,\n \"b\" : 2\n } }", assertEquals("{ a : {\n \"a\" : 1,\n \"b\" : 2\n } }",
configDocument.withValue("a", configVal).render) configDocument.withValue("a", configVal).render)
} }
} }

View File

@ -229,4 +229,4 @@ class ConfigNodeTest extends TestUtils {
assertEquals(finalText, newNode.render()) assertEquals(finalText, newNode.render())
} }
} }

View File

@ -875,17 +875,6 @@ class PublicApiTest extends TestUtils {
assertTrue("wrong exception: " + e.getMessage, e.getMessage.contains("include statements nested")) assertTrue("wrong exception: " + e.getMessage, e.getMessage.contains("include statements nested"))
} }
// We would ideally make this case NOT throw an exception but we need to do some work
// to get there, see https://github.com/typesafehub/config/issues/160
@Test
def detectIncludeFromList() {
val e = intercept[ConfigException.Parse] {
ConfigFactory.load("include-from-list.conf")
}
assertTrue("wrong exception: " + e.getMessage, e.getMessage.contains("limitation"))
}
@Test @Test
def missingOverrideResourceFails() { def missingOverrideResourceFails() {
assertEquals("config.file is not set", null, System.getProperty("config.file")) assertEquals("config.file is not set", null, System.getProperty("config.file"))