Browse Source

Putting everything into version control.

Sam Jaffe 6 năm trước cách đây
mục cha
commit
3e62bbbb1f
21 tập tin đã thay đổi với 962 bổ sung0 xóa
  1. 126 0
      pom.xml
  2. 58 0
      src/main/lombok/org/leumasjaffe/json/JsonHelper.java
  3. 36 0
      src/main/lombok/org/leumasjaffe/json/schema/Schema.java
  4. 9 0
      src/main/lombok/org/leumasjaffe/json/schema/Tester.java
  5. 54 0
      src/main/lombok/org/leumasjaffe/json/schema/factory/SchemaFactory.java
  6. 85 0
      src/main/lombok/org/leumasjaffe/json/schema/factory/SchemaV6Factory.java
  7. 21 0
      src/main/lombok/org/leumasjaffe/json/schema/tester/AllOfTester.java
  8. 21 0
      src/main/lombok/org/leumasjaffe/json/schema/tester/AnyOfTester.java
  9. 22 0
      src/main/lombok/org/leumasjaffe/json/schema/tester/ContainsTester.java
  10. 122 0
      src/main/lombok/org/leumasjaffe/json/schema/tester/FormatTester.java
  11. 19 0
      src/main/lombok/org/leumasjaffe/json/schema/tester/NotTester.java
  12. 23 0
      src/main/lombok/org/leumasjaffe/json/schema/tester/NumberTester.java
  13. 21 0
      src/main/lombok/org/leumasjaffe/json/schema/tester/OneOfTester.java
  14. 24 0
      src/main/lombok/org/leumasjaffe/json/schema/tester/PropertyNameTester.java
  15. 50 0
      src/main/lombok/org/leumasjaffe/json/schema/tester/PropertyTester.java
  16. 25 0
      src/main/lombok/org/leumasjaffe/json/schema/tester/SizeTester.java
  17. 22 0
      src/main/lombok/org/leumasjaffe/json/schema/tester/TypeTester.java
  18. 21 0
      src/main/lombok/org/leumasjaffe/json/schema/tester/UniqueItemTester.java
  19. 15 0
      src/test/java/org/leumasjaffe/json/JsonTesterSuite.java
  20. 68 0
      src/test/java/org/leumasjaffe/json/tester/SizeTesterTest.java
  21. 120 0
      src/test/java/org/leumasjaffe/json/tester/TypeTesterTest.java

+ 126 - 0
pom.xml

