diff --git a/lib/src/integration/gsonTest/GsonStandaloneNodeTest.java b/lib/src/integration/gsonTest/GsonStandaloneNodeTest.java new file mode 100644 index 00000000..a1f40135 --- /dev/null +++ b/lib/src/integration/gsonTest/GsonStandaloneNodeTest.java @@ -0,0 +1,79 @@ +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import dev.harrel.jsonschema.*; +import dev.harrel.jsonschema.internal.StandaloneNode; +import dev.harrel.jsonschema.providers.GsonModule; +import dev.harrel.jsonschema.providers.GsonNode; +import dev.harrel.jsonschema.util.JsonNodeMock; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +class GsonStandaloneNodeTest extends ProviderTestBundle { + // factory to be promoted to prod code someday + private final JsonNodeFactory standaloneFactory = new JsonNodeFactory() { + private final Gson gson = new GsonBuilder() + .registerTypeHierarchyAdapter(JsonNode.class, new GsonModule.TypeAdapter()) + .create(); + + @Override + public dev.harrel.jsonschema.JsonNode wrap(Object node) { + return switch (node) { + case StandaloneNode sNode when sNode.getJsonPointer().isEmpty() -> sNode; + case StandaloneNode sNode -> sNode.copy(""); + case JsonNode otherNode -> gson.fromJson(gson.toJson(otherNode), JsonNode.class); + case JsonElement providerNode -> gson.fromJson(gson.toJson(providerNode), JsonNode.class); + default -> + throw new IllegalArgumentException("Cannot wrap an instance of " + node.getClass().getName()); + }; + } + + @Override + public dev.harrel.jsonschema.JsonNode create(String rawJson) { + return gson.fromJson(rawJson, JsonNode.class); + } + }; + + @Override + public JsonNodeFactory getJsonNodeFactory() { + return standaloneFactory; + } + + @Test + void shouldInstantiateValidatorFactory() { + new ValidatorFactory(); + } + + @Test + void shouldPassForGsonFactory() { + new ValidatorFactory() + .withJsonNodeFactory(new GsonNode.Factory()) + .validate("{}", "{}"); + } + + @Test + void shouldFailForDefaultFactory() { + assertThatThrownBy(() -> new ValidatorFactory().validate("{}", "{}")) + .isInstanceOf(NoClassDefFoundError.class); + } + + @Test + void shouldWrapForValidArgument() { + JsonElement object = new Gson().toJsonTree(Map.of()); + JsonNode wrap = new GsonNode.Factory().wrap(object); + assertThat(wrap).isNotNull(); + assertThat(wrap.getNodeType()).isEqualTo(SimpleType.OBJECT); + } + + @Test + void shouldWrapRudimentaryJsonNodeImpl() { + JsonNodeFactory factory = getJsonNodeFactory(); + JsonNode node = factory.wrap(new JsonNodeMock()); + assertThat(node.isBoolean()).isTrue(); + assertThat(node.asBoolean()).isFalse(); + } +} diff --git a/lib/src/integration/jacksonTest/JacksonStandaloneNodeTest.java b/lib/src/integration/jacksonTest/JacksonStandaloneNodeTest.java new file mode 100644 index 00000000..d73e99ea --- /dev/null +++ b/lib/src/integration/jacksonTest/JacksonStandaloneNodeTest.java @@ -0,0 +1,94 @@ +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.harrel.jsonschema.JsonNodeFactory; +import dev.harrel.jsonschema.ProviderTestBundle; +import dev.harrel.jsonschema.SimpleType; +import dev.harrel.jsonschema.ValidatorFactory; +import dev.harrel.jsonschema.internal.StandaloneNode; +import dev.harrel.jsonschema.providers.GsonNode; +import dev.harrel.jsonschema.providers.JacksonModule; +import dev.harrel.jsonschema.providers.JacksonNode; +import dev.harrel.jsonschema.util.JsonNodeMock; +import org.assertj.core.api.AssertionsForClassTypes; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.UncheckedIOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class JacksonStandaloneNodeTest extends ProviderTestBundle { + // factory to be promoted to prod code someday + private final JsonNodeFactory standaloneFactory = new JsonNodeFactory() { + private final ObjectMapper mapper = new ObjectMapper().registerModule(new JacksonModule()); + + @Override + public dev.harrel.jsonschema.JsonNode wrap(Object node) { + return switch (node) { + case StandaloneNode sNode when sNode.getJsonPointer().isEmpty() -> sNode; + case StandaloneNode sNode -> sNode.copy(""); + case dev.harrel.jsonschema.JsonNode otherNode -> mapper.convertValue(otherNode, dev.harrel.jsonschema.JsonNode.class); + case JsonNode providerNode -> mapper.convertValue(providerNode, dev.harrel.jsonschema.JsonNode.class); + default -> throw new IllegalArgumentException("Cannot wrap an instance of " + node.getClass().getName()); + }; + } + + @Override + public dev.harrel.jsonschema.JsonNode create(String rawJson) { + try { + return mapper.readValue(rawJson, dev.harrel.jsonschema.JsonNode.class); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + }; + + @Override + public JsonNodeFactory getJsonNodeFactory() { + return standaloneFactory; + } + + @Test + void shouldInstantiateValidatorFactory() { + new ValidatorFactory(); + } + + @Test + void shouldPassForJacksonFactory() { + new ValidatorFactory() + .withJsonNodeFactory(new JacksonNode.Factory()) + .validate("{}", "{}"); + } + + @Test + void shouldPassForDefaultFactory() { + new ValidatorFactory().validate("{}", "{}"); + } + + @Test + void shouldFailForGsonFactory() { + AssertionsForClassTypes.assertThatThrownBy( + () -> new ValidatorFactory() + .withJsonNodeFactory(new GsonNode.Factory()) + .validate("{}", "{}")) + .isInstanceOf(NoClassDefFoundError.class); + } + + @Test + void shouldWrapForValidArgument() throws JsonProcessingException { + JsonNode object = new ObjectMapper().readTree("{}"); + dev.harrel.jsonschema.JsonNode wrap = new JacksonNode.Factory().wrap(object); + assertThat(wrap).isNotNull(); + assertThat(wrap.getNodeType()).isEqualTo(SimpleType.OBJECT); + } + + @Test + void shouldFailWrapForInvalidArgument() { + dev.harrel.jsonschema.JsonNode node = new JsonNodeMock(); + JacksonNode.Factory factory = new JacksonNode.Factory(); + assertThatThrownBy(() -> factory.wrap(node)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/lib/src/integration/jacksonTest/JacksonTest.java b/lib/src/integration/jacksonTest/JacksonTest.java index 8d206a13..226a51b9 100644 --- a/lib/src/integration/jacksonTest/JacksonTest.java +++ b/lib/src/integration/jacksonTest/JacksonTest.java @@ -49,7 +49,7 @@ void shouldFailForGsonFactory() { @Test void shouldWrapForValidArgument() throws JsonProcessingException { JsonNode object = new ObjectMapper().readTree("{}"); - JacksonNode wrap = new JacksonNode.Factory().wrap(object); + dev.harrel.jsonschema.JsonNode wrap = new JacksonNode.Factory().wrap(object); assertThat(wrap).isNotNull(); assertThat(wrap.getNodeType()).isEqualTo(SimpleType.OBJECT); } diff --git a/lib/src/main/java/dev/harrel/jsonschema/Applicators.java b/lib/src/main/java/dev/harrel/jsonschema/Applicators.java index 039326d2..5005cc04 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/Applicators.java +++ b/lib/src/main/java/dev/harrel/jsonschema/Applicators.java @@ -1,5 +1,7 @@ package dev.harrel.jsonschema; +import dev.harrel.jsonschema.internal.StandaloneNode; + import java.math.BigInteger; import java.util.*; import java.util.regex.Pattern; @@ -358,7 +360,7 @@ public Result evaluate(EvaluationContext ctx, JsonNode node) { boolean valid = true; for (String propName : node.asObject().keySet()) { - valid = ctx.resolveInternalRefAndValidate(schemaRef, new StringNode(propName, node.getJsonPointer())) && valid; + valid = ctx.resolveInternalRefAndValidate(schemaRef, new StandaloneNode(node.getJsonPointer(), SimpleType.STRING, propName)) && valid; } return valid ? Result.success() : Result.failure(); } diff --git a/lib/src/main/java/dev/harrel/jsonschema/Evaluators.java b/lib/src/main/java/dev/harrel/jsonschema/Evaluators.java index e0c61fa4..51e68be9 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/Evaluators.java +++ b/lib/src/main/java/dev/harrel/jsonschema/Evaluators.java @@ -48,26 +48,26 @@ class ConstEvaluator implements Evaluator { @Override public Result evaluate(EvaluationContext ctx, JsonNode node) { - boolean valid = canUseNativeEquals(constNode) && canUseNativeEquals(node) ? constNode.equals(node) : JsonNodeUtil.equals(constNode, node); + boolean valid = canUseNativeEquals(constNode, node) ? constNode.equals(node) : JsonNodeUtil.equals(constNode, node); return valid ? Result.success() : Result.formattedFailure("const", node.toPrintableString(), constNode.toPrintableString()); } } class EnumEvaluator implements Evaluator { + private final JsonNode arrayNode; private final Set enumNodes; - private final boolean canUseNativeEquals; EnumEvaluator(JsonNode node) { if (!node.isArray()) { throw new IllegalArgumentException(); } + this.arrayNode = node; this.enumNodes = unmodifiableSet(new LinkedHashSet<>(node.asArray())); - this.canUseNativeEquals = canUseNativeEquals(node); } @Override public Result evaluate(EvaluationContext ctx, JsonNode node) { - if (canUseNativeEquals && canUseNativeEquals(node)) { + if (canUseNativeEquals(arrayNode, node)) { return enumNodes.contains(node) ? Result.success() : Result.formattedFailure("enum", createArgsSupplier(node)); } else { for (JsonNode enumNode : enumNodes) { @@ -387,7 +387,7 @@ public Result evaluate(EvaluationContext ctx, JsonNode node) { } List jsonNodes = node.asArray(); - if (canUseNativeEquals(node)) { + if (canUseNativeEquals(node, node)) { Set parsed = new HashSet<>(); for (int i = 0; i < jsonNodes.size(); i++) { if (!parsed.add(jsonNodes.get(i))) { diff --git a/lib/src/main/java/dev/harrel/jsonschema/JsonNodeFactoryWrapper.java b/lib/src/main/java/dev/harrel/jsonschema/JsonNodeFactoryWrapper.java new file mode 100644 index 00000000..5c88ed81 --- /dev/null +++ b/lib/src/main/java/dev/harrel/jsonschema/JsonNodeFactoryWrapper.java @@ -0,0 +1,25 @@ +package dev.harrel.jsonschema; + +import dev.harrel.jsonschema.internal.StandaloneNode; + +final class JsonNodeFactoryWrapper implements JsonNodeFactory { + private final JsonNodeFactory delegate; + + JsonNodeFactoryWrapper(JsonNodeFactory delegate) { + this.delegate = delegate; + } + + @Override + public JsonNode wrap(Object node) { + if (node instanceof StandaloneNode) { + StandaloneNode sNode = (StandaloneNode) node; + return sNode.getJsonPointer().isEmpty() ? sNode : sNode.copy(""); + } + return delegate.wrap(node); + } + + @Override + public JsonNode create(String rawJson) { + return delegate.create(rawJson); + } +} diff --git a/lib/src/main/java/dev/harrel/jsonschema/StringNode.java b/lib/src/main/java/dev/harrel/jsonschema/StringNode.java deleted file mode 100644 index 55216417..00000000 --- a/lib/src/main/java/dev/harrel/jsonschema/StringNode.java +++ /dev/null @@ -1,59 +0,0 @@ -package dev.harrel.jsonschema; - -import java.math.BigDecimal; -import java.math.BigInteger; -import java.util.List; -import java.util.Map; - -import static java.util.Collections.emptyList; -import static java.util.Collections.emptyMap; - -final class StringNode implements JsonNode { - private final String value; - private final String jsonPointer; - - public StringNode(String value, String jsonPointer) { - this.value = value; - this.jsonPointer = jsonPointer; - } - - @Override - public SimpleType getNodeType() { - return SimpleType.STRING; - } - - @Override - public String getJsonPointer() { - return jsonPointer; - } - - @Override - public boolean asBoolean() { - return false; - } - - @Override - public String asString() { - return value; - } - - @Override - public BigInteger asInteger() { - return null; - } - - @Override - public BigDecimal asNumber() { - return null; - } - - @Override - public List asArray() { - return emptyList(); - } - - @Override - public Map asObject() { - return emptyMap(); - } -} diff --git a/lib/src/main/java/dev/harrel/jsonschema/ValidatorFactory.java b/lib/src/main/java/dev/harrel/jsonschema/ValidatorFactory.java index a7ddeaa7..ee278bc3 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/ValidatorFactory.java +++ b/lib/src/main/java/dev/harrel/jsonschema/ValidatorFactory.java @@ -17,7 +17,7 @@ public final class ValidatorFactory { private final Map dialects = new HashMap<>(Dialects.OFFICIAL_DIALECTS); private Dialect defaultDialect = new Dialects.Draft2020Dialect(); private EvaluatorFactory evaluatorFactory; - private Supplier schemaNodeFactory = JacksonNode.Factory::new; + private Supplier schemaNodeFactory = () -> new JacksonNode.Factory(); // cannot be a method reference due to eager class loading private Supplier instanceNodeFactory = schemaNodeFactory; private SchemaResolver schemaResolver = new DefaultSchemaResolver(); private MessageProvider messageProvider = MessageProvider.fromLocale(Locale.getDefault()); @@ -116,8 +116,8 @@ public ValidatorFactory withJsonNodeFactory(JsonNodeFactory jsonNodeFactory) { public ValidatorFactory withJsonNodeFactories(JsonNodeFactory schemaNodeFactory, JsonNodeFactory instanceNodeFactory) { Objects.requireNonNull(schemaNodeFactory); Objects.requireNonNull(instanceNodeFactory); - this.schemaNodeFactory = () -> schemaNodeFactory; - this.instanceNodeFactory = () -> instanceNodeFactory; + this.schemaNodeFactory = () -> new JsonNodeFactoryWrapper(schemaNodeFactory); + this.instanceNodeFactory = () -> new JsonNodeFactoryWrapper(instanceNodeFactory); return this; } diff --git a/lib/src/main/java/dev/harrel/jsonschema/internal/AbstractJsonNode.java b/lib/src/main/java/dev/harrel/jsonschema/internal/AbstractJsonNode.java index f54aa446..ada1dbcf 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/internal/AbstractJsonNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/internal/AbstractJsonNode.java @@ -124,12 +124,4 @@ private void ensureInitialized() { protected abstract List createArray(); protected abstract Map createObject(); protected abstract SimpleType computeNodeType(T node); - - protected static boolean canConvertToInteger(BigDecimal bigDecimal) { - return bigDecimal.scale() <= 0 || bigDecimal.stripTrailingZeros().scale() <= 0; - } - - protected static HashMap newHashMap(int realCapacity) { - return new HashMap<>((int) Math.ceil(realCapacity / 0.75)); - } } diff --git a/lib/src/main/java/dev/harrel/jsonschema/internal/InternalProviderUtil.java b/lib/src/main/java/dev/harrel/jsonschema/internal/InternalProviderUtil.java index 6c19dd7a..b8885a86 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/internal/InternalProviderUtil.java +++ b/lib/src/main/java/dev/harrel/jsonschema/internal/InternalProviderUtil.java @@ -1,14 +1,27 @@ + package dev.harrel.jsonschema.internal; import dev.harrel.jsonschema.JsonNode; +import java.math.BigDecimal; +import java.util.HashMap; + /** * Internal helper class. */ public final class InternalProviderUtil { private InternalProviderUtil() {} - public static boolean canUseNativeEquals(JsonNode node) { - return node instanceof AbstractJsonNode; + public static boolean canUseNativeEquals(JsonNode n1, JsonNode n2) { + return n1 instanceof StandaloneNode && n2 instanceof StandaloneNode || + n1 instanceof AbstractJsonNode && n2 instanceof AbstractJsonNode; + } + + public static boolean canConvertToInteger(BigDecimal bigDecimal) { + return bigDecimal.scale() <= 0 || bigDecimal.stripTrailingZeros().scale() <= 0; + } + + public static HashMap newHashMap(int realCapacity) { + return new HashMap<>((int) Math.ceil(realCapacity / 0.75)); } } diff --git a/lib/src/main/java/dev/harrel/jsonschema/internal/StandaloneNode.java b/lib/src/main/java/dev/harrel/jsonschema/internal/StandaloneNode.java new file mode 100644 index 00000000..6e75fcf9 --- /dev/null +++ b/lib/src/main/java/dev/harrel/jsonschema/internal/StandaloneNode.java @@ -0,0 +1,121 @@ +package dev.harrel.jsonschema.internal; + +import dev.harrel.jsonschema.JsonNode; +import dev.harrel.jsonschema.SimpleType; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.*; + +import static dev.harrel.jsonschema.internal.InternalProviderUtil.newHashMap; + +/** + * Internal base class for all JSON provider implementations. + * Not part of the contract and not intended for external use. + */ +public final class StandaloneNode implements JsonNode { + private final String jsonPointer; + private final SimpleType type; + private final Object value; + private Object altNumber; + + public StandaloneNode(String jsonPointer, SimpleType type, Object value) { + this(jsonPointer, type, value, null); + } + + public StandaloneNode(String jsonPointer, SimpleType type, Object value, Object altNumber) { + this.jsonPointer = Objects.requireNonNull(jsonPointer); + this.type = Objects.requireNonNull(type); + this.value = value; + this.altNumber = altNumber; + } + + @Override + public String getJsonPointer() { + return jsonPointer; + } + + @Override + public SimpleType getNodeType() { + return type; + } + + @Override + public boolean asBoolean() { + return (Boolean) value; + } + + @Override + public String asString() { + return String.valueOf(value); + } + + @Override + public BigInteger asInteger() { + if (value instanceof BigInteger) { + return (BigInteger) value; + } + if (altNumber == null) { + altNumber = ((BigDecimal) value).toBigInteger(); + } + return (BigInteger) altNumber; + } + + @Override + public BigDecimal asNumber() { + if (value instanceof BigDecimal) { + return (BigDecimal) value; + } + if (altNumber == null) { + altNumber = new BigDecimal((BigInteger) value); + } + return (BigDecimal) altNumber; + } + + @Override + @SuppressWarnings("unchecked") + public List asArray() { + return (List) value; + } + + @Override + @SuppressWarnings("unchecked") + public Map asObject() { + return (Map) value; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof StandaloneNode)) { + return false; + } + StandaloneNode that = (StandaloneNode) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + @SuppressWarnings("unchecked") + public StandaloneNode copy(String jsonPointer) { + if (isArray()) { + List li = (List) value; + List copy = new ArrayList<>(li.size()); + for (int i = 0; i < li.size(); i++) { + copy.add(li.get(i).copy(jsonPointer + "/" + i)); + } + return new StandaloneNode(jsonPointer, type, copy); + } else if (isObject()) { + Map map = (Map) value; + Map copy = newHashMap(map.size()); + for (Map.Entry entry : map.entrySet()) { + copy.put(entry.getKey(), entry.getValue().copy(jsonPointer + "/" + JsonNode.encodeJsonPointer(entry.getKey()))); + } + return new StandaloneNode(jsonPointer, type, copy); + } else { + return new StandaloneNode(jsonPointer, type, value); + } + } +} diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/GsonModule.java b/lib/src/main/java/dev/harrel/jsonschema/providers/GsonModule.java new file mode 100644 index 00000000..1ff75ed7 --- /dev/null +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/GsonModule.java @@ -0,0 +1,116 @@ +package dev.harrel.jsonschema.providers; + +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import dev.harrel.jsonschema.JsonNode; +import dev.harrel.jsonschema.SimpleType; +import dev.harrel.jsonschema.internal.StandaloneNode; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static dev.harrel.jsonschema.internal.InternalProviderUtil.canConvertToInteger; + +// todo doc +public final class GsonModule { + private GsonModule() {} + + public static class TypeAdapter extends com.google.gson.TypeAdapter { + @Override + public void write(JsonWriter out, JsonNode value) throws IOException { + writeNode(out, value); + } + + private void writeNode(JsonWriter out, JsonNode value) throws IOException { + switch (value.getNodeType()) { + case NULL: + out.nullValue(); + return; + case BOOLEAN: + out.value(value.asBoolean()); + return; + case STRING: + out.value(value.asString()); + return; + case INTEGER: + out.value(value.asInteger()); + return; + case NUMBER: + out.value(value.asNumber()); + return; + case ARRAY: + List arr = value.asArray(); + out.beginArray(); + for (int i = 0; i < arr.size(); i++) { + writeNode(out, arr.get(i)); + } + out.endArray(); + return; + case OBJECT: + Map map = value.asObject(); + out.beginObject(); + for (Map.Entry entry : map.entrySet()) { + out.name(entry.getKey()); + writeNode(out, entry.getValue()); + } + out.endObject(); + } + } + + @Override + public JsonNode read(JsonReader in) throws IOException { + return readNode(in, ""); + } + + private JsonNode readNode(JsonReader in, String jsonPointer) throws IOException { + switch (in.peek()) { + case NULL: + in.nextNull(); + return new StandaloneNode(jsonPointer, SimpleType.NULL, null); + case BOOLEAN: + return new StandaloneNode(jsonPointer, SimpleType.BOOLEAN, in.nextBoolean()); + case STRING: + return new StandaloneNode(jsonPointer, SimpleType.STRING, in.nextString()); + case NUMBER: + BigDecimal val = new BigDecimal(in.nextString()); + if (canConvertToInteger(val)) { + return new StandaloneNode(jsonPointer, SimpleType.INTEGER, val.toBigInteger(), val); + } else { + return new StandaloneNode(jsonPointer, SimpleType.NUMBER, val); + } + case BEGIN_ARRAY: + return readArray(in, jsonPointer); + case BEGIN_OBJECT: + return readObject(in, jsonPointer); + default: + throw new IllegalArgumentException("Unexpected token: " + in.peek().name()); + } + } + + private JsonNode readArray(JsonReader in, String jsonPointer) throws IOException { + in.beginArray(); + List arr = new ArrayList<>(); + while (in.peek() != JsonToken.END_ARRAY) { + arr.add(readNode(in, jsonPointer + "/" + arr.size())); + } + in.endArray(); + return new StandaloneNode(jsonPointer, SimpleType.ARRAY, arr); + } + + private JsonNode readObject(JsonReader in, String jsonPointer) throws IOException { + in.beginObject(); + Map obj = new LinkedHashMap<>(); + while (in.peek() != JsonToken.END_OBJECT) { + String name = in.nextName(); + obj.put(name, readNode(in, jsonPointer + "/" + JsonNode.encodeJsonPointer(name))); + } + in.endObject(); + return new StandaloneNode(jsonPointer, SimpleType.OBJECT, obj); + } + } +} diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/GsonNode.java b/lib/src/main/java/dev/harrel/jsonschema/providers/GsonNode.java index f02f243a..e09be32b 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/GsonNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/GsonNode.java @@ -12,6 +12,9 @@ import java.math.BigDecimal; import java.util.*; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.canConvertToInteger; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.newHashMap; + public final class GsonNode extends AbstractJsonNode { private GsonNode(JsonElement node, String jsonPointer) { super(Objects.requireNonNull(node), jsonPointer); diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonModule.java b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonModule.java new file mode 100644 index 00000000..d5866784 --- /dev/null +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonModule.java @@ -0,0 +1,143 @@ +package dev.harrel.jsonschema.providers; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import dev.harrel.jsonschema.JsonNode; +import dev.harrel.jsonschema.SimpleType; +import dev.harrel.jsonschema.internal.StandaloneNode; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.*; + +import static dev.harrel.jsonschema.internal.InternalProviderUtil.canConvertToInteger; + +// todo doc +public final class JacksonModule extends SimpleModule { + + public JacksonModule() { + super(JacksonModule.class.getName(), Version.unknownVersion(), + Collections.singletonMap(JsonNode.class, new Deserializer()), + Collections.singletonList(new Serializer())); + } + + public static final class Serializer extends JsonSerializer { + @Override + public void serialize(JsonNode value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + writeNode(value, gen); + } + + @Override + public Class handledType() { + return JsonNode.class; + } + + private void writeNode(JsonNode value, JsonGenerator gen) throws IOException { + switch (value.getNodeType()) { + case NULL: + gen.writeNull(); + return; + case BOOLEAN: + gen.writeBoolean(value.asBoolean()); + return; + case STRING: + gen.writeString(value.asString()); + return; + case INTEGER: + gen.writeNumber(value.asInteger()); + return; + case NUMBER: + gen.writeNumber(value.asNumber()); + return; + case ARRAY: + List arr = value.asArray(); + gen.writeStartArray(null, arr.size()); + for (int i = 0; i < arr.size(); i++) { + writeNode(arr.get(i), gen); + } + gen.writeEndArray(); + return; + case OBJECT: + Map map = value.asObject(); + gen.writeStartObject(null, map.size()); + for (Map.Entry entry : map.entrySet()) { + gen.writeFieldName(entry.getKey()); + writeNode(entry.getValue(), gen); + } + gen.writeEndObject(); + } + } + } + + public static final class Deserializer extends JsonDeserializer { + @Override + public JsonNode deserialize(JsonParser p, DeserializationContext ctx) throws IOException { + return readNode(p, ""); + } + + /* Using deprecated API to support older versions as well */ + @Override + @SuppressWarnings("deprecation") + public JsonNode getNullValue() { + return new StandaloneNode("", SimpleType.NULL, null); + } + + private JsonNode readNode(JsonParser p, String jsonPointer) throws IOException { + switch (p.getCurrentToken()) { + case VALUE_NULL: + return new StandaloneNode(jsonPointer, SimpleType.NULL, null); + case VALUE_TRUE: + case VALUE_FALSE: + return new StandaloneNode(jsonPointer, SimpleType.BOOLEAN, p.getBooleanValue()); + case VALUE_STRING: + return new StandaloneNode(jsonPointer, SimpleType.STRING, p.getText()); + case VALUE_NUMBER_INT: + return new StandaloneNode(jsonPointer, SimpleType.INTEGER, p.getBigIntegerValue()); + case VALUE_NUMBER_FLOAT: + return readNumber(p, jsonPointer); + case START_ARRAY: + return readArray(p, jsonPointer); + case START_OBJECT: + return readObject(p, jsonPointer); + default: + throw new IllegalArgumentException("Unexpected token: " + p.currentToken().name()); + } + } + + private JsonNode readNumber(JsonParser p, String jsonPointer) throws IOException { + BigDecimal val = p.getDecimalValue(); + if (canConvertToInteger(val)) { + return new StandaloneNode(jsonPointer, SimpleType.INTEGER, val.toBigInteger(), val); + } else { + return new StandaloneNode(jsonPointer, SimpleType.NUMBER, val); + } + } + + private JsonNode readArray(JsonParser p, String jsonPointer) throws IOException { + List arr = new ArrayList<>(); + while (p.nextToken() != JsonToken.END_ARRAY) { + arr.add(readNode(p, jsonPointer + "/" + arr.size())); + } + return new StandaloneNode(jsonPointer, SimpleType.ARRAY, arr); + } + + /* Using deprecated API to support older versions as well */ + @SuppressWarnings("deprecation") + private JsonNode readObject(JsonParser p, String jsonPointer) throws IOException { + Map obj = new LinkedHashMap<>(); + while (p.nextToken() != JsonToken.END_OBJECT) { + String name = p.getCurrentName(); + p.nextToken(); + obj.put(name, readNode(p, jsonPointer + "/" + JsonNode.encodeJsonPointer(name))); + } + return new StandaloneNode(jsonPointer, SimpleType.OBJECT, obj); + } + } +} diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java index 805252e3..6a7c4fec 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/JacksonNode.java @@ -1,7 +1,6 @@ package dev.harrel.jsonschema.providers; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.*; import dev.harrel.jsonschema.JsonNode; import dev.harrel.jsonschema.JsonNodeFactory; import dev.harrel.jsonschema.SimpleType; @@ -11,6 +10,9 @@ import java.math.BigDecimal; import java.util.*; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.canConvertToInteger; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.newHashMap; + public final class JacksonNode extends AbstractJsonNode { private JacksonNode(com.fasterxml.jackson.databind.JsonNode node, String jsonPointer) { super(Objects.requireNonNull(node), jsonPointer); @@ -29,7 +31,9 @@ protected List createArray() { return elements; } + /* Using deprecated API to support older versions as well */ @Override + @SuppressWarnings("deprecation") protected Map createObject() { Map map = newHashMap(node.size()); for (Iterator> iterator = node.fields(); iterator.hasNext(); ) { diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/JakartaJsonNode.java b/lib/src/main/java/dev/harrel/jsonschema/providers/JakartaJsonNode.java index c79ba5c3..692ffcd4 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/JakartaJsonNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/JakartaJsonNode.java @@ -12,6 +12,9 @@ import java.math.BigDecimal; import java.util.*; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.canConvertToInteger; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.newHashMap; + public final class JakartaJsonNode extends AbstractJsonNode { private JakartaJsonNode(JsonValue node, String jsonPointer) { super(Objects.requireNonNull(node), jsonPointer); diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/JettisonNode.java b/lib/src/main/java/dev/harrel/jsonschema/providers/JettisonNode.java index 297929aa..e80de571 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/JettisonNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/JettisonNode.java @@ -9,6 +9,8 @@ import java.util.*; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.newHashMap; + public final class JettisonNode extends SimpleJsonNode { private JettisonNode(Object node, String jsonPointer) { diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/JsonSmartNode.java b/lib/src/main/java/dev/harrel/jsonschema/providers/JsonSmartNode.java index 50842fe8..88d36346 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/JsonSmartNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/JsonSmartNode.java @@ -9,6 +9,7 @@ import java.util.*; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.newHashMap; import static net.minidev.json.parser.JSONParser.MODE_JSON_SIMPLE; public final class JsonSmartNode extends SimpleJsonNode { diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/KotlinxJsonNode.java b/lib/src/main/java/dev/harrel/jsonschema/providers/KotlinxJsonNode.java index 5e64f12e..305e1950 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/KotlinxJsonNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/KotlinxJsonNode.java @@ -9,6 +9,9 @@ import java.math.BigDecimal; import java.util.*; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.canConvertToInteger; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.newHashMap; + public final class KotlinxJsonNode extends AbstractJsonNode { private KotlinxJsonNode(JsonElement node, String jsonPointer) { super(Objects.requireNonNull(node), jsonPointer); diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/OrgJsonNode.java b/lib/src/main/java/dev/harrel/jsonschema/providers/OrgJsonNode.java index b644af55..e2d10551 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/OrgJsonNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/OrgJsonNode.java @@ -8,6 +8,8 @@ import java.util.*; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.newHashMap; + public final class OrgJsonNode extends SimpleJsonNode { private OrgJsonNode(Object node, String jsonPointer) { diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/SimpleJsonNode.java b/lib/src/main/java/dev/harrel/jsonschema/providers/SimpleJsonNode.java index 1db31c5b..33ecf8e0 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/SimpleJsonNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/SimpleJsonNode.java @@ -6,6 +6,8 @@ import java.math.BigDecimal; import java.math.BigInteger; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.canConvertToInteger; + abstract class SimpleJsonNode extends AbstractJsonNode { SimpleJsonNode(Object node, String jsonPointer) { super(node, jsonPointer); diff --git a/lib/src/main/java/dev/harrel/jsonschema/providers/SnakeYamlNode.java b/lib/src/main/java/dev/harrel/jsonschema/providers/SnakeYamlNode.java index 45450fa4..ab729a19 100644 --- a/lib/src/main/java/dev/harrel/jsonschema/providers/SnakeYamlNode.java +++ b/lib/src/main/java/dev/harrel/jsonschema/providers/SnakeYamlNode.java @@ -14,6 +14,9 @@ import java.math.BigInteger; import java.util.*; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.canConvertToInteger; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.newHashMap; + public final class SnakeYamlNode extends AbstractJsonNode { private static final SafeConstructor CONSTR = new SafeConstructor(new LoaderOptions()); private static final SafeConstructor.ConstructYamlBool BOOLEAN_CREATOR = CONSTR.new ConstructYamlBool(); diff --git a/lib/src/test/java/dev/harrel/jsonschema/JsonNodeFactoriesTest.java b/lib/src/test/java/dev/harrel/jsonschema/JsonNodeFactoriesTest.java index 347b0613..5d3d6c28 100644 --- a/lib/src/test/java/dev/harrel/jsonschema/JsonNodeFactoriesTest.java +++ b/lib/src/test/java/dev/harrel/jsonschema/JsonNodeFactoriesTest.java @@ -116,7 +116,7 @@ void shouldUseSchemaFactoryWhenResolvingSchema() { .withSchemaResolver(resolver) .createValidator(); URI uri = URI.create("urn:test"); - JacksonNode jacksonNode = new JacksonNode.Factory().create(""" + JsonNode jacksonNode = new JacksonNode.Factory().create(""" { "$ref": "urn:x" }"""); @@ -133,7 +133,7 @@ void shouldUseSchemaFactoryWhenResolvingMetaSchema() { .withSchemaResolver(resolver) .createValidator(); URI uri = URI.create("urn:test"); - JacksonNode jacksonNode = new JacksonNode.Factory().create("{}"); + JsonNode jacksonNode = new JacksonNode.Factory().create("{}"); Exception exception = catchException(() -> validator.registerSchema(uri, jacksonNode)); assertThat(exception).isInstanceOf(MetaSchemaResolvingException.class); diff --git a/lib/src/test/java/dev/harrel/jsonschema/SchemaRegistryTest.java b/lib/src/test/java/dev/harrel/jsonschema/SchemaRegistryTest.java index 70b6f5ea..0a3f2a92 100644 --- a/lib/src/test/java/dev/harrel/jsonschema/SchemaRegistryTest.java +++ b/lib/src/test/java/dev/harrel/jsonschema/SchemaRegistryTest.java @@ -16,7 +16,7 @@ void shouldRestoreStateProperly() { SchemaRegistry schemaRegistry = new SchemaRegistry(); SchemaParsingContext ctx = new SchemaParsingContext(new MetaSchemaData(new Dialects.Draft2020Dialect()), URI.create("urn:test"), emptyMap()); JacksonNode.Factory factory = new JacksonNode.Factory(); - JacksonNode rootSchemaNode = factory.create(""" + JsonNode rootSchemaNode = factory.create(""" { "properties": { "field": true diff --git a/lib/src/testFixtures/java/dev/harrel/jsonschema/JsonNodeTest.java b/lib/src/testFixtures/java/dev/harrel/jsonschema/JsonNodeTest.java index 6e3d6289..0213f129 100644 --- a/lib/src/testFixtures/java/dev/harrel/jsonschema/JsonNodeTest.java +++ b/lib/src/testFixtures/java/dev/harrel/jsonschema/JsonNodeTest.java @@ -1,5 +1,6 @@ package dev.harrel.jsonschema; +import dev.harrel.jsonschema.util.JsonNodeMock; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -365,7 +366,7 @@ void objectEquals() { void isNotEqualToDifferentType() { JsonNodeFactory nodeFactory = getJsonNodeFactory(); JsonNode node = nodeFactory.create("{}"); - assertThat(node.equals(new StringNode("hello?", ""))).isFalse(); + assertThat(node.equals(new JsonNodeMock())).isFalse(); assertThat(node.equals(new Object())).isFalse(); } } diff --git a/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Deserializer.java b/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Deserializer.java new file mode 100644 index 00000000..0c0b145f --- /dev/null +++ b/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Deserializer.java @@ -0,0 +1,73 @@ +package dev.harrel.json.providers.jackson3; + +import dev.harrel.jsonschema.JsonNode; +import dev.harrel.jsonschema.SimpleType; +import dev.harrel.jsonschema.internal.StandaloneNode; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.ValueDeserializer; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public final class Jackson3Deserializer extends ValueDeserializer { + @Override + public JsonNode deserialize(JsonParser p, DeserializationContext ctx) throws JacksonException { + return readNode(p, ""); + } + + @Override + public JsonNode getNullValue(DeserializationContext ctx) { + return new StandaloneNode("", SimpleType.NULL, null); + } + + private JsonNode readNode(JsonParser p, String jsonPointer) { + return switch (p.currentToken()) { + case VALUE_NULL -> new StandaloneNode(jsonPointer, SimpleType.NULL, null); + case VALUE_TRUE, VALUE_FALSE -> new StandaloneNode(jsonPointer, SimpleType.BOOLEAN, p.getBooleanValue()); + case VALUE_STRING -> new StandaloneNode(jsonPointer, SimpleType.STRING, p.getString()); + case VALUE_NUMBER_INT -> new StandaloneNode(jsonPointer, SimpleType.INTEGER, p.getBigIntegerValue()); + case VALUE_NUMBER_FLOAT -> readNumber(p, jsonPointer); + case START_ARRAY -> readArray(p, jsonPointer); + case START_OBJECT -> readObject(p, jsonPointer); + case NOT_AVAILABLE, + END_OBJECT, + END_ARRAY, + PROPERTY_NAME, + VALUE_EMBEDDED_OBJECT -> throw new UnsupportedOperationException(p.currentToken().name()); // todo better msg + }; + } + + private JsonNode readNumber(JsonParser p, String jsonPointer) { + BigDecimal val = p.getDecimalValue(); + // todo reuse + if (val.scale() <= 0 || val.stripTrailingZeros().scale() <= 0) { + return new StandaloneNode(jsonPointer, SimpleType.INTEGER, val.toBigInteger()); + } else { + return new StandaloneNode(jsonPointer, SimpleType.NUMBER, val); + } + } + + private JsonNode readArray(JsonParser p, String jsonPointer) { + List arr = new ArrayList<>(); + while (p.nextToken() != JsonToken.END_ARRAY) { + arr.add(readNode(p, jsonPointer + "/" + arr.size())); + } + return new StandaloneNode(jsonPointer, SimpleType.ARRAY, arr); + } + + private JsonNode readObject(JsonParser p, String jsonPointer) { + Map obj = new LinkedHashMap<>(); + while (p.nextToken() != JsonToken.END_OBJECT) { + String name = p.currentName(); + p.nextToken(); + obj.put(name, readNode(p, jsonPointer + "/" + JsonNode.encodeJsonPointer(name))); + } + return new StandaloneNode(jsonPointer, SimpleType.OBJECT, obj); + } +} diff --git a/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java b/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java index 3fbb2450..4701fada 100644 --- a/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java +++ b/providers/jackson3/src/main/java/dev/harrel/json/providers/jackson3/Jackson3Node.java @@ -4,13 +4,18 @@ import dev.harrel.jsonschema.JsonNodeFactory; import dev.harrel.jsonschema.SimpleType; import dev.harrel.jsonschema.internal.AbstractJsonNode; +import dev.harrel.jsonschema.internal.StandaloneNode; import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.module.SimpleModule; import java.math.BigDecimal; import java.util.*; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.canConvertToInteger; +import static dev.harrel.jsonschema.internal.InternalProviderUtil.newHashMap; + public final class Jackson3Node extends AbstractJsonNode { private Jackson3Node(tools.jackson.databind.JsonNode node, String jsonPointer) { super(Objects.requireNonNull(node), jsonPointer); diff --git a/providers/jackson3/src/test/java/dev/harrel/json/providers/jackson3/Jackson3Test.java b/providers/jackson3/src/test/java/dev/harrel/json/providers/jackson3/Jackson3Test.java index 890c5e13..84feb761 100644 --- a/providers/jackson3/src/test/java/dev/harrel/json/providers/jackson3/Jackson3Test.java +++ b/providers/jackson3/src/test/java/dev/harrel/json/providers/jackson3/Jackson3Test.java @@ -1,9 +1,6 @@ package dev.harrel.json.providers.jackson3; -import dev.harrel.jsonschema.JsonNodeFactory; -import dev.harrel.jsonschema.ProviderTestBundle; -import dev.harrel.jsonschema.SimpleType; -import dev.harrel.jsonschema.ValidatorFactory; +import dev.harrel.jsonschema.*; import dev.harrel.jsonschema.providers.GsonNode; import dev.harrel.jsonschema.util.JsonNodeMock; import org.assertj.core.api.AssertionsForClassTypes; @@ -43,7 +40,7 @@ void shouldFailForGsonFactory() { @Test void shouldWrapForValidArgument() { tools.jackson.databind.JsonNode object = new ObjectMapper().readTree("{}"); - Jackson3Node wrap = new Jackson3Node.Factory().wrap(object); + JsonNode wrap = new Jackson3Node.Factory().wrap(object); assertThat(wrap).isNotNull(); assertThat(wrap.getNodeType()).isEqualTo(SimpleType.OBJECT); }