Merge pull request #102 from typesafehub/wip/havocp-checkvalid-indexed-objects

Fix for #101 checkValid handling of indexed objects
This commit is contained in:
Havoc Pennington 2013-12-20 05:12:44 -08:00
commit 975836628e
4 changed files with 103 additions and 20 deletions

View File

@ -339,6 +339,11 @@ public abstract class ConfigException extends RuntimeException implements Serial
public String problem() { public String problem() {
return problem; return problem;
} }
@Override
public String toString() {
return "ValidationProblem(" + path + "," + origin + "," + problem + ")";
}
} }
/** /**

View File

@ -728,7 +728,8 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
return false; return false;
} }
} else if (reference instanceof SimpleConfigList) { } else if (reference instanceof SimpleConfigList) {
if (value instanceof SimpleConfigList) { // objects may be convertible to lists if they have numeric keys
if (value instanceof SimpleConfigList || value instanceof SimpleConfigObject) {
return true; return true;
} else { } else {
return false; return false;
@ -772,6 +773,25 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
} }
} }
private static void checkListCompatibility(Path path, SimpleConfigList listRef,
SimpleConfigList listValue, List<ConfigException.ValidationProblem> accumulator) {
if (listRef.isEmpty() || listValue.isEmpty()) {
// can't verify type, leave alone
} else {
AbstractConfigValue refElement = listRef.get(0);
for (ConfigValue elem : listValue) {
AbstractConfigValue e = (AbstractConfigValue) elem;
if (!haveCompatibleTypes(refElement, e)) {
addProblem(accumulator, path, e.origin(), "List at '" + path.render()
+ "' contains wrong value type, expecting list of "
+ getDesc(refElement) + " but got element of type " + getDesc(e));
// don't add a problem for every last array element
break;
}
}
}
}
private static void checkValid(Path path, ConfigValue reference, AbstractConfigValue value, private static void checkValid(Path path, ConfigValue reference, AbstractConfigValue value,
List<ConfigException.ValidationProblem> accumulator) { List<ConfigException.ValidationProblem> accumulator) {
// Unmergeable is supposed to be impossible to encounter in here // Unmergeable is supposed to be impossible to encounter in here
@ -784,22 +804,16 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
} else if (reference instanceof SimpleConfigList && value instanceof SimpleConfigList) { } else if (reference instanceof SimpleConfigList && value instanceof SimpleConfigList) {
SimpleConfigList listRef = (SimpleConfigList) reference; SimpleConfigList listRef = (SimpleConfigList) reference;
SimpleConfigList listValue = (SimpleConfigList) value; SimpleConfigList listValue = (SimpleConfigList) value;
if (listRef.isEmpty() || listValue.isEmpty()) { checkListCompatibility(path, listRef, listValue, accumulator);
// can't verify type, leave alone } else if (reference instanceof SimpleConfigList && value instanceof SimpleConfigObject) {
} else { // attempt conversion of indexed object to list
AbstractConfigValue refElement = listRef.get(0); SimpleConfigList listRef = (SimpleConfigList) reference;
for (ConfigValue elem : listValue) { AbstractConfigValue listValue = DefaultTransformer.transform(value,
AbstractConfigValue e = (AbstractConfigValue) elem; ConfigValueType.LIST);
if (!haveCompatibleTypes(refElement, e)) { if (listValue instanceof SimpleConfigList)
addProblem(accumulator, path, e.origin(), "List at '" + path.render() checkListCompatibility(path, listRef, (SimpleConfigList) listValue, accumulator);
+ "' contains wrong value type, expecting list of " else
+ getDesc(refElement) + " but got element of type " addWrongType(accumulator, reference, value, path);
+ getDesc(e));
// don't add a problem for every last array element
break;
}
}
}
} }
} else { } else {
addWrongType(accumulator, reference, value, path); addWrongType(accumulator, reference, value, path);

View File

@ -107,7 +107,9 @@ class PropertiesTest extends TestUtils {
props.setProperty("a.4", "4") props.setProperty("a.4", "4")
val conf = ConfigFactory.parseProperties(props, ConfigParseOptions.defaults()) val conf = ConfigFactory.parseProperties(props, ConfigParseOptions.defaults())
val reference = ConfigFactory.parseString("{ a : [0,1,2,3,4] }")
assertEquals(Seq(0, 1, 2, 3, 4), conf.getIntList("a").asScala.toSeq) assertEquals(Seq(0, 1, 2, 3, 4), conf.getIntList("a").asScala.toSeq)
conf.checkValid(reference)
} }
@Test @Test
@ -120,7 +122,9 @@ class PropertiesTest extends TestUtils {
props.setProperty("a.4", "2") props.setProperty("a.4", "2")
val conf = ConfigFactory.parseProperties(props, ConfigParseOptions.defaults()) val conf = ConfigFactory.parseProperties(props, ConfigParseOptions.defaults())
val reference = ConfigFactory.parseString("{ a : [0,1,2] }")
assertEquals(Seq(0, 1, 2), conf.getIntList("a").asScala.toSeq) assertEquals(Seq(0, 1, 2), conf.getIntList("a").asScala.toSeq)
conf.checkValid(reference)
} }
@Test @Test
@ -137,7 +141,9 @@ class PropertiesTest extends TestUtils {
props.setProperty("a.4", "4") props.setProperty("a.4", "4")
val conf = ConfigFactory.parseProperties(props, ConfigParseOptions.defaults()) val conf = ConfigFactory.parseProperties(props, ConfigParseOptions.defaults())
val reference = ConfigFactory.parseString("{ a : [0,1,2,3,4] }")
assertEquals(Seq(0, 1, 2, 3, 4), conf.getIntList("a").asScala.toSeq) assertEquals(Seq(0, 1, 2, 3, 4), conf.getIntList("a").asScala.toSeq)
conf.checkValid(reference)
} }
@Test @Test
@ -173,7 +179,9 @@ class PropertiesTest extends TestUtils {
a = [-2, -1] ${a} a = [-2, -1] ${a}
""") """)
val conf = conf2.withFallback(conf1).resolve() val conf = conf2.withFallback(conf1).resolve()
val reference = ConfigFactory.parseString("{ a : [-2,-1,0,1,2,3,4,5,6] }")
assertEquals(Seq(-2, -1, 0, 1, 2, 3, 4, 5, 6), conf.getIntList("a").asScala.toSeq) assertEquals(Seq(-2, -1, 0, 1, 2, 3, 4, 5, 6), conf.getIntList("a").asScala.toSeq)
conf.checkValid(reference)
} }
} }

View File

@ -15,8 +15,8 @@ class ValidationTest extends TestUtils {
sealed abstract class Problem(path: String, line: Int) { sealed abstract class Problem(path: String, line: Int) {
def check(p: ConfigException.ValidationProblem) { def check(p: ConfigException.ValidationProblem) {
assertEquals(path, p.path()) assertEquals("matching path", path, p.path())
assertEquals(line, p.origin().lineNumber()) assertEquals("matching line", line, p.origin().lineNumber())
} }
protected def assertMessage(p: ConfigException.ValidationProblem, re: String) { protected def assertMessage(p: ConfigException.ValidationProblem, re: String) {
@ -58,7 +58,8 @@ class ValidationTest extends TestUtils {
for ((problem, expected) <- problems zip expecteds) { for ((problem, expected) <- problems zip expecteds) {
expected.check(problem) expected.check(problem)
} }
assertEquals(expecteds.size, problems.size) assertEquals("found expected validation problems, got '" + problems + "' and expected '" + expecteds + "'",
expecteds.size, problems.size)
} }
@Test @Test
@ -118,4 +119,59 @@ class ValidationTest extends TestUtils {
assertTrue("expected different message, got: " + e.getMessage, assertTrue("expected different message, got: " + e.getMessage,
e.getMessage.contains("resolve")) e.getMessage.contains("resolve"))
} }
@Test
def validationCatchesListOverriddenWithNumber() {
val reference = parseConfig("""{ a : [{},{},{}] }""")
val conf = parseConfig("""{ a : 42 }""")
val e = intercept[ConfigException.ValidationFailed] {
conf.checkValid(reference)
}
val expecteds = Seq(WrongType("a", 1, "list", "number"))
checkException(e, expecteds)
}
@Test
def validationCatchesListOverriddenWithDifferentList() {
val reference = parseConfig("""{ a : [true,false,false] }""")
val conf = parseConfig("""{ a : [42,43] }""")
val e = intercept[ConfigException.ValidationFailed] {
conf.checkValid(reference)
}
val expecteds = Seq(WrongElementType("a", 1, "boolean", "number"))
checkException(e, expecteds)
}
@Test
def validationAllowsListOverriddenWithSameTypeList() {
val reference = parseConfig("""{ a : [1,2,3] }""")
val conf = parseConfig("""{ a : [4,5] }""")
conf.checkValid(reference)
}
@Test
def validationCatchesListOverriddenWithNoIndexesObject() {
val reference = parseConfig("""{ a : [1,2,3] }""")
val conf = parseConfig("""{ a : { notANumber: foo } }""")
val e = intercept[ConfigException.ValidationFailed] {
conf.checkValid(reference)
}
val expecteds = Seq(WrongType("a", 1, "list", "object"))
checkException(e, expecteds)
}
@Test
def validationAllowsListOverriddenWithIndexedObject() {
val reference = parseConfig("""{ a : [a,b,c] }""")
val conf = parseConfig("""{ a : { "0" : x, "1" : y } }""")
conf.checkValid(reference)
assertEquals("got the sequence from overriding list with indexed object",
Seq("x", "y"), conf.getStringList("a").asScala)
}
} }