Implement unquoted path keys, { foo.bar : 42 }

This commit is contained in:
Havoc Pennington 2011-11-12 01:42:43 -05:00
parent a3128871ac
commit c18f10c6dc
9 changed files with 198 additions and 27 deletions

View File

@ -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)) {

View 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
}

View File

@ -36,6 +36,6 @@
"nulls" : {
"null" : null,
"nullAgain" : null
"nullAgain" : ${nulls.null}
}
}

View File

@ -0,0 +1,17 @@
{
"a" : {
"s" : 303,
"b" : {
"r" : 302,
"c" : {
"q" : 301,
"d" : {
"e" : 401,
"a" : 1,
"b" : 2,
"z" : 102
}
}
}
}
}

View 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
}

View 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
}

View File

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

View File

@ -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)
}
}

View File

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