mirror of
https://github.com/lightbend/config.git
synced 2025-03-19 22:00:42 +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
|
||||
|
||||
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
|
||||
|
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
|
||||
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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -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");
|
||||
|
@ -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("'['"))
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user