@@ -0,0 +1,126 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>org.leumasjaffe</groupId>
+  <artifactId>json-validator</artifactId>
+  <version>0.0.1-SNAPSHOT</version>
+  <build>
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <groupId>org.eclipse.m2e</groupId>
+          <artifactId>lifecycle-mapping</artifactId>
+          <version>1.0.0</version>
+          <configuration>
+            <lifecycleMappingMetadata>
+              <pluginExecutions>
+                <pluginExecution>
+                  <pluginExecutionFilter>
+                    <groupId>org.projectlombok</groupId>
+                    <artifactId>lombok-maven-plugin</artifactId>
+                    <versionRange>[1,)</versionRange>
+                    <goals>
+                      <goal>delombok</goal>
+                    </goals>
+                  </pluginExecutionFilter>
+                  <action>
+                    <ignore />
+                  </action>
+                </pluginExecution>
+              </pluginExecutions>
+            </lifecycleMappingMetadata>
+          </configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+    <sourceDirectory>target/generated-sources/delombok</sourceDirectory>
+    <plugins>
+      <plugin>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <version>3.5.1</version>
+        <configuration>
+          <compilerVersion>1.8</compilerVersion>
+          <source>1.8</source>
+          <target>1.8</target>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.projectlombok</groupId>
+        <artifactId>lombok-maven-plugin</artifactId>
+        <version>1.16.18.0</version>
+        <executions>
+          <execution>
+            <id>delombok</id>
+            <phase>generate-sources</phase>
+            <goals>
+              <goal>delombok</goal>
+            </goals>
+          </execution>
+        </executions>
+        <configuration>
+          <addOutputDirectory>false</addOutputDirectory>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-assembly-plugin</artifactId>
+        <version>2.4</version>
+        <configuration>
+          <archive>
+            <manifest>
+              <mainClass>org.leumasjaffe.todolist.App</mainClass>
+            </manifest>
+          </archive>
+          <descriptorRefs>
+            <descriptorRef>jar-with-dependencies</descriptorRef>
+          </descriptorRefs>
+        </configuration>
+        <executions>
+          <execution>
+            <id>make-assembly</id> <!-- this is used for inheritance merges -->
+            <phase>package</phase> <!-- bind to the packaging phase -->
+            <goals>
+              <goal>single</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-dependency-plugin</artifactId>
+        <version>2.5.1</version>
+        <executions>
+          <execution>
+            <id>copy-dependencies</id>
+            <phase>package</phase>
+            <goals>
+              <goal>copy-dependencies</goal>
+            </goals>
+            <configuration>
+              <outputDirectory>
+                ${project.build.directory}/dependency-jars/
+              </outputDirectory>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+  <dependencies>
+    <dependency>
+      <groupId>commons-validator</groupId>
+      <artifactId>commons-validator</artifactId>
+      <version>1.6</version>
+    </dependency>
+    <dependency>
+      <groupId>org.projectlombok</groupId>
+      <artifactId>lombok</artifactId>
+      <version>1.16.8</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-databind</artifactId>
+      <version>2.7.3</version>
+    </dependency>
+  </dependencies>
+</project>

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

@@ -0,0 +1,58 @@
+package org.leumasjaffe.json;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import lombok.experimental.UtilityClass;
+
+@UtilityClass
+
+public class JsonHelper {
+	public List<JsonNode> toArray(final JsonNode array) {
+		return StreamSupport.stream(array.spliterator(), false).collect(Collectors.toList());
+	}
+	
+	public <T> List<T> toArray(final JsonNode array, Function<JsonNode, T> transform) {
+		return StreamSupport.stream(array.spliterator(), false).map(transform).collect(Collectors.toList());
+	}
+	
+	public Set<String> fieldNames(final JsonNode object) {
+		Set<String> rval = new HashSet<>();
+		object.fieldNames().forEachRemaining(rval::add);
+		return rval;
+	}
+	
+	public <T> Set<T> fieldNames(final JsonNode object, Function<String, T> transform) {
+		Set<T> rval = new HashSet<>();
+		object.fieldNames().forEachRemaining(s -> rval.add(transform.apply(s)));
+		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, Function<JsonNode, T> transform) {
+		Map<String, T> rval = new HashMap<>();
+		object.fields().forEachRemaining(pair -> rval.put(pair.getKey(), transform.apply(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(),
+				transform.apply(pair.getKey(), pair.getValue())));
+		return rval;
+	}
+}

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

@@ -0,0 +1,36 @@
+package org.leumasjaffe.json.schema;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+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";
+	Map<String, Tester> children = new HashMap<>();
+	
+	public Schema(Tester self) {
+		children.put(SELF, self);
+	}
+
+	public Schema(Map<String, Tester> fields) {
+		children.putAll(fields);
+	}
+
+	@Override
+	public boolean accepts(JsonNode node) {
+		if (children.isEmpty()) {
+			return true;
+		} else if (children.containsKey(SELF)) {
+			return children.get(SELF).accepts(node);
+		}
+		// TODO
+		return false;
+	}
+}

+ 9 - 0
src/main/lombok/org/leumasjaffe/json/schema/Tester.java

@@ -0,0 +1,9 @@
+package org.leumasjaffe.json.schema;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+public interface Tester {
+	static final Tester ACCEPT = j -> true;
+	static final Tester REJECT = j -> false;
+	boolean accepts(final JsonNode node);
+}

