Skip to content

Commit

Permalink
Support schema resource (#922)
Browse files Browse the repository at this point in the history
* Support schema resources

* Refactor

* Refactor

* Refactor

* Support uri change in id

* Fix schema resource parent and evaluation path

* Fix

* Fix remote ref paths

* Fix

* Fix schema location

* Support anchors

* Refactor

* Refactor

* Refactor

* Refactor shift subschema loading to factory

* Fix ref

* Refactor ref

* Refactor discriminator

* Refactor

* Refactor validation context

* Load validators in constructor

* Schema location

* Refactor ref validator

* Fix enum
  • Loading branch information
justin-tay authored Jan 20, 2024
1 parent 5a94df7 commit 9b73d10
Show file tree
Hide file tree
Showing 36 changed files with 1,309 additions and 362 deletions.
15 changes: 12 additions & 3 deletions src/main/java/com/networknt/schema/AbsoluteIri.java
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,23 @@ public static String resolve(String parent, String iri) {
} else {
scheme = scheme + 3;
}
int slash = parent.lastIndexOf('/');
if (slash != -1 && slash > scheme) {
base = parent.substring(0, slash);
base = parent(base, scheme);
while (iri.startsWith("../")) {
base = parent(base, scheme);
iri = iri.substring(3);
}
return base + "/" + iri;
}
}
}

protected static String parent(String iri, int scheme) {
int slash = iri.lastIndexOf('/');
if (slash != -1 && slash > scheme) {
return iri.substring(0, slash);
}
return iri;
}

/**
* Returns the scheme and authority components of the IRI.
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/networknt/schema/AllOfValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNo
final ObjectNode allOfEntry = (ObjectNode) arrayElements.next();
final JsonNode $ref = allOfEntry.get("$ref");
if (null != $ref) {
final ValidationContext.DiscriminatorContext currentDiscriminatorContext = this.validationContext
final DiscriminatorContext currentDiscriminatorContext = executionContext
.getCurrentDiscriminatorContext();
if (null != currentDiscriminatorContext) {
final ObjectNode discriminator = currentDiscriminatorContext
Expand Down
8 changes: 4 additions & 4 deletions src/main/java/com/networknt/schema/AnyOfValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public class AnyOfValidator extends BaseJsonValidator {
private static final String DISCRIMINATOR_REMARK = "and the discriminator-selected candidate schema didn't pass validation";

private final List<JsonSchema> schemas = new ArrayList<>();
private final ValidationContext.DiscriminatorContext discriminatorContext;
private final DiscriminatorContext discriminatorContext;

public AnyOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.ANY_OF, validationContext);
Expand All @@ -42,7 +42,7 @@ public AnyOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath
}

if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) {
this.discriminatorContext = new ValidationContext.DiscriminatorContext();
this.discriminatorContext = new DiscriminatorContext();
} else {
this.discriminatorContext = null;
}
Expand All @@ -57,7 +57,7 @@ public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNo
ValidatorState state = executionContext.getValidatorState();

if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) {
this.validationContext.enterDiscriminatorContext(this.discriminatorContext, instanceLocation);
executionContext.enterDiscriminatorContext(this.discriminatorContext, instanceLocation);
}

boolean initialHasMatchedNode = state.hasMatchedNode();
Expand Down Expand Up @@ -148,7 +148,7 @@ public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNo
}
} finally {
if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) {
this.validationContext.leaveDiscriminatorContextImmediately(instanceLocation);
executionContext.leaveDiscriminatorContextImmediately(instanceLocation);
}

Scope parentScope = collectorContext.exitDynamicScope();
Expand Down
24 changes: 22 additions & 2 deletions src/main/java/com/networknt/schema/BaseJsonValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.networknt.schema.ValidationContext.DiscriminatorContext;
import com.networknt.schema.i18n.DefaultMessageSource;

import org.slf4j.Logger;
Expand Down Expand Up @@ -69,6 +68,20 @@ public BaseJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationP
: PathType.DEFAULT;
}

/**
* Copy constructor.
*
* @param copy to copy from
*/
protected BaseJsonValidator(BaseJsonValidator copy) {
super(copy);
this.suppressSubSchemaRetrieval = copy.suppressSubSchemaRetrieval;
this.applyDefaultsStrategy = copy.applyDefaultsStrategy;
this.pathType = copy.pathType;
this.schemaNode = copy.schemaNode;
this.validationContext = copy.validationContext;
}

