Browse Source

Adding tests for handling objects.

Sam Jaffe 6 years ago
parent
commit
7600b8b891

+ 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(),

+ 2 - 5
src/main/lombok/org/leumasjaffe/json/schema/ArrayTester.java

@@ -5,17 +5,14 @@ import java.util.List;
 
 import com.fasterxml.jackson.databind.JsonNode;
 
+import lombok.AllArgsConstructor;
 import lombok.NoArgsConstructor;
 
 public interface ArrayTester extends Tester {
-	@NoArgsConstructor
+	@NoArgsConstructor @AllArgsConstructor
 	class Status {
 		public boolean accepted = true;
 		public List<JsonNode> unprocessed = new ArrayList<>();
-		
-		public Status(boolean accepted) {
-			this.accepted = accepted;
-		}
 	}
 	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);
+}

+ 23 - 0
src/main/lombok/org/leumasjaffe/json/schema/Schema.java

@@ -29,6 +29,9 @@ public class Schema implements Tester {
 			"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<>();
 	
@@ -65,6 +68,7 @@ public class Schema implements Tester {
 		case NUMBER: return acceptsNumber(node);
 		case STRING: return acceptsString(node);
 		case ARRAY: return acceptsArray(node);
+		case OBJECT: return acceptsObject(node);
 		default: return acceptsUniversal(node);
 		}
 	}
@@ -102,6 +106,25 @@ public class Schema implements Tester {
 		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);
+		}
+		return status.accepted && acceptsUniversal(node);
+	}
+
 	private boolean acceptsUniversal(JsonNode node) {
 		for (String key : getKeys(UNIVERSAL_MATCHERS)) {
 			if (!children.get(key).accepts(node)) {

+ 17 - 3
src/main/lombok/org/leumasjaffe/json/schema/tester/AllItemsTester.java

@@ -1,9 +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;
@@ -15,7 +17,7 @@ import lombok.experimental.FieldDefaults;
 
 @RequiredArgsConstructor
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
-public class AllItemsTester implements Tester, ArrayTester {
+public class AllItemsTester implements Tester, ArrayTester, ObjectTester {
 	JsonNodeType type;
 	Tester schema;
 	
@@ -31,8 +33,8 @@ public class AllItemsTester implements Tester, ArrayTester {
 	}
 
 	@Override
-	public Status accepts(List<JsonNode> data) {
-		Status out = new Status();
+	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))) {
 				out.accepted = false;
@@ -42,4 +44,16 @@ public class AllItemsTester implements Tester, ArrayTester {
 		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 out;
+	}
+
 }

+ 10 - 3
src/main/lombok/org/leumasjaffe/json/schema/tester/FixedTester.java

@@ -1,8 +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;
@@ -14,7 +16,7 @@ import lombok.experimental.FieldDefaults;
 
 @RequiredArgsConstructor
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
-public class FixedTester implements Tester, ArrayTester {
+public class FixedTester implements Tester, ArrayTester, ObjectTester {
 	public static final FixedTester ACCEPT = new FixedTester(true);
 	public static final FixedTester REJECT = new FixedTester(false);
 	
@@ -31,8 +33,13 @@ public class FixedTester implements Tester, ArrayTester {
 	}
 
 	@Override
-	public Status accepts(List<JsonNode> data) {
-		return new Status(returns);
+	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);
 	}
 
 }

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

+ 108 - 4
src/test/java/org/leumasjaffe/json/schema/SchemaTest.java

@@ -4,10 +4,12 @@ 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;
@@ -15,8 +17,11 @@ 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;
@@ -25,8 +30,13 @@ 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<>();
@@ -65,7 +75,17 @@ public class SchemaTest {
 		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));
@@ -162,7 +182,7 @@ public class SchemaTest {
 	}
 	
 	@Test
-	public void testHandlesAdditionalItemsMatcherWhenArrayItems() {
+	public void testHandlesAdditionalMatcherWhenArrayItems() {
 		Map<String, Tester> tests = new HashMap<>();
 		tests.put("items", new ItemsTester(getNumberSchema(), getStringSchema()));
 		tests.put("additionalItems", FixedTester.REJECT);
@@ -183,7 +203,7 @@ public class SchemaTest {
 	}
 
 	@Test
-	public void testHandlesAdditionalItemsNotCalledIfFailsInItems() {
+	public void testHandlesAdditionalNotCalledIfFailsInItems() {
 		Map<String, Tester> tests = new HashMap<>();
 		tests.put("items", new ItemsTester(getNumberSchema(), getStringSchema()));
 		tests.put("additionalItems", FixedTester.ACCEPT);
@@ -211,4 +231,88 @@ public class SchemaTest {
 		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));
+	}}