diff --git a/src/main/java/io/vertx/openapi/contract/OpenAPIContract.java b/src/main/java/io/vertx/openapi/contract/OpenAPIContract.java index 5ac400d9..75136574 100644 --- a/src/main/java/io/vertx/openapi/contract/OpenAPIContract.java +++ b/src/main/java/io/vertx/openapi/contract/OpenAPIContract.java @@ -12,134 +12,91 @@ package io.vertx.openapi.contract; -import static io.vertx.core.Future.failedFuture; -import static io.vertx.openapi.contract.OpenAPIContractException.createInvalidContract; -import static io.vertx.openapi.impl.Utils.readYamlOrJson; -import static java.util.Collections.emptyMap; - import io.vertx.codegen.annotations.Nullable; import io.vertx.codegen.annotations.VertxGen; import io.vertx.core.Future; -import io.vertx.core.Promise; import io.vertx.core.Vertx; import io.vertx.core.http.HttpMethod; -import io.vertx.core.internal.ContextInternal; import io.vertx.core.json.JsonObject; -import io.vertx.json.schema.JsonSchema; -import io.vertx.json.schema.JsonSchemaValidationException; import io.vertx.json.schema.SchemaRepository; -import io.vertx.openapi.contract.impl.OpenAPIContractImpl; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; @VertxGen public interface OpenAPIContract { + /** + * Instantiates a new builder for an openapi-contract. + * + * @param vertx The vert.x instance + * @return A new builder. + */ + static OpenAPIContractBuilder builder(Vertx vertx) { + return new OpenAPIContractBuilder(vertx); + } + /** * Resolves / dereferences the passed contract and creates an {@link OpenAPIContract} instance. * - * @param vertx The related Vert.x instance. - * @param unresolvedContractPath The path to the unresolved contract. + * @param vertx The related Vert.x instance. + * @param contractPath The path to the contract. * @return A succeeded {@link Future} holding an {@link OpenAPIContract} instance, otherwise a failed {@link Future}. */ - static Future from(Vertx vertx, String unresolvedContractPath) { - return readYamlOrJson(vertx, unresolvedContractPath).compose(json -> from(vertx, json)); + static Future from(Vertx vertx, String contractPath) { + return builder(vertx).setContractPath(contractPath).build(); } /** * Resolves / dereferences the passed contract and creates an {@link OpenAPIContract} instance. * - * @param vertx The related Vert.x instance. - * @param unresolvedContract The unresolved contract. + * @param vertx The related Vert.x instance. + * @param contract The contract. * @return A succeeded {@link Future} holding an {@link OpenAPIContract} instance, otherwise a failed {@link Future}. */ - static Future from(Vertx vertx, JsonObject unresolvedContract) { - return from(vertx, unresolvedContract, emptyMap()); + static Future from(Vertx vertx, JsonObject contract) { + return builder(vertx) + .setContract(contract) + .build(); } /** * Resolves / dereferences the passed contract and creates an {@link OpenAPIContract} instance. *

- * This method can be used in case that the contract is split into several files. These files can be passed in a - * Map that has the reference as key and the path to the file as value. + * This method can be used in case that the contract is split into several parts. These parts can be passed in a + * Map that has the reference as key and the path to the part as value. * - * @param vertx The related Vert.x instance. - * @param unresolvedContractPath The path to the unresolved contract. - * @param additionalContractFiles The additional contract files + * @param vertx The related Vert.x instance. + * @param contractPath The path to the contract. + * @param additionalContractPartPaths The additional contract part paths * @return A succeeded {@link Future} holding an {@link OpenAPIContract} instance, otherwise a failed {@link Future}. */ - static Future from(Vertx vertx, String unresolvedContractPath, - Map additionalContractFiles) { + static Future from(Vertx vertx, String contractPath, + Map additionalContractPartPaths) { - Map> jsonFilesFuture = new HashMap<>(); - jsonFilesFuture.put(unresolvedContractPath, readYamlOrJson(vertx, unresolvedContractPath)); - additionalContractFiles.forEach((key, value) -> jsonFilesFuture.put(key, readYamlOrJson(vertx, value))); - - return Future.all(new ArrayList<>(jsonFilesFuture.values())).compose(compFut -> { - Map resolvedFiles = new HashMap<>(); - additionalContractFiles.keySet().forEach(key -> resolvedFiles.put(key, jsonFilesFuture.get(key).result())); - return from(vertx, jsonFilesFuture.get(unresolvedContractPath).result(), resolvedFiles); - }); + return builder(vertx) + .setContractPath(contractPath) + .setAdditionalContractPartPaths(additionalContractPartPaths) + .build(); } /** * Resolves / dereferences the passed contract and creates an {@link OpenAPIContract} instance. *

- * This method can be used in case that the contract is split into several files. These files can be passed in a - * Map that has the reference as key and the path to the file as value. + * This method can be used in case that the contract is split into several parts. These parts can be passed in a + * Map that has the reference as key and the part as value. * * @param vertx The related Vert.x instance. - * @param unresolvedContract The unresolved contract. - * @param additionalContractFiles The additional contract files + * @param contract The unresolved contract. + * @param additionalContractParts The additional contract parts * @return A succeeded {@link Future} holding an {@link OpenAPIContract} instance, otherwise a failed {@link Future}. */ - static Future from(Vertx vertx, JsonObject unresolvedContract, - Map additionalContractFiles) { - if (unresolvedContract == null) { - return failedFuture(createInvalidContract("Spec must not be null")); - } - - OpenAPIVersion version = OpenAPIVersion.fromContract(unresolvedContract); - String baseUri = "app://"; - - ContextInternal ctx = (ContextInternal) vertx.getOrCreateContext(); - Promise promise = ctx.promise(); - - version.getRepository(vertx, baseUri) - .compose(repository -> { - List> validationFutures = new ArrayList<>(additionalContractFiles.size()); - for (String ref : additionalContractFiles.keySet()) { - // Todo: As soon a more modern Java version is used the validate part could be extracted in a private static - // method and reused below. - JsonObject file = additionalContractFiles.get(ref); - Future validationFuture = version.validateAdditionalContractFile(vertx, repository, file) - .compose(v -> vertx.executeBlocking(() -> repository.dereference(ref, JsonSchema.of(ref, file)))); - - validationFutures.add(validationFuture); - } - return Future.all(validationFutures).map(repository); - }).compose(repository -> version.validateContract(vertx, repository, unresolvedContract).compose(res -> { - try { - res.checkValidity(); - return version.resolve(vertx, repository, unresolvedContract); - } catch (JsonSchemaValidationException | UnsupportedOperationException e) { - return failedFuture(createInvalidContract(null, e)); - } - }) - .map(resolvedSpec -> (OpenAPIContract) new OpenAPIContractImpl(resolvedSpec, version, repository))) - .recover(e -> { - // Convert any non-openapi exceptions into an OpenAPIContractException - if (e instanceof OpenAPIContractException) { - return failedFuture(e); - } - - return failedFuture( - createInvalidContract("Found issue in specification for reference: " + e.getMessage(), e)); - }).onComplete(promise); - - return promise.future(); + static Future from(Vertx vertx, JsonObject contract, + Map additionalContractParts) { + return builder(vertx) + .setContract(contract) + .setAdditionalContractParts(additionalContractParts) + .build(); + } /** diff --git a/src/main/java/io/vertx/openapi/contract/OpenAPIContractBuilder.java b/src/main/java/io/vertx/openapi/contract/OpenAPIContractBuilder.java new file mode 100644 index 00000000..0fc05dd7 --- /dev/null +++ b/src/main/java/io/vertx/openapi/contract/OpenAPIContractBuilder.java @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2025, Lukas Jelonek + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + */ +package io.vertx.openapi.contract; + +import static io.vertx.core.Future.failedFuture; +import static io.vertx.core.Future.succeededFuture; +import static io.vertx.openapi.contract.OpenAPIContractException.createInvalidContract; + +import io.vertx.codegen.annotations.GenIgnore; +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import io.vertx.core.internal.ContextInternal; +import io.vertx.core.json.JsonObject; +import io.vertx.json.schema.JsonSchema; +import io.vertx.json.schema.JsonSchemaValidationException; +import io.vertx.openapi.contract.impl.OpenAPIContractImpl; +import io.vertx.openapi.impl.Utils; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Builder for OpenAPIContracts.
+ *

+ * In the simplest case (you only have one contract) you must either provide a path to your openapi-contract in json + * or yaml format or an already parsed openapi-spec as a {@link JsonObject}. + * See {@link OpenAPIContractBuilder#setContractPath(String)} and {@link OpenAPIContractBuilder#setContract(JsonObject)}. + *
+ * If your contract is split across different files you must load the main contract as described above and additionally + * provide the referenced contract parts. See {@link OpenAPIContractBuilder#putAdditionalContractPartPath(String, String)}, + * {@link OpenAPIContractBuilder#setAdditionalContractPartPaths(Map)}, + * {@link OpenAPIContractBuilder#putAdditionalContractPart(String, JsonObject)}, + * {@link OpenAPIContractBuilder#setAdditionalContractParts(Map)}. + *
+ */ +@GenIgnore +public class OpenAPIContractBuilder { + + public static class OpenAPIContractBuilderException extends RuntimeException { + public OpenAPIContractBuilderException(String message) { + super(message); + } + } + + private final Vertx vertx; + private String contractPath; + private JsonObject contract; + private final Map additionalContractPartPaths = new HashMap<>(); + private final Map additionalContractParts = new HashMap<>(); + + public OpenAPIContractBuilder(Vertx vertx) { + this.vertx = vertx; + } + + /** + * Sets the path to the contract. Either provide the path to the contract or the parsed contract, + * not both. Overrides the contract set by {@link #setContract(JsonObject)}. + * + * @param contractPath The path to the contract + * @return The builder, for a fluent interface + */ + public OpenAPIContractBuilder setContractPath(String contractPath) { + this.contractPath = contractPath; + this.contract = null; + return this; + } + + /** + * Sets the contract. Either provide the contract or the path to the contract, + * not both. Overrides the contract set by {@link #setContractPath(String)}. + * + * @param contract The parsed contract + * @return The builder, for a fluent interface + */ + public OpenAPIContractBuilder setContract(JsonObject contract) { + this.contract = contract; + this.contractPath = null; + return this; + } + + /** + * Puts an additional contract part path that is referenced by the main contract. This method can be + * called multiple times to add multiple referenced additional contract parts. Overrides a previously + * added additional contract part when the same reference is used. + * + * @param ref The unique reference of the additional contract part. + * @param path The path to the contract part. + * @return The builder, for a fluent interface + */ + public OpenAPIContractBuilder putAdditionalContractPartPath(String ref, String path) { + additionalContractPartPaths.put(ref, path); + additionalContractParts.remove(ref); + return this; + } + + /** + * Uses the additional contract part paths from the provided map to resolve referenced contract parts. + * Replaces all previously put additional contract part paths by {@link #putAdditionalContractPartPath(String, String)}. + * If the same reference is used it overrides the additional contract part added by {@link #putAdditionalContractPart(String, JsonObject)} + * or {@link #setAdditionalContractParts(Map)}. + * + * @param contractPartPaths A map that contains all additional contract part paths. + * @return The builder, for a fluent interface. + */ + public OpenAPIContractBuilder setAdditionalContractPartPaths(Map contractPartPaths) { + additionalContractPartPaths.clear(); + for (var e : contractPartPaths.entrySet()) { + putAdditionalContractPartPath(e.getKey(), e.getValue()); + additionalContractParts.remove(e.getKey()); + } + return this; + } + + /** + * Puts an additional contract part that is referenced by the main contract. This method can be + * called multiple times to add multiple referenced additional contract parts. + * + * @param ref The unique reference of the additional contract part. + * @param contractPart The additional contract part. + * @return The builder, for a fluent interface + */ + public OpenAPIContractBuilder putAdditionalContractPart(String ref, JsonObject contractPart) { + additionalContractParts.put(ref, contractPart); + additionalContractPartPaths.remove(ref); + return this; + } + + /** + * Uses the additional contract parts from the provided map to resolve referenced additional contract parts. + * Replaces all previously put additional contract parts by {@link #putAdditionalContractPart(String, JsonObject)}. + * If the same reference is used also replaces the additional contract part paths added by {@link #putAdditionalContractPartPath(String, String)} + * or {@link #setAdditionalContractPartPaths(Map)}. + * + * @param contractParts A map that contains additional contract parts. + * @return The builder, for a fluent interface. + */ + public OpenAPIContractBuilder setAdditionalContractParts(Map contractParts) { + additionalContractParts.clear(); + for (var e : contractParts.entrySet()) { + putAdditionalContractPart(e.getKey(), e.getValue()); + additionalContractPartPaths.remove(e.getKey()); + } + return this; + } + + /** + * Builds the contract. + * + * @return The contract. + */ + public Future build() { + + if (contractPath == null && contract == null) { + return Future.failedFuture(new OpenAPIContractBuilderException( + "Neither a contract path nor a contract is set. One of them must be set.")); + } + + return Future.all(resolveContract(), resolveContractParts()).compose(v -> buildOpenAPIContract()); + } + + private Future buildOpenAPIContract() { + OpenAPIVersion version = OpenAPIVersion.fromContract(contract); + String baseUri = "app://"; + + ContextInternal ctx = (ContextInternal) vertx.getOrCreateContext(); + Promise promise = ctx.promise(); + + version.getRepository(vertx, baseUri) + .compose(repository -> { + var validationFutures = additionalContractParts.entrySet() + .stream() + .map(entry -> version.validateAdditionalContractPart(vertx, repository, entry.getValue()) + .compose(v -> vertx.executeBlocking( + () -> repository.dereference(entry.getKey(), JsonSchema.of(entry.getKey(), entry.getValue()))))) + .collect(Collectors.toList()); + return Future.all(validationFutures).map(repository); + }).compose(repository -> version.validateContract(vertx, repository, contract).compose(res -> { + try { + res.checkValidity(); + return version.resolve(vertx, repository, contract); + } catch (JsonSchemaValidationException | UnsupportedOperationException e) { + return failedFuture(createInvalidContract(null, e)); + } + }) + .map(resolvedSpec -> new OpenAPIContractImpl(resolvedSpec, version, repository))) + .recover(e -> { + // Convert any non-openapi exceptions into an OpenAPIContractException + if (e instanceof OpenAPIContractException) { + return failedFuture(e); + } + return failedFuture( + createInvalidContract("Found issue in specification for reference: " + e.getMessage(), e)); + }).onComplete(promise); + + return promise.future(); + } + + private Future resolveContract() { + if (contractPath == null) { + return succeededFuture(); + } + return Utils.readYamlOrJson(vertx, contractPath).onSuccess(c -> contract = c).mapEmpty(); + } + + private Future resolveContractParts() { + if (additionalContractPartPaths.isEmpty()) { + return succeededFuture(); + } + return Future.all(additionalContractPartPaths.entrySet().stream() + .map(e -> Utils.readYamlOrJson(vertx, e.getValue()) + .map(c -> additionalContractParts.put(e.getKey(), c))) + .collect(Collectors.toList())).mapEmpty(); + } +} diff --git a/src/main/java/io/vertx/openapi/contract/OpenAPIVersion.java b/src/main/java/io/vertx/openapi/contract/OpenAPIVersion.java index a38a801f..fc8afdd6 100644 --- a/src/main/java/io/vertx/openapi/contract/OpenAPIVersion.java +++ b/src/main/java/io/vertx/openapi/contract/OpenAPIVersion.java @@ -85,13 +85,29 @@ public Future validateContract(Vertx vertx, SchemaRepository repo, J /** * Validates additional contract files against the openapi schema. If validations fails, try to validate against the * json schema specifications only. + *
+ * Use {@link #validateAdditionalContractPart(Vertx, SchemaRepository, JsonObject)} instead. * - * @param vertx The related Vert.x instance. - * @param repo The SchemaRepository to do the validations with. - * @param file The additional json contract to validate. + * @param vertx The related Vert.x instance. + * @param repo The SchemaRepository to do the validations with. + * @param file The additional json contract to validate. */ + @Deprecated public Future validateAdditionalContractFile(Vertx vertx, SchemaRepository repo, JsonObject file) { - return vertx.executeBlocking(() -> repo.validator(draft.getIdentifier()).validate(file)) + return this.validateAdditionalContractPart(vertx, repo, file); + + } + + /** + * Validates an additional contract against the openapi schema. If validations fails, try to validate against the + * json schema specifications only. + * + * @param vertx The related Vert.x instance. + * @param repo The SchemaRepository to do the validations with. + * @param part The additional json contract to validate. + */ + public Future validateAdditionalContractPart(Vertx vertx, SchemaRepository repo, JsonObject part) { + return vertx.executeBlocking(() -> repo.validator(draft.getIdentifier()).validate(part)) .compose(this::checkOutputUnit) .mapEmpty(); } diff --git a/src/test/java/io/vertx/tests/contract/OpenAPIContractBuilderTest.java b/src/test/java/io/vertx/tests/contract/OpenAPIContractBuilderTest.java new file mode 100644 index 00000000..8d0122ed --- /dev/null +++ b/src/test/java/io/vertx/tests/contract/OpenAPIContractBuilderTest.java @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2025, Lukas Jelonek + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + */ +package io.vertx.tests.contract; + +import static com.google.common.truth.Truth.assertThat; +import static io.vertx.tests.ResourceHelper.getRelatedTestResourcePath; + +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; +import io.vertx.openapi.contract.OpenAPIContract; +import io.vertx.openapi.contract.OpenAPIContractBuilder; +import io.vertx.openapi.contract.OpenAPIContractException; +import io.vertx.openapi.impl.Utils; +import io.vertx.tests.ResourceHelper; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Tests the OpenAPIContractBuilder. Only tests the different constellations a contract can be built from. + */ +@ExtendWith(VertxExtension.class) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +public class OpenAPIContractBuilderTest { + + private static final String CONTRACT_PATH = "v3.1/petstore.json"; + private static final Path BASE_PATH = + getRelatedTestResourcePath(OpenAPIContractBuilderTest.class).resolve("from_with_path_and_additional_files"); + private static final Path SPLIT_CONTRACT_PATH = BASE_PATH.resolve("petstore.json"); + private static final Path SPLIT_CONTRACT_REFERENCE_PATH = BASE_PATH.resolve("components.json"); + private static final String SPLIT_CONTRACT_REFERENCE_KEY = "https://example.com/petstore"; + + @Test + void should_create_contract_when_valid_contract_path_is_provided(Vertx vertx, VertxTestContext ctx) { + OpenAPIContract.builder(vertx) + .setContractPath(CONTRACT_PATH) + .build() + .onComplete(ctx.succeedingThenComplete()); + } + + @Test + void should_create_contract_when_valid_contract_is_provided(Vertx vertx, VertxTestContext ctx) { + var contract = ResourceHelper.loadJson(vertx, Paths.get(CONTRACT_PATH)); + OpenAPIContract.builder(vertx) + .setContract(contract) + .build() + .onComplete(ctx.succeedingThenComplete()); + } + + @Test + void should_create_contract_when_valid_contract_path_and_additional_contract_paths_are_provided(Vertx vertx, + VertxTestContext ctx) { + OpenAPIContract.builder(vertx) + .setContractPath(SPLIT_CONTRACT_PATH.toString()) + .putAdditionalContractPartPath(SPLIT_CONTRACT_REFERENCE_KEY, SPLIT_CONTRACT_REFERENCE_PATH.toString()) + .build() + .onComplete(ctx.succeedingThenComplete()); + } + + @Test + void should_create_contract_when_valid_contract_and_additional_contract_path_is_provided(Vertx vertx, + VertxTestContext ctx) { + var contract = ResourceHelper.loadJson(vertx, + Paths.get(SPLIT_CONTRACT_PATH.toString())); + OpenAPIContract.builder(vertx) + .setContract(contract) + .putAdditionalContractPartPath(SPLIT_CONTRACT_REFERENCE_KEY, + SPLIT_CONTRACT_REFERENCE_PATH.toString()) + .build() + .onComplete(ctx.succeedingThenComplete()); + } + + @Test + void should_create_contract_when_valid_contract_path_and_additional_contract_part_is_provided(Vertx vertx, + VertxTestContext ctx) { + var components = ResourceHelper.loadJson(vertx, SPLIT_CONTRACT_REFERENCE_PATH); + OpenAPIContract.builder(vertx) + .setContractPath(SPLIT_CONTRACT_PATH.toString()) + .putAdditionalContractPart(SPLIT_CONTRACT_REFERENCE_KEY, components) + .build() + .onComplete(ctx.succeedingThenComplete()); + } + + @Test + void should_fail_when_no_contract_or_contract_path_is_provided(Vertx vertx, VertxTestContext ctx) { + OpenAPIContract.builder(vertx) + .build() + .onComplete(ctx.failing(t -> ctx.verify(() -> { + assertThat(t).isInstanceOf(OpenAPIContractBuilder.OpenAPIContractBuilderException.class); + assertThat(t).hasMessageThat() + .isEqualTo("Neither a contract path nor a contract is set. One of them must be set."); + ctx.completeNow(); + }))); + } + + @Test + void should_fail_when_contract_is_invalid(Vertx vertx, VertxTestContext ctx) { + OpenAPIContract.builder(vertx) + .setContract(JsonObject.of()) + .build() + .onComplete(ctx.failing(t -> ctx.verify(() -> { + assertThat(t).isInstanceOf(OpenAPIContractException.class); + ctx.completeNow(); + }))); + } + + /** + * To test the override mechanisms for additional contracts we use the following setup:
+ * We load a contract and add two additional contracts that exist in two versions, distinguishable + * by their title. Then we replace one of them with the other version and check if the correct + * version has been loaded. + */ + @Nested + @ExtendWith(VertxExtension.class) + class TestSetupOfAdditionalContractParts { + private static final String REF1_ID = "http://example.com/ref1"; + private static final String REF2_ID = "http://example.com/ref2"; + + private final Path BASE_PATH = + getRelatedTestResourcePath(TestSetupOfAdditionalContractParts.class).resolve("builder"); + private final String CONTRACT_FILE = BASE_PATH.resolve("contract.yaml").toString(); + private final String REF1_1_FILE = BASE_PATH.resolve("ref1.1.yaml").toString(); + private final String REF1_2_FILE = BASE_PATH.resolve("ref1.2.yaml").toString(); + private final String REF2_1_FILE = BASE_PATH.resolve("ref2.1.yaml").toString(); + private final String REF2_2_FILE = BASE_PATH.resolve("ref2.2.yaml").toString(); + + private Vertx vertx; + + @BeforeEach + void init(Vertx vertx) { + this.vertx = vertx; + + } + + private JsonObject content(String path) { + return Utils.readYamlOrJson(vertx, path).await(); + } + + @Test + void set_additional_contract_part_should_override_existing_path(Vertx vertx) { + var c = OpenAPIContract.builder(vertx) + .setContractPath(CONTRACT_FILE) + .putAdditionalContractPartPath(REF1_ID, REF1_1_FILE) + .putAdditionalContractPartPath(REF2_ID, REF2_1_FILE) + .setAdditionalContractParts(Map.of(REF1_ID, content(REF1_2_FILE))) + .build() + .await(); + should_have(c, "ref1.2", "ref2.1"); + } + + @Test + void put_additional_contract_part_should_override_existing_path(Vertx vertx) { + var c = OpenAPIContract.builder(vertx) + .setContractPath(CONTRACT_FILE) + .putAdditionalContractPartPath(REF1_ID, REF1_1_FILE) + .putAdditionalContractPartPath(REF2_ID, REF2_1_FILE) + .putAdditionalContractPart(REF1_ID, content(REF1_2_FILE)) + .build() + .await(); + should_have(c, "ref1.2", "ref2.1"); + } + + @Test + void set_additional_contract_path_should_override_existing_contract_part(Vertx vertx) { + var c = OpenAPIContract.builder(vertx) + .setContractPath(CONTRACT_FILE) + .putAdditionalContractPart(REF1_ID, content(REF1_1_FILE)) + .putAdditionalContractPart(REF2_ID, content(REF2_1_FILE)) + .setAdditionalContractPartPaths(Map.of(REF2_ID, REF2_2_FILE)) + .build() + .await(); + should_have(c, "ref1.1", "ref2.2"); + } + + @Test + void put_additional_contract_path_should_override_existing_additional_contract_part(Vertx vertx) { + var c = OpenAPIContract.builder(vertx) + .setContractPath(CONTRACT_FILE) + .putAdditionalContractPart(REF1_ID, content(REF1_1_FILE)) + .putAdditionalContractPart(REF2_ID, content(REF2_1_FILE)) + .putAdditionalContractPartPath(REF2_ID, REF2_2_FILE) + .build() + .await(); + should_have(c, "ref1.1", "ref2.2"); + } + + private void should_have(OpenAPIContract contract, String requestDescription, String responseDescription) { + var c1 = contract.getSchemaRepository().find((REF1_ID)); + var t1 = c1.get("info").getString("title"); + var c2 = contract.getSchemaRepository().find((REF2_ID)); + var t2 = c2.get("info").getString("title"); + assertThat(t1).isEqualTo(requestDescription); + assertThat(t2).isEqualTo(responseDescription); + } + + @Test + void set_additional_contract_parts_should_replace_existing_contract_part(Vertx vertx) { + var c = OpenAPIContract.builder(vertx) + .setContractPath(CONTRACT_FILE) + .putAdditionalContractPart(REF1_ID, content(REF1_1_FILE)) + .putAdditionalContractPart(REF2_ID, content(REF2_1_FILE)) + .setAdditionalContractParts(Map.of(REF2_ID, content(REF2_2_FILE))) + .build() + .await(); + assertThat(c.getSchemaRepository().find(REF1_ID)).isNull(); + } + + @Test + void set_additional_contract_paths_should_replace_existing_contract_paths(Vertx vertx) { + var c = OpenAPIContract.builder(vertx) + .setContractPath(CONTRACT_FILE) + .putAdditionalContractPartPath(REF1_ID, REF1_1_FILE) + .putAdditionalContractPartPath(REF2_ID, REF2_1_FILE) + .setAdditionalContractPartPaths(Map.of(REF2_ID, REF2_2_FILE)) + .build() + .await(); + assertThat(c.getSchemaRepository().find(REF1_ID)).isNull(); + } + } + +} diff --git a/src/test/java/io/vertx/tests/contract/OpenAPIContractTest.java b/src/test/java/io/vertx/tests/contract/OpenAPIContractTest.java index 448a47fe..b4609211 100644 --- a/src/test/java/io/vertx/tests/contract/OpenAPIContractTest.java +++ b/src/test/java/io/vertx/tests/contract/OpenAPIContractTest.java @@ -27,6 +27,7 @@ import io.vertx.junit5.VertxExtension; import io.vertx.junit5.VertxTestContext; import io.vertx.openapi.contract.OpenAPIContract; +import io.vertx.openapi.contract.OpenAPIContractBuilder; import io.vertx.openapi.contract.OpenAPIContractException; import java.io.IOException; import java.nio.file.Files; @@ -93,8 +94,9 @@ void testFromWithPathAndAdditionalContractFiles(String path, Map @Timeout(value = 2, timeUnit = TimeUnit.SECONDS) void testFromFailsInvalidSpecMustNotNull(Vertx vertx, VertxTestContext testContext) { OpenAPIContract.from(vertx, (JsonObject) null).onComplete(testContext.failing(t -> testContext.verify(() -> { - assertThat(t).isInstanceOf(OpenAPIContractException.class); - assertThat(t).hasMessageThat().isEqualTo("The passed OpenAPI contract is invalid: Spec must not be null"); + assertThat(t).isInstanceOf(OpenAPIContractBuilder.OpenAPIContractBuilderException.class); + assertThat(t).hasMessageThat() + .isEqualTo("Neither a contract path nor a contract is set. One of them must be set."); testContext.completeNow(); }))); } diff --git a/src/test/resources/io/vertx/tests/contract/builder/contract.yaml b/src/test/resources/io/vertx/tests/contract/builder/contract.yaml new file mode 100644 index 00000000..59c659fb --- /dev/null +++ b/src/test/resources/io/vertx/tests/contract/builder/contract.yaml @@ -0,0 +1,5 @@ +openapi: 3.1.0 +info: + version: 1.0.0 + title: Builder-test +paths: {} diff --git a/src/test/resources/io/vertx/tests/contract/builder/ref1.1.yaml b/src/test/resources/io/vertx/tests/contract/builder/ref1.1.yaml new file mode 100644 index 00000000..fdf38877 --- /dev/null +++ b/src/test/resources/io/vertx/tests/contract/builder/ref1.1.yaml @@ -0,0 +1,5 @@ +openapi: 3.1.0 +info: + version: 1.0.0 + title: ref1.1 +components: {} diff --git a/src/test/resources/io/vertx/tests/contract/builder/ref1.2.yaml b/src/test/resources/io/vertx/tests/contract/builder/ref1.2.yaml new file mode 100644 index 00000000..32454de4 --- /dev/null +++ b/src/test/resources/io/vertx/tests/contract/builder/ref1.2.yaml @@ -0,0 +1,6 @@ +openapi: 3.1.0 +info: + version: 1.0.0 + title: ref1.2 +components: {} + diff --git a/src/test/resources/io/vertx/tests/contract/builder/ref2.1.yaml b/src/test/resources/io/vertx/tests/contract/builder/ref2.1.yaml new file mode 100644 index 00000000..7c379287 --- /dev/null +++ b/src/test/resources/io/vertx/tests/contract/builder/ref2.1.yaml @@ -0,0 +1,5 @@ +openapi: 3.1.0 +info: + version: 1.0.0 + title: ref2.1 +components: {} diff --git a/src/test/resources/io/vertx/tests/contract/builder/ref2.2.yaml b/src/test/resources/io/vertx/tests/contract/builder/ref2.2.yaml new file mode 100644 index 00000000..3b0513c5 --- /dev/null +++ b/src/test/resources/io/vertx/tests/contract/builder/ref2.2.yaml @@ -0,0 +1,5 @@ +openapi: 3.1.0 +info: + version: 1.0.0 + title: ref2.2 +components: {}