+ 54 - 0
src/main/lombok/org/leumasjaffe/json/schema/factory/SchemaFactory.java

@@ -0,0 +1,54 @@
+package org.leumasjaffe.json.schema.factory;
+
+import java.util.List;
+import java.util.regex.Pattern;
+
+import org.leumasjaffe.json.JsonHelper;
+import org.leumasjaffe.json.schema.Schema;
+import org.leumasjaffe.json.schema.Tester;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+public class SchemaFactory {	
+	public final Tester create(final JsonNode object) {
+		switch (object.getNodeType()) {
+		case BOOLEAN:
+			return new Schema(object.asBoolean() ? Tester.ACCEPT : Tester.REJECT);
+		case OBJECT:
+			final SchemaFactory versioned = getVersionFactory(object.path("$ref").asText());
+			return new Schema(JsonHelper.fields(object, versioned::createMapping));
+		default:
+			throw new IllegalStateException("Expected OBJECT or BOOLEAN, got " + object.getNodeType());
+		}
+	}
+		
+	protected String getVersion() {
+		return "";
+	}
+
+	private final SchemaFactory getVersionFactory(final String version) {
+		if (version.isEmpty() || version.equals(getVersion())) {
+			return this;
+		} else {
+			switch (getVersionInt(version)) {
+			case 6: return new SchemaV6Factory();
+			default:
+				throw new IllegalArgumentException("Unsupported schema version: " + version);
+			}
+		}
+	}
+
+	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);
+	}
+
+	protected final List<Tester> createArray(final JsonNode array) {
+		assert(array.isArray() && array.size() >= 1);
+		return JsonHelper.toArray(array, this::create);
+	}
+	
+	protected Tester createMapping(final String key, final JsonNode value) {
+		throw new UnsupportedOperationException("Calling create(key, value) without a versioned instance");
+	}
+}

+ 85 - 0
src/main/lombok/org/leumasjaffe/json/schema/factory/SchemaV6Factory.java

@@ -0,0 +1,85 @@
+package org.leumasjaffe.json.schema.factory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import org.leumasjaffe.json.JsonHelper;
+import org.leumasjaffe.json.schema.Tester;
+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.FormatTester;
+import org.leumasjaffe.json.schema.tester.NotTester;
+import org.leumasjaffe.json.schema.tester.NumberTester;
+import org.leumasjaffe.json.schema.tester.OneOfTester;
+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.TypeTester;
+import org.leumasjaffe.json.schema.tester.UniqueItemTester;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.JsonNodeType;
+
+class SchemaV6Factory extends SchemaFactory {
+	@Override
+	protected String getVersion() {
+		return "http://json-schema.org/draft-06/schema#";
+	}
+	
+	@Override
+	protected Tester createMapping(final String key, final JsonNode value) {
+		switch (key) {
+		case "$id": return Tester.ACCEPT;
+		case "$schema": return Tester.ACCEPT;
+		// case "$ref": ; // TODO Implement reference propagating
+		case "title": return JsonNode::isTextual;
+		case "description": return JsonNode::isTextual;
+		case "default": return Tester.ACCEPT;
+		case "examples": return JsonNode::isArray;
+		case "multipleOf": return new NumberTester(d -> d % value.asDouble() == 0);
+		case "maximum": return new NumberTester(d -> d <= value.asDouble());
+		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 "maxItems": return new SizeTester(JsonNodeType.ARRAY, i -> i < value.asInt());
+		case "minItems": return new SizeTester(JsonNodeType.ARRAY, i -> i >= value.asInt(0));
+		case "uniqueItems": return value.asBoolean() ? UniqueItemTester.INSTANCE : Tester.ACCEPT;
+		case "contains": return new ContainsTester(create(value));
+		case "maxProperties": return new SizeTester(JsonNodeType.OBJECT, i -> i < value.asInt());
+		case "minProperties": return new SizeTester(JsonNodeType.OBJECT, i -> i >= value.asInt(0));
+		case "required": return json -> JsonHelper.toArray(value, JsonNode::asText).stream().allMatch(json::has);
+		case "additionalProperties": return create(value);
+		// case "definitions": ; // TODO Implement definitions creation
+		case "properties": {
+			final List<PropertyTester.Pair> list = new ArrayList<>();
+			value.fields().forEachRemaining(e -> {
+				final String k = e.getKey();
+				list.add(new PropertyTester.Pair(s -> k.equals(s), create(e.getValue())));
+			});
+			return new PropertyTester(list);
+		}
+		case "patternProperties": {
+			final List<PropertyTester.Pair> list = new ArrayList<>();
+			value.fields().forEachRemaining(e -> {
+				final Pattern k = Pattern.compile(e.getKey());
+				list.add(new PropertyTester.Pair(s -> k.matcher(s).matches(), create(e.getValue())));
+			});
+			return new PropertyTester(list);
+		}
+		// case "dependencies": ; // TODO Implement array(required) and object(schema) versions
+		case "propertyNames": return new PropertyNameTester(create(value));
+		case "const": return value::equals;
+		case "enum": new AnyOfTester(JsonHelper.toArray(value, v -> (Tester) v::equals));
+		case "type": return TypeTester.fromType(value.asText());
+		case "format": return FormatTester.forCode(value.asText());
+		case "allOf": return new AllOfTester(createArray(value));
+		case "anyOf": return new AnyOfTester(createArray(value));
+		case "oneOf": return new OneOfTester(createArray(value));
+		case "not": return new NotTester(create(value));
+		default:
+			throw new IllegalArgumentException("Unknown matcher: " + key);
+		}
+	}
+}

