浏览代码

Add tests and feature for confirming the path of a validation failure

Sam Jaffe 6 年之前
父节点
当前提交
e0810534d6

+ 5 - 0
pom.xml

@@ -122,5 +122,10 @@
       <artifactId>jackson-databind</artifactId>
       <version>2.7.3</version>
     </dependency>
+    <dependency>
+      <groupId>org.leumasjaffe</groupId>
+      <artifactId>container</artifactId>
+      <version>0.2.1</version>
+    </dependency>
   </dependencies>
 </project>

+ 28 - 11
src/main/lombok/org/leumasjaffe/json/schema/ValidationException.java

@@ -1,9 +1,12 @@
 package org.leumasjaffe.json.schema;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 
+import org.leumasjaffe.container.Either;
+
 import lombok.AccessLevel;
 import lombok.Getter;
 import lombok.experimental.FieldDefaults;
@@ -11,24 +14,38 @@ import lombok.experimental.FieldDefaults;
 @SuppressWarnings("serial")
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 public class ValidationException extends IllegalArgumentException {
+	List<Either<String, Integer>> jsonPath = new ArrayList<>();
 	@Getter List<ValidationException> causingExceptions; 
-
-	public ValidationException() {
-		this.causingExceptions = Collections.emptyList();
+	
+	private ValidationException(Either<String, Integer> path, String message,
+			ValidationException... causes) {
+		super(message);
+		this.jsonPath.add(path);
+		this.causingExceptions = Collections.unmodifiableList(Arrays.asList(causes));
 	}
 
-	public ValidationException(String s) {
-		super(s);
-		this.causingExceptions = Collections.emptyList();
+	public ValidationException(String key, String message) {
+		this(Either.ofLeft(key), message);
+	}
+	
+	public ValidationException(int index, String message) {
+		this(Either.ofRight(index), message);
 	}
 
-	public ValidationException(ValidationException... causes) {
-		this.causingExceptions = Collections.unmodifiableList(Arrays.asList(causes));
+	public ValidationException(String key, String message,
+			ValidationException... causes) {
+		this(Either.ofLeft(key), message, causes);
 	}
 
-	public ValidationException(String message, ValidationException... causes) {
-		super(message);
-		this.causingExceptions = Collections.unmodifiableList(Arrays.asList(causes));
+	public ValidationException(int index, String message,
+			ValidationException... causes) {
+		this(Either.ofRight(index), message, causes);
 	}
 
+	public String getPath() {
+		final StringBuilder path = new StringBuilder("#");
+		jsonPath.forEach(e -> path.append('/').append(e));
+		return path.toString();
+	}
+	
 }

+ 5 - 0
src/main/lombok/org/leumasjaffe/json/schema/tester/EqualsTester.java

@@ -30,6 +30,11 @@ public class EqualsTester extends SimpleValidationTester {
 		this(false, values);
 	}
 	
+	@Override
+	String name() {
+		return isExplicitlySingleValue ? "const" : "enum";
+	}
+	
 	@Override
 	String errorMessage(final JsonNode node) {
 		return node + (isExplicitlySingleValue ? ERROR_CONST : ERROR_ENUM);

+ 5 - 0
src/main/lombok/org/leumasjaffe/json/schema/tester/FormatTester.java

@@ -135,6 +135,11 @@ public abstract class FormatTester extends SimpleValidationTester {
 	
 	String format;
 	
+	@Override
+	String name() {
+		return "format";
+	}
+	
 	@Override
 	String errorMessage(final JsonNode node) {
 		return node + " does not match format: '" + format + "'";

+ 17 - 1
src/main/lombok/org/leumasjaffe/json/schema/tester/NumberTester.java

@@ -20,7 +20,18 @@ public class NumberTester extends SimpleValidationTester {
 			case MAXIMUM: return target >= against;
 			case EXCLUSIVE_MAXIMUM: return target > against;
 			case MULTIPLE_OF: return against % target == 0;
-			default: return false;
+			default: throw new IllegalStateException();
+			}
+		}
+		
+		public String jsonName() {
+			switch(this) {
+			case MINIMUM: return "minimum";
+			case EXCLUSIVE_MINIMUM: return "exclusiveMinimum";
+			case MAXIMUM: return "maximum";
+			case EXCLUSIVE_MAXIMUM: return "exclusiveMaximum";
+			case MULTIPLE_OF: return "multipleOf";
+			default: throw new IllegalStateException();
 			}
 		}
 	}
@@ -48,6 +59,11 @@ public class NumberTester extends SimpleValidationTester {
 	Rule rule;
 	double value;
 	
+	@Override
+	String name() {
+		return rule.jsonName();
+	}
+	
 	@Override
 	String errorMessage(final JsonNode node) {
 		return node + " is not " + rule.toString() + value;

+ 2 - 1
src/main/lombok/org/leumasjaffe/json/schema/tester/SimpleValidationTester.java

@@ -6,8 +6,9 @@ import org.leumasjaffe.json.schema.ValidationException;
 import com.fasterxml.jackson.databind.JsonNode;
 
 abstract class SimpleValidationTester implements Tester {
+	abstract String name();
 	abstract String errorMessage(final JsonNode node);
 	public final void validate(final JsonNode node) throws ValidationException {
-		if (!accepts(node)) throw new ValidationException(errorMessage(node));
+		if (!accepts(node)) throw new ValidationException(name(), errorMessage(node));
 	}
 }

+ 5 - 0
src/main/lombok/org/leumasjaffe/json/schema/tester/TypeTester.java

@@ -29,6 +29,11 @@ public class TypeTester extends SimpleValidationTester {
 	String name;
 	Predicate<JsonNode> test;
 	
+	@Override
+	String name() {
+		return "type";
+	}
+	
 	@Override
 	String errorMessage(final JsonNode node) {
 		return "type mismatch. expected: " + name + " got: " + node.getNodeType();

+ 17 - 0
src/test/java/org/leumasjaffe/json/schema/tester/EqualsTesterTest.java

@@ -1,5 +1,6 @@
 package org.leumasjaffe.json.schema.tester;
 
+import static org.hamcrest.core.Is.*;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.fail;
 import static org.leumasjaffe.json.schema.matcher.Accepts.accepts;
@@ -62,4 +63,20 @@ public class EqualsTesterTest {
 		enumTest.validate(new DoubleNode(1.0000000001));
 	}
 	
+	@Test
+	public void testValidationPathIsConstOrEnum() {
+		try {
+			constTest.validate(new DoubleNode(1.0000000001));
+			fail("No exception was thrown...");
+		} catch (ValidationException ve) {
+			assertThat(ve.getPath(), is("#/const"));
+		}
+		try {
+			enumTest.validate(new DoubleNode(1.0000000001));
+			fail("No exception was thrown...");
+		} catch (ValidationException ve) {
+			assertThat(ve.getPath(), is("#/enum"));
+		}
+	}
+	
 }

+ 11 - 0
src/test/java/org/leumasjaffe/json/schema/tester/FormatTesterTest.java

@@ -1,5 +1,6 @@
 package org.leumasjaffe.json.schema.tester;
 
+import static org.hamcrest.core.Is.is;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.fail;
 import static org.leumasjaffe.json.schema.matcher.Accepts.accepts;
@@ -156,4 +157,14 @@ public class FormatTesterTest {
 		FormatTester.forCode("ipv4").validate(new TextNode("::1"));
 	}
 	
+	@Test
+	public void testValidationPathIsNameOfMatcher() {
+		try {
+			FormatTester.forCode("ipv4").validate(new TextNode("::1"));
+			fail("No exception was thrown...");
+		} catch (ValidationException ve) {
+			assertThat(ve.getPath(), is("#/format"));
+		}
+	}
+	
 }

+ 51 - 0
src/test/java/org/leumasjaffe/json/schema/tester/NumberTesterTest.java

@@ -1,5 +1,6 @@
 package org.leumasjaffe.json.schema.tester;
 
+import static org.hamcrest.core.Is.is;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.fail;
 import static org.leumasjaffe.json.schema.matcher.AcceptedTypes.acceptsTypes;
@@ -95,4 +96,54 @@ public class NumberTesterTest {
 		NumberTester.multipleOf(1.0).validate(new DoubleNode(0.5));
 	}
 	
+	@Test
+	public void testValidationPathIsMinimum() {
+		try {
+			NumberTester.minimum(1.0).validate(new DoubleNode(0.5));
+			fail("No exception was thrown...");
+		} catch (ValidationException ve) {
+			assertThat(ve.getPath(), is("#/minimum"));
+		}
+	}
+	
+	@Test
+	public void testValidationPathIsExclusiveMinimum() {
+		try {
+			NumberTester.exclusiveMinimum(1.0).validate(new DoubleNode(0.5));
+			fail("No exception was thrown...");
+		} catch (ValidationException ve) {
+			assertThat(ve.getPath(), is("#/exclusiveMinimum"));
+		}
+	}
+	
+	@Test
+	public void testValidationPathIsMaximum() {
+		try {
+			NumberTester.maximum(1.0).validate(new DoubleNode(1.5));
+			fail("No exception was thrown...");
+		} catch (ValidationException ve) {
+			assertThat(ve.getPath(), is("#/maximum"));
+		}
+	}
+	
+	@Test
+	public void testValidationPathIsExclusiveMaximum() {
+		try {
+			NumberTester.exclusiveMaximum(1.0).validate(new DoubleNode(1.5));
+			fail("No exception was thrown...");
+		} catch (ValidationException ve) {
+			assertThat(ve.getPath(), is("#/exclusiveMaximum"));
+		}
+	}
+	
+	@Test
+	public void testValidationPathIsMultipleOf() {
+		try {
+			NumberTester.multipleOf(1.0).validate(new DoubleNode(0.5));
+			fail("No exception was thrown...");
+		} catch (ValidationException ve) {
+			assertThat(ve.getPath(), is("#/multipleOf"));
+		}
+	}
+	
 }

+ 12 - 0
src/test/java/org/leumasjaffe/json/schema/tester/TypeTesterTest.java

@@ -1,5 +1,6 @@
 package org.leumasjaffe.json.schema.tester;
 
+import static org.hamcrest.core.Is.is;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.fail;
 import static org.leumasjaffe.json.schema.matcher.Accepts.accepts;
@@ -139,4 +140,15 @@ public class TypeTesterTest {
 	public void testThrowsOnValidationFailure() {
 		TypeTester.fromType("object").validate(new DoubleNode(0.5));
 	}
+	
+	@Test
+	public void testValidationPathIsType() {
+		try {
+			TypeTester.fromType("object").validate(new DoubleNode(0.5));
+			fail("No exception was thrown...");
+		} catch (ValidationException ve) {
+			assertThat(ve.getPath(), is("#/type"));
+		}
+	}
+	
 }