mirror of
https://github.com/lightbend/config.git
synced 2025-03-17 04:40:41 +08:00
Implement unquoted path keys, { foo.bar : 42 }
This commit is contained in:
parent
a3128871ac
commit
c18f10c6dc
@ -15,6 +15,7 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Map;
|
||||
import java.util.Stack;
|
||||
|
||||
@ -273,6 +274,58 @@ final class Parser {
|
||||
}
|
||||
}
|
||||
|
||||
private static AbstractConfigObject createValueUnderPath(Path path,
|
||||
AbstractConfigValue value) {
|
||||
// for path foo.bar, we are creating
|
||||
// { "foo" : { "bar" : value } }
|
||||
List<String> keys = new ArrayList<String>();
|
||||
|
||||
String key = path.first();
|
||||
Path remaining = path.remainder();
|
||||
while (key != null) {
|
||||
keys.add(key);
|
||||
if (remaining == null) {
|
||||
break;
|
||||
} else {
|
||||
key = remaining.first();
|
||||
remaining = remaining.remainder();
|
||||
}
|
||||
}
|
||||
ListIterator<String> i = keys.listIterator(keys.size());
|
||||
String deepest = i.previous();
|
||||
AbstractConfigObject o = new SimpleConfigObject(value.origin(),
|
||||
Collections.<String, AbstractConfigValue> singletonMap(
|
||||
deepest, value));
|
||||
while (i.hasPrevious()) {
|
||||
Map<String, AbstractConfigValue> m = Collections.<String, AbstractConfigValue> singletonMap(
|
||||
i.previous(), o);
|
||||
o = new SimpleConfigObject(value.origin(), m);
|
||||
}
|
||||
|
||||
return o;
|
||||
}
|
||||
|
||||
private Path parseKey(Token token) {
|
||||
if (flavor == SyntaxFlavor.JSON) {
|
||||
if (Tokens.isValueWithType(token, ConfigValueType.STRING)) {
|
||||
String key = (String) Tokens.getValue(token).unwrapped();
|
||||
return Path.newKey(key);
|
||||
} else {
|
||||
throw parseError("Expecting close brace } or a field name, got "
|
||||
+ token);
|
||||
}
|
||||
} else {
|
||||
List<Token> expression = new ArrayList<Token>();
|
||||
Token t = token;
|
||||
while (Tokens.isValue(t) || Tokens.isUnquotedText(t)) {
|
||||
expression.add(t);
|
||||
t = nextToken(); // note: don't cross a newline
|
||||
}
|
||||
putBack(t); // put back the token we ended with
|
||||
return parsePathExpression(expression.iterator(), lineOrigin());
|
||||
}
|
||||
}
|
||||
|
||||
private AbstractConfigObject parseObject() {
|
||||
// invoked just after the OPEN_CURLY
|
||||
Map<String, AbstractConfigValue> values = new HashMap<String, AbstractConfigValue>();
|
||||
@ -280,8 +333,13 @@ final class Parser {
|
||||
boolean afterComma = false;
|
||||
while (true) {
|
||||
Token t = nextTokenIgnoringNewline();
|
||||
if (Tokens.isValueWithType(t, ConfigValueType.STRING)) {
|
||||
String key = (String) Tokens.getValue(t).unwrapped();
|
||||
if (t == Tokens.CLOSE_CURLY) {
|
||||
if (afterComma) {
|
||||
throw parseError("expecting a field name after comma, got a close brace }");
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
Path path = parseKey(t);
|
||||
Token afterKey = nextTokenIgnoringNewline();
|
||||
if (afterKey != Tokens.COLON) {
|
||||
throw parseError("Key not followed by a colon, followed by token "
|
||||
@ -292,34 +350,45 @@ final class Parser {
|
||||
Token valueToken = nextTokenIgnoringNewline();
|
||||
AbstractConfigValue newValue = parseValue(valueToken);
|
||||
|
||||
// In strict JSON, dups should be an error; while in
|
||||
// our custom config language, they should be merged if the
|
||||
// value is an object (or substitution that could become
|
||||
// an object).
|
||||
AbstractConfigValue existing = values.get(key);
|
||||
String key = path.first();
|
||||
Path remaining = path.remainder();
|
||||
|
||||
if (existing != null) {
|
||||
if (flavor == SyntaxFlavor.JSON) {
|
||||
throw parseError("JSON does not allow duplicate fields: '"
|
||||
if (remaining == null) {
|
||||
AbstractConfigValue existing = values.get(key);
|
||||
if (existing != null) {
|
||||
// In strict JSON, dups should be an error; while in
|
||||
// our custom config language, they should be merged
|
||||
// if the value is an object (or substitution that
|
||||
// could become an object).
|
||||
|
||||
if (flavor == SyntaxFlavor.JSON) {
|
||||
throw parseError("JSON does not allow duplicate fields: '"
|
||||
+ key
|
||||
+ "' was already seen at "
|
||||
+ existing.origin().description());
|
||||
} else {
|
||||
newValue = newValue.withFallback(existing);
|
||||
} else {
|
||||
newValue = newValue.withFallback(existing);
|
||||
}
|
||||
}
|
||||
values.put(key, newValue);
|
||||
} else {
|
||||
if (flavor == SyntaxFlavor.JSON) {
|
||||
throw new ConfigException.BugOrBroken(
|
||||
"somehow got multi-element path in JSON mode");
|
||||
}
|
||||
|
||||
AbstractConfigObject obj = createValueUnderPath(
|
||||
remaining, newValue);
|
||||
AbstractConfigValue existing = values.get(key);
|
||||
if (existing != null) {
|
||||
obj = obj.withFallback(existing);
|
||||
}
|
||||
values.put(key, obj);
|
||||
}
|
||||
values.put(key, newValue);
|
||||
|
||||
afterComma = false;
|
||||
} else if (t == Tokens.CLOSE_CURLY) {
|
||||
if (afterComma) {
|
||||
throw parseError("expecting a field name after comma, got a close brace }");
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
throw parseError("Expecting close brace } or a field name, got "
|
||||
+ t);
|
||||
}
|
||||
|
||||
t = nextTokenIgnoringNewline();
|
||||
if (t == Tokens.CLOSE_CURLY) {
|
||||
break;
|
||||
@ -466,6 +535,11 @@ final class Parser {
|
||||
List<Element> buf = new ArrayList<Element>();
|
||||
buf.add(new Element("", false));
|
||||
|
||||
if (!expression.hasNext()) {
|
||||
throw new ConfigException.BadPath(origin, "",
|
||||
"Expecting a field name or path here, but got nothing");
|
||||
}
|
||||
|
||||
while (expression.hasNext()) {
|
||||
Token t = expression.next();
|
||||
if (Tokens.isValueWithType(t, ConfigValueType.STRING)) {
|
||||
|
29
src/test/resources/equiv01/path-keys.conf
Normal file
29
src/test/resources/equiv01/path-keys.conf
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
ints.fortyTwo : 42,
|
||||
ints.fortyTwoAgain : 42,
|
||||
|
||||
floats.fortyTwoPointOne : 42.1,
|
||||
floats.fortyTwoPointOneAgain : 42.1,
|
||||
|
||||
strings.abcd : "abcd",
|
||||
strings.abcdAgain : "abcd",
|
||||
strings.a : "a",
|
||||
strings.b : "b",
|
||||
strings.c : "c",
|
||||
strings.d : "d",
|
||||
strings.concatenated : "null bar 42 baz true 3.14 hi",
|
||||
|
||||
arrays."empty" : [],
|
||||
arrays."1" : [ 1 ],
|
||||
arrays.12 : [1, 2],
|
||||
arrays.123 : [1, 2, 3],
|
||||
arrays.ofString : [ "a", "b", "c" ],
|
||||
|
||||
booleans.true : true,
|
||||
booleans.trueAgain : true,
|
||||
booleans.false : false,
|
||||
booleans.falseAgain : false,
|
||||
|
||||
nulls.null : null,
|
||||
nulls.nullAgain : null
|
||||
}
|
@ -36,6 +36,6 @@
|
||||
|
||||
"nulls" : {
|
||||
"null" : null,
|
||||
"nullAgain" : null
|
||||
"nullAgain" : ${nulls.null}
|
||||
}
|
||||
}
|
||||
|
17
src/test/resources/equiv02/original.json
Normal file
17
src/test/resources/equiv02/original.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"a" : {
|
||||
"s" : 303,
|
||||
"b" : {
|
||||
"r" : 302,
|
||||
"c" : {
|
||||
"q" : 301,
|
||||
"d" : {
|
||||
"e" : 401,
|
||||
"a" : 1,
|
||||
"b" : 2,
|
||||
"z" : 102
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
32
src/test/resources/equiv02/path-keys-weird-whitespace.conf
Normal file
32
src/test/resources/equiv02/path-keys-weird-whitespace.conf
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
a.b.c.d
|
||||
: { "a" : 1, "z" : 101 },
|
||||
a.b.c
|
||||
: { "q" : 301 },
|
||||
a.b :
|
||||
|
||||
{ "r" : 302 },
|
||||
|
||||
a
|
||||
|
||||
: { "s" : 303 },
|
||||
|
||||
a.b.c.d
|
||||
|
||||
|
||||
: { "b" : 2, "z" : 102 },
|
||||
|
||||
|
||||
a.b.c.d.e
|
||||
|
||||
: 400,
|
||||
|
||||
|
||||
a.b.c.d.e
|
||||
|
||||
:
|
||||
|
||||
|
||||
401
|
||||
|
||||
}
|
9
src/test/resources/equiv02/path-keys.conf
Normal file
9
src/test/resources/equiv02/path-keys.conf
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
a.b.c.d : { "a" : 1, "z" : 101 },
|
||||
a.b.c : { "q" : 301 },
|
||||
a.b : { "r" : 302 },
|
||||
a : { "s" : 303 },
|
||||
a.b.c.d : { "b" : 2, "z" : 102 },
|
||||
a.b.c.d.e : 400,
|
||||
a.b.c.d.e : 401
|
||||
}
|
@ -99,7 +99,7 @@ class ConfParserTest extends TestUtils {
|
||||
assertEquals(path("a_c"), parsePath("a_c"))
|
||||
assertEquals(path("-"), parsePath("\"-\""))
|
||||
|
||||
for (invalid <- Seq("a.", ".b", "a..b", "a${b}c", "\"\".", ".\"\"")) {
|
||||
for (invalid <- Seq("", "a.", ".b", "a..b", "a${b}c", "\"\".", ".\"\"")) {
|
||||
try {
|
||||
intercept[ConfigException.BadPath] {
|
||||
parsePath(invalid)
|
||||
|
@ -85,7 +85,8 @@ class EquivalentsTest extends TestUtils {
|
||||
|
||||
// This is a little "checksum" to be sure we really tested what we were expecting.
|
||||
// it breaks every time you add a file, so you have to update it.
|
||||
assertEquals(1, dirCount)
|
||||
assertEquals(3, fileCount)
|
||||
assertEquals(2, dirCount)
|
||||
// this is the number of files not named original.*
|
||||
assertEquals(6, fileCount)
|
||||
}
|
||||
}
|
||||
|
@ -106,8 +106,7 @@ abstract trait TestUtils {
|
||||
"{ \"a\" : [ }", // [ is not a valid value
|
||||
"{ \"foo\" : 10, }", // extra trailing comma
|
||||
"{ \"foo\" : 10, true }", // non-key after comma
|
||||
"{ foo : \"bar\" }", // no quotes on key
|
||||
"{ foo : bar }", // no quotes on key or value
|
||||
"{ foo \n bar : 10 }", // newline in the middle of the unquoted key
|
||||
"[ 1, \\", // ends with backslash
|
||||
// these two problems are ignored by the lift tokenizer
|
||||
"[:\"foo\", \"bar\"]", // colon in an array; lift doesn't throw (tokenizer erases it)
|
||||
@ -129,6 +128,7 @@ abstract trait TestUtils {
|
||||
"[${]", // unclosed substitution
|
||||
"[$]", // '$' by itself
|
||||
"[$ ]", // '$' by itself with spaces after
|
||||
"[${}]", // empty substitution (no path)
|
||||
"""{ "a" : [1,2], "b" : y${a}z }""", // trying to interpolate an array in a string
|
||||
"""{ "a" : { "c" : 2 }, "b" : y${a}z }""", // trying to interpolate an object in a string
|
||||
"""{ "a" : ${a} }""", // simple cycle
|
||||
@ -143,6 +143,8 @@ abstract trait TestUtils {
|
||||
"""{ "foo" : "bar" }""",
|
||||
"""["foo", "bar"]""",
|
||||
"""{ "foo" : 42 }""",
|
||||
"{ \"foo\"\n : 42 }", // newline after key
|
||||
"{ \"foo\" : \n 42 }", // newline after colon
|
||||
"""[10, 11]""",
|
||||
"""[10,"foo"]""",
|
||||
"""{ "foo" : "bar", "baz" : "boo" }""",
|
||||
@ -159,6 +161,13 @@ abstract trait TestUtils {
|
||||
private val validConfInvalidJson = List[ParseTest](
|
||||
"""{ "foo" : bar }""", // no quotes on value
|
||||
"""{ "foo" : null bar 42 baz true 3.14 "hi" }""", // bunch of values to concat into a string
|
||||
"{ foo : \"bar\" }", // no quotes on key
|
||||
"{ foo : bar }", // no quotes on key or value
|
||||
"{ foo.bar : bar }", // path expression in key
|
||||
"{ foo.\"hello world\".baz : bar }", // partly-quoted path expression in key
|
||||
"{ foo.bar \n : bar }", // newline after path expression in key
|
||||
"{ foo bar : bar }", // whitespace in the key
|
||||
"{ true : bar }", // key is a non-string token
|
||||
ParseTest(true, """{ "foo" : "bar", "foo" : "bar2" }"""), // dup keys - lift just returns both
|
||||
"[ foo ]", // not a known token in JSON
|
||||
"[ t ]", // start of "true" but ends wrong in JSON
|
||||
|
Loading…
Reference in New Issue
Block a user