diff options
author | Valerij Fredriksen <valerijf@vespa.ai> | 2024-02-26 15:49:47 +0100 |
---|---|---|
committer | Valerij Fredriksen <valerijf@vespa.ai> | 2024-02-26 15:49:47 +0100 |
commit | db245fbde8ab6546520747917ab7ab5b94ec17dc (patch) | |
tree | 113c6bb12dadb4a0815de9f9f33ec3c88aab1395 /container-core | |
parent | e3cb3fe6e1684cbb54b44b4ae26193d3bdd624a1 (diff) |
Move Json to vespajlib
Diffstat (limited to 'container-core')
3 files changed, 1 insertions, 367 deletions
diff --git a/container-core/src/main/java/com/yahoo/restapi/Json.java b/container-core/src/main/java/com/yahoo/restapi/Json.java deleted file mode 100644 index 518dade2d22..00000000000 --- a/container-core/src/main/java/com/yahoo/restapi/Json.java +++ /dev/null @@ -1,241 +0,0 @@ -package com.yahoo.restapi; - -import com.yahoo.slime.Cursor; -import com.yahoo.slime.Inspector; -import com.yahoo.slime.ObjectTraverser; -import com.yahoo.slime.Slime; -import com.yahoo.slime.SlimeUtils; -import com.yahoo.slime.Type; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.OptionalDouble; -import java.util.OptionalLong; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import static com.yahoo.slime.Type.ARRAY; -import static com.yahoo.slime.Type.STRING; - -/** - * A {@link Slime} wrapper that throws {@link RestApiException.BadRequest} on missing members or invalid types. - * - * @author bjorncs - */ -public class Json implements Iterable<Json> { - - private final Inspector inspector; - // Used to keep track of the path to the current node for error messages - private final String path; - - public static Json of(Slime slime) { return of(slime.get()); } - public static Json of(Inspector inspector) { return new Json(inspector, ""); } - public static Json of(String json) { return of(SlimeUtils.jsonToSlime(json)); } - - private Json(Inspector inspector, String path) { - this.inspector = Objects.requireNonNull(inspector); - this.path = Objects.requireNonNull(path); - } - - public Json f(String field) { return field(field); } - public Json field(String field) { - requireType(Type.OBJECT); - return new Json(inspector.field(field), path.isEmpty() ? field : "%s.%s".formatted(path, field)); - } - - public Json a(int index) { return entry(index); } - public Json entry(int index) { - requireType(ARRAY); - return new Json(inspector.entry(index), "%s[%d]".formatted(path, index)); - } - - public int length() { return inspector.children(); } - public boolean has(String field) { return inspector.field(field).valid(); } - public boolean isPresent() { return inspector.valid(); } - public boolean isMissing() { return !isPresent(); } - - public Optional<String> asOptionalString() { return Optional.ofNullable(asString(null)); } - public String asString() { requireType(STRING); return inspector.asString(); } - public String asString(String defaultValue) { - if (isMissing()) return defaultValue; - return asString(); - } - - public OptionalLong asOptionalLong() { return isMissing() ? OptionalLong.empty() : OptionalLong.of(asLong()); } - public long asLong() { requireType(Type.LONG, Type.DOUBLE); return inspector.asLong(); } - public long asLong(long defaultValue) { - if (isMissing()) return defaultValue; - return asLong(); - } - - public OptionalDouble asOptionalDouble() { return isMissing() ? OptionalDouble.empty() : OptionalDouble.of(asDouble()); } - public double asDouble() { requireType(Type.LONG, Type.DOUBLE); return inspector.asDouble(); } - public double asDouble(double defaultValue) { - if (isMissing()) return defaultValue; - return asDouble(); - } - - public Optional<Boolean> asOptionalBool() { return isMissing() ? Optional.empty() : Optional.of(asBool()); } - public boolean asBool() { requireType(Type.BOOL); return inspector.asBool(); } - public boolean asBool(boolean defaultValue) { - if (isMissing()) return defaultValue; - return asBool(); - } - - public List<Json> toList() { - var list = new ArrayList<Json>(length()); - forEachEntry(json -> list.add(json)); - return List.copyOf(list); - } - - public Stream<Json> stream() { return StreamSupport.stream(this.spliterator(), false); } - - public String toJson(boolean pretty) { return SlimeUtils.toJson(inspector, !pretty); } - - public boolean isString() { return inspector.type() == STRING; } - public boolean isArray() { return inspector.type() == ARRAY; } - public boolean isLong() { return inspector.type() == Type.LONG; } - public boolean isDouble() { return inspector.type() == Type.DOUBLE; } - public boolean isBool() { return inspector.type() == Type.BOOL; } - public boolean isNumber() { return isLong() || isDouble(); } - public boolean isObject() { return inspector.type() == Type.OBJECT; } - - @Override - public Iterator<Json> iterator() { - requireType(ARRAY); - return new Iterator<>() { - private int current = 0; - @Override public boolean hasNext() { return current < length(); } - @Override public Json next() { return entry(current++); } - }; - } - - public void forEachField(BiConsumer<String, Json> consumer) { - requireType(Type.OBJECT); - inspector.traverse((ObjectTraverser) (name, inspector) -> { - consumer.accept(name, field(name)); - }); - } - - public void forEachEntry(BiConsumer<Integer, Json> consumer) { - requireType(ARRAY); - for (int i = 0; i < length(); i++) { - consumer.accept(i, entry(i)); - } - } - - public void forEachEntry(Consumer<Json> consumer) { - requireType(ARRAY); - for (int i = 0; i < length(); i++) { - consumer.accept(entry(i)); - } - } - - private void requireType(Type... types) { - requirePresent(); - if (!List.of(types).contains(inspector.type())) throw createInvalidTypeException(types); - } - - private void requirePresent() { if (isMissing()) throw createMissingMemberException(); } - - private RestApiException.BadRequest createInvalidTypeException(Type... expected) { - var expectedTypesString = Arrays.stream(expected).map(this::toString).collect(Collectors.joining("' or '", "'", "'")); - var pathString = path.isEmpty() ? "JSON" : "JSON member '%s'".formatted(path); - return new RestApiException.BadRequest( - "Expected %s to be a %s but got '%s'" - .formatted(pathString, expectedTypesString, toString(inspector.type()))); - } - - private RestApiException.BadRequest createMissingMemberException() { - return new RestApiException.BadRequest(path.isEmpty() ? "Missing JSON" : "Missing JSON member '%s'".formatted(path)); - } - - private String toString(Type type) { - return switch (type) { - case NIX -> "null"; - case BOOL -> "boolean"; - case LONG -> "integer"; - case DOUBLE -> "float"; - case STRING, DATA -> "string"; - case ARRAY -> "array"; - case OBJECT -> "object"; - }; - } - - @Override - public String toString() { - return "Json{" + - "inspector=" + inspector + - ", path='" + path + '\'' + - '}'; - } - - /** Provides a fluent API for building a {@link Slime} instance. */ - public static class Builder { - protected final Cursor cursor; - - public static Builder.Array newArray() { return new Builder.Array(new Slime().setArray()); } - public static Builder.Object newObject() { return new Builder.Object(new Slime().setObject()); } - public static Builder.Object existingObject(Cursor cursor) { return new Builder.Object(cursor); } - - private Builder(Cursor cursor) { this.cursor = cursor; } - - public static class Array extends Builder { - private Array(Cursor cursor) { super(cursor); } - - public Builder.Array add(Builder.Array array) { - SlimeUtils.copyArray(array.cursor, cursor.addArray()); return this; - } - public Builder.Array add(Builder.Object object) { - SlimeUtils.copyObject(object.cursor, cursor.addObject()); return this; - } - public Builder.Array add(Json json) { - SlimeUtils.addValue(json.inspector, cursor.addObject()); return this; - } - /** Note: does not return {@code this}! */ - public Builder.Array addArray() { return new Array(cursor.addArray()); } - /** Note: does not return {@code this}! */ - public Builder.Object addObject() { return new Object(cursor.addObject()); } - - public Builder.Array add(String value) { cursor.addString(value); return this; } - public Builder.Array add(long value) { cursor.addLong(value); return this; } - public Builder.Array add(double value) { cursor.addDouble(value); return this; } - public Builder.Array add(boolean value) { cursor.addBool(value); return this; } - } - - public static class Object extends Builder { - private Object(Cursor cursor) { super(cursor); } - - public Builder.Object set(String field, Builder.Array array) { - SlimeUtils.copyArray(array.cursor, cursor.setArray(field)); return this; - } - public Builder.Object set(String field, Builder.Object object) { - SlimeUtils.copyObject(object.cursor, cursor.setObject(field)); return this; - } - public Builder.Object set(String field, Json json) { - SlimeUtils.setObjectEntry(json.inspector, field, cursor); return this; - } - /** Note: does not return {@code this}! */ - public Builder.Array setArray(String field) { return new Array(cursor.setArray(field)); } - /** Note: does not return {@code this}! */ - public Builder.Object setObject(String field) { return new Object(cursor.setObject(field)); } - - public Builder.Object set(String field, String value) { cursor.setString(field, value); return this; } - public Builder.Object set(String field, long value) { cursor.setLong(field, value); return this; } - public Builder.Object set(String field, double value) { cursor.setDouble(field, value); return this; } - public Builder.Object set(String field, boolean value) { cursor.setBool(field, value); return this; } - public Builder.Object set(String field, BigDecimal value) { cursor.setString(field, value.toPlainString()); return this; } - } - - public Cursor slimeCursor() { return cursor; } - public Json build() { return Json.of(cursor); } - } -} diff --git a/container-core/src/main/java/com/yahoo/restapi/RestApiMappers.java b/container-core/src/main/java/com/yahoo/restapi/RestApiMappers.java index 3204c6c348a..ef3bb1662f2 100644 --- a/container-core/src/main/java/com/yahoo/restapi/RestApiMappers.java +++ b/container-core/src/main/java/com/yahoo/restapi/RestApiMappers.java @@ -1,6 +1,7 @@ // Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.restapi; +import ai.vespa.json.Json; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.yahoo.container.jdisc.HttpResponse; diff --git a/container-core/src/test/java/com/yahoo/restapi/JsonTest.java b/container-core/src/test/java/com/yahoo/restapi/JsonTest.java deleted file mode 100644 index 0ef4872c908..00000000000 --- a/container-core/src/test/java/com/yahoo/restapi/JsonTest.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.yahoo.restapi; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * @author bjorncs - */ -class JsonTest { - - @Test - void parses_json_correctly() { - var text = - """ - { - "string": "bar", - "integer": 42, - "floaty": 8.25, - "bool": true, - "array": [1, 2, 3], - "quux": { - "corge": "grault" - } - } - """; - var json = Json.of(text); - - // Primitive members - assertEquals("bar", json.f("string").asString()); - assertTrue(json.f("string").asOptionalString().isPresent()); - assertEquals("bar", json.f("string").asOptionalString().get()); - assertEquals(42L, json.f("integer").asLong()); - assertEquals(42D, json.f("integer").asDouble()); - assertEquals(8.25D, json.f("floaty").asDouble()); - assertEquals(8L, json.f("floaty").asLong()); - assertTrue(json.f("bool").asBool()); - - // Array member - assertEquals(3, json.f("array").length()); - assertEquals(1L, json.f("array").entry(0).asLong()); - assertEquals(2L, json.f("array").entry(1).asLong()); - assertEquals(3L, json.f("array").entry(2).asLong()); - json.f("array").forEachEntry((i, entry) -> assertEquals(i + 1, entry.asLong())); - int counter = 0; - for (var entry : json.f("array")) { - assertEquals(++counter, entry.asLong()); - } - - // Object member - assertEquals("grault", json.f("quux").f("corge").asString()); - json.f("quux").forEachField((name, child) -> { - assertEquals("corge", name); - assertEquals("grault", child.asString()); - }); - } - - @Test - void throws_on_missing_and_invalid_members() { - var text = - """ - { - "string": "bar" - } - """; - var json = Json.of(text); - - var exception = assertThrows(RestApiException.BadRequest.class, () -> json.f("unknown").asString()); - assertEquals("Missing JSON member 'unknown'", exception.getMessage()); - - exception = assertThrows(RestApiException.BadRequest.class, () -> json.a(0)); - assertEquals("Expected JSON to be a 'array' but got 'object'", exception.getMessage()); - - exception = assertThrows(RestApiException.BadRequest.class, () -> json.f("string").f("unknown")); - assertEquals("Expected JSON member 'string' to be a 'object' but got 'string'", exception.getMessage()); - - exception = assertThrows(RestApiException.BadRequest.class, () -> json.f("string").asLong()); - assertEquals("Expected JSON member 'string' to be a 'integer' or 'float' but got 'string'", exception.getMessage()); - } - - @Test - void fallback_to_default_if_field_missing() { - var text = - """ - { - "string": "bar" - } - """; - var json = Json.of(text); - assertEquals("foo", json.f("unknown").asString("foo")); - assertEquals("foo", json.f("unknown").asOptionalString().orElse("foo")); - assertEquals("bar", json.f("string").asString("foo")); - assertEquals("bar", json.f("string").asOptionalString().orElse("foo")); - } - - @Test - void builds_expected_json() { - var expected = - """ - { - "string": "bar", - "integer": 42, - "floaty": 8.25, - "bool": true, - "array": [ - 1, - 2, - 3 - ], - "quux": { - "corge": "grault" - } - } - """; - var json = Json.Builder.newObject() - .set("string", "bar") - .set("integer", 42) - .set("floaty", 8.25) - .set("bool", true) - .set("array", Json.Builder.newArray().add(1).add(2).add(3)) - .set("quux", Json.Builder.newObject().set("corge", "grault")) - .build() - .toJson(true); - assertEquals(expected, json); - } -} |