Skip to content
Browse files

Implement array and object concatenation

  path : [ /bin ]
  path : ${path} [ /usr/bin ]

This added very few lines of code or bytecode!
It's just a natural extension of the existing
string concatenation.

But it did add a fair few lines of specification
and tests.
  • Loading branch information...
1 parent 8189be0 commit 47e168a92f2628e5863882677ab38b11c71449a0 @havocp havocp committed Apr 6, 2012
View
123 HOCON.md
@@ -231,21 +231,41 @@ reserved keywords to allow future extensions to this spec.
### Value concatenation
-The value of an object field or an array element may consist of
-multiple values which are concatenated into one string.
+The value of an object field or array element may consist of
+multiple values which are combined. There are three kinds of value
+concatenation:
-Only simple values participate in value concatenation. Recall that
-a simple value is any value other than arrays and objects.
+ - if all the values are simple values (neither objects nor
+ arrays), they are concatenated into a string.
+ - if all the values are arrays, they are concatenated into
+ one array.
+ - if all the values are objects, they are merged (as with
+ duplicate keys) into one object.
+
+String value concatenation is allowed in object field keys, in
+addition to object field values and array elements. Objects and
+arrays do not make sense as object field keys.
+
+#### String value concatenation
+
+String value concatenation is the trick that makes unquoted
+strings work; it also supports substitutions (`${foo}` syntax) in
+strings.
+
+Only simple values participate in string value
+concatenation. Recall that a simple value is any value other than
+arrays and objects.
As long as simple values are separated only by non-newline
whitespace, the _whitespace between them is preserved_ and the
values, along with the whitespace, are concatenated into a string.
-Value concatenations never span a newline, or a character that is
-not part of a simple value.
+String value concatenations never span a newline, or a character
+that is not part of a simple value.
-A value concatenation may appear in any place that a string may
-appear, including object keys, object values, and array elements.
+A string value concatenation may appear in any place that a string
+may appear, including object keys, object values, and array
+elements.
Whenever a value would appear in JSON, a HOCON parser instead
collects multiple values (including the whitespace between them)
@@ -261,11 +281,11 @@ whitespace is kept and the leading and trailing whitespace is
trimmed. The equivalent string, written in quoted form, would be
`"foo bar baz"`.
-Value concatenation `foo bar` (two unquoted strings with
+Value concatenating `foo bar` (two unquoted strings with
whitespace) and quoted string `"foo bar"` would result in the same
in-memory representation, seven characters.
-For purposes of value concatenation, non-string values are
+For purposes of string value concatenation, non-string values are
converted to strings as follows (strings shown as quoted strings):
- `true` and `false` become the strings `"true"` and `"false"`.
@@ -278,7 +298,7 @@ converted to strings as follows (strings shown as quoted strings):
as it was written in the file.
- a substitution is replaced with its value which is then
converted to a string as above.
- - it is invalid for arrays or objects to appear in a value
+ - it is invalid for arrays or objects to appear in a string value
concatenation.
A single value is never converted to a string. That is, it would
@@ -287,6 +307,87 @@ parsed as a boolean-typed value. Only `true foo` (`true` with
another simple value on the same line) should be parsed as a value
concatenation and converted to a string.
+#### Array and object concatenation
+
+Arrays can be concatenated with arrays, and objects with objects,
+but it is an error if they are mixed.
+
+For purposes of concatenation, "array" also means "substitution
+that resolves to an array" and "object" also means "substitution
+that resolves to an object."
+
+Within an object field value or array element, if only non-newline
+whitespace separates the end of a first array or object or
+substitution from the start of a second array or object or
+substitution, the two values are concatenated. Newlines may occur
+_within_ the array or object, but not _between_ them. Newlines
+_between_ prevent concatenation.
+
+For objects, "concatenation" means "merging", so the second object
+overrides the first.
+
+Arrays and objects cannot be field keys, whether concatenation is
+involved or not.
+
+Here are several ways to define `a` to the same object value:
+
+ // one object
+ a : { b : 1, c : 2 }
+ // two objects that are merged via concatenation rules
+ a : { b : 1 } { c : 2 }
+ // two fields that are merged
+ a : { b : 1 }
+ a : { c : 2 }
+
+Here are several ways to define `a` to the same array value:
+
+ // one array
+ a : [ 1, 2, 3, 4 ]
+ // two arrays that are concatenated
+ a : [ 1, 2 ] [ 3, 4 ]
+ // a later definition referring to an earlier
+ // (see "self-referential substitutions" below)
+ a : [ 1, 2 ]
+ a : ${a} [ 3, 4 ]
+
+A common use of object concatenation is "inheritance":
+
+ data-center-generic = { cluster-size = 6 }
+ data-center-east = ${data-center-generic} { name = "east" }
+
+A common use of array concatenation is to add to paths:
+
+ path = [ /bin ]
+ path = ${path} [ /usr/bin ]
+
+#### Note: Arrays without commas or newlines
+
+Arrays allow you to use newlines instead of commas, but not
+whitespace instead of commas. Non-newline whitespace will produce
+concatenation rather than separate elements.
+
+ // this is an array with one element, the string "1 2 3 4"
+ [ 1 2 3 4 ]
+ // this is an array of four integers
+ [ 1
+ 2
+ 3
+ 4 ]
+
+ // an array of one element, the array [ 1, 2, 3, 4 ]
+ [ [ 1, 2 ] [ 3, 4 ] ]
+ // an array of two arrays
+ [ [ 1, 2 ]
+ [ 3, 4 ] ]
+
+If this gets confusing, just use commas. The concatenation
+behavior is useful rather than surprising in cases like:
+
+ [ This is an unquoted string my name is ${name}, Hello ${world} ]
+ [ ${a} ${b}, ${x} ${y} ]
+
+Non-newline whitespace is never an element or field separator.
+
### Path expressions
Path expressions are used to write out a path through the object
View
60 README.md
@@ -176,6 +176,9 @@ Tentatively called "Human-Optimized Config Object Notation" or
HOCON, also called `.conf`, see HOCON.md in this directory for more
detail.
+After processing a `.conf` file, the result is always just a JSON
+tree that you could have written (less conveniently) in JSON.
+
### Features of HOCON
- Comments, with `#` or `//`
@@ -328,6 +331,56 @@ value just disappear if the substitution is not found:
// this array could have one or two elements
path = [ "a", ${?OPTIONAL_A} ]
+### Concatenation
+
+Values _on the same line_ are concatenated (for strings and
+arrays) or merged (for objects).
+
+This is why unquoted strings work, here the number `42` and the
+string `foo` are concatenated into a string `42 foo`:
+
+ key : 42 foo
+
+When concatenating values into a string, leading and trailing
+whitespace is stripped but whitespace between values is kept.
+
+Unquoted strings also support substitutions of course:
+
+ tasks-url : ${base-url}/tasks
+
+A concatenation can refer to earlier values of the same field:
+
+ path : "/bin"
+ path : ${path}":/usr/bin"
+
+Arrays can be concatenated as well:
+
+ path : [ "/bin" ]
+ path : ${path} [ "/usr/bin" ]
+
+When objects are "concatenated," they are merged, so object
+concatenation is just a shorthand for defining the same object
+twice. The long way (mentioned earlier) is:
+
+ data-center-generic = { cluster-size = 6 }
+ data-center-east = ${data-center-generic}
+ data-center-east = { name = "east" }
+
+The concatenation-style shortcut is:
+
+ data-center-generic = { cluster-size = 6 }
+ data-center-east = ${data-center-generic} { name = "east" }
+
+When concatenating objects and arrays, newlines are allowed
+_inside_ each object or array, but not between them.
+
+Non-newline whitespace is never a field or element separator. So
+`[ 1 2 3 4 ]` is an array with one unquoted string element
+`"1 2 3 4"`. To get an array of four numbers you need either commas or
+newlines separating the numbers.
+
+See the spec for full details on concatenation.
+
## Future Directions
Here are some features that might be nice to add.
@@ -337,13 +390,6 @@ Here are some features that might be nice to add.
deterministic order based on their filename.
If you include a file and it turns out to be a directory then
it would be processed in this way.
- - some way to merge array types. One approach could be:
- `searchPath=${searchPath} ["/usr/local/foo"]`, here
- arrays would have to be merged if a series of them appear after
- a key, similar to how strings are concatenated already.
- For consistency, maybe objects would also support this
- syntax, though there's an existing way to merge objects
- (duplicate fields).
- including URLs (which would allow forcing file: when inside
a classpath resource, among other things)
View
120 config/src/main/java/com/typesafe/config/impl/ConfigConcatenation.java
@@ -7,6 +7,7 @@
import java.util.List;
import com.typesafe.config.ConfigException;
+import com.typesafe.config.ConfigObject;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigValueType;
@@ -29,6 +30,22 @@
ConfigConcatenation(ConfigOrigin origin, List<AbstractConfigValue> pieces) {
super(origin);
this.pieces = pieces;
+
+ if (pieces.size() < 2)
+ throw new ConfigException.BugOrBroken("Created concatenation with less than 2 items: "
+ + this);
+
+ boolean hadUnmergeable = false;
+ for (AbstractConfigValue p : pieces) {
+ if (p instanceof ConfigConcatenation)
+ throw new ConfigException.BugOrBroken(
+ "ConfigConcatenation should never be nested: " + this);
+ if (p instanceof Unmergeable)
+ hadUnmergeable = true;
+ }
+ if (!hadUnmergeable)
+ throw new ConfigException.BugOrBroken(
+ "Created concatenation without an unmergeable in it: " + this);
}
private ConfigException.NotResolved notResolved() {
@@ -65,6 +82,85 @@ protected boolean ignoresFallbacks() {
return Collections.singleton(this);
}
+ /**
+ * Add left and right, or their merger, to builder.
+ */
+ private static void join(ArrayList<AbstractConfigValue> builder,
+ AbstractConfigValue right) {
+ AbstractConfigValue left = builder.get(builder.size() - 1);
+ // Since this depends on the type of two instances, I couldn't think
+ // of much alternative to an instanceof chain. Visitors are sometimes
+ // used for multiple dispatch but seems like overkill.
+ AbstractConfigValue joined = null;
+ if (left instanceof ConfigObject && right instanceof ConfigObject) {
+ joined = right.withFallback(left);
+ } else if (left instanceof SimpleConfigList && right instanceof SimpleConfigList) {
+ joined = ((SimpleConfigList)left).concatenate((SimpleConfigList)right);
+ } else if (left instanceof ConfigConcatenation || right instanceof ConfigConcatenation) {
+ throw new ConfigException.BugOrBroken("unflattened ConfigConcatenation");
+ } else if (left instanceof Unmergeable || right instanceof Unmergeable) {
+ // leave joined=null, cannot join
+ } else {
+ // handle primitive type or primitive type mixed with object or list
+ String s1 = left.transformToString();
+ String s2 = right.transformToString();
+ if (s1 == null || s2 == null) {
+ throw new ConfigException.WrongType(left.origin(),
+ "Cannot concatenate object or list with a non-object-or-list, " + left
+ + " and " + right + " are not compatible");
+ } else {
+ ConfigOrigin joinedOrigin = SimpleConfigOrigin.mergeOrigins(left.origin(),
+ right.origin());
+ joined = new ConfigString(joinedOrigin, s1 + s2);
+ }
+ }
+
+ if (joined == null) {
+ builder.add(right);
+ } else {
+ builder.remove(builder.size() - 1);
+ builder.add(joined);
+ }
+ }
+
+ static List<AbstractConfigValue> consolidate(List<AbstractConfigValue> pieces) {
+ if (pieces.size() < 2) {
+ return pieces;
+ } else {
+ List<AbstractConfigValue> flattened = new ArrayList<AbstractConfigValue>(pieces.size());
+ for (AbstractConfigValue v : pieces) {
+ if (v instanceof ConfigConcatenation) {
+ flattened.addAll(((ConfigConcatenation) v).pieces);
+ } else {
+ flattened.add(v);
+ }
+ }
+
+ ArrayList<AbstractConfigValue> consolidated = new ArrayList<AbstractConfigValue>(
+ flattened.size());
+ for (AbstractConfigValue v : flattened) {
+ if (consolidated.isEmpty())
+ consolidated.add(v);
+ else
+ join(consolidated, v);
+ }
+
+ return consolidated;
+ }
+ }
+
+ static AbstractConfigValue concatenate(List<AbstractConfigValue> pieces) {
+ List<AbstractConfigValue> consolidated = consolidate(pieces);
+ if (consolidated.isEmpty()) {
+ return null;
+ } else if (consolidated.size() == 1) {
+ return consolidated.get(0);
+ } else {
+ ConfigOrigin mergedOrigin = SimpleConfigOrigin.mergeOrigins(consolidated);
+ return new ConfigConcatenation(mergedOrigin, consolidated);
+ }
+ }
+
@Override
AbstractConfigValue resolveSubstitutions(ResolveContext context) throws NotPossibleToResolve {
List<AbstractConfigValue> resolved = new ArrayList<AbstractConfigValue>(pieces.size());
@@ -75,28 +171,16 @@ AbstractConfigValue resolveSubstitutions(ResolveContext context) throws NotPossi
if (r == null) {
// it was optional... omit
} else {
- switch (r.valueType()) {
- case LIST:
- case OBJECT:
- // cannot substitute lists and objects into strings
- // we know p was a ConfigReference since it wasn't
- // a ConfigString
- String pathString = ((ConfigReference) p).expression().toString();
- throw new ConfigException.WrongType(r.origin(), pathString,
- "not a list or object", r.valueType().name());
- default:
- resolved.add(r);
- }
+ resolved.add(r);
}
}
// now need to concat everything
- StringBuilder sb = new StringBuilder();
- for (AbstractConfigValue r : resolved) {
- sb.append(r.transformToString());
- }
-
- return new ConfigString(origin(), sb.toString());
+ List<AbstractConfigValue> joined = consolidate(resolved);
+ if (joined.size() != 1)
+ throw new ConfigException.BugOrBroken(
+ "Resolved list should always join to exactly one value, not " + joined);
+ return joined.get(0);
}
@Override
View
120 config/src/main/java/com/typesafe/config/impl/Parser.java
@@ -246,6 +246,14 @@ private boolean checkElementSeparator() {
}
}
+ private static SubstitutionExpression tokenToSubstitutionExpression(Token valueToken) {
+ List<Token> expression = Tokens.getSubstitutionPathExpression(valueToken);
+ Path path = parsePathExpression(expression.iterator(), valueToken.origin());
+ boolean optional = Tokens.getSubstitutionOptional(valueToken);
+
+ return new SubstitutionExpression(path, optional);
+ }
+
// merge a bunch of adjacent values into one
// value; change unquoted text into a string
// value.
@@ -254,18 +262,39 @@ private void consolidateValueTokens() {
if (flavor == ConfigSyntax.JSON)
return;
- List<Token> values = null; // create only if we have value tokens
+ // create only if we have value tokens
+ List<AbstractConfigValue> values = null;
TokenWithComments firstValueWithComments = null;
- TokenWithComments t = nextTokenIgnoringNewline(); // ignore a
- // newline up
- // front
- while (Tokens.isValue(t.token) || Tokens.isUnquotedText(t.token)
- || Tokens.isSubstitution(t.token)) {
+ // ignore a newline up front
+ TokenWithComments t = nextTokenIgnoringNewline();
+ while (true) {
+ AbstractConfigValue v = null;
+ if (Tokens.isValue(t.token)) {
+ // if we consolidateValueTokens() multiple times then
+ // this value could be a concatenation, object, array,
+ // or substitution already.
+ v = Tokens.getValue(t.token);
+ } else if (Tokens.isUnquotedText(t.token)) {
+ v = new ConfigString(t.token.origin(), Tokens.getUnquotedText(t.token));
+ } else if (Tokens.isSubstitution(t.token)) {
+ v = new ConfigReference(t.token.origin(),
+ tokenToSubstitutionExpression(t.token));
+ } else if (t.token == Tokens.OPEN_CURLY || t.token == Tokens.OPEN_SQUARE) {
+ // there may be newlines _within_ the objects and arrays
+ v = parseValue(t);
+ } else {
+ break;
+ }
+
+ if (v == null)
+ throw new ConfigException.BugOrBroken("no value");
+
if (values == null) {
- values = new ArrayList<Token>();
+ values = new ArrayList<AbstractConfigValue>();
firstValueWithComments = t;
}
- values.add(t.token);
+ values.add(v);
+
t = nextToken(); // but don't consolidate across a newline
}
// the last one wasn't a value token
@@ -274,79 +303,10 @@ private void consolidateValueTokens() {
if (values == null)
return;
- if (values.size() == 1 && Tokens.isValue(firstValueWithComments.token)) {
- // a single value token requires no consolidation
- putBack(firstValueWithComments);
- return;
- }
-
- // this will be a list of String and SubstitutionExpression
- List<Object> minimized = new ArrayList<Object>();
-
- // we have multiple value tokens or one unquoted text token;
- // collapse into a string token.
- StringBuilder sb = new StringBuilder();
- ConfigOrigin firstOrigin = null;
- for (Token valueToken : values) {
- if (Tokens.isValue(valueToken)) {
- AbstractConfigValue v = Tokens.getValue(valueToken);
- sb.append(v.transformToString());
- if (firstOrigin == null)
- firstOrigin = v.origin();
- } else if (Tokens.isUnquotedText(valueToken)) {
- String text = Tokens.getUnquotedText(valueToken);
- if (firstOrigin == null)
- firstOrigin = valueToken.origin();
- sb.append(text);
- } else if (Tokens.isSubstitution(valueToken)) {
- if (firstOrigin == null)
- firstOrigin = valueToken.origin();
-
- if (sb.length() > 0) {
- // save string so far
- minimized.add(sb.toString());
- sb.setLength(0);
- }
- // now save substitution
- List<Token> expression = Tokens
- .getSubstitutionPathExpression(valueToken);
- Path path = parsePathExpression(expression.iterator(), valueToken.origin());
- boolean optional = Tokens.getSubstitutionOptional(valueToken);
-
- minimized.add(new SubstitutionExpression(path, optional));
- } else {
- throw new ConfigException.BugOrBroken(
- "should not be trying to consolidate token: "
- + valueToken);
- }
- }
-
- if (sb.length() > 0) {
- // save string so far
- minimized.add(sb.toString());
- }
-
- if (minimized.isEmpty())
- throw new ConfigException.BugOrBroken(
- "trying to consolidate values to nothing");
-
- Token consolidated = null;
-
- if (minimized.size() == 1 && minimized.get(0) instanceof String) {
- consolidated = Tokens.newString(firstOrigin,
- (String) minimized.get(0));
- } else if (minimized.size() == 1 && minimized.get(0) instanceof SubstitutionExpression) {
- // a substitution expression ${}
- consolidated = Tokens.newValue(new ConfigReference(firstOrigin,
- (SubstitutionExpression) minimized.get(0)));
- } else {
- // a value concatenation with a substitution expression in it
- List<AbstractConfigValue> vs = ConfigConcatenation.valuesFromPieces(
- firstOrigin, minimized);
- consolidated = Tokens.newValue(new ConfigConcatenation(firstOrigin, vs));
- }
+ AbstractConfigValue consolidated = ConfigConcatenation.concatenate(values);
- putBack(new TokenWithComments(consolidated, firstValueWithComments.comments));
+ putBack(new TokenWithComments(Tokens.newValue(consolidated),
+ firstValueWithComments.comments));
}
private ConfigOrigin lineOrigin() {
View
9 config/src/main/java/com/typesafe/config/impl/SimpleConfigList.java
@@ -400,6 +400,15 @@ protected SimpleConfigList newCopy(ConfigOrigin newOrigin) {
return new SimpleConfigList(newOrigin, value);
}
+ final SimpleConfigList concatenate(SimpleConfigList other) {
+ ConfigOrigin combinedOrigin = SimpleConfigOrigin.mergeOrigins(origin(), other.origin());
+ List<AbstractConfigValue> combined = new ArrayList<AbstractConfigValue>(value.size()
+ + other.value.size());
+ combined.addAll(value);
+ combined.addAll(other.value);
+ return new SimpleConfigList(combinedOrigin, combined);
+ }
+
// This ridiculous hack is because some JDK versions apparently can't
// serialize an array, which is used to implement ArrayList and EmptyList.
// maybe
View
15 config/src/main/java/com/typesafe/config/impl/SimpleConfigOrigin.java
@@ -30,8 +30,7 @@
final private List<String> commentsOrNull;
protected SimpleConfigOrigin(String description, int lineNumber, int endLineNumber,
- OriginType originType,
- String urlOrNull, List<String> commentsOrNull) {
+ OriginType originType, String urlOrNull, List<String> commentsOrNull) {
this.description = description;
this.lineNumber = lineNumber;
this.endLineNumber = endLineNumber;
@@ -308,6 +307,18 @@ private static SimpleConfigOrigin mergeThree(SimpleConfigOrigin a, SimpleConfigO
}
}
+ static ConfigOrigin mergeOrigins(ConfigOrigin a, ConfigOrigin b) {
+ return mergeTwo((SimpleConfigOrigin) a, (SimpleConfigOrigin) b);
+ }
+
+ static ConfigOrigin mergeOrigins(List<? extends AbstractConfigValue> stack) {
+ List<ConfigOrigin> origins = new ArrayList<ConfigOrigin>(stack.size());
+ for (AbstractConfigValue v : stack) {
+ origins.add(v.origin());
+ }
+ return mergeOrigins(origins);
+ }
+
static ConfigOrigin mergeOrigins(Collection<? extends ConfigOrigin> stack) {
if (stack.isEmpty()) {
throw new ConfigException.BugOrBroken("can't merge empty list of origins");
View
186 config/src/test/scala/com/typesafe/config/impl/ConcatenationTest.scala
@@ -10,6 +10,7 @@ import com.typesafe.config.ConfigException
import com.typesafe.config.ConfigResolveOptions
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
+import scala.collection.JavaConverters._
class ConcatenationTest extends TestUtils {
@@ -44,22 +45,35 @@ class ConcatenationTest extends TestUtils {
@Test
def noObjectsInStringConcat() {
- val e = intercept[ConfigException.Parse] {
+ val e = intercept[ConfigException.WrongType] {
parseConfig(""" a : abc { x : y } """)
}
assertTrue("wrong exception: " + e.getMessage,
- e.getMessage.contains("Expecting") &&
- e.getMessage.contains("'{'"))
+ e.getMessage.contains("Cannot concatenate") &&
+ e.getMessage.contains("abc") &&
+ e.getMessage.contains("""{"x" : "y"}"""))
+ }
+
+ @Test
+ def noObjectConcatWithNull() {
+ val e = intercept[ConfigException.WrongType] {
+ parseConfig(""" a : null { x : y } """)
+ }
+ assertTrue("wrong exception: " + e.getMessage,
+ e.getMessage.contains("Cannot concatenate") &&
+ e.getMessage.contains("null") &&
+ e.getMessage.contains("""{"x" : "y"}"""))
}
@Test
def noArraysInStringConcat() {
- val e = intercept[ConfigException.Parse] {
- parseConfig(""" a : abc { x : y } """)
+ val e = intercept[ConfigException.WrongType] {
+ parseConfig(""" a : abc [1, 2] """)
}
assertTrue("wrong exception: " + e.getMessage,
- e.getMessage.contains("Expecting") &&
- e.getMessage.contains("'{'"))
+ e.getMessage.contains("Cannot concatenate") &&
+ e.getMessage.contains("abc") &&
+ e.getMessage.contains("[1,2]"))
}
@Test
@@ -68,8 +82,8 @@ class ConcatenationTest extends TestUtils {
parseConfig(""" a : abc ${x}, x : { y : z } """).resolve()
}
assertTrue("wrong exception: " + e.getMessage,
- e.getMessage.contains("not a list or object") &&
- e.getMessage.contains("OBJECT"))
+ e.getMessage.contains("Cannot concatenate") &&
+ e.getMessage.contains("abc"))
}
@Test
@@ -78,7 +92,157 @@ class ConcatenationTest extends TestUtils {
parseConfig(""" a : abc ${x}, x : [1,2] """).resolve()
}
assertTrue("wrong exception: " + e.getMessage,
- e.getMessage.contains("not a list or object") &&
- e.getMessage.contains("LIST"))
+ e.getMessage.contains("Cannot concatenate") &&
+ e.getMessage.contains("abc"))
+ }
+
+ @Test
+ def noSubstitutionsListConcat() {
+ val conf = parseConfig(""" a : [1,2] [3,4] """)
+ assertEquals(Seq(1, 2, 3, 4), conf.getList("a").unwrapped().asScala)
+ }
+
+ @Test
+ def listConcatWithSubstitutions() {
+ val conf = parseConfig(""" a : ${x} [3,4] ${y}, x : [1,2], y : [5,6] """).resolve()
+ assertEquals(Seq(1, 2, 3, 4, 5, 6), conf.getList("a").unwrapped().asScala)
+ }
+
+ @Test
+ def listConcatSelfReferential() {
+ val conf = parseConfig(""" a : [1, 2], a : ${a} [3,4], a : ${a} [5,6] """).resolve()
+ assertEquals(Seq(1, 2, 3, 4, 5, 6), conf.getList("a").unwrapped().asScala)
+ }
+
+ @Test
+ def noSubstitutionsListConcatCannotSpanLines() {
+ val e = intercept[ConfigException.Parse] {
+ parseConfig(""" a : [1,2]
+ [3,4] """)
+ }
+ assertTrue("wrong exception: " + e.getMessage,
+ e.getMessage.contains("expecting") &&
+ e.getMessage.contains("'['"))
+ }
+
+ @Test
+ def listConcatCanSpanLinesInsideBrackets() {
+ val conf = parseConfig(""" a : [1,2
+ ] [3,4] """)
+ assertEquals(Seq(1, 2, 3, 4), conf.getList("a").unwrapped().asScala)
+ }
+
+ @Test
+ def noSubstitutionsObjectConcat() {
+ val conf = parseConfig(""" a : { b : c } { x : y } """)
+ assertEquals(Map("b" -> "c", "x" -> "y"), conf.getObject("a").unwrapped().asScala)
+ }
+
+ @Test
+ def objectConcatMergeOrder() {
+ val conf = parseConfig(""" a : { b : 1 } { b : 2 } { b : 3 } { b : 4 } """)
+ assertEquals(4, conf.getInt("a.b"))
+ }
+
+ @Test
+ def objectConcatWithSubstitutions() {
+ val conf = parseConfig(""" a : ${x} { b : 1 } ${y}, x : { a : 0 }, y : { c : 2 } """).resolve()
+ assertEquals(Map("a" -> 0, "b" -> 1, "c" -> 2), conf.getObject("a").unwrapped().asScala)
+ }
+
+ @Test
+ def objectConcatSelfReferential() {
+ val conf = parseConfig(""" a : { a : 0 }, a : ${a} { b : 1 }, a : ${a} { c : 2 } """).resolve()
+ assertEquals(Map("a" -> 0, "b" -> 1, "c" -> 2), conf.getObject("a").unwrapped().asScala)
+ }
+
+ @Test
+ def objectConcatSelfReferentialOverride() {
+ val conf = parseConfig(""" a : { b : 3 }, a : { b : 2 } ${a} """).resolve()
+ assertEquals(Map("b" -> 3), conf.getObject("a").unwrapped().asScala)
+ }
+
+ @Test
+ def noSubstitutionsObjectConcatCannotSpanLines() {
+ val e = intercept[ConfigException.Parse] {
+ parseConfig(""" a : { b : c }
+ { x : y }""")
+ }
+ assertTrue("wrong exception: " + e.getMessage,
+ e.getMessage.contains("expecting") &&
+ e.getMessage.contains("'{'"))
+ }
+
+ @Test
+ def objectConcatCanSpanLinesInsideBraces() {
+ val conf = parseConfig(""" a : { b : c
+ } { x : y } """)
+ assertEquals(Map("b" -> "c", "x" -> "y"), conf.getObject("a").unwrapped().asScala)
+ }
+
+ @Test
+ def stringConcatInsideArrayValue() {
+ val conf = parseConfig(""" a : [ foo bar 10 ] """)
+ assertEquals(Seq("foo bar 10"), conf.getStringList("a").asScala)
+ }
+
+ @Test
+ def stringNonConcatInsideArrayValue() {
+ val conf = parseConfig(""" a : [ foo
+ bar
+ 10 ] """)
+ assertEquals(Seq("foo", "bar", "10"), conf.getStringList("a").asScala)
+ }
+
+ @Test
+ def objectConcatInsideArrayValue() {
+ val conf = parseConfig(""" a : [ { b : c } { x : y } ] """)
+ assertEquals(Seq(Map("b" -> "c", "x" -> "y")), conf.getObjectList("a").asScala.map(_.unwrapped().asScala))
+ }
+
+ @Test
+ def objectNonConcatInsideArrayValue() {
+ val conf = parseConfig(""" a : [ { b : c }
+ { x : y } ] """)
+ assertEquals(Seq(Map("b" -> "c"), Map("x" -> "y")), conf.getObjectList("a").asScala.map(_.unwrapped().asScala))
+ }
+
+ @Test
+ def listConcatInsideArrayValue() {
+ val conf = parseConfig(""" a : [ [1, 2] [3, 4] ] """)
+ assertEquals(List(List(1, 2, 3, 4)),
+ // well that's a little silly
+ conf.getList("a").unwrapped().asScala.toList.map(_.asInstanceOf[java.util.List[_]].asScala.toList))
+ }
+
+ @Test
+ def listNonConcatInsideArrayValue() {
+ val conf = parseConfig(""" a : [ [1, 2]
+ [3, 4] ] """)
+ assertEquals(List(List(1, 2), List(3, 4)),
+ // well that's a little silly
+ conf.getList("a").unwrapped().asScala.toList.map(_.asInstanceOf[java.util.List[_]].asScala.toList))
+ }
+
+ @Test
+ def stringConcatsAreKeys() {
+ val conf = parseConfig(""" 123 foo : "value" """)
+ assertEquals("value", conf.getString("123 foo"))
+ }
+
+ @Test
+ def objectsAreNotKeys() {
+ val e = intercept[ConfigException.Parse] {
+ parseConfig("""{ { a : 1 } : "value" }""")
+ }
+ assertTrue("wrong exception: " + e.getMessage, e.getMessage.contains("expecting a close") && e.getMessage.contains("'{'"))
+ }
+
+ @Test
+ def arraysAreNotKeys() {
+ val e = intercept[ConfigException.Parse] {
+ parseConfig("""{ [ "a" ] : "value" }""")
+ }
+ assertTrue("wrong exception: " + e.getMessage, e.getMessage.contains("expecting a close") && e.getMessage.contains("'['"))
}
}

0 comments on commit 47e168a

Please sign in to comment.
Something went wrong with that request. Please try again.