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.
This commit is contained in:
Havoc Pennington 2012-04-06 00:35:47 -04:00
parent 8189be0f16
commit 47e168a92f
7 changed files with 504 additions and 129 deletions

123
HOCON.md
View File

@ -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 File

@ -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 File

@ -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<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 @@ 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<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 @@ 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<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 File

@ -246,6 +246,14 @@ final class Parser {
}
}
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 @@ final class Parser {
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 @@ 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<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));
}
putBack(new TokenWithComments(consolidated, firstValueWithComments.comments));
putBack(new TokenWithComments(Tokens.newValue(consolidated),
firstValueWithComments.comments));
}
private ConfigOrigin lineOrigin() {

View File

@ -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<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 File

@ -30,8 +30,7 @@ final class SimpleConfigOrigin implements ConfigOrigin, Serializable {
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 @@ final class SimpleConfigOrigin implements ConfigOrigin, Serializable {
}
}
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 File

@ -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("'['"))
}
}