Переглянути джерело

Merge branch 'test_schema'

* test_schema:
  Add support for root-schema referencing.
  Add support for reference/definition
  Fix bug where we don't do type checks. Test Dependency schema creation
  Add DependencyTester and tests
  Adding tests for handling objects.
  Fix NPE. Add tests for fallback and other cases to Arrays
  Test implicit additionalItems schema
  Start testing items/additionalItems
  Adding disabled test for validating the draft schema against itself. This requires the implementation of the "definitions" and "$ref" properties.
  Add more error checking and schema remapping tests to SchemaFactoryTest.
  Expand testing for error states.
  Make sure to capture entire error body.
  Fixing typo with schema. Adding tests for schema mapping.
  Remove useless case by making things a bit easier.
  Add tests for boolean type in a schema
  Add tests for string schemas
  Fixing size tests to be inclusive on both sides.
  Add tests and impl that Number tests will perform universal checks as well
  Add and implement tests for number handling
  Start writing tests for Schema...
Sam Jaffe 6 роки тому
батько
коміт
08e10500fc
21 змінених файлів з 1137 додано та 43 видалено
  1. 6 0
      src/main/lombok/org/leumasjaffe/json/JsonHelper.java
  2. 18 0
      src/main/lombok/org/leumasjaffe/json/schema/ArrayTester.java
  3. 18 0
      src/main/lombok/org/leumasjaffe/json/schema/ObjectTester.java
  4. 105 5
      src/main/lombok/org/leumasjaffe/json/schema/Schema.java
  5. 92 5
      src/main/lombok/org/leumasjaffe/json/schema/factory/SchemaFactory.java
  6. 18 6
      src/main/lombok/org/leumasjaffe/json/schema/factory/SchemaV6Factory.java
  7. 25 4
      src/main/lombok/org/leumasjaffe/json/schema/tester/AllItemsTester.java
  8. 37 0
      src/main/lombok/org/leumasjaffe/json/schema/tester/DependencyTester.java
  9. 16 1
      src/main/lombok/org/leumasjaffe/json/schema/tester/FixedTester.java
  10. 15 0
      src/main/lombok/org/leumasjaffe/json/schema/tester/FormatTester.java
  11. 17 7
      src/main/lombok/org/leumasjaffe/json/schema/tester/ItemsTester.java
  12. 18 10
      src/main/lombok/org/leumasjaffe/json/schema/tester/PropertyTester.java
  13. 2 0
      src/test/java/org/leumasjaffe/json/schema/JsonFactorySuite.java
  14. 2 1
      src/test/java/org/leumasjaffe/json/schema/JsonSchemaSuite.java
  15. 2 0
      src/test/java/org/leumasjaffe/json/schema/JsonTesterSuite.java
  16. 318 0
      src/test/java/org/leumasjaffe/json/schema/SchemaTest.java
  17. 100 0
      src/test/java/org/leumasjaffe/json/schema/factory/SchemaFactoryTest.java
  18. 126 3
      src/test/java/org/leumasjaffe/json/schema/factory/SchemaV6FactoryTest.java
  19. 47 0
      src/test/java/org/leumasjaffe/json/schema/tester/DependencyTesterTest.java
  20. 1 1
      src/test/java/org/leumasjaffe/json/schema/tester/StubTester.java
  21. 154 0
      src/test/resources/schema6.json

+ 6 - 0
src/main/lombok/org/leumasjaffe/json/JsonHelper.java

@@ -31,6 +31,12 @@ public class JsonHelper {
 		return rval;
 	}
 