private static JsonSchema obtainSubSchemaNode(final JsonNode schemaNode, final ValidationContext validationContext) {
final JsonNode node = schemaNode.get("id");

Expand Down Expand Up @@ -112,7 +125,7 @@ protected static void debug(Logger logger, JsonNode node, JsonNode rootNode, Jso
* @param discriminatorPropertyValue the value of the <code>discriminator/propertyName</code> field
* @param jsonSchema the {@link JsonSchema} to check
*/
protected static void checkDiscriminatorMatch(final ValidationContext.DiscriminatorContext currentDiscriminatorContext,
protected static void checkDiscriminatorMatch(final DiscriminatorContext currentDiscriminatorContext,
final ObjectNode discriminator,
final String discriminatorPropertyValue,
final JsonSchema jsonSchema) {
Expand Down Expand Up @@ -249,6 +262,13 @@ public JsonSchema getParentSchema() {
return this.parentSchema;
}

public JsonSchema getEvaluationParentSchema() {
if (this.evaluationParentSchema != null) {
return this.evaluationParentSchema;
}
return getParentSchema();
}

protected JsonSchema fetchSubSchemaNode(ValidationContext validationContext) {
return this.suppressSubSchemaRetrieval ? null : obtainSubSchemaNode(this.schemaNode, validationContext);
}
Expand Down
41 changes: 41 additions & 0 deletions src/main/java/com/networknt/schema/CachedSupplier.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.networknt.schema;

import java.util.function.Supplier;

/**
* Supplier that caches the output.
*
* @param <T> the type cached
*/
public class CachedSupplier<T> implements Supplier<T> {
private final Supplier<T> delegate;
private T cache = null;

public CachedSupplier(Supplier<T> delegate) {
this.delegate = delegate;
}

@Override
public T get() {
if (cache == null) {
cache = delegate.get();
}
return cache;
}

}
41 changes: 41 additions & 0 deletions src/main/java/com/networknt/schema/DiscriminatorContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.networknt.schema;

import java.util.HashMap;
import java.util.Map;

import com.fasterxml.jackson.databind.node.ObjectNode;

public class DiscriminatorContext {
private final Map<String, ObjectNode> discriminators = new HashMap<>();

private boolean discriminatorMatchFound = false;

public void registerDiscriminator(final SchemaLocation schemaLocation, final ObjectNode discriminator) {
this.discriminators.put("#" + schemaLocation.getFragment().toString(), discriminator);
}

public ObjectNode getDiscriminatorForPath(final SchemaLocation schemaLocation) {
return this.discriminators.get("#" + schemaLocation.getFragment().toString());
}

public ObjectNode getDiscriminatorForPath(final String schemaLocation) {
return this.discriminators.get(schemaLocation);
}

public void markMatch() {
this.discriminatorMatchFound = true;
}

public boolean isDiscriminatorMatchFound() {
return this.discriminatorMatchFound;
}

/**
* Returns true if we have a discriminator active. In this case no valid match in anyOf should lead to validation failure
*
* @return true in case there are discriminator candidates
*/
public boolean isActive() {
return !this.discriminators.isEmpty();
}
}
60 changes: 57 additions & 3 deletions src/main/java/com/networknt/schema/EnumValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
package com.networknt.schema;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.DecimalNode;
import com.fasterxml.jackson.databind.node.NullNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.math.BigDecimal;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
Expand All @@ -45,7 +47,10 @@ public EnumValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath,
for (JsonNode n : schemaNode) {
if (n.isNumber()) {
// convert to DecimalNode for number comparison
nodes.add(DecimalNode.valueOf(n.decimalValue()));
nodes.add(processNumberNode(n));
} else if (n.isArray()) {
ArrayNode a = processArrayNode((ArrayNode) n);
nodes.add(a);
} else {
nodes.add(n);
}
Expand All @@ -65,7 +70,6 @@ public EnumValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath,
sb.append("null");
}
}
//
sb.append(']');

error = sb.toString();
Expand All @@ -78,7 +82,11 @@ public EnumValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath,
public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) {
debug(logger, node, rootNode, instanceLocation);

if (node.isNumber()) node = DecimalNode.valueOf(node.decimalValue());
if (node.isNumber()) {
node = processNumberNode(node);
} else if (node.isArray()) {
node = processArrayNode((ArrayNode) node);
}
if (!nodes.contains(node) && !( this.validationContext.getConfig().isTypeLoose() && isTypeLooseContainsInEnum(node))) {
return Collections.singleton(message().instanceLocation(instanceLocation)
.locale(executionContext.getExecutionConfig().getLocale()).arguments(error).build());
Expand All @@ -105,4 +113,50 @@ private boolean isTypeLooseContainsInEnum(JsonNode node) {
return false;
}

/**
* Processes the number and ensures trailing zeros are stripped.
*
* @param n the node
* @return the node
*/
protected JsonNode processNumberNode(JsonNode n) {
return DecimalNode.valueOf(new BigDecimal(n.decimalValue().toPlainString()));
}

/**
* Processes the array and ensures that numbers within have trailing zeroes stripped.
*
* @param node the node
* @return the node
*/
protected ArrayNode processArrayNode(ArrayNode node) {
if (!hasNumber(node)) {
return node;
}
ArrayNode a = (ArrayNode) node.deepCopy();
for (int x = 0; x < a.size(); x++) {
JsonNode v = a.get(x);
if (v.isNumber()) {
v = processNumberNode(v);
a.set(x, v);
}
}
return a;
}

/**
* Determines if the array node contains a number.
*
* @param node the node
* @return the node
*/
protected boolean hasNumber(ArrayNode node) {
for (int x = 0; x < node.size(); x++) {
JsonNode v = node.get(x);
if (v.isNumber()) {
return true;
}
}
return false;
}
}
18 changes: 18 additions & 0 deletions src/main/java/com/networknt/schema/ExecutionContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@

package com.networknt.schema;

import java.util.Stack;

/**
* Stores the execution context for the validation run.
*/
public class ExecutionContext {
private ExecutionConfig executionConfig;
private CollectorContext collectorContext;
private ValidatorState validatorState = null;
private Stack<DiscriminatorContext> discriminatorContexts = new Stack<>();

/**
* Creates an execution context.
Expand Down Expand Up @@ -113,4 +116,19 @@ public ValidatorState getValidatorState() {
public void setValidatorState(ValidatorState validatorState) {
this.validatorState = validatorState;
}

public DiscriminatorContext getCurrentDiscriminatorContext() {
if (!this.discriminatorContexts.empty()) {
return this.discriminatorContexts.peek();
}
return null; // this is the case when we get on a schema that has a discriminator, but it's not used in anyOf
}

public void enterDiscriminatorContext(final DiscriminatorContext ctx, @SuppressWarnings("unused") JsonNodePath instanceLocation) {
this.discriminatorContexts.push(ctx);
}

public void leaveDiscriminatorContextImmediately(@SuppressWarnings("unused") JsonNodePath instanceLocation) {
this.discriminatorContexts.pop();
}
}
8 changes: 8 additions & 0 deletions src/main/java/com/networknt/schema/JsonMetaSchema.java
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,14 @@ public String readId(JsonNode schemaNode) {
return readText(schemaNode, this.idKeyword);
}

public String readAnchor(JsonNode schemaNode) {
boolean supportsAnchor = this.keywords.containsKey("$anchor");
if (supportsAnchor) {
return readText(schemaNode, "$anchor");
}
return null;
}

public JsonNode getNodeByFragmentRef(String ref, JsonNode node) {
boolean supportsAnchor = this.keywords.containsKey("$anchor");
String refName = supportsAnchor ? ref.substring(1) : ref;
Expand Down
Loading

0 comments on commit 9b73d10

Please sign in to comment.