+ 21 - 0
src/main/lombok/org/leumasjaffe/json/schema/tester/AllOfTester.java

@@ -0,0 +1,21 @@
+package org.leumasjaffe.json.schema.tester;
+
+import java.util.List;
+
+import org.leumasjaffe.json.schema.Tester;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.FieldDefaults;
+
+@RequiredArgsConstructor
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class AllOfTester implements Tester {
+	List<Tester> children;
+
+	public boolean accepts(JsonNode node) {
+		return children.parallelStream().allMatch(t -> t.accepts(node));
+	}
+}

+ 21 - 0
src/main/lombok/org/leumasjaffe/json/schema/tester/AnyOfTester.java

@@ -0,0 +1,21 @@
+package org.leumasjaffe.json.schema.tester;
+
+import java.util.List;
+
+import org.leumasjaffe.json.schema.Tester;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.FieldDefaults;
+
+@RequiredArgsConstructor
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class AnyOfTester implements Tester {
+	List<Tester> children;
+
+	public boolean accepts(JsonNode node) {
+		return children.parallelStream().filter(t -> t.accepts(node)).count() == 1L;
+	}
+}

+ 22 - 0
src/main/lombok/org/leumasjaffe/json/schema/tester/ContainsTester.java

@@ -0,0 +1,22 @@
+package org.leumasjaffe.json.schema.tester;
+
+import org.leumasjaffe.json.JsonHelper;
+import org.leumasjaffe.json.schema.Tester;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.FieldDefaults;
+
+@RequiredArgsConstructor
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class ContainsTester implements Tester {
+	Tester schema;
+
+	@Override
+	public boolean accepts(JsonNode node) {
+		return JsonHelper.toArray(node).stream().anyMatch(schema::accepts);
+	}
+
+}

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

