diff --git a/src/it/java/io/weaviate/integration/CollectionsITest.java b/src/it/java/io/weaviate/integration/CollectionsITest.java index 03871d6cf..997fd1b6f 100644 --- a/src/it/java/io/weaviate/integration/CollectionsITest.java +++ b/src/it/java/io/weaviate/integration/CollectionsITest.java @@ -7,6 +7,8 @@ import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.Test; +import com.google.gson.JsonParser; + import io.weaviate.ConcurrentTest; import io.weaviate.client6.v1.api.WeaviateApiException; import io.weaviate.client6.v1.api.WeaviateClient; @@ -63,6 +65,66 @@ public void testCreateGetDelete() throws IOException { Assertions.assertThat(noCollection).as("after delete").isEmpty(); } + @Test + public void testCreateFromJsonAndGetConfigAsJson() throws IOException { + var collectionName = ns("Things"); + + // Act: create from a raw schema document, including a quantizer nested inside a + // "dynamic" index -- configuration CollectionConfig cannot currently round-trip. + client.collections.createFromJson(""" + { + "class": "%s", + "description": "created from raw JSON", + "properties": [ + { "name": "username", "dataType": ["text"] }, + { "name": "age", "dataType": ["int"] } + ], + "vectorConfig": { + "default": { + "vectorIndexType": "dynamic", + "vectorizer": { "none": {} }, + "vectorIndexConfig": { + "threshold": 10000, + "hnsw": { "rq": { "enabled": true, "bits": 8 } } + } + } + } + } + """.formatted(collectionName)); + + // Assert: the server accepted every option, including the ones the typed API + // would have dropped on the way out. + var raw = client.collections.getConfigAsJson(collectionName); + Assertions.assertThat(raw).as("raw schema").get(InstanceOfAssertFactories.STRING) + .contains(collectionName) + .contains("created from raw JSON"); + + var document = JsonParser.parseString(raw.get()).getAsJsonObject(); + Assertions.assertThat(document.get("class").getAsString()).isEqualTo(collectionName); + Assertions.assertThat(document.getAsJsonObject("vectorConfig") + .getAsJsonObject("default").get("vectorIndexType").getAsString()) + .as("dynamic index").isEqualTo("dynamic"); + Assertions.assertThat(document.getAsJsonObject("vectorConfig") + .getAsJsonObject("default").getAsJsonObject("vectorIndexConfig") + .getAsJsonObject("hnsw").getAsJsonObject("rq").get("enabled").getAsBoolean()) + .as("rq survives the raw round-trip").isTrue(); + + // Assert: listAsJson sees it too. + Assertions.assertThat(client.collections.listAsJson()) + .as("full schema").contains(collectionName); + + // Assert: a missing collection is empty rather than an error. + client.collections.delete(collectionName); + Assertions.assertThat(client.collections.getConfigAsJson(collectionName)) + .as("after delete").isEmpty(); + } + + @Test + public void testCreateFromJsonRejectsDocumentWithoutClass() { + Assertions.assertThatThrownBy(() -> client.collections.createFromJson("{ \"description\": \"no name\" }")) + .as("no \"class\" key").isInstanceOf(IllegalArgumentException.class); + } + @Test public void testCrossReferences() throws IOException { // Arrange: Create Owners collection diff --git a/src/main/java/io/weaviate/client6/v1/api/collections/CreateCollectionRequest.java b/src/main/java/io/weaviate/client6/v1/api/collections/CreateCollectionRequest.java index a22957328..cff6a1f3e 100644 --- a/src/main/java/io/weaviate/client6/v1/api/collections/CreateCollectionRequest.java +++ b/src/main/java/io/weaviate/client6/v1/api/collections/CreateCollectionRequest.java @@ -2,15 +2,82 @@ import java.util.Collections; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; + import io.weaviate.client6.v1.internal.json.JSON; import io.weaviate.client6.v1.internal.rest.Endpoint; import io.weaviate.client6.v1.internal.rest.SimpleEndpoint; -public record CreateCollectionRequest(CollectionConfig collection) { - public static final Endpoint _ENDPOINT = new SimpleEndpoint<>( - request -> "POST", - request -> "/schema/", - request -> Collections.emptyMap(), - request -> JSON.serialize(request.collection), - (statusCode, response) -> JSON.deserialize(response, CollectionConfig.class)); +/** + * Create a collection from a {@link CollectionConfig} or from a raw JSON schema + * definition. + * + * @param Type of the payload. A {@code String} is sent verbatim, anything + * else is serialized first. + */ +public record CreateCollectionRequest(T collection) { + /** + * Endpoint which sends the payload to {@code POST /schema}. + * + *

+ * A {@code String} payload is forwarded byte-for-byte, so it may use any + * option the server accepts — including ones this client version does not + * model. Nothing is validated client-side either: a typo in a key surfaces as a + * server error rather than a compile error. + * + *

+ * The server echoes the stored configuration back, which this endpoint discards + * — a raw payload may well describe a collection that + * {@link CollectionConfig} cannot represent. + */ + public static Endpoint, Void> endpoint() { + return SimpleEndpoint.sideEffect( + request -> "POST", + request -> "/schema/", + request -> Collections.emptyMap(), + request -> request.collection instanceof String json + ? json + : JSON.serialize(request.collection)); + } + + /** + * Name of the collection defined by a raw JSON document (its {@code "class"} + * key), which the client needs in order to return a handle for it. + * + *

+ * This is the only part of the document the client reads. Calling it before + * sending doubles as validation: a document that is not usable as a + * {@code POST /schema} payload fails before the request leaves the process + * rather than after a round-trip. + * + * @throws IllegalArgumentException in case the string is not a JSON object or + * does not carry a {@code "class"} name. + */ + public static String collectionNameFromJson(String json) { + if (json == null || json.isBlank()) { + throw new IllegalArgumentException("collection JSON must not be null or blank"); + } + + JsonElement document; + try { + document = JSON.toJsonElement(json); + } catch (JsonParseException e) { + throw new IllegalArgumentException("collection JSON is not valid JSON", e); + } + + if (!document.isJsonObject()) { + throw new IllegalArgumentException("collection JSON must be a JSON object"); + } + + var collectionName = document.getAsJsonObject().get("class"); + if (collectionName == null + || !collectionName.isJsonPrimitive() + || !collectionName.getAsJsonPrimitive().isString() + || collectionName.getAsString().isBlank()) { + throw new IllegalArgumentException( + "collection JSON must have a non-empty string \"class\" key with the collection name"); + } + return collectionName.getAsString(); + } } diff --git a/src/main/java/io/weaviate/client6/v1/api/collections/GetConfigRequest.java b/src/main/java/io/weaviate/client6/v1/api/collections/GetConfigRequest.java index d4c73aa58..c096d206e 100644 --- a/src/main/java/io/weaviate/client6/v1/api/collections/GetConfigRequest.java +++ b/src/main/java/io/weaviate/client6/v1/api/collections/GetConfigRequest.java @@ -3,15 +3,24 @@ import java.util.Collections; import java.util.Optional; -import io.weaviate.client6.v1.internal.json.JSON; import io.weaviate.client6.v1.internal.rest.Endpoint; import io.weaviate.client6.v1.internal.rest.OptionalEndpoint; public record GetConfigRequest(String collectionName) { - public static final Endpoint> _ENDPOINT = OptionalEndpoint - .noBodyOptional( - request -> "GET", - request -> "/schema/" + request.collectionName, - request -> Collections.emptyMap(), - (statusCode, response) -> JSON.deserialize(response, CollectionConfig.class)); + /** + * Endpoint which deserializes the schema document into {@code cls}. + * + *

+ * Pass {@code String.class} to get the response body exactly as the server sent + * it. {@link CollectionConfig} only carries what this client version models, so + * the raw document is what you want when rendering or diffing a schema rather + * than reading individual settings. + */ + public static Endpoint> endpoint(Class cls) { + return OptionalEndpoint.noBodyOptional( + request -> "GET", + request -> "/schema/" + request.collectionName, + request -> Collections.emptyMap(), + cls); + } } diff --git a/src/main/java/io/weaviate/client6/v1/api/collections/ListCollectionRequest.java b/src/main/java/io/weaviate/client6/v1/api/collections/ListCollectionRequest.java index 9cc6f87e8..804c5460c 100644 --- a/src/main/java/io/weaviate/client6/v1/api/collections/ListCollectionRequest.java +++ b/src/main/java/io/weaviate/client6/v1/api/collections/ListCollectionRequest.java @@ -1,16 +1,26 @@ package io.weaviate.client6.v1.api.collections; import java.util.Collections; -import java.util.List; -import io.weaviate.client6.v1.internal.json.JSON; import io.weaviate.client6.v1.internal.rest.Endpoint; import io.weaviate.client6.v1.internal.rest.SimpleEndpoint; public record ListCollectionRequest() { - public static final Endpoint> _ENDPOINT = SimpleEndpoint.noBody( - request -> "GET", - request -> "/schema", - request -> Collections.emptyMap(), - (gson, response) -> JSON.deserialize(response, ListCollectionResponse.class).collections()); + /** + * Endpoint which deserializes the schema document into {@code cls}. + * + *

+ * Pass {@link ListCollectionResponse} to get the collections mapped onto + * {@link CollectionConfig}, or {@code String.class} for the response body + * exactly as the server sent it. + * + * @see GetConfigRequest#endpoint(Class) + */ + public static Endpoint endpoint(Class cls) { + return SimpleEndpoint.noBody( + request -> "GET", + request -> "/schema", + request -> Collections.emptyMap(), + cls); + } } diff --git a/src/main/java/io/weaviate/client6/v1/api/collections/WeaviateCollectionsClient.java b/src/main/java/io/weaviate/client6/v1/api/collections/WeaviateCollectionsClient.java index e6101f682..cafb58f13 100644 --- a/src/main/java/io/weaviate/client6/v1/api/collections/WeaviateCollectionsClient.java +++ b/src/main/java/io/weaviate/client6/v1/api/collections/WeaviateCollectionsClient.java @@ -179,11 +179,59 @@ public CollectionHandle> create(String collectionName, * or the server being unavailable. */ public CollectionHandle> create(CollectionConfig collection) throws IOException { - this.restTransport.performRequest(new CreateCollectionRequest(collection), - CreateCollectionRequest._ENDPOINT); + this.restTransport.performRequest(new CreateCollectionRequest<>(collection), + CreateCollectionRequest.endpoint()); return use(collection.collectionName()); } + /** + * Create a new Weaviate collection from a raw JSON schema definition. + * + *

+ * The document is forwarded to the server verbatim, so it may use any option + * the server accepts — including ones this client version does not model. + * Nothing is validated client-side: invalid configuration surfaces as a + * {@link WeaviateApiException}. Use {@link #create(CollectionConfig)} when you + * want the type-checked builder API instead. + * + *

{@code
+   * client.collections.createFromJson("""
+   *     {
+   *       "class": "Song",
+   *       "properties": [
+   *         { "name": "title", "dataType": ["text"] },
+   *         { "name": "yearReleased", "dataType": ["int"] }
+   *       ],
+   *       "vectorConfig": {
+   *         "default": {
+   *           "vectorizer": { "none": {} },
+   *           "vectorIndexType": "hnsw"
+   *         }
+   *       }
+   *     }
+   *     """);
+   * }
+ * + * @param json JSON object describing the collection. Must contain a + * {@code "class"} key with the collection name. + * @return Handle for the created collection. + * @throws IllegalArgumentException in case the string is not a JSON object or + * does not carry a {@code "class"} name. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent + * successfully due to a malformed request, a + * networking error or the server being + * unavailable. + */ + public CollectionHandle> createFromJson(String json) throws IOException { + // Read the name -- and reject an unusable document -- before sending anything. + var collectionName = CreateCollectionRequest.collectionNameFromJson(json); + this.restTransport.performRequest(new CreateCollectionRequest<>(json), + CreateCollectionRequest.endpoint()); + return use(collectionName); + } + /** * Fetch Weaviate collection configuration. * @@ -196,7 +244,48 @@ public CollectionHandle> create(CollectionConfig collection) * or the server being unavailable. */ public Optional getConfig(String collectionName) throws IOException { - return this.restTransport.performRequest(new GetConfigRequest(collectionName), GetConfigRequest._ENDPOINT); + return this.restTransport.performRequest(new GetConfigRequest(collectionName), + GetConfigRequest.endpoint(CollectionConfig.class)); + } + + /** + * Fetch a collection's schema exactly as the server returned it. + * + *

+ * {@link #getConfig(String)} maps the response onto {@link CollectionConfig} + * and therefore drops anything this client version does not model. Use this + * method when you need the complete picture — rendering a schema, + * diffing two collections, or reading a module option that has no typed + * accessor yet. + * + * @param collectionName Collection name. + * @return the raw JSON body if a collection with this name exists. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + * @see #createFromJson(String) + */ + public Optional getConfigAsJson(String collectionName) throws IOException { + return this.restTransport.performRequest(new GetConfigRequest(collectionName), + GetConfigRequest.endpoint(String.class)); + } + + /** + * Fetch the schema of all collections exactly as the server returned it. + * + * @return the raw JSON body, an object with a {@code "classes"} array. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + * @see #getConfigAsJson(String) + */ + public String listAsJson() throws IOException { + return this.restTransport.performRequest(new ListCollectionRequest(), + ListCollectionRequest.endpoint(String.class)); } /** @@ -210,7 +299,8 @@ public Optional getConfig(String collectionName) throws IOExce * or the server being unavailable. */ public List list() throws IOException { - return this.restTransport.performRequest(new ListCollectionRequest(), ListCollectionRequest._ENDPOINT); + return this.restTransport.performRequest(new ListCollectionRequest(), + ListCollectionRequest.endpoint(ListCollectionResponse.class)).collections(); } /** diff --git a/src/main/java/io/weaviate/client6/v1/api/collections/WeaviateCollectionsClientAsync.java b/src/main/java/io/weaviate/client6/v1/api/collections/WeaviateCollectionsClientAsync.java index ba80f715e..30328d932 100644 --- a/src/main/java/io/weaviate/client6/v1/api/collections/WeaviateCollectionsClientAsync.java +++ b/src/main/java/io/weaviate/client6/v1/api/collections/WeaviateCollectionsClientAsync.java @@ -147,8 +147,34 @@ public CompletableFuture>> create(Stri * Create a new Weaviate collection with {@link CollectionConfig}. */ public CompletableFuture>> create(CollectionConfig collection) { - return this.restTransport.performRequestAsync(new CreateCollectionRequest(collection), - CreateCollectionRequest._ENDPOINT).thenApply(__ -> use(collection.collectionName())); + return this.restTransport.performRequestAsync(new CreateCollectionRequest<>(collection), + CreateCollectionRequest.endpoint()) + .thenApply(__ -> use(collection.collectionName())); + } + + /** + * Create a new Weaviate collection from a raw JSON schema definition. + * + *

+ * The document is forwarded to the server verbatim, so it may use any option + * the server accepts — including ones this client version does not model. + * Nothing is validated client-side: invalid configuration surfaces as a + * {@link io.weaviate.client6.v1.api.WeaviateApiException}. Use + * {@link #create(CollectionConfig)} when you want the type-checked builder API + * instead. + * + * @param json JSON object describing the collection. Must contain a + * {@code "class"} key with the collection name. + * @throws IllegalArgumentException in case the string is not a JSON object or + * does not carry a {@code "class"} name. + * @see WeaviateCollectionsClient#createFromJson(String) + */ + public CompletableFuture>> createFromJson(String json) { + // Read the name -- and reject an unusable document -- before sending anything. + var collectionName = CreateCollectionRequest.collectionNameFromJson(json); + return this.restTransport.performRequestAsync(new CreateCollectionRequest<>(json), + CreateCollectionRequest.endpoint()) + .thenApply(__ -> use(collectionName)); } /** @@ -157,11 +183,38 @@ public CompletableFuture>> create(Coll * @param collectionName Collection name. */ public CompletableFuture> getConfig(String collectionName) { - return this.restTransport.performRequestAsync(new GetConfigRequest(collectionName), GetConfigRequest._ENDPOINT); + return this.restTransport.performRequestAsync(new GetConfigRequest(collectionName), + GetConfigRequest.endpoint(CollectionConfig.class)); + } + + /** + * Fetch a collection's schema exactly as the server returned it. + * + *

+ * {@link #getConfig(String)} maps the response onto {@link CollectionConfig} + * and therefore drops anything this client version does not model. + * + * @param collectionName Collection name. + * @see WeaviateCollectionsClient#getConfigAsJson(String) + */ + public CompletableFuture> getConfigAsJson(String collectionName) { + return this.restTransport.performRequestAsync(new GetConfigRequest(collectionName), + GetConfigRequest.endpoint(String.class)); + } + + /** + * Fetch the schema of all collections exactly as the server returned it. + * + * @see WeaviateCollectionsClient#listAsJson() + */ + public CompletableFuture listAsJson() { + return this.restTransport.performRequestAsync(new ListCollectionRequest(), + ListCollectionRequest.endpoint(String.class)); } public CompletableFuture> list() { - return this.restTransport.performRequestAsync(new ListCollectionRequest(), ListCollectionRequest._ENDPOINT); + return this.restTransport.performRequestAsync(new ListCollectionRequest(), + ListCollectionRequest.endpoint(ListCollectionResponse.class)).thenApply(ListCollectionResponse::collections); } /** diff --git a/src/main/java/io/weaviate/client6/v1/internal/rest/SimpleEndpoint.java b/src/main/java/io/weaviate/client6/v1/internal/rest/SimpleEndpoint.java index b65dc6130..439dbc7f8 100644 --- a/src/main/java/io/weaviate/client6/v1/internal/rest/SimpleEndpoint.java +++ b/src/main/java/io/weaviate/client6/v1/internal/rest/SimpleEndpoint.java @@ -16,7 +16,20 @@ protected static BiFunction nullResponse() { return NULL_RESPONSE; } + /** + * Response deserializer for {@code cls}. + * + *

+ * {@code String.class} is a special case: the body is returned as the server + * sent it, without being parsed. Endpoints parameterized by response type use + * this to offer a raw-JSON alternative to a model class, which is the only way + * to see fields the client does not model yet. + */ + @SuppressWarnings("unchecked") protected static BiFunction deserializeClass(Class cls) { + if (cls == String.class) { + return (statusCode, response) -> (T) response; + } return (statusCode, response) -> JSON.deserialize(response, cls); } diff --git a/src/test/java/io/weaviate/client6/v1/api/collections/CreateCollectionRequestTest.java b/src/test/java/io/weaviate/client6/v1/api/collections/CreateCollectionRequestTest.java new file mode 100644 index 000000000..d5118e48d --- /dev/null +++ b/src/test/java/io/weaviate/client6/v1/api/collections/CreateCollectionRequestTest.java @@ -0,0 +1,97 @@ +package io.weaviate.client6.v1.api.collections; + +import org.assertj.core.api.Assertions; +import org.junit.Test; + +import com.google.gson.JsonParser; + +import io.weaviate.client6.v1.internal.rest.Endpoint; + +public class CreateCollectionRequestTest { + private static final Endpoint, Void> RAW = // + CreateCollectionRequest.endpoint(); + + private static final Endpoint, Void> TYPED = // + CreateCollectionRequest.endpoint(); + + @Test + public void test_endpoint_passesRawBodyThroughVerbatim() { + // Includes a key the client does not model to prove nothing is stripped. + var json = """ + { "class": "Things", "someFutureOption": { "enabled": true } } + """; + var request = new CreateCollectionRequest<>(json); + + Assertions.assertThat(RAW.method(request)).isEqualTo("POST"); + Assertions.assertThat(RAW.requestUrl(request)).isEqualTo("/schema/"); + Assertions.assertThat(RAW.queryParameters(request)).isEmpty(); + Assertions.assertThat(RAW.body(request)).as("body is forwarded byte-for-byte").isEqualTo(json); + } + + @Test + public void test_endpoint_serializesTypedPayload() { + var request = new CreateCollectionRequest<>(CollectionConfig.of("Things")); + + Assertions.assertThat(TYPED.method(request)).isEqualTo("POST"); + Assertions.assertThat(TYPED.requestUrl(request)).isEqualTo("/schema/"); + + var body = JsonParser.parseString(TYPED.body(request)).getAsJsonObject(); + Assertions.assertThat(body.get("class").getAsString()).isEqualTo("Things"); + } + + @Test + public void test_collectionNameFromJson() { + Assertions.assertThat(CreateCollectionRequest.collectionNameFromJson(""" + { "class": "Things" } + """)).isEqualTo("Things"); + } + + @Test + public void test_rejects_blankInput() { + Assertions.assertThatThrownBy(() -> CreateCollectionRequest.collectionNameFromJson(" ")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("must not be null or blank"); + } + + @Test + public void test_rejects_nullInput() { + Assertions.assertThatThrownBy(() -> CreateCollectionRequest.collectionNameFromJson(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("must not be null or blank"); + } + + @Test + public void test_rejects_malformedJson() { + Assertions.assertThatThrownBy(() -> CreateCollectionRequest.collectionNameFromJson("{ \"class\": ")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("not valid JSON"); + } + + @Test + public void test_rejects_nonObjectJson() { + Assertions.assertThatThrownBy(() -> CreateCollectionRequest.collectionNameFromJson("[ { \"class\": \"Things\" } ]")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("must be a JSON object"); + } + + @Test + public void test_rejects_missingClass() { + Assertions.assertThatThrownBy(() -> CreateCollectionRequest.collectionNameFromJson("{ \"description\": \"x\" }")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("\"class\""); + } + + @Test + public void test_rejects_blankClass() { + Assertions.assertThatThrownBy(() -> CreateCollectionRequest.collectionNameFromJson("{ \"class\": \" \" }")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("\"class\""); + } + + @Test + public void test_rejects_nonStringClass() { + Assertions.assertThatThrownBy(() -> CreateCollectionRequest.collectionNameFromJson("{ \"class\": 42 }")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("\"class\""); + } +} diff --git a/src/test/java/io/weaviate/client6/v1/api/collections/GetConfigRequestTest.java b/src/test/java/io/weaviate/client6/v1/api/collections/GetConfigRequestTest.java new file mode 100644 index 000000000..b83e4dc39 --- /dev/null +++ b/src/test/java/io/weaviate/client6/v1/api/collections/GetConfigRequestTest.java @@ -0,0 +1,86 @@ +package io.weaviate.client6.v1.api.collections; + +import java.util.Optional; + +import org.assertj.core.api.Assertions; +import org.junit.Test; + +import io.weaviate.client6.v1.internal.rest.JsonEndpoint; + +public class GetConfigRequestTest { + @SuppressWarnings("unchecked") + private static final JsonEndpoint> GET_RAW = // + (JsonEndpoint>) GetConfigRequest.endpoint(String.class); + + @SuppressWarnings("unchecked") + private static final JsonEndpoint> GET_TYPED = // + (JsonEndpoint>) GetConfigRequest.endpoint(CollectionConfig.class); + + @SuppressWarnings("unchecked") + private static final JsonEndpoint LIST_RAW = // + (JsonEndpoint) ListCollectionRequest.endpoint(String.class); + + @SuppressWarnings("unchecked") + private static final JsonEndpoint LIST_TYPED = // + (JsonEndpoint) ListCollectionRequest + .endpoint(ListCollectionResponse.class); + + // Includes a key the client does not model, and a quantizer nested inside a + // "dynamic" index, neither of which survives CollectionConfig deserialization. + private static final String RAW_SCHEMA = """ + { + "class": "Things", + "someFutureOption": { "enabled": true }, + "vectorConfig": { "default": { + "vectorIndexType": "dynamic", + "vectorIndexConfig": { "hnsw": { "rq": { "enabled": true, "bits": 8 } } } + }} + } + """; + + @Test + public void test_endpoint() { + var request = new GetConfigRequest("Things"); + + Assertions.assertThat(GET_RAW.method(request)).isEqualTo("GET"); + Assertions.assertThat(GET_RAW.requestUrl(request)).isEqualTo("/schema/Things"); + Assertions.assertThat(GET_RAW.queryParameters(request)).isEmpty(); + Assertions.assertThat(GET_RAW.body(request)).isNull(); + } + + @Test + public void test_stringResponseIsNotParsed() { + Assertions.assertThat(GET_RAW.deserializeResponse(200, RAW_SCHEMA)) + .as("body is returned verbatim").contains(RAW_SCHEMA); + } + + @Test + public void test_typedResponseIsDeserialized() { + Assertions.assertThat(GET_TYPED.deserializeResponse(200, "{ \"class\": \"Things\" }")).get() + .extracting(CollectionConfig::collectionName).isEqualTo("Things"); + } + + @Test + public void test_missingCollectionIsEmpty() { + Assertions.assertThat(GET_RAW.deserializeResponse(404, "{\"error\":[]}")) + .isEqualTo(Optional.empty()); + Assertions.assertThat(GET_RAW.isError(404)) + .as("404 is not an error, it means 'no such collection'").isFalse(); + } + + @Test + public void test_listEndpoint() { + var request = new ListCollectionRequest(); + + Assertions.assertThat(LIST_RAW.method(request)).isEqualTo("GET"); + Assertions.assertThat(LIST_RAW.requestUrl(request)).isEqualTo("/schema"); + Assertions.assertThat(LIST_RAW.queryParameters(request)).isEmpty(); + Assertions.assertThat(LIST_RAW.body(request)).isNull(); + + var raw = "{ \"classes\": [ { \"class\": \"Things\" } ] }"; + Assertions.assertThat(LIST_RAW.deserializeResponse(200, raw)) + .as("body is returned verbatim").isEqualTo(raw); + Assertions.assertThat(LIST_TYPED.deserializeResponse(200, raw).collections()) + .extracting(CollectionConfig::collectionName).containsExactly("Things"); + } +}