mirror of
https://github.com/lightbend/config.git
synced 2025-03-21 23:00:43 +08:00
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:
parent
8189be0f16
commit
47e168a92f
123
HOCON.md
123
HOCON.md
@ -231,21 +231,41 @@ reserved keywords to allow future extensions to this spec.
|
|||||||
|
|
||||||
### Value concatenation
|
### Value concatenation
|
||||||
|
|
||||||
The value of an object field or an array element may consist of
|
The value of an object field or array element may consist of
|
||||||
multiple values which are concatenated into one string.
|
multiple values which are combined. There are three kinds of value
|
||||||
|
concatenation:
|
||||||
|
|
||||||
Only simple values participate in value concatenation. Recall that
|
- if all the values are simple values (neither objects nor
|
||||||
a simple value is any value other than arrays and objects.
|
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
|
As long as simple values are separated only by non-newline
|
||||||
whitespace, the _whitespace between them is preserved_ and the
|
whitespace, the _whitespace between them is preserved_ and the
|
||||||
values, along with the whitespace, are concatenated into a string.
|
values, along with the whitespace, are concatenated into a string.
|
||||||
|
|
||||||
Value concatenations never span a newline, or a character that is
|
String value concatenations never span a newline, or a character
|
||||||
not part of a simple value.
|
that is not part of a simple value.
|
||||||
|
|
||||||
A value concatenation may appear in any place that a string may
|
A string value concatenation may appear in any place that a string
|
||||||
appear, including object keys, object values, and array elements.
|
may appear, including object keys, object values, and array
|
||||||
|
elements.
|
||||||
|
|
||||||
Whenever a value would appear in JSON, a HOCON parser instead
|
Whenever a value would appear in JSON, a HOCON parser instead
|
||||||
collects multiple values (including the whitespace between them)
|
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
|
trimmed. The equivalent string, written in quoted form, would be
|
||||||
`"foo bar baz"`.
|
`"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
|
whitespace) and quoted string `"foo bar"` would result in the same
|
||||||
in-memory representation, seven characters.
|
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):
|
converted to strings as follows (strings shown as quoted strings):
|
||||||
|
|
||||||
- `true` and `false` become the strings `"true"` and `"false"`.
|
- `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.
|
as it was written in the file.
|
||||||
- a substitution is replaced with its value which is then
|
- a substitution is replaced with its value which is then
|
||||||
converted to a string as above.
|
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.
|
concatenation.
|
||||||
|
|
||||||
A single value is never converted to a string. That is, it would
|
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
|
another simple value on the same line) should be parsed as a value
|
||||||
concatenation and converted to a string.
|
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
|
||||||
|
|
||||||
Path expressions are used to write out a path through the object
|
Path expressions are used to write out a path through the object
|
||||||
|
60
README.md
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
|
HOCON, also called `.conf`, see HOCON.md in this directory for more
|
||||||
detail.
|
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
|
### Features of HOCON
|
||||||
|
|
||||||
- Comments, with `#` or `//`
|
- Comments, with `#` or `//`
|
||||||
@ -328,6 +331,56 @@ value just disappear if the substitution is not found:
|
|||||||
// this array could have one or two elements
|
// this array could have one or two elements
|
||||||
path = [ "a", ${?OPTIONAL_A} ]
|
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
|
## Future Directions
|
||||||
|
|
||||||
Here are some features that might be nice to add.
|
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.
|
deterministic order based on their filename.
|
||||||
If you include a file and it turns out to be a directory then
|
If you include a file and it turns out to be a directory then
|
||||||
it would be processed in this way.
|
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
|
- including URLs (which would allow forcing file: when inside
|
||||||
a classpath resource, among other things)
|
a classpath resource, among other things)
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import java.util.Collections;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import com.typesafe.config.ConfigException;
|
import com.typesafe.config.ConfigException;
|
||||||
|
import com.typesafe.config.ConfigObject;
|
||||||
import com.typesafe.config.ConfigOrigin;
|
import com.typesafe.config.ConfigOrigin;
|
||||||
import com.typesafe.config.ConfigValueType;
|
import com.typesafe.config.ConfigValueType;
|
||||||
|
|
||||||
@ -29,6 +30,22 @@ final class ConfigConcatenation extends AbstractConfigValue implements Unmergeab
|
|||||||
ConfigConcatenation(ConfigOrigin origin, List<AbstractConfigValue> pieces) {
|
ConfigConcatenation(ConfigOrigin origin, List<AbstractConfigValue> pieces) {
|
||||||
super(origin);
|
super(origin);
|
||||||
this.pieces = pieces;
|
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() {
|
private ConfigException.NotResolved notResolved() {
|
||||||
@ -65,6 +82,85 @@ final class ConfigConcatenation extends AbstractConfigValue implements Unmergeab
|
|||||||
return Collections.singleton(this);
|
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
|
@Override
|
||||||
AbstractConfigValue resolveSubstitutions(ResolveContext context) throws NotPossibleToResolve {
|
AbstractConfigValue resolveSubstitutions(ResolveContext context) throws NotPossibleToResolve {
|
||||||
List<AbstractConfigValue> resolved = new ArrayList<AbstractConfigValue>(pieces.size());
|
List<AbstractConfigValue> resolved = new ArrayList<AbstractConfigValue>(pieces.size());
|
||||||
@ -75,28 +171,16 @@ final class ConfigConcatenation extends AbstractConfigValue implements Unmergeab
|
|||||||
if (r == null) {
|
if (r == null) {
|
||||||
// it was optional... omit
|
// it was optional... omit
|
||||||
} else {
|
} else {
|
||||||
switch (r.valueType()) {
|
resolved.add(r);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// now need to concat everything
|
// now need to concat everything
|
||||||
StringBuilder sb = new StringBuilder();
|
List<AbstractConfigValue> joined = consolidate(resolved);
|
||||||
for (AbstractConfigValue r : resolved) {
|
if (joined.size() != 1)
|
||||||
sb.append(r.transformToString());
|
throw new ConfigException.BugOrBroken(
|
||||||
}
|
"Resolved list should always join to exactly one value, not " + joined);
|
||||||
|
return joined.get(0);
|
||||||
return new ConfigString(origin(), sb.toString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -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
|
// merge a bunch of adjacent values into one
|
||||||
// value; change unquoted text into a string
|
// value; change unquoted text into a string
|
||||||
// value.
|
// value.
|
||||||
@ -254,18 +262,39 @@ final class Parser {
|
|||||||
if (flavor == ConfigSyntax.JSON)
|
if (flavor == ConfigSyntax.JSON)
|
||||||
return;
|
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 firstValueWithComments = null;
|
||||||
TokenWithComments t = nextTokenIgnoringNewline(); // ignore a
|
// ignore a newline up front
|
||||||
// newline up
|
TokenWithComments t = nextTokenIgnoringNewline();
|
||||||
// front
|
while (true) {
|
||||||
while (Tokens.isValue(t.token) || Tokens.isUnquotedText(t.token)
|
AbstractConfigValue v = null;
|
||||||
|| Tokens.isSubstitution(t.token)) {
|
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) {
|
if (values == null) {
|
||||||
values = new ArrayList<Token>();
|
values = new ArrayList<AbstractConfigValue>();
|
||||||
firstValueWithComments = t;
|
firstValueWithComments = t;
|
||||||
}
|
}
|
||||||
values.add(t.token);
|
values.add(v);
|
||||||
|
|
||||||
t = nextToken(); // but don't consolidate across a newline
|
t = nextToken(); // but don't consolidate across a newline
|
||||||
}
|
}
|
||||||
// the last one wasn't a value token
|
// the last one wasn't a value token
|
||||||
@ -274,79 +303,10 @@ final class Parser {
|
|||||||
if (values == null)
|
if (values == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (values.size() == 1 && Tokens.isValue(firstValueWithComments.token)) {
|
AbstractConfigValue consolidated = ConfigConcatenation.concatenate(values);
|
||||||
// a single value token requires no consolidation
|
|
||||||
putBack(firstValueWithComments);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// this will be a list of String and SubstitutionExpression
|
putBack(new TokenWithComments(Tokens.newValue(consolidated),
|
||||||
List<Object> minimized = new ArrayList<Object>();
|
firstValueWithComments.comments));
|
||||||
|
|
||||||
// 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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ConfigOrigin lineOrigin() {
|
private ConfigOrigin lineOrigin() {
|
||||||
|
@ -400,6 +400,15 @@ final class SimpleConfigList extends AbstractConfigValue implements ConfigList {
|
|||||||
return new SimpleConfigList(newOrigin, value);
|
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
|
// This ridiculous hack is because some JDK versions apparently can't
|
||||||
// serialize an array, which is used to implement ArrayList and EmptyList.
|
// serialize an array, which is used to implement ArrayList and EmptyList.
|
||||||
// maybe
|
// maybe
|
||||||
|
@ -30,8 +30,7 @@ final class SimpleConfigOrigin implements ConfigOrigin, Serializable {
|
|||||||
final private List<String> commentsOrNull;
|
final private List<String> commentsOrNull;
|
||||||
|
|
||||||
protected SimpleConfigOrigin(String description, int lineNumber, int endLineNumber,
|
protected SimpleConfigOrigin(String description, int lineNumber, int endLineNumber,
|
||||||
OriginType originType,
|
OriginType originType, String urlOrNull, List<String> commentsOrNull) {
|
||||||
String urlOrNull, List<String> commentsOrNull) {
|
|
||||||
this.description = description;
|
this.description = description;
|
||||||
this.lineNumber = lineNumber;
|
this.lineNumber = lineNumber;
|
||||||
this.endLineNumber = endLineNumber;
|
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) {
|
static ConfigOrigin mergeOrigins(Collection<? extends ConfigOrigin> stack) {
|
||||||
if (stack.isEmpty()) {
|
if (stack.isEmpty()) {
|
||||||
throw new ConfigException.BugOrBroken("can't merge empty list of origins");
|
throw new ConfigException.BugOrBroken("can't merge empty list of origins");
|
||||||
|
@ -10,6 +10,7 @@ import com.typesafe.config.ConfigException
|
|||||||
import com.typesafe.config.ConfigResolveOptions
|
import com.typesafe.config.ConfigResolveOptions
|
||||||
import com.typesafe.config.Config
|
import com.typesafe.config.Config
|
||||||
import com.typesafe.config.ConfigFactory
|
import com.typesafe.config.ConfigFactory
|
||||||
|
import scala.collection.JavaConverters._
|
||||||
|
|
||||||
class ConcatenationTest extends TestUtils {
|
class ConcatenationTest extends TestUtils {
|
||||||
|
|
||||||
@ -44,22 +45,35 @@ class ConcatenationTest extends TestUtils {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
def noObjectsInStringConcat() {
|
def noObjectsInStringConcat() {
|
||||||
val e = intercept[ConfigException.Parse] {
|
val e = intercept[ConfigException.WrongType] {
|
||||||
parseConfig(""" a : abc { x : y } """)
|
parseConfig(""" a : abc { x : y } """)
|
||||||
}
|
}
|
||||||
assertTrue("wrong exception: " + e.getMessage,
|
assertTrue("wrong exception: " + e.getMessage,
|
||||||
e.getMessage.contains("Expecting") &&
|
e.getMessage.contains("Cannot concatenate") &&
|
||||||
e.getMessage.contains("'{'"))
|
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
|
@Test
|
||||||
def noArraysInStringConcat() {
|
def noArraysInStringConcat() {
|
||||||
val e = intercept[ConfigException.Parse] {
|
val e = intercept[ConfigException.WrongType] {
|
||||||
parseConfig(""" a : abc { x : y } """)
|
parseConfig(""" a : abc [1, 2] """)
|
||||||
}
|
}
|
||||||
assertTrue("wrong exception: " + e.getMessage,
|
assertTrue("wrong exception: " + e.getMessage,
|
||||||
e.getMessage.contains("Expecting") &&
|
e.getMessage.contains("Cannot concatenate") &&
|
||||||
e.getMessage.contains("'{'"))
|
e.getMessage.contains("abc") &&
|
||||||
|
e.getMessage.contains("[1,2]"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -68,8 +82,8 @@ class ConcatenationTest extends TestUtils {
|
|||||||
parseConfig(""" a : abc ${x}, x : { y : z } """).resolve()
|
parseConfig(""" a : abc ${x}, x : { y : z } """).resolve()
|
||||||
}
|
}
|
||||||
assertTrue("wrong exception: " + e.getMessage,
|
assertTrue("wrong exception: " + e.getMessage,
|
||||||
e.getMessage.contains("not a list or object") &&
|
e.getMessage.contains("Cannot concatenate") &&
|
||||||
e.getMessage.contains("OBJECT"))
|
e.getMessage.contains("abc"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -78,7 +92,157 @@ class ConcatenationTest extends TestUtils {
|
|||||||
parseConfig(""" a : abc ${x}, x : [1,2] """).resolve()
|
parseConfig(""" a : abc ${x}, x : [1,2] """).resolve()
|
||||||
}
|
}
|
||||||
assertTrue("wrong exception: " + e.getMessage,
|
assertTrue("wrong exception: " + e.getMessage,
|
||||||
e.getMessage.contains("not a list or object") &&
|
e.getMessage.contains("Cannot concatenate") &&
|
||||||
e.getMessage.contains("LIST"))
|
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("'['"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user