@@ -0,0 +1,122 @@
+package org.leumasjaffe.json.schema.tester;
+
+import java.net.URISyntaxException;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+
+import org.apache.commons.validator.routines.DomainValidator;
+import org.apache.commons.validator.routines.EmailValidator;
+import org.apache.commons.validator.routines.InetAddressValidator;
+import org.leumasjaffe.json.schema.Tester;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.FieldDefaults;
+
+@RequiredArgsConstructor
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public abstract class FormatTester implements Tester {
+	static Tester UUID = new FormatTester("uuid") {
+		@Override
+		public boolean accepts(JsonNode node) {
+			final String pattern = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
+			return node.isTextual() && node.asText().matches(pattern);
+		}
+	};
+	
+	static Tester DATE_TIME = new FormatTester("date-time") {
+		@Override
+		public boolean accepts(JsonNode node) {
+			if (!node.isTextual()) return false;
+			try {
+				DateTimeFormatter.ISO_DATE_TIME.parse(node.asText());
+			} catch (DateTimeParseException e) {
+				return false;
+			}
+			return true;
+		}
+	};
+	
+	static Tester EMAIL = new FormatTester("email") {
+		@Override
+		public boolean accepts(JsonNode node) {
+			return node.isTextual() && EmailValidator.getInstance().isValid(node.asText());
+		}
+	};
+	
+	static Tester HOSTNAME = new FormatTester("hostname") {
+		@Override
+		public boolean accepts(JsonNode node) {
+			return node.isTextual() && DomainValidator.getInstance().isValid(node.asText());
+		}
+	};
+	
+	static Tester IPV4 = new FormatTester("ipv4") {
+		@Override
+		public boolean accepts(JsonNode node) {
+			return node.isTextual() && InetAddressValidator.getInstance().isValidInet4Address(node.asText());
+		}
+	};
+	
+	static Tester IPV6 = new FormatTester("ipv6") {
+		@Override
+		public boolean accepts(JsonNode node) {
+			return node.isTextual() && InetAddressValidator.getInstance().isValidInet6Address(node.asText());
+		}
+	};
+	
+	static Tester URI = new FormatTester("uri") {
+		@Override
+		public boolean accepts(JsonNode node) {
+			if (!node.isTextual()) return false;
+			try {
+				// TODO: RFC 2396 -> RFC 3986
+				new java.net.URI(node.asText());
+			} catch (URISyntaxException e) {
+				return false;
+			}
+			return true;
+		}
+	};
+	
+	static Tester URI_REFERNCE = new FormatTester("uri") {
+		@Override
+		public boolean accepts(JsonNode node) {
+			if (!node.isTextual()) return false;
+			try {
+				// TODO: RFC 2396 -> RFC 3986
+				return !new java.net.URI(node.asText()).isAbsolute();
+			} catch (URISyntaxException e) {
+				return false;
+			}
+		}
+	};
+	
+	static Tester JSON_POINTER = new FormatTester("json-pointer") {
+		@Override
+		public boolean accepts(JsonNode node) {
+			final String pattern = "^(/([\\u00-\\u2E\\u30-\\u7D\\u7F-\\u10FFFF]|~0|~1)*)*$";
+			return node.isTextual() && node.asText().matches(pattern);
+		}
+	};
+	
+	String format;
+	
+	public static Tester forCode(String asText) {
+		switch (asText) {
+		case "date-time": return DATE_TIME;
+		case "email": return EMAIL;
+		case "hostname": return HOSTNAME;
+		case "ipv4": return IPV4;
+		case "ipv6": return IPV6;
+		case "uri": return URI;
+		case "uri-reference": return URI_REFERNCE;
+//		case "uri-template":
+		case "json-pointer": return JSON_POINTER;	
+		case "uuid": return UUID;
+		default: throw new IllegalArgumentException("Unknown format code '" + asText + "'");
+		}
+	}
+}

+ 19 - 0
src/main/lombok/org/leumasjaffe/json/schema/tester/NotTester.java

@@ -0,0 +1,19 @@
+package org.leumasjaffe.json.schema.tester;
+
+import org.leumasjaffe.json.schema.Tester;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.FieldDefaults;
+
+@RequiredArgsConstructor
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class NotTester implements Tester {
+	Tester child;
+
+	public boolean accepts(JsonNode node) {
+		return !child.accepts(node);
+	}
+}

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