+	public Map<String, JsonNode> fields(final JsonNode object) {
+		Map<String, JsonNode> rval = new HashMap<>();
+		object.fields().forEachRemaining(pair -> rval.put(pair.getKey(), pair.getValue()));
+		return rval;
+	}
+
 	public <T> Map<String, T> fields(final JsonNode object, BiFunction<String, JsonNode, T> transform) {
 		Map<String, T> rval = new HashMap<>();
 		object.fields().forEachRemaining(pair -> rval.put(pair.getKey(),

+ 18 - 0
src/main/lombok/org/leumasjaffe/json/schema/ArrayTester.java

@@ -0,0 +1,18 @@
+package org.leumasjaffe.json.schema;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
+
+public interface ArrayTester extends Tester {
+	@NoArgsConstructor @AllArgsConstructor
+	class Status {
+		public boolean accepted = true;
+		public List<JsonNode> unprocessed = new ArrayList<>();
+	}
+	Status accepts(List<JsonNode> data);
+}

+ 18 - 0
src/main/lombok/org/leumasjaffe/json/schema/ObjectTester.java

@@ -0,0 +1,18 @@
+package org.leumasjaffe.json.schema;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
+
+public interface ObjectTester extends Tester {
+	@NoArgsConstructor @AllArgsConstructor
+	class Status {
+		public boolean accepted = true;
+		public Map<String, JsonNode> unprocessed = new HashMap<>();
+	}
+	Status accepts(Map<String, JsonNode> data);
+}

+ 105 - 5
src/main/lombok/org/leumasjaffe/json/schema/Schema.java

@@ -1,24 +1,44 @@
 package org.leumasjaffe.json.schema;
 
+import static org.leumasjaffe.json.schema.tester.FixedTester.ACCEPT;
+
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import org.leumasjaffe.json.JsonHelper;
+
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.JsonNodeType;
 
 import lombok.AccessLevel;
-import lombok.NoArgsConstructor;
 import lombok.experimental.FieldDefaults;
 
-@NoArgsConstructor
 @FieldDefaults(level=AccessLevel.PRIVATE)
 public class Schema implements Tester {
 	private static final String SELF = "$self";
+	private static final List<String> UNIVERSAL_MATCHERS = Arrays.asList(
+			"const", "enum", "type");
+	private static final List<String> NUMBER_MATCHERS = Arrays.asList(
+		"multipleOf", "minimum", "exclusiveMinimum", "maximum",
+		"exclusiveMaximum");
+	private static final List<String> STRING_MATCHERS = Arrays.asList(
+			"maxLength", "minLength", "pattern", "format");
+	private static final List<String> ARRAY_MATCHERS = Arrays.asList(
+			"maxItems", "minItems", "uniqueItems", "contains");
+	private static final List<String> OBJECT_MATCHERS = Arrays.asList(
+			"maxProperties", "minProperties", "required", "propertyNames",
+			"dependencies");
+	
 	Map<String, Tester> children = new HashMap<>();
 	
+	public Schema() {
+		this(ACCEPT);
+	}
+	
 	public Schema(Tester self) {
 		children.put(SELF, self);
 	}
@@ -26,7 +46,7 @@ public class Schema implements Tester {
 	public Schema(Map<String, Tester> fields) {
 		children.putAll(fields);
 	}
-
+	
 	@Override
 	public JsonNodeType[] acceptedTypes() {
 		final Set<JsonNodeType> set = new HashSet<>();
@@ -41,8 +61,88 @@ public class Schema implements Tester {
 			return true;
 		} else if (children.containsKey(SELF)) {
 			return children.get(SELF).accepts(node);
+		} else if (children.containsKey("$ref")) {
+			return children.get("$ref").accepts(node);
+		} else if (!canProcess(node)) {
+			return false;
+		}
+		switch (node.getNodeType()) {
+		case NUMBER: return acceptsNumber(node);
+		case STRING: return acceptsString(node);
+		case ARRAY: return acceptsArray(node);
+		case OBJECT: return acceptsObject(node);
+		default: return acceptsUniversal(node);
+		}
+	}
+
+	private boolean acceptsNumber(JsonNode node) {
+		for (String key : getKeys(NUMBER_MATCHERS)) {
+			if (!children.get(key).accepts(node)) {
+				return false;
+			}
+		}
+		return acceptsUniversal(node);
+	}
+	
+	private boolean acceptsString(JsonNode node) {
+		for (String key : getKeys(STRING_MATCHERS)) {
+			if (!children.get(key).accepts(node)) {
+				return false;
+			}
+		}
+		return acceptsUniversal(node);
+	}
+	
+	private boolean acceptsArray(JsonNode node) {
+		for (String key : getKeys(ARRAY_MATCHERS)) {
+			if (!children.get(key).accepts(node)) {
+				return false;
+			}
+		}
+		ArrayTester items = (ArrayTester) children.getOrDefault("items", ACCEPT);
+		ArrayTester addtlItems = (ArrayTester) children.getOrDefault("additionalItems", ACCEPT);
+		ArrayTester.Status status = items.accepts(JsonHelper.toArray(node));
+		if (status.accepted && !status.unprocessed.isEmpty()) {
+			status = addtlItems.accepts(status.unprocessed);
+		}
+		return status.accepted && acceptsUniversal(node);
+	}
+
+	private boolean acceptsObject(JsonNode node) {
+		for (String key : getKeys(OBJECT_MATCHERS)) {
+			if (!children.get(key).accepts(node)) {
+				return false;
+			}
+		}
+		ObjectTester props = (ObjectTester) children.getOrDefault("properties", ACCEPT);
+		ObjectTester patrnProps = (ObjectTester) children.getOrDefault("patternProperties", ACCEPT);
+		ObjectTester addtlProps = (ObjectTester) children.getOrDefault("additionalProperties", ACCEPT);
+		ObjectTester.Status status = props.accepts(JsonHelper.fields(node));
+		if (status.accepted && !status.unprocessed.isEmpty()) {
+			status = patrnProps.accepts(status.unprocessed);
+		}
+		if (status.accepted && !status.unprocessed.isEmpty()) {
+			status = addtlProps.accepts(status.unprocessed);
 		}
-		// TODO
-		return false;
+		return status.accepted && acceptsUniversal(node);
+	}
+
+	private boolean acceptsUniversal(JsonNode node) {
+		for (String key : getKeys(UNIVERSAL_MATCHERS)) {
+			if (!children.get(key).accepts(node)) {
+				return false;
+			}
+		}
+		return true;
+	}
+
+	private Set<String> getKeys(List<String> allowed) {
+		Set<String> keys = new HashSet<>(children.keySet());
+		keys.retainAll(allowed);
+		return keys;
+	}
+
+	private boolean canProcess(JsonNode node) {
+		return Arrays.asList(acceptedTypes()).contains(node.getNodeType());
 	}
 }

+ 92 - 5
src/main/lombok/org/leumasjaffe/json/schema/factory/SchemaFactory.java

@@ -1,7 +1,11 @@
 package org.leumasjaffe.json.schema.factory;
 
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import org.leumasjaffe.json.JsonHelper;
@@ -12,8 +16,15 @@ import org.leumasjaffe.json.schema.tester.FixedTester;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.JsonNodeType;
 
+import lombok.AccessLevel;
 import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.FieldDefaults;
 
+@NoArgsConstructor
+@FieldDefaults(level=AccessLevel.PROTECTED)
 public class SchemaFactory {	
 	@AllArgsConstructor
 	static final class SimpleTester implements Tester {
@@ -31,13 +42,73 @@ public class SchemaFactory {
 		}
 	}
 	
+	@AllArgsConstructor
+	static final class DeferredTester implements Tester {
+		Supplier<Tester> actual;
+		
+		@Override
+		public JsonNodeType[] acceptedTypes() {
+			return ANY;
+		}
+
+		@Override
+		public boolean accepts(JsonNode node) {
+			return actual.get().accepts(node);
+		}
+	}
+	
+	
+	@FieldDefaults(level=AccessLevel.PRIVATE)
+	static final class SharedData {
+		@Getter(AccessLevel.PRIVATE) Tester schema = null;
+		@Getter(AccessLevel.PROTECTED) Definitions definitions = null;
+		JsonNode root = null;
+	}
+	
+	@RequiredArgsConstructor
+	@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+	protected class Definitions {
+		JsonNode localJson;
+		Map<String, Tester> computed = new HashMap<>();
+		
+		public Tester get(final String path) {
+			computed.computeIfAbsent(path, this::createTester);
+			return computed.get(path);
+		}
+		
+		private Tester createTester(final String path) {
+			if (path.equals("#")) {
+				return new DeferredTester(SchemaFactory.this.shared::getSchema);
+			} else if (path.startsWith("#")) {
+				JsonNode current = localJson;
+				final String[] tokens = path.substring(2).split("/");
+				for (final String tok : tokens) {
+					if (tok.matches("^\\d+$")) {
+						current = current.path(Integer.parseInt(tok));
+					} else {
+						current = current.path(tok);
+					}
+				}
+				return SchemaFactory.this.create(current);
+			} else {
+				throw new IllegalArgumentException("Can't do URI searches yet...");
+			}
+		}
+	}
+	
+	SharedData shared = new SharedData();
+	
+	protected SchemaFactory(SharedData shared) {
+		this.shared = shared;
+	}
+
 	public final Tester create(final JsonNode object) {
 		switch (object.getNodeType()) {
 		case BOOLEAN:
 			return new Schema(object.asBoolean() ? FixedTester.ACCEPT : FixedTester.REJECT);
 		case OBJECT:
-			final SchemaFactory versioned = getVersionFactory(object.path("$ref").asText());
-			return new Schema(JsonHelper.fields(object, versioned::createMapping));
+			final SchemaFactory versioned = factory(object);
+			return shared.schema = new Schema(JsonHelper.fields(object, versioned::createMapping));
 		default:
 			throw new IllegalStateException("Expected OBJECT or BOOLEAN, got " + object.getNodeType());
 		}
@@ -46,26 +117,42 @@ public class SchemaFactory {
 	protected String getVersion() {
 		return "";
 	}
+	
+	private final SchemaFactory factory(final JsonNode object) {
+		return getVersionFactory(object.path("$schema").asText()).postInit(object);
+	}
 
 	private final SchemaFactory getVersionFactory(final String version) {
 		if (version.isEmpty() || version.equals(getVersion())) {
 			return this;
 		} else {
 			switch (getVersionInt(version)) {
-			case 6: return new SchemaV6Factory();
+			case 6: return new SchemaV6Factory(shared);
 			default:
 				throw new IllegalArgumentException("Unsupported schema version: " + version);
 			}
 		}
 	}
+	
+	private final SchemaFactory postInit(final JsonNode root) {
+		if (shared.root == null) {
+			shared.root = root;
+			shared.definitions = this.new Definitions(root);
+		}
+		return this;
+	}
 
 	private static int getVersionInt(final String version) {
 		final Pattern pat = Pattern.compile("http://json-schema.org/draft-(\\d+)/schema#");
-		return Integer.parseInt(pat.matcher(version).group(1), 10);
+		final Matcher m = pat.matcher(version);
+		m.matches();
+		return Integer.parseInt(m.group(1), 10);
 	}
 
 	protected final List<Tester> createArray(final JsonNode array) {
-		assert(array.isArray() && array.size() >= 1);
+		if (!array.isArray() || array.size() == 0) {
+			throw new IllegalArgumentException("Required an array with at least one element");
+		}
 		return JsonHelper.toArray(array, this::create);
 	}
 	

+ 18 - 6
src/main/lombok/org/leumasjaffe/json/schema/factory/SchemaV6Factory.java

@@ -11,6 +11,7 @@ import org.leumasjaffe.json.schema.tester.AllItemsTester;
 import org.leumasjaffe.json.schema.tester.AllOfTester;
 import org.leumasjaffe.json.schema.tester.AnyOfTester;
 import org.leumasjaffe.json.schema.tester.ContainsTester;
+import org.leumasjaffe.json.schema.tester.DependencyTester;
 import org.leumasjaffe.json.schema.tester.EqualsTester;
 import org.leumasjaffe.json.schema.tester.FixedTester;
 import org.leumasjaffe.json.schema.tester.FormatTester;
@@ -26,7 +27,14 @@ import org.leumasjaffe.json.schema.tester.UniqueItemTester;
 
 import com.fasterxml.jackson.databind.JsonNode;
 
+import lombok.NoArgsConstructor;
+
+@NoArgsConstructor
 class SchemaV6Factory extends SchemaFactory {
+	protected SchemaV6Factory(SharedData shared) {
+		super(shared);
+	}
+
 	@Override
 	protected String getVersion() {
 		return "http://json-schema.org/draft-06/schema#";
@@ -37,7 +45,7 @@ class SchemaV6Factory extends SchemaFactory {
 		switch (key) {
 		case "$id": return FixedTester.ACCEPT;
 		case "$schema": return FixedTester.ACCEPT;
-		// case "$ref": ; // TODO Implement reference propagating
+		case "$ref": return shared.getDefinitions().get(value.asText());
 		case "title": return FixedTester.ACCEPT;
 		case "description": return FixedTester.ACCEPT;
 		case "default": return FixedTester.ACCEPT;
@@ -47,28 +55,32 @@ class SchemaV6Factory extends SchemaFactory {
 		case "exclusiveMaximum": return new NumberTester(d -> d < value.asDouble());
 		case "minimum": return new NumberTester(d -> d >= value.asDouble());
 		case "exclusiveMinimum": return new NumberTester(d -> d > value.asDouble());
-		case "maxLength": return new SizeTester(STRING, i -> i < value.asInt());
+		case "maxLength": return new SizeTester(STRING, i -> i <= value.asInt());
 		case "minLength": return new SizeTester(STRING, i -> i >= value.asInt(0));
 		case "pattern": return new SimpleTester(STRING, j -> j.asText().matches(value.asText()));
 		case "additionalItems": return new AllItemsTester(ARRAY, create(value));
 		case "items": return value.isArray() ? new ItemsTester(createArray(value)) : new AllItemsTester(ARRAY, create(value));
-		case "maxItems": return new SizeTester(ARRAY, i -> i < value.asInt());
+		case "maxItems": return new SizeTester(ARRAY, i -> i <= value.asInt());
 		case "minItems": return new SizeTester(ARRAY, i -> i >= value.asInt(0));
 		case "uniqueItems": return value.asBoolean() ? UniqueItemTester.INSTANCE : FixedTester.ACCEPT;
 		case "contains": return new ContainsTester(create(value));
-		case "maxProperties": return new SizeTester(OBJECT, i -> i < value.asInt());
+		case "maxProperties": return new SizeTester(OBJECT, i -> i <= value.asInt());
 		case "minProperties": return new SizeTester(OBJECT, i -> i >= value.asInt(0));
 		case "required": {
 			final List<String> reqKeys = JsonHelper.toArray(value, JsonNode::asText);
 			return new SimpleTester(OBJECT, json -> reqKeys.stream().allMatch(json::has));
 		}
 		case "additionalProperties": return new AllItemsTester(OBJECT, create(value));
-		// case "definitions": ; // TODO Implement definitions creation
+		case "definitions": return FixedTester.ACCEPT;
 		case "properties": return new PropertyTester(JsonHelper.values(value,
 				(k, v) -> new PropertyTester.Pair(stringEqual(k), create(v))));
 		case "patternProperties": return new PropertyTester(JsonHelper.values(value,
 				(k, v) -> new PropertyTester.Pair(stringMatches(k), create(v))));
-		// case "dependencies": ; // TODO Implement array(required) and object(schema) versions
+	    case "dependencies":
+	    	return new DependencyTester(JsonHelper.fields(value, (k, v) -> {
+	    		if (v.isArray()) return createMapping("required", v);
+	    		else return create(v);
+	    	}));
 		case "propertyNames": return new PropertyNameTester(create(value));
 		case "const": return new EqualsTester(value);
 		case "enum": return new EqualsTester(JsonHelper.toArray(value));

+ 25 - 4
src/main/lombok/org/leumasjaffe/json/schema/tester/AllItemsTester.java

@@ -1,8 +1,11 @@
 package org.leumasjaffe.json.schema.tester;
 
 import java.util.List;
+import java.util.Map;
 
 import org.leumasjaffe.json.JsonHelper;
+import org.leumasjaffe.json.schema.ArrayTester;
+import org.leumasjaffe.json.schema.ObjectTester;
 import org.leumasjaffe.json.schema.Tester;
 
 import com.fasterxml.jackson.databind.JsonNode;
@@ -14,7 +17,7 @@ import lombok.experimental.FieldDefaults;
 
 @RequiredArgsConstructor
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
-public class AllItemsTester implements Tester {
+public class AllItemsTester implements Tester, ArrayTester, ObjectTester {
 	JsonNodeType type;
 	Tester schema;
 	
@@ -26,13 +29,31 @@ public class AllItemsTester implements Tester {
 	@Override
 	public boolean accepts(JsonNode node) {
 		if (node.getNodeType() != type) return false;
-		List<JsonNode> data = JsonHelper.toArray(node);
+		return accepts(JsonHelper.toArray(node)).accepted;
+	}
+
+	@Override
+	public ArrayTester.Status accepts(List<JsonNode> data) {
+		ArrayTester.Status out = new ArrayTester.Status();
 		for (int i = 0; i < data.size(); ++i) {
 			if (!schema.accepts(data.get(i))) {
-				return false;
+				out.accepted = false;
+				break;
+			}
+		}
+		return out;
+	}
+
+	@Override
+	public ObjectTester.Status accepts(Map<String, JsonNode> data) {
+		ObjectTester.Status out = new ObjectTester.Status();
+		for (Map.Entry<String, JsonNode> pair : data.entrySet()) {
+			if (!schema.accepts(pair.getValue())) {
+				out.accepted = false;
+				break;
 			}
 		}
-		return true;
+		return out;
 	}
 
 }

+ 37 - 0
src/main/lombok/org/leumasjaffe/json/schema/tester/DependencyTester.java

@@ -0,0 +1,37 @@
+package org.leumasjaffe.json.schema.tester;
+
+import java.util.Map;
+
+import org.leumasjaffe.json.JsonHelper;
+import org.leumasjaffe.json.schema.Tester;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.JsonNodeType;
+
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.FieldDefaults;
+
+@RequiredArgsConstructor
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class DependencyTester implements Tester {
+	Map<String, Tester> testers;
+
+	@Override
+	public JsonNodeType[] acceptedTypes() {
+		return new JsonNodeType[]{JsonNodeType.OBJECT};
+	}
+
+	@Override
+	public boolean accepts(JsonNode node) {
+		final Map<String, JsonNode> object = JsonHelper.fields(node);
+		object.keySet().retainAll(testers.keySet());
+		for (String key : object.keySet()) {
+			if (!testers.get(key).accepts(node)) {
+				return false;
+			}
+		}
+		return true;
+	}
+
+}

+ 16 - 1
src/main/lombok/org/leumasjaffe/json/schema/tester/FixedTester.java

@@ -1,5 +1,10 @@
 package org.leumasjaffe.json.schema.tester;
 
+import java.util.List;
+import java.util.Map;
+
+import org.leumasjaffe.json.schema.ArrayTester;
+import org.leumasjaffe.json.schema.ObjectTester;
 import org.leumasjaffe.json.schema.Tester;
 
 import com.fasterxml.jackson.databind.JsonNode;
@@ -11,7 +16,7 @@ import lombok.experimental.FieldDefaults;
 
 @RequiredArgsConstructor
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
-public class FixedTester implements Tester {
+public class FixedTester implements Tester, ArrayTester, ObjectTester {
 	public static final FixedTester ACCEPT = new FixedTester(true);
 	public static final FixedTester REJECT = new FixedTester(false);
 	
@@ -27,4 +32,14 @@ public class FixedTester implements Tester {
 		return returns;
 	}
 
+	@Override
+	public ArrayTester.Status accepts(List<JsonNode> data) {
+		return new ArrayTester.Status(returns, data);
+	}
+
+	@Override
+	public ObjectTester.Status accepts(Map<String, JsonNode> data) {
+		return new ObjectTester.Status(returns, data);
+	}
+
 }

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

@@ -3,6 +3,8 @@ package org.leumasjaffe.json.schema.tester;
 import java.net.URISyntaxException;
 import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeParseException;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
 
 import org.apache.commons.validator.routines.DomainValidator;
 import org.apache.commons.validator.routines.EmailValidator;
@@ -119,6 +121,18 @@ public abstract class FormatTester implements Tester {
 		}
 	};
 	
+	static Tester REGEX = new FormatTester("regex") {
+		@Override
+		public boolean accepts(JsonNode node) {
+			try {
+				Pattern.compile(node.asText());
+				return true;
+			} catch (PatternSyntaxException e) {
+				return false;
+			}
+		}
+	};
+	
 	String format;
 	
 	@Override
@@ -138,6 +152,7 @@ public abstract class FormatTester implements Tester {
 		case "uri-template": return URI_TEMPLATE;
 		case "json-pointer": return JSON_POINTER;	
 		case "uuid": return UUID;
+		case "regex": return REGEX;
 		default: throw new IllegalArgumentException("Unknown format code '" + asText + "'");
 		}
 	}

+ 17 - 7
src/main/lombok/org/leumasjaffe/json/schema/tester/ItemsTester.java

@@ -5,6 +5,7 @@ import java.util.List;
 
 import org.leumasjaffe.json.JsonHelper;
 import org.leumasjaffe.json.schema.Tester;
+import org.leumasjaffe.json.schema.ArrayTester;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.JsonNodeType;
@@ -15,7 +16,7 @@ import lombok.experimental.FieldDefaults;
 
 @RequiredArgsConstructor
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
-public class ItemsTester implements Tester {
+public class ItemsTester implements ArrayTester {
 	List<Tester> schemas;
 
 	public ItemsTester(Tester...testers) {
@@ -26,17 +27,26 @@ public class ItemsTester implements Tester {
 	public JsonNodeType[] acceptedTypes() {
 		return new JsonNodeType[]{JsonNodeType.ARRAY};
 	}
-	
+
 	@Override
 	public boolean accepts(JsonNode node) {
 		if (!node.isArray()) return false;
-		List<JsonNode> data = JsonHelper.toArray(node);
-		for (int i = 0; i < Math.min(schemas.size(), data.size()); ++i) {
+		return accepts(JsonHelper.toArray(node)).accepted;
+	}
+
+	@Override
+	public Status accepts(List<JsonNode> data) {
+		Status out = new Status();
+		if (data.size() > schemas.size()) {
+			out.unprocessed.addAll(data.subList(schemas.size(), data.size()));
+			data.removeAll(out.unprocessed);
+		}
+		for (int i = 0; i < data.size(); ++i) {
 			if (!schemas.get(i).accepts(data.get(i))) {
-				return false;
+				out.accepted = false;
+				break;
 			}
 		}
-		return true;
+		return out;
 	}
-
 }

+ 18 - 10
src/main/lombok/org/leumasjaffe/json/schema/tester/PropertyTester.java

@@ -1,12 +1,13 @@
 package org.leumasjaffe.json.schema.tester;
 
 import java.util.Arrays;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
+import org.leumasjaffe.json.JsonHelper;
+import org.leumasjaffe.json.schema.ObjectTester;
 import org.leumasjaffe.json.schema.Tester;
 
 import com.fasterxml.jackson.databind.JsonNode;
@@ -19,7 +20,7 @@ import lombok.experimental.FieldDefaults;
 
 @RequiredArgsConstructor
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
-public class PropertyTester implements Tester {
+public class PropertyTester implements ObjectTester {
 	@AllArgsConstructor
 	public static class Pair {
 		Predicate<String> keyMatches;
@@ -39,22 +40,29 @@ public class PropertyTester implements Tester {
 
 	@Override
 	public boolean accepts(final JsonNode node) {
-		final Iterator<Map.Entry<String, JsonNode>> iter = node.fields();
-		while (iter.hasNext()) {
-			final Map.Entry<String, JsonNode> data = iter.next();
+		return accepts(JsonHelper.fields(node)).accepted;
+	}
+
+	@Override
+	public Status accepts(Map<String, JsonNode> data) {
+		Status out = new Status();
+		for (Map.Entry<String, JsonNode> pair : data.entrySet()) {
 			final List<Pair> stream = schema.stream()
-					.filter(p -> p.keyMatches.test(data.getKey()))
+					.filter(p -> p.keyMatches.test(pair.getKey()))
 					.collect(Collectors.toList());
 			if (stream.size() == 0) {
+				out.unprocessed.put(pair.getKey(), pair.getValue());
 				continue;
 			} else if (stream.size() > 1) {
 				// TODO: Don't accept multiple
-				return false;
-			} else if (!valueMatches(data, stream.get(0))) {
-				return false;
+				out.accepted = false;
+				break;
+			} else if (!valueMatches(pair, stream.get(0))) {
+				out.accepted = false;
+				break;
 			}
 		}
-		return true;
+		return out;
 	}
 
 	private boolean valueMatches(final Map.Entry<String, JsonNode> data, Pair p) {

+ 2 - 0
src/test/java/org/leumasjaffe/json/schema/JsonFactorySuite.java

@@ -2,10 +2,12 @@ package org.leumasjaffe.json.schema;
 
 import org.junit.runner.RunWith;
 import org.junit.runners.Suite;
+import org.leumasjaffe.json.schema.factory.SchemaFactoryTest;
 import org.leumasjaffe.json.schema.factory.SchemaV6FactoryTest;
 
 @RunWith(Suite.class)
 @Suite.SuiteClasses({
+	SchemaFactoryTest.class,
 	SchemaV6FactoryTest.class
 })
 public class JsonFactorySuite {

+ 2 - 1
src/test/java/org/leumasjaffe/json/schema/JsonSchemaSuite.java

@@ -6,7 +6,8 @@ import org.junit.runners.Suite;
 @RunWith(Suite.class)
 @Suite.SuiteClasses({
 	JsonFactorySuite.class,
-	JsonTesterSuite.class
+	JsonTesterSuite.class,
+	SchemaTest.class
 })
 public class JsonSchemaSuite {
 

+ 2 - 0
src/test/java/org/leumasjaffe/json/schema/JsonTesterSuite.java

@@ -6,6 +6,7 @@ import org.leumasjaffe.json.schema.tester.AllItemsTesterTest;
 import org.leumasjaffe.json.schema.tester.AllOfTesterTest;
 import org.leumasjaffe.json.schema.tester.AnyOfTesterTest;
 import org.leumasjaffe.json.schema.tester.ContainsTesterTest;
+import org.leumasjaffe.json.schema.tester.DependencyTesterTest;
 import org.leumasjaffe.json.schema.tester.FormatTesterTest;
 import org.leumasjaffe.json.schema.tester.ItemsTesterTest;
 import org.leumasjaffe.json.schema.tester.NotTesterTest;
@@ -23,6 +24,7 @@ import org.leumasjaffe.json.schema.tester.UniqueItemTesterTest;
 	AllOfTesterTest.class,
 	AnyOfTesterTest.class,
 	ContainsTesterTest.class,
+	DependencyTesterTest.class,
 	FormatTesterTest.class,
 	ItemsTesterTest.class,
 	NotTesterTest.class,

+ 318 - 0
src/test/java/org/leumasjaffe/json/schema/SchemaTest.java

@@ -0,0 +1,318 @@
+package org.leumasjaffe.json.schema;
+
+import static com.fasterxml.jackson.databind.node.JsonNodeType.*;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.*;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Test;
+import org.leumasjaffe.json.schema.tester.AllItemsTester;
+import org.leumasjaffe.json.schema.tester.ContainsTester;
+import org.leumasjaffe.json.schema.tester.EqualsTester;
+import org.leumasjaffe.json.schema.tester.FixedTester;
+import org.leumasjaffe.json.schema.tester.FormatTester;
+import org.leumasjaffe.json.schema.tester.ItemsTester;
+import org.leumasjaffe.json.schema.tester.MockTester;
+import org.leumasjaffe.json.schema.tester.NumberTester;
+import org.leumasjaffe.json.schema.tester.PropertyNameTester;
+import org.leumasjaffe.json.schema.tester.PropertyTester;
+import org.leumasjaffe.json.schema.tester.SizeTester;
+import org.leumasjaffe.json.schema.tester.StubTester;
+import org.leumasjaffe.json.schema.tester.TypeTester;
+import org.leumasjaffe.json.schema.tester.UniqueItemTester;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.BooleanNode;
+import com.fasterxml.jackson.databind.node.DoubleNode;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.NullNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+
+// TODO: patternProperties
+// TODO: dependencies
+// TODO: $ref
+// TODO: additionalItems with mixed results
+public class SchemaTest {
+	private Schema getConstSchema(JsonNode json) {
+		Map<String, Tester> tests = new HashMap<>();
+		tests.put("const", new EqualsTester(json));
+		return new Schema(tests);
+	}
+	
+	private Schema getEnumSchema(JsonNode... jsons) {
+		Map<String, Tester> tests = new HashMap<>();
+		tests.put("enum", new EqualsTester(jsons));
+		return new Schema(tests);
+	}
+
+	private Schema getNumberSchema() {
+		Map<String, Tester> tests = new HashMap<>();
+		tests.put("minimum", new NumberTester(d -> d >= 0.0));
+		tests.put("multipleOf", new NumberTester(d -> Math.abs(d % 0.25) < 1E-7));
+		tests.put("exclusiveMaximum", new NumberTester(d -> d < 1.0));
+		return new Schema(tests);
+	}
+	
+	private Schema getStringSchema() {
+		Map<String, Tester> tests = new HashMap<>();
+		tests.put("maxLength", new SizeTester(STRING, s -> s <= 30));
+		tests.put("minLength", new SizeTester(STRING, s -> s >= 10));
+		tests.put("pattern", (StubTester) j -> j.asText().matches("^https://.*"));
+		tests.put("format", FormatTester.forCode("uri"));
+		return new Schema(tests);
+	}
+	
+	private Schema getArraySchema() {
+		Map<String, Tester> tests = new HashMap<>();
+		tests.put("maxItems", new SizeTester(ARRAY, s -> s <= 3));
+		tests.put("minItems", new SizeTester(ARRAY, s -> s >= 1));
+		tests.put("uniqueItems", new UniqueItemTester());
+		tests.put("contains", new ContainsTester(getNumberSchema()));
+		return new Schema(tests);
+	}
+
+	private Schema getObjectSchema() {
+		Map<String, Tester> tests = new HashMap<>();
+		tests.put("maxProperties", new SizeTester(OBJECT, s -> s <= 3));
+		tests.put("minProperties", new SizeTester(OBJECT, s -> s >= 2));
+		tests.put("required", (StubTester) json -> json.has("string"));
+		tests.put("propertyNames", new PropertyNameTester((StubTester) j -> j.asText().matches("^[a-z]*$")));
+		// TODO Dependencies
+		return new Schema(tests);
+	}
+
+	@Test
+	public void testAcceptsAnyIfNoMatchers() {
+		assertThat(new Schema().acceptedTypes().length, is(Tester.ANY.length));
+	}
+
+	@Test
+	public void testRejectsWrongType() {
+		Map<String, Tester> tests = new HashMap<>();
+		tests.put("example", new MockTester(STRING, FixedTester.ACCEPT));
+		assertFalse(new Schema(tests).accepts(NullNode.getInstance()));
+	}
+
+	@Test
+	public void testHandlesUniversalChecksForBoolean() {
+		assertTrue(getConstSchema(BooleanNode.TRUE).accepts(BooleanNode.TRUE));
+		assertFalse(getConstSchema(BooleanNode.TRUE).accepts(NullNode.getInstance()));
+		assertTrue(getEnumSchema(BooleanNode.TRUE, BooleanNode.FALSE)
+				.accepts(BooleanNode.TRUE));
+		assertTrue(getEnumSchema(BooleanNode.TRUE, BooleanNode.FALSE)
+				.accepts(BooleanNode.FALSE));
+		assertFalse(getEnumSchema(BooleanNode.TRUE, BooleanNode.FALSE)
+				.accepts(NullNode.getInstance()));
+	}
+
+	@Test
+	public void testHandlesMultipleTestsForNumber() {
+		assertFalse(getNumberSchema().accepts(new DoubleNode(-0.25)));
+		assertTrue(getNumberSchema().accepts(new DoubleNode(0.0)));
+		assertTrue(getNumberSchema().accepts(new DoubleNode(0.5)));
+		assertFalse(getNumberSchema().accepts(new DoubleNode(0.6)));
+		assertFalse(getNumberSchema().accepts(new DoubleNode(1.0)));
+	}
+
+	@Test
+	public void testHandlesOtherNumberChecks() {
+		Map<String, Tester> tests = new HashMap<>();
+		tests.put("exclusiveMinimum", new NumberTester(d -> d > 0.0));
+		tests.put("maximum", new NumberTester(d -> d <= 1.0));
+		Schema schema = new Schema(tests);
+		
+		assertFalse(schema.accepts(new DoubleNode(0.0)));
+		assertTrue(schema.accepts(new DoubleNode(0.5)));
+		assertTrue(schema.accepts(new DoubleNode(0.6)));
+		assertTrue(schema.accepts(new DoubleNode(1.0)));
+		assertFalse(schema.accepts(new DoubleNode(1.1)));
+	}
+
+	@Test
+	public void testHandlesUniversalChecksForNumber() {
+		assertTrue(getConstSchema(new DoubleNode(0.0)).accepts(new DoubleNode(0.0)));
+		assertFalse(getConstSchema(new DoubleNode(0.0)).accepts(new DoubleNode(0.5)));
+		assertTrue(getEnumSchema(new DoubleNode(0.0), new DoubleNode(1.0))
+				.accepts(new DoubleNode(0.0)));
+		assertTrue(getEnumSchema(new DoubleNode(0.0), new DoubleNode(1.0))
+				.accepts(new DoubleNode(1.0)));
+		assertFalse(getEnumSchema(new DoubleNode(0.0), new DoubleNode(1.0))
+				.accepts(new DoubleNode(0.5)));
+	}
+	
+	@Test
+	public void testHandlesMultipleTestsForString() {
+		assertFalse(getStringSchema().accepts(new TextNode("https://a")));
+		assertTrue(getStringSchema().accepts(new TextNode("https://google.com")));
+		assertFalse(getStringSchema().accepts(new TextNode("http://google.com")));
+		assertFalse(getStringSchema().accepts(new TextNode("https://google.com/query?q=Hippopotomonstrosesquippedaliophobia")));
+	}
+	
+	@Test
+	public void testHandlesUniversalChecksForString() {
+		assertTrue(getConstSchema(new TextNode("A")).accepts(new TextNode("A")));
+		assertFalse(getConstSchema(new TextNode("A")).accepts(new TextNode("C")));
+		assertTrue(getEnumSchema(new TextNode("A"), new TextNode("B"))
+				.accepts(new TextNode("A")));
+		assertTrue(getEnumSchema(new TextNode("A"), new TextNode("B"))
+				.accepts(new TextNode("B")));
+		assertFalse(getEnumSchema(new TextNode("A"), new TextNode("B"))
+				.accepts(new TextNode("C")));
+	}
+
+	@Test
+	public void testHandlesMultipleTestsForArray() {
+		final ArrayNode array = new ArrayNode(JsonNodeFactory.instance);
+		assertFalse(getArraySchema().accepts(array));
+		array.add("Test");
+		assertFalse(getArraySchema().accepts(array));
+		array.add(0.5);
+		assertTrue(getArraySchema().accepts(array));
+		array.add(0.5);
+		assertFalse(getArraySchema().accepts(array));
+		array.remove(2);
+		array.add(true);
+		array.add(false);
+		assertFalse(getArraySchema().accepts(array));
+	}
+	
+	@Test
+	public void testHandlesAdditionalMatcherWhenArrayItems() {
+		Map<String, Tester> tests = new HashMap<>();
+		tests.put("items", new ItemsTester(getNumberSchema(), getStringSchema()));
+		tests.put("additionalItems", FixedTester.REJECT);
+		Schema schema = new Schema(tests);
+
+		final ArrayNode node = new ArrayNode(JsonNodeFactory.instance);
+		node.add(0.5);
+		node.add("https://google.com");
+		assertTrue(schema.accepts(node));
+		node.add(true);
+		assertFalse(schema.accepts(node));
+
+		tests.put("additionalItems", FixedTester.ACCEPT);
+		assertTrue(new Schema(tests).accepts(node));
+
+		tests.remove("additionalItems");
+		assertTrue(new Schema(tests).accepts(node));
+	}
+
+	@Test
+	public void testHandlesAdditionalNotCalledIfFailsInItems() {
+		Map<String, Tester> tests = new HashMap<>();
+		tests.put("items", new ItemsTester(getNumberSchema(), getStringSchema()));
+		tests.put("additionalItems", FixedTester.ACCEPT);
+		Schema schema = new Schema(tests);
+
+		final ArrayNode node = new ArrayNode(JsonNodeFactory.instance);
+		node.add(0.5);
+		node.add("https://a");
+		node.add(true);
+		assertFalse(schema.accepts(node));
+	}
+	
+	@Test
+	public void testHandlesUniversalChecksForArray() {
+		final ArrayNode expected = new ArrayNode(JsonNodeFactory.instance);
+		expected.add(true);
+
+		Map<String, Tester> tests = new HashMap<>();
+		tests.put("const", new EqualsTester(expected));
+		Schema schema = new Schema(tests);
+
+		final ArrayNode node = new ArrayNode(JsonNodeFactory.instance);
+		assertFalse(schema.accepts(node));
+		node.add(true);
+		assertTrue(schema.accepts(node));
+	}
+
+
+	@Test
+	public void testHandlesMultipleTestsForObject() {
+		final ObjectNode object = new ObjectNode(JsonNodeFactory.instance);
+		assertFalse(getObjectSchema().accepts(object));
+		object.set("string", new TextNode("https://google.com"));
+		assertFalse(getObjectSchema().accepts(object));
+		object.set("float", new DoubleNode(0.5));
+		assertTrue(getObjectSchema().accepts(object));
+		object.set("Caps", BooleanNode.TRUE);
+		assertFalse(getObjectSchema().accepts(object));
+		object.remove("Caps");
+		object.set("bool", BooleanNode.TRUE);
+		assertTrue(getObjectSchema().accepts(object));
+		object.set("null", NullNode.getInstance());
+		assertFalse(getObjectSchema().accepts(object));
+	}
+	
+	@Test
+	public void testHandlesAdditionalMatcherWhenObjectProps() {
+		Map<String, Tester> tests = new HashMap<>();
+		tests.put("properties", new PropertyTester(Arrays.asList(
+				new PropertyTester.Pair("float"::equals, getNumberSchema()),
+				new PropertyTester.Pair("string"::equals, getStringSchema()))));
+		tests.put("additionalProperties", FixedTester.REJECT);
+		Schema schema = new Schema(tests);
+
+		final ObjectNode node = new ObjectNode(JsonNodeFactory.instance);
+		node.set("float", new DoubleNode(0.5));
+		node.set("string", new TextNode("https://google.com"));
+		assertTrue(schema.accepts(node));
+		node.set("boolean", BooleanNode.TRUE);
+		assertFalse(schema.accepts(node));
+
+		tests.put("additionalProperties", FixedTester.ACCEPT);
+		assertTrue(new Schema(tests).accepts(node));
+
+		tests.remove("additionalProperties");
+		assertTrue(new Schema(tests).accepts(node));
+	}
+		
+	@Test
+	public void testAdditionalPropertiesRunsOnEachUnmatched() {
+		Map<String, Tester> tests = new HashMap<>();
+		tests.put("additionalProperties", new AllItemsTester(OBJECT, 
+				TypeTester.fromType("string")));
+		Schema schema = new Schema(tests);
+
+		final ObjectNode node = new ObjectNode(JsonNodeFactory.instance);
+		node.set("string", new TextNode("https://google.com"));
+		assertTrue(schema.accepts(node));
+		node.set("float", new DoubleNode(0.5));
+		assertFalse(schema.accepts(node));
+	}
+
+	@Test
+	public void testHandlesAdditionalNotCalledIfFailsInProps() {
+		Map<String, Tester> tests = new HashMap<>();
+		tests.put("properties", new PropertyTester(Arrays.asList(
+				new PropertyTester.Pair("float"::equals, getNumberSchema()),
+				new PropertyTester.Pair("string"::equals, getStringSchema()))));
+		tests.put("additionalProperties", FixedTester.ACCEPT);
+		Schema schema = new Schema(tests);
+
+		final ObjectNode node = new ObjectNode(JsonNodeFactory.instance);
+		node.set("float", new DoubleNode(0.5));
+		node.set("string", new TextNode("https://a"));
+		node.set("boolean", BooleanNode.TRUE);
+		assertFalse(schema.accepts(node));
+	}
+	
+	@Test
+	public void testHandlesUniversalChecksForObject() {
+		final ObjectNode expected = new ObjectNode(JsonNodeFactory.instance);
+		expected.set("A", BooleanNode.TRUE);
+
+		Map<String, Tester> tests = new HashMap<>();
+		tests.put("const", new EqualsTester(expected));
+		Schema schema = new Schema(tests);
+
+		final ObjectNode node = new ObjectNode(JsonNodeFactory.instance);
+		assertFalse(schema.accepts(node));
+		node.set("A", BooleanNode.TRUE);
+		assertTrue(schema.accepts(node));
+	}}

+ 100 - 0
src/test/java/org/leumasjaffe/json/schema/factory/SchemaFactoryTest.java

@@ -0,0 +1,100 @@
+package org.leumasjaffe.json.schema.factory;
+
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class SchemaFactoryTest {
+	SchemaFactory factory;
+	ObjectMapper mapper;
+	
+	private String getError(Throwable from) {
+		final StringWriter sw = new StringWriter();
+		final PrintWriter pw = new PrintWriter(sw);
+		from.printStackTrace(pw);
+		return sw.toString();
+	}
+	
+	@Before
+	public void setUp() {
+		factory = new SchemaFactory();
+		mapper = new ObjectMapper();
+	}
+	
+	private JsonNode readTree(String data) {
+		try {
+			return mapper.readTree(data);
+		} catch (IOException e) {
+			throw new RuntimeException(e);
+		}
+	}
+	
+	@Test(expected=UnsupportedOperationException.class)
+	public void testFailsIfNoSchemaMember() {
+		factory.create(readTree("{ \"$id\": \"unit-test-schema\" }"));
+	}
+
+	@Test(expected=IllegalArgumentException.class)
+	public void testFailsIfIllegalSchemaVersionMember() {
+		factory.create(readTree("{ \"$id\": \"unit-test-schema\", \"$schema\":\"http://json-schema.org/draft-00/schema#\" }"));
+	}
+	
+	@Test
+	public void testCanRouteV6Schemas() {
+		try {
+			factory.create(readTree("{" +
+					"\"$id\": \"unit-test-schema\"," +
+					"\"$schema\":\"http://json-schema.org/draft-06/schema#\"" +
+				"}"));
+		} catch (Throwable t) {
+			fail(getError(t));
+		}
+	}
+	
+	@Test
+	public void testSchemaWithinSchemaDoesNotNeedRedeclare() {
+		try {
+			factory.create(readTree("{" +
+				"\"$id\": \"unit-test-schema\"," +
+				"\"$schema\":\"http://json-schema.org/draft-06/schema#\"," +
+				"\"not\": { \"type\": \"string\" }" +
+			"}"));
+		} catch (Throwable t) {
+			fail(getError(t));
+		}
+	}
+	
+	@Test
+	public void testSchemaWithinSchemaCanRedeclare() {
+		try {
+			factory.create(readTree("{" +
+				"\"$id\": \"unit-test-schema\"," +
+				"\"$schema\":\"http://json-schema.org/draft-06/schema#\"," +
+				"\"not\": {" +
+					"\"$schema\":\"http://json-schema.org/draft-06/schema#\"," +
+					"\"type\": \"string\"" +
+				"}" +
+			"}"));
+		} catch (Throwable t) {
+			fail(getError(t));
+		}
+	}
+	
+	
+	@Test
+	public void testSchemaValidatesDraftV6Schema() throws JsonProcessingException, IOException {
+		JsonNode node = mapper.readTree(new File("src/test/resources/schema6.json"));
+		assertTrue(factory.create(node).accepts(node));
+	}
+
+}

+ 126 - 3
src/test/java/org/leumasjaffe/json/schema/factory/SchemaV6FactoryTest.java

@@ -43,6 +43,11 @@ public class SchemaV6FactoryTest {
 		final Map.Entry<String, JsonNode> pair = schema.fields().next();
 		return factory.createMapping(pair.getKey(), pair.getValue());
 	}
+	
+	@Test(expected=IllegalArgumentException.class)
+	public void testRejectsGarbage() {
+		fromSingleElement("{ \"pants\": true }");
+	}
 
 	@Test
 	public void testIDSchema() {
@@ -136,7 +141,7 @@ public class SchemaV6FactoryTest {
 	
 	@Test
 	public void testMaxLengthSchema() {
-		Tester test = fromSingleElement("{ \"maxLength\": 2 }");
+		Tester test = fromSingleElement("{ \"maxLength\": 1 }");
 		assertFalse(test.accepts(NullNode.getInstance()));
 		assertFalse(test.accepts(new DoubleNode(1.5)));
 		assertTrue(test.accepts(new TextNode("")));
@@ -195,7 +200,7 @@ public class SchemaV6FactoryTest {
 	
 	@Test
 	public void testMaxItemsSchema() {
-		Tester test = fromSingleElement("{ \"maxItems\": 2 }");
+		Tester test = fromSingleElement("{ \"maxItems\": 1 }");
 		assertFalse(test.accepts(NullNode.getInstance()));
 		assertFalse(test.accepts(new DoubleNode(1.5)));
 		final ArrayNode node = new ArrayNode(JsonNodeFactory.instance);
@@ -243,7 +248,7 @@ public class SchemaV6FactoryTest {
 	
 	@Test
 	public void testMaxPropertiesSchema() {
-		Tester test = fromSingleElement("{ \"maxProperties\": 2 }");
+		Tester test = fromSingleElement("{ \"maxProperties\": 1 }");
 		assertFalse(test.accepts(NullNode.getInstance()));
 		assertFalse(test.accepts(new DoubleNode(1.5)));
 		final ObjectNode node = new ObjectNode(JsonNodeFactory.instance);
@@ -284,6 +289,19 @@ public class SchemaV6FactoryTest {
 		assertFalse(test.accepts(readTree("{ \"B\": {} }")));
 	}
 	
+	@Test
+	public void testDefinitionsSchemaInLocalJson() {
+		Tester defArray = factory.create(readTree("{" +
+			"\"$ref\": \"#/definitions/0\"," +
+			"\"definitions\": [" +
+				"{ \"type\": \"boolean\" }" +
+			"]" +
+		"}"));
+		assertFalse(defArray.accepts(NullNode.instance));
+		assertFalse(defArray.accepts(new ObjectNode(JsonNodeFactory.instance)));
+		assertTrue(defArray.accepts(BooleanNode.TRUE));
+	}
+	
 	@Test
 	public void testPropertiesSchema() {
 		Tester test = fromSingleElement("{ \"properties\": { \"A\": true, \"B\": false } }");
@@ -302,6 +320,38 @@ public class SchemaV6FactoryTest {
 		assertFalse(test.accepts(readTree("{ \"A_\": {} }")));
 	}
 	
+	@Test
+	public void testDependencySchema() {
+		Tester arrayDep = fromSingleElement("{" +
+			"\"dependencies\": {" +
+				"\"A\": [ \"B\", \"C\" ]" +
+			"}" +
+		"}");
+		assertTrue(arrayDep.accepts(readTree("{ \"B\": true }")));
+		assertFalse(arrayDep.accepts(readTree("{ \"A\": true, \"B\": true }")));
+		assertTrue(arrayDep.accepts(readTree("{ \"A\": true, \"B\": true, \"C\": true }")));
+		
+		Tester schemaDep = fromSingleElement("{" +
+			"\"dependencies\": {" +
+				"\"D\": { \"properties\": { \"B\": { \"type\": \"integer\" } } }" +
+			"}" +
+		"}");
+		assertTrue(schemaDep.accepts(readTree("{ \"B\": true }")));
+		assertFalse(schemaDep.accepts(readTree("{ \"D\": true, \"B\": true }")));
+		assertTrue(schemaDep.accepts(readTree("{ \"D\": true, \"B\": 5 }")));
+		
+		Tester bothDep = fromSingleElement("{" +
+			"\"dependencies\": {" +
+				"\"A\": [ \"B\", \"C\" ]," +
+				"\"D\": { \"properties\": { \"B\": { \"type\": \"integer\" } } }" +
+			"}" +
+		"}");
+		assertTrue(bothDep.accepts(readTree("{ \"A\": true, \"B\": true, \"C\": true }")));
+		assertTrue(bothDep.accepts(readTree("{ \"D\": true, \"B\": 5 }")));
+		assertFalse(bothDep.accepts(readTree("{ \"A\": true, \"D\": true, \"B\": true, \"C\": true }")));
+		assertTrue(bothDep.accepts(readTree("{ \"A\": true, \"D\": true, \"B\": 5, \"C\": true }")));
+	}
+	
 	@Test
 	public void testPropertyNamesSchema() {
 		Tester test = fromSingleElement("{ \"propertyNames\": false }");
@@ -337,4 +387,77 @@ public class SchemaV6FactoryTest {
 		assertTrue(sing.accepts(BooleanNode.TRUE));
 		assertFalse(sing.accepts(new DoubleNode(1.5)));
 	}
+	
+	@Test(expected=IllegalArgumentException.class)
+	public void testAllOfSchemaRequiresArray() {
+		fromSingleElement("{ \"allOf\": true }");
+	}
+	
+	@Test(expected=IllegalArgumentException.class)
+	public void testAllOfSchemaRequiresNonEmptyArray() {
+		fromSingleElement("{ \"allOf\": [] }");
+	}
+	
+	@Test
+	public void testAllOfSchemaPassesNonEmptyArray() {
+		try {
+			fromSingleElement("{ \"allOf\": [ false ] }");
+		} catch (IllegalArgumentException ex) {
+			fail("Couldn't parse");
+		}
+	}
+	
+	@Test(expected=IllegalArgumentException.class)
+	public void testAnyOfSchemaRequiresArray() {
+		fromSingleElement("{ \"anyOf\": true }");
+	}
+	
+	@Test(expected=IllegalArgumentException.class)
+	public void testAnyOfSchemaRequiresNonEmptyArray() {
+		fromSingleElement("{ \"anyOf\": [] }");
+	}
+	
+	@Test
+	public void testAnyOfSchemaPassesNonEmptyArray() {
+		try {
+			fromSingleElement("{ \"anyOf\": [ false ] }");
+		} catch (IllegalArgumentException ex) {
+			fail("Couldn't parse");
+		}
+	}
+	
+	@Test(expected=IllegalArgumentException.class)
+	public void testOneOfSchemaRequiresArray() {
+		fromSingleElement("{ \"oneOf\": true }");
+	}
+	
+	@Test(expected=IllegalArgumentException.class)
+	public void testOneOfSchemaRequiresNonEmptyArray() {
+		fromSingleElement("{ \"oneOf\": [] }");
+	}
+	
+	@Test
+	public void testOneOfSchemaPassesNonEmptyArray() {
+		try {
+			fromSingleElement("{ \"oneOf\": [ false ] }");
+		} catch (IllegalArgumentException ex) {
+			fail("Couldn't parse");
+		}
+	}
+	
+	@Test(expected=IllegalStateException.class)
+	public void testNotSchemaRequiresSubSchema() {
+		fromSingleElement("{ \"not\": [] }");
+	}
+	
+	@Test
+	public void testNotSchemaPassesBooleanAndObject() {
+		try {
+			fromSingleElement("{ \"not\": {} }");
+			fromSingleElement("{ \"not\": false }");
+		} catch (IllegalArgumentException ex) {
+			fail("Couldn't parse");
+		}
+	}
+	
 }

+ 47 - 0
src/test/java/org/leumasjaffe/json/schema/tester/DependencyTesterTest.java

@@ -0,0 +1,47 @@
+package org.leumasjaffe.json.schema.tester;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.*;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Test;
+import org.leumasjaffe.json.schema.Tester;
+import org.leumasjaffe.json.schema.tester.PropertyTester.Pair;
+
+import com.fasterxml.jackson.databind.node.BooleanNode;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.JsonNodeType;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+public class DependencyTesterTest {
+
+	@Test
+	public void testAcceptsObjects() {
+		assertThat(new DependencyTester(new HashMap<>()).acceptedTypes(),
+				is(new JsonNodeType[]{JsonNodeType.OBJECT}));
+	}
+	
+	@Test
+	public void testDependencyAppliesToSourceJson() {
+		Map<String, Tester> expect = new HashMap<>();
+		expect.put("A", TypeTester.fromType("boolean"));
+		
+		final ObjectNode node = new ObjectNode(JsonNodeFactory.instance);
+		node.set("A", BooleanNode.TRUE);
+		
+		assertFalse(new DependencyTester(expect).accepts(node));
+	}
+
+	@Test
+	public void testDependencyCanInjectSchema() {
+		Map<String, Tester> expect = new HashMap<>();
+		expect.put("A", new PropertyTester(new Pair("A"::equals, FixedTester.ACCEPT)));
+		
+		final ObjectNode node = new ObjectNode(JsonNodeFactory.instance);
+		node.set("A", BooleanNode.TRUE);
+		
+		assertTrue(new DependencyTester(expect).accepts(node));
+	}
+}

+ 1 - 1
src/test/java/org/leumasjaffe/json/schema/tester/StubTester.java

@@ -4,6 +4,6 @@ import org.leumasjaffe.json.schema.Tester;
 
 import com.fasterxml.jackson.databind.node.JsonNodeType;
 
-interface StubTester extends Tester {
+public interface StubTester extends Tester {
 	default JsonNodeType[] acceptedTypes() { return ANY; }
 }

+ 154 - 0
src/test/resources/schema6.json

@@ -0,0 +1,154 @@
+{
+    "$schema": "http://json-schema.org/draft-06/schema#",
+    "$id": "http://json-schema.org/draft-06/schema#",
+    "title": "Core schema meta-schema",
+    "definitions": {
+        "schemaArray": {
+            "type": "array",
+            "minItems": 1,
+            "items": { "$ref": "#" }
+        },
+        "nonNegativeInteger": {
+            "type": "integer",
+            "minimum": 0
+        },
+        "nonNegativeIntegerDefault0": {
+            "allOf": [
+                { "$ref": "#/definitions/nonNegativeInteger" },
+                { "default": 0 }
+            ]
+        },
+        "simpleTypes": {
+            "enum": [
+                "array",
+                "boolean",
+                "integer",
+                "null",
+                "number",
+                "object",
+                "string"
+            ]
+        },
+        "stringArray": {
+            "type": "array",
+            "items": { "type": "string" },
+            "uniqueItems": true,
+            "default": []
+        }
+    },
+    "type": ["object", "boolean"],
+    "properties": {
+        "$id": {
+            "type": "string",
+            "format": "uri-reference"
+        },
+        "$schema": {
+            "type": "string",
+            "format": "uri"
+        },
+        "$ref": {
+            "type": "string",
+            "format": "uri-reference"
+        },
+        "title": {
+            "type": "string"
+        },
+        "description": {
+            "type": "string"
+        },
+        "default": {},
+        "examples": {
+            "type": "array",
+            "items": {}
+        },
+        "multipleOf": {
+            "type": "number",
+            "exclusiveMinimum": 0
+        },
+        "maximum": {
+            "type": "number"
+        },
+        "exclusiveMaximum": {
+            "type": "number"
+        },
+        "minimum": {
+            "type": "number"
+        },
+        "exclusiveMinimum": {
+            "type": "number"
+        },
+        "maxLength": { "$ref": "#/definitions/nonNegativeInteger" },
+        "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
+        "pattern": {
+            "type": "string",
+            "format": "regex"
+        },
+        "additionalItems": { "$ref": "#" },
+        "items": {
+            "anyOf": [
+                { "$ref": "#" },
+                { "$ref": "#/definitions/schemaArray" }
+            ],
+            "default": {}
+        },
+        "maxItems": { "$ref": "#/definitions/nonNegativeInteger" },
+        "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
+        "uniqueItems": {
+            "type": "boolean",
+            "default": false
+        },
+        "contains": { "$ref": "#" },
+        "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" },
+        "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
+        "required": { "$ref": "#/definitions/stringArray" },
+        "additionalProperties": { "$ref": "#" },
+        "definitions": {
+            "type": "object",
+            "additionalProperties": { "$ref": "#" },
+            "default": {}
+        },
+        "properties": {
+            "type": "object",
+            "additionalProperties": { "$ref": "#" },
+            "default": {}
+        },
+        "patternProperties": {
+            "type": "object",
+            "additionalProperties": { "$ref": "#" },
+            "default": {}
+        },
+        "dependencies": {
+            "type": "object",
+            "additionalProperties": {
+                "anyOf": [
+                    { "$ref": "#" },
+                    { "$ref": "#/definitions/stringArray" }
+                ]
+            }
+        },
+        "propertyNames": { "$ref": "#" },
+        "const": {},
+        "enum": {
+            "type": "array",
+            "minItems": 1,
+            "uniqueItems": true
+        },
+        "type": {
+            "anyOf": [
+                { "$ref": "#/definitions/simpleTypes" },
+                {
+                    "type": "array",
+                    "items": { "$ref": "#/definitions/simpleTypes" },
+                    "minItems": 1,
+                    "uniqueItems": true
+                }
+            ]
+        },
+        "format": { "type": "string" },
+        "allOf": { "$ref": "#/definitions/schemaArray" },
+        "anyOf": { "$ref": "#/definitions/schemaArray" },
+        "oneOf": { "$ref": "#/definitions/schemaArray" },
+        "not": { "$ref": "#" }
+    },
+    "default": {}
+}