Browse Source

Add support for root-schema referencing.

Sam Jaffe 6 years ago
parent
commit
8244fd2c07

+ 47 - 8
src/main/lombok/org/leumasjaffe/json/schema/factory/SchemaFactory.java

@@ -4,6 +4,7 @@ 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;
 
@@ -17,11 +18,13 @@ 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 {
@@ -39,6 +42,29 @@ 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 {
@@ -51,7 +77,9 @@ public class SchemaFactory {
 		}
 		
 		private Tester createTester(final String path) {
-			if (path.startsWith("#")) {
+			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) {
@@ -68,20 +96,19 @@ public class SchemaFactory {
 		}
 	}
 	
-	protected Definitions defs = null;
+	SharedData shared = new SharedData();
 	
-	protected SchemaFactory(Definitions defs) {
-		this.defs = defs;
+	protected SchemaFactory(SharedData shared) {
+		this.shared = shared;
 	}
 
 	public final Tester create(final JsonNode object) {
-		if (defs == null) { defs = this.new Definitions(object); }
 		switch (object.getNodeType()) {
 		case BOOLEAN:
 			return new Schema(object.asBoolean() ? FixedTester.ACCEPT : FixedTester.REJECT);
 		case OBJECT:
-			final SchemaFactory versioned = getVersionFactory(object.path("$schema").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());
 		}
@@ -90,18 +117,30 @@ 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(defs);
+			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#");

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

@@ -31,8 +31,8 @@ import lombok.NoArgsConstructor;
 
 @NoArgsConstructor
 class SchemaV6Factory extends SchemaFactory {
-	protected SchemaV6Factory(Definitions defs) {
-		super(defs);
+	protected SchemaV6Factory(SharedData shared) {
+		super(shared);
 	}
 
 	@Override
@@ -45,7 +45,7 @@ class SchemaV6Factory extends SchemaFactory {
 		switch (key) {
 		case "$id": return FixedTester.ACCEPT;
 		case "$schema": return FixedTester.ACCEPT;
-		case "$ref": return defs.get(value.asText());
+		case "$ref": return shared.getDefinitions().get(value.asText());
 		case "title": return FixedTester.ACCEPT;
 		case "description": return FixedTester.ACCEPT;
 		case "default": return FixedTester.ACCEPT;

+ 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 + "'");
 		}
 	}

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

@@ -8,7 +8,6 @@ import java.io.PrintWriter;
 import java.io.StringWriter;
 
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
@@ -92,7 +91,7 @@ public class SchemaFactoryTest {
 	}
 	
 	
-	@Test @Ignore
+	@Test
 	public void testSchemaValidatesDraftV6Schema() throws JsonProcessingException, IOException {
 		JsonNode node = mapper.readTree(new File("src/test/resources/schema6.json"));
 		assertTrue(factory.create(node).accepts(node));