@@ -0,0 +1,23 @@
+package org.leumasjaffe.json.schema.tester;
+
+import java.util.function.DoublePredicate;
+
+import org.leumasjaffe.json.schema.Tester;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.FieldDefaults;
+
+@RequiredArgsConstructor
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class NumberTester implements Tester {
+	DoublePredicate pred;
+
+	@Override
+	public boolean accepts(JsonNode node) {
+		return node.isNumber() && pred.test(node.asDouble());
+	}
+
+}

+ 21 - 0
src/main/lombok/org/leumasjaffe/json/schema/tester/OneOfTester.java

@@ -0,0 +1,21 @@
+package org.leumasjaffe.json.schema.tester;
+
+import java.util.List;
+
+import org.leumasjaffe.json.schema.Tester;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.FieldDefaults;
+
+@RequiredArgsConstructor
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class OneOfTester implements Tester {
+	List<Tester> children;
+
+	public boolean accepts(JsonNode node) {
+		return children.parallelStream().anyMatch(t -> t.accepts(node));
+	}
+}

+ 24 - 0
src/main/lombok/org/leumasjaffe/json/schema/tester/PropertyNameTester.java

@@ -0,0 +1,24 @@
+package org.leumasjaffe.json.schema.tester;
+
+import org.leumasjaffe.json.JsonHelper;
+import org.leumasjaffe.json.schema.Tester;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.FieldDefaults;
+
+@RequiredArgsConstructor
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class PropertyNameTester implements Tester {
+	Tester schema;
+
+	@Override
+	public boolean accepts(JsonNode node) {
+		return JsonHelper.fieldNames(node, TextNode::valueOf).stream()
+				.allMatch(schema::accepts);
+	}
+
+}

+ 50 - 0
src/main/lombok/org/leumasjaffe/json/schema/tester/PropertyTester.java

@@ -0,0 +1,50 @@
+package org.leumasjaffe.json.schema.tester;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+import org.leumasjaffe.json.schema.Tester;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.FieldDefaults;
+
+@RequiredArgsConstructor
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class PropertyTester implements Tester {
+	@AllArgsConstructor
+	public static class Pair {
+		Predicate<String> keyMatches;
+		Tester contentMatches;
+	}
+	
+	List<Pair> schema;
+
+	@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();
+			final Stream<Pair> stream = schema.stream()
+					.filter(p -> p.keyMatches.test(data.getKey()));
+			if (stream.count() > 0) {
+				// TODO: Don't accept multiple
+				return false;
+			} else if (stream.findFirst().map(p -> valueMatches(data, p)).orElse(false)) {
+				return false;
+			}
+		}
+		return true;
+	}
+
+	private boolean valueMatches(final Map.Entry<String, JsonNode> data, Pair p) {
+		return p.contentMatches.accepts(data.getValue());
+	}
+
+}

+ 25 - 0
src/main/lombok/org/leumasjaffe/json/schema/tester/SizeTester.java

@@ -0,0 +1,25 @@
+package org.leumasjaffe.json.schema.tester;
+
+import java.util.function.IntPredicate;
+
+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 SizeTester implements Tester {
+	JsonNodeType type;
+	IntPredicate pred;
+
+	@Override
+	public boolean accepts(JsonNode node) {
+		return node.getNodeType() == type && pred.test(node.size());
+	}
+
+}

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

@@ -0,0 +1,22 @@
+package org.leumasjaffe.json.schema.tester;
+
+import org.leumasjaffe.json.schema.Tester;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+public abstract class TypeTester implements Tester {
+
+	public static Tester fromType(final String type) {
+		switch (type) {
+		case "array": return JsonNode::isArray;
+		case "boolean": return JsonNode::isBoolean;
+		case "integer": return JsonNode::isInt;
+		case "null": return JsonNode::isNull;
+		case "number": return JsonNode::isNumber;
+		case "object": return JsonNode::isObject;
+		case "string": return JsonNode::isTextual;
+		default: throw new IllegalArgumentException("Invalid type: " + type);
+		}
+	}
+
+}

