From 47e168a92f2628e5863882677ab38b11c71449a0 Mon Sep 17 00:00:00 2001 From: Havoc Pennington Date: Fri, 6 Apr 2012 00:35:47 -0400 Subject: [PATCH] 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. --- HOCON.md | 123 ++++++++++-- README.md | 60 +++++- .../config/impl/ConfigConcatenation.java | 120 +++++++++-- .../java/com/typesafe/config/impl/Parser.java | 120 ++++------- .../config/impl/SimpleConfigList.java | 9 + .../config/impl/SimpleConfigOrigin.java | 15 +- .../config/impl/ConcatenationTest.scala | 186 ++++++++++++++++-- 7 files changed, 504 insertions(+), 129 deletions(-) diff --git a/HOCON.md b/HOCON.md index a70995c3..750cf12a 100644 --- a/HOCON.md +++ b/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 diff --git a/README.md b/README.md index b868eb1b..17670f40 100644 --- a/README.md +++ b/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) 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 0eda954a..33fb421c 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigConcatenation.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigConcatenation.java @@ -7,6 +7,7 @@ import java.util.Collections; 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 @@ final class ConfigConcatenation extends AbstractConfigValue implements Unmergeab ConfigConcatenation(ConfigOrigin origin, List 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 @@ final class ConfigConcatenation extends AbstractConfigValue implements Unmergeab return Collections.singleton(this); } + /** + * Add left and right, or their merger, to builder. + */ + private static void join(ArrayList 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 consolidate(List pieces) { + if (pieces.size() < 2) { + return pieces; + } else { + List flattened = new ArrayList(pieces.size()); + for (AbstractConfigValue v : pieces) { + if (v instanceof ConfigConcatenation) { + flattened.addAll(((ConfigConcatenation) v).pieces); + } else { + flattened.add(v); + } + } + + ArrayList consolidated = new ArrayList( + flattened.size()); + for (AbstractConfigValue v : flattened) { + if (consolidated.isEmpty()) + consolidated.add(v); + else + join(consolidated, v); + } + + return consolidated; + } + } + + static AbstractConfigValue concatenate(List pieces) { + List 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 resolved = new ArrayList(pieces.size()); @@ -75,28 +171,16 @@ final class ConfigConcatenation extends AbstractConfigValue implements Unmergeab 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 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 diff --git a/config/src/main/java/com/typesafe/config/impl/Parser.java b/config/src/main/java/com/typesafe/config/impl/Parser.java index a2db2a94..f56938b5 100644 --- a/config/src/main/java/com/typesafe/config/impl/Parser.java +++ b/config/src/main/java/com/typesafe/config/impl/Parser.java @@ -246,6 +246,14 @@ final class Parser { } } + private static SubstitutionExpression tokenToSubstitutionExpression(Token valueToken) { + List 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 @@ final class Parser { if (flavor == ConfigSyntax.JSON) return; - List values = null; // create only if we have value tokens + // create only if we have value tokens + List 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(); + values = new ArrayList(); 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 @@ final class Parser { if (values == null) return; - if (values.size() == 1 && Tokens.isValue(firstValueWithComments.token)) { - // a single value token requires no consolidation - putBack(firstValueWithComments); - return; - } + AbstractConfigValue consolidated = ConfigConcatenation.concatenate(values); - // this will be a list of String and SubstitutionExpression - List minimized = new ArrayList(); - - // 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 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 vs = ConfigConcatenation.valuesFromPieces( - firstOrigin, minimized); - consolidated = Tokens.newValue(new ConfigConcatenation(firstOrigin, vs)); - } - - putBack(new TokenWithComments(consolidated, firstValueWithComments.comments)); + putBack(new TokenWithComments(Tokens.newValue(consolidated), + firstValueWithComments.comments)); } private ConfigOrigin lineOrigin() { diff --git a/config/src/main/java/com/typesafe/config/impl/SimpleConfigList.java b/config/src/main/java/com/typesafe/config/impl/SimpleConfigList.java index 8d255d0c..110e6192 100644 --- a/config/src/main/java/com/typesafe/config/impl/SimpleConfigList.java +++ b/config/src/main/java/com/typesafe/config/impl/SimpleConfigList.java @@ -400,6 +400,15 @@ final class SimpleConfigList extends AbstractConfigValue implements ConfigList { return new SimpleConfigList(newOrigin, value); } + final SimpleConfigList concatenate(SimpleConfigList other) { + ConfigOrigin combinedOrigin = SimpleConfigOrigin.mergeOrigins(origin(), other.origin()); + List combined = new ArrayList(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 diff --git a/config/src/main/java/com/typesafe/config/impl/SimpleConfigOrigin.java b/config/src/main/java/com/typesafe/config/impl/SimpleConfigOrigin.java index 23351c1e..387446ff 100644 --- a/config/src/main/java/com/typesafe/config/impl/SimpleConfigOrigin.java +++ b/config/src/main/java/com/typesafe/config/impl/SimpleConfigOrigin.java @@ -30,8 +30,7 @@ final class SimpleConfigOrigin implements ConfigOrigin, Serializable { final private List commentsOrNull; protected SimpleConfigOrigin(String description, int lineNumber, int endLineNumber, - OriginType originType, - String urlOrNull, List commentsOrNull) { + OriginType originType, String urlOrNull, List commentsOrNull) { this.description = description; this.lineNumber = lineNumber; this.endLineNumber = endLineNumber; @@ -308,6 +307,18 @@ final class SimpleConfigOrigin implements ConfigOrigin, Serializable { } } + static ConfigOrigin mergeOrigins(ConfigOrigin a, ConfigOrigin b) { + return mergeTwo((SimpleConfigOrigin) a, (SimpleConfigOrigin) b); + } + + static ConfigOrigin mergeOrigins(List stack) { + List origins = new ArrayList(stack.size()); + for (AbstractConfigValue v : stack) { + origins.add(v.origin()); + } + return mergeOrigins(origins); + } + static ConfigOrigin mergeOrigins(Collection stack) { if (stack.isEmpty()) { throw new ConfigException.BugOrBroken("can't merge empty list of origins"); diff --git a/config/src/test/scala/com/typesafe/config/impl/ConcatenationTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConcatenationTest.scala index b0d2a8b8..0665f294 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConcatenationTest.scala +++ b/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("'['")) } }