+ 21 - 0
src/main/lombok/org/leumasjaffe/json/schema/tester/UniqueItemTester.java

@@ -0,0 +1,21 @@
+package org.leumasjaffe.json.schema.tester;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.leumasjaffe.json.schema.Tester;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+public class UniqueItemTester implements Tester {
+	public static final Tester INSTANCE = new UniqueItemTester();
+
+	@Override
+	public boolean accepts(final JsonNode node) {
+		if (!node.isArray()) return false;
+		final Set<JsonNode> nodes = new HashSet<>();
+		node.iterator().forEachRemaining(nodes::add);
+		return nodes.size() == node.size();
+	}
+
+}

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

@@ -0,0 +1,15 @@
+package org.leumasjaffe.json;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+import org.leumasjaffe.json.tester.SizeTesterTest;
+import org.leumasjaffe.json.tester.TypeTesterTest;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+	SizeTesterTest.class, 
+	TypeTesterTest.class
+})
+public class JsonTesterSuite {
+
+}

+ 68 - 0
src/test/java/org/leumasjaffe/json/tester/SizeTesterTest.java

@@ -0,0 +1,68 @@
+package org.leumasjaffe.json.tester;
+
+import static org.junit.Assert.*;
+
+import java.util.function.IntPredicate;
+
+import static com.fasterxml.jackson.databind.node.JsonNodeType.ARRAY;
+import static com.fasterxml.jackson.databind.node.JsonNodeType.OBJECT;
+
+import org.junit.Test;
+import org.leumasjaffe.json.schema.tester.SizeTester;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.NullNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import lombok.AccessLevel;
+import lombok.experimental.FieldDefaults;
+
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class SizeTesterTest {
+	static IntPredicate NON_ZERO = i -> i > 0;
+
+	@Test
+	public void arrayMatcherRejectsObject() {
+		final SizeTester notEmptyArray = new SizeTester(ARRAY, NON_ZERO);
+		final JsonNode node = new ObjectNode(JsonNodeFactory.instance);
+		assertFalse(notEmptyArray.accepts(node));
+	}
+
+	@Test
+	public void objectMatcherRejectsArray() {
+		final SizeTester notEmptyArray = new SizeTester(OBJECT, NON_ZERO);
+		final JsonNode node = new ArrayNode(JsonNodeFactory.instance);
+		assertFalse(notEmptyArray.accepts(node));
+	}
+
+	@Test
+	public void arrayMatcherRejectsTooSmall() {
+		final SizeTester notEmptyArray = new SizeTester(ARRAY, NON_ZERO);
+		final JsonNode node = new ArrayNode(JsonNodeFactory.instance);
+		assertFalse(notEmptyArray.accepts(node));
+	}
+
+	@Test
+	public void objectMatcherRejectsTooSmall() {
+		final SizeTester notEmptyArray = new SizeTester(OBJECT, NON_ZERO);
+		final JsonNode node = new ObjectNode(JsonNodeFactory.instance);
+		assertFalse(notEmptyArray.accepts(node));
+	}
+
+	@Test
+	public void arrayMatcherAcceptsGoodSize() {
+		final SizeTester notEmptyArray = new SizeTester(ARRAY, NON_ZERO);
+		final ArrayNode node = new ArrayNode(JsonNodeFactory.instance);
+		node.add(NullNode.getInstance());
+		assertTrue(notEmptyArray.accepts(node));
+	}
+
+	@Test
+	public void objectMatcherAcceptsGoodSize() {
+		final SizeTester notEmptyArray = new SizeTester(OBJECT, NON_ZERO);
+		final ObjectNode node = new ObjectNode(JsonNodeFactory.instance);
+		node.set("_", NullNode.getInstance());
+		assertTrue(notEmptyArray.accepts(node));
+	}}

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

@@ -0,0 +1,120 @@
+package org.leumasjaffe.json.tester;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+import org.leumasjaffe.json.schema.Tester;
+import org.leumasjaffe.json.schema.tester.TypeTester;
+
+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.IntNode;
+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;
+
+import lombok.AccessLevel;
+import lombok.experimental.FieldDefaults;
+
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+public class TypeTesterTest {
+	JsonNode jNull = NullNode.getInstance();
+	JsonNode bool = BooleanNode.TRUE;
+	JsonNode integral = new IntNode(5);
+	JsonNode floating = new DoubleNode(1.5);
+	JsonNode text = new TextNode("hello");
+	JsonNode array = new ArrayNode(JsonNodeFactory.instance);
+	JsonNode object = new ObjectNode(JsonNodeFactory.instance);
+	
+	@Test(expected=IllegalArgumentException.class)
+	public void testRejectsCustomType() {
+		TypeTester.fromType("blob");
+	}
+
+	@Test
+	public void testMatcheNullNode() {
+		final Tester t = TypeTester.fromType("null");
+		assertTrue(t.accepts(jNull));
+		assertFalse(t.accepts(bool));
+		assertFalse(t.accepts(integral));
+		assertFalse(t.accepts(floating));
+		assertFalse(t.accepts(text));
+		assertFalse(t.accepts(array));
+		assertFalse(t.accepts(object));
+	}
+
+	@Test
+	public void testMatchesBooleanNode() {
+		final Tester t = TypeTester.fromType("boolean");
+		assertFalse(t.accepts(jNull));
+		assertTrue(t.accepts(bool));
+		assertFalse(t.accepts(integral));
+		assertFalse(t.accepts(floating));
+		assertFalse(t.accepts(text));
+		assertFalse(t.accepts(array));
+		assertFalse(t.accepts(object));
+	}
+	
+	@Test
+	public void testMatchesIntegerNode() {
+		final Tester t = TypeTester.fromType("integer");
+		assertFalse(t.accepts(jNull));
+		assertFalse(t.accepts(bool));
+		assertTrue(t.accepts(integral));
+		assertFalse(t.accepts(floating));
+		assertFalse(t.accepts(text));
+		assertFalse(t.accepts(array));
+		assertFalse(t.accepts(object));
+	}
+	
+	@Test
+	public void testMatchesDoubleNode() {
+		final Tester t = TypeTester.fromType("number");
+		assertFalse(t.accepts(jNull));
+		assertFalse(t.accepts(bool));
+		assertTrue(t.accepts(integral));
+		assertTrue(t.accepts(floating));
+		assertFalse(t.accepts(text));
+		assertFalse(t.accepts(array));
+		assertFalse(t.accepts(object));
+	}
+	
+	@Test
+	public void testMatchesTextNode() {
+		final Tester t = TypeTester.fromType("string");
+		assertFalse(t.accepts(jNull));
+		assertFalse(t.accepts(bool));
+		assertFalse(t.accepts(integral));
+		assertFalse(t.accepts(floating));
+		assertTrue(t.accepts(text));
+		assertFalse(t.accepts(array));
+		assertFalse(t.accepts(object));
+	}
+	
+	@Test
+	public void testMatchesArrayNode() {
+		final Tester t = TypeTester.fromType("array");
+		assertFalse(t.accepts(jNull));
+		assertFalse(t.accepts(bool));
+		assertFalse(t.accepts(integral));
+		assertFalse(t.accepts(floating));
+		assertFalse(t.accepts(text));
+		assertTrue(t.accepts(array));
+		assertFalse(t.accepts(object));
+	}
+	
+	@Test
+	public void testMatchesObjectNode() {
+		final Tester t = TypeTester.fromType("object");
+		assertFalse(t.accepts(jNull));
+		assertFalse(t.accepts(bool));
+		assertFalse(t.accepts(integral));
+		assertFalse(t.accepts(floating));
+		assertFalse(t.accepts(text));
+		assertFalse(t.accepts(array));
+		assertTrue(t.accepts(object));
+	}
+}