Skip to content

Commit 5cec341

Browse files
committed
Add builder for OpenAPIContract.
Will allow to add new fields to the contract setup without requiring to change or to add new factory methods.
1 parent f7674b6 commit 5cec341

File tree

8 files changed

+501
-54
lines changed

8 files changed

+501
-54
lines changed

src/main/java/io/vertx/openapi/contract/OpenAPIContract.java

Lines changed: 25 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@
3838
@VertxGen
3939
public interface OpenAPIContract {
4040

41+
/**
42+
* Instantiates a new builder for an openapi-contract.
43+
* @param vertx The vert.x instance
44+
* @return A new builder.
45+
*/
46+
static OpenAPIContractBuilder builder(Vertx vertx) {
47+
return new OpenAPIContractBuilder(vertx);
48+
}
49+
4150
/**
4251
* Resolves / dereferences the passed contract and creates an {@link OpenAPIContract} instance.
4352
*
@@ -46,7 +55,7 @@ public interface OpenAPIContract {
4655
* @return A succeeded {@link Future} holding an {@link OpenAPIContract} instance, otherwise a failed {@link Future}.
4756
*/
4857
static Future<OpenAPIContract> from(Vertx vertx, String unresolvedContractPath) {
49-
return readYamlOrJson(vertx, unresolvedContractPath).compose(json -> from(vertx, json));
58+
return builder(vertx).setContract(unresolvedContractPath).build();
5059
}
5160

5261
/**
@@ -57,7 +66,11 @@ static Future<OpenAPIContract> from(Vertx vertx, String unresolvedContractPath)
5766
* @return A succeeded {@link Future} holding an {@link OpenAPIContract} instance, otherwise a failed {@link Future}.
5867
*/
5968
static Future<OpenAPIContract> from(Vertx vertx, JsonObject unresolvedContract) {
60-
return from(vertx, unresolvedContract, emptyMap());
69+
if (unresolvedContract == null)
70+
return Future.failedFuture(OpenAPIContractException.createInvalidContract("Spec must not be null"));
71+
return builder(vertx)
72+
.setContract(unresolvedContract)
73+
.build();
6174
}
6275

6376
/**
@@ -74,15 +87,10 @@ static Future<OpenAPIContract> from(Vertx vertx, JsonObject unresolvedContract)
7487
static Future<OpenAPIContract> from(Vertx vertx, String unresolvedContractPath,
7588
Map<String, String> additionalContractFiles) {
7689

77-
Map<String, Future<JsonObject>> jsonFilesFuture = new HashMap<>();
78-
jsonFilesFuture.put(unresolvedContractPath, readYamlOrJson(vertx, unresolvedContractPath));
79-
additionalContractFiles.forEach((key, value) -> jsonFilesFuture.put(key, readYamlOrJson(vertx, value)));
80-
81-
return Future.all(new ArrayList<>(jsonFilesFuture.values())).compose(compFut -> {
82-
Map<String, JsonObject> resolvedFiles = new HashMap<>();
83-
additionalContractFiles.keySet().forEach(key -> resolvedFiles.put(key, jsonFilesFuture.get(key).result()));
84-
return from(vertx, jsonFilesFuture.get(unresolvedContractPath).result(), resolvedFiles);
85-
});
90+
return builder(vertx)
91+
.setContract(unresolvedContractPath)
92+
.setAdditionalContentFiles(additionalContractFiles)
93+
.build();
8694
}
8795

8896
/**
@@ -98,49 +106,12 @@ static Future<OpenAPIContract> from(Vertx vertx, String unresolvedContractPath,
98106
*/
99107
static Future<OpenAPIContract> from(Vertx vertx, JsonObject unresolvedContract,
100108
Map<String, JsonObject> additionalContractFiles) {
101-
if (unresolvedContract == null) {
102-
return failedFuture(createInvalidContract("Spec must not be null"));
103-
}
104-
105-
OpenAPIVersion version = OpenAPIVersion.fromContract(unresolvedContract);
106-
String baseUri = "app://";
107-
108-
ContextInternal ctx = (ContextInternal) vertx.getOrCreateContext();
109-
Promise<OpenAPIContract> promise = ctx.promise();
110-
111-
version.getRepository(vertx, baseUri)
112-
.compose(repository -> {
113-
List<Future<?>> validationFutures = new ArrayList<>(additionalContractFiles.size());
114-
for (String ref : additionalContractFiles.keySet()) {
115-
// Todo: As soon a more modern Java version is used the validate part could be extracted in a private static
116-
// method and reused below.
117-
JsonObject file = additionalContractFiles.get(ref);
118-
Future<?> validationFuture = version.validateAdditionalContractFile(vertx, repository, file)
119-
.compose(v -> vertx.executeBlocking(() -> repository.dereference(ref, JsonSchema.of(ref, file))));
120-
121-
validationFutures.add(validationFuture);
122-
}
123-
return Future.all(validationFutures).map(repository);
124-
}).compose(repository ->
125-
version.validateContract(vertx, repository, unresolvedContract).compose(res -> {
126-
try {
127-
res.checkValidity();
128-
return version.resolve(vertx, repository, unresolvedContract);
129-
} catch (JsonSchemaValidationException | UnsupportedOperationException e) {
130-
return failedFuture(createInvalidContract(null, e));
131-
}
132-
})
133-
.map(resolvedSpec -> (OpenAPIContract) new OpenAPIContractImpl(resolvedSpec, version, repository))
134-
).recover(e -> {
135-
//Convert any non-openapi exceptions into an OpenAPIContractException
136-
if(e instanceof OpenAPIContractException) {
137-
return failedFuture(e);
138-
}
139-
140-
return failedFuture(createInvalidContract("Found issue in specification for reference: " + e.getMessage(), e));
141-
}).onComplete(promise);
142-
143-
return promise.future();
109+
if (unresolvedContract == null)
110+
return Future.failedFuture(OpenAPIContractException.createInvalidContract("Spec must not be null"));
111+
return builder(vertx)
112+
.setContract(unresolvedContract)
113+
.setAdditionalContent(additionalContractFiles)
114+
.build();
144115
}
145116

146117
/**
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
/*
2+
* Copyright (c) 2025, Lukas Jelonek
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7+
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
8+
*
9+
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10+
*
11+
*/
12+
package io.vertx.openapi.contract;
13+
14+
import io.vertx.codegen.annotations.GenIgnore;
15+
import io.vertx.core.Future;
16+
import io.vertx.core.Promise;
17+
import io.vertx.core.Vertx;
18+
import io.vertx.core.internal.ContextInternal;
19+
import io.vertx.core.json.JsonObject;
20+
import io.vertx.json.schema.JsonSchema;
21+
import io.vertx.json.schema.JsonSchemaValidationException;
22+
import io.vertx.openapi.contract.impl.OpenAPIContractImpl;
23+
import io.vertx.openapi.impl.Utils;
24+
25+
import java.util.ArrayList;
26+
import java.util.HashMap;
27+
import java.util.List;
28+
import java.util.Map;
29+
import java.util.stream.Collectors;
30+
31+
import static io.vertx.core.Future.failedFuture;
32+
import static io.vertx.openapi.contract.OpenAPIContractException.createInvalidContract;
33+
34+
@GenIgnore
35+
public class OpenAPIContractBuilder {
36+
37+
public static class OpenAPIContractBuilderException extends RuntimeException {
38+
public OpenAPIContractBuilderException(String message) {
39+
super(message);
40+
}
41+
}
42+
43+
private final Vertx vertx;
44+
private String contractFile;
45+
private JsonObject contract;
46+
private final Map<String, String> additionalContentFiles = new HashMap<>();
47+
private final Map<String, JsonObject> additionalContent = new HashMap<>();
48+
49+
public OpenAPIContractBuilder(Vertx vertx) {
50+
this.vertx = vertx;
51+
}
52+
53+
/**
54+
* Sets the path to the contract file. Either provide the path to the contract or the parsed contract,
55+
* not both. Overrides the contract set by {@link #contract(JsonObject)}.
56+
*
57+
* @param contractPath The path to the contract file
58+
* @return The builder, for a fluent interface
59+
*/
60+
public OpenAPIContractBuilder setContract(String contractPath) {
61+
this.contractFile = contractPath;
62+
this.contract = null;
63+
return this;
64+
}
65+
66+
/**
67+
* Sets the contract. Either provide the contract or the path to the contract,
68+
* not both. Overrides the contract set by {@link #contract(String)}.
69+
*
70+
* @param contract The parsed contract
71+
* @return The builder, for a fluent interface
72+
*/
73+
public OpenAPIContractBuilder setContract(JsonObject contract) {
74+
this.contract = contract;
75+
this.contractFile = null;
76+
return this;
77+
}
78+
79+
/**
80+
* Puts a contract that is referenced by the main contract. This method can be
81+
* called multiple times to add multiple referenced contracts. Overrides a previously
82+
* added contract, when the same key is used.
83+
*
84+
* @param key The unique key for the contract.
85+
* @param path The path to the contract file.
86+
* @return The builder, for a fluent interface
87+
*/
88+
public OpenAPIContractBuilder putAdditionalContentFile(String key, String path) {
89+
additionalContentFiles.put(key, path);
90+
additionalContent.remove(key);
91+
return this;
92+
}
93+
94+
/**
95+
* Uses the contract files from the provided map to resolve referenced contracts.
96+
* Replaces all previously put contracts by {@link #putAdditionalContentFile(String, String)}.
97+
* If the same key is used also overrides the contracts set by {@link #putAdditionalContent(String, JsonObject)}
98+
* and {@link #setAdditionalContent(Map)}.
99+
*
100+
* @param contractFiles A map that contains all additional contract files.
101+
* @return The builder, for a fluent interface.
102+
*/
103+
public OpenAPIContractBuilder setAdditionalContentFiles(Map<String, String> contractFiles) {
104+
additionalContentFiles.clear();
105+
for (var e : contractFiles.entrySet()) {
106+
putAdditionalContentFile(e.getKey(), e.getValue());
107+
additionalContent.remove(e.getKey());
108+
}
109+
return this;
110+
}
111+
112+
113+
/**
114+
* Adds a contract that is referenced by the main contract. This method can be
115+
* called multiple times to add multiple referenced contracts.
116+
*
117+
* @param key The unique key for the contract.
118+
* @param content The parsed contract.
119+
* @return The builder, for a fluent interface
120+
*/
121+
public OpenAPIContractBuilder putAdditionalContent(String key, JsonObject content) {
122+
additionalContent.put(key, content);
123+
additionalContentFiles.remove(key);
124+
return this;
125+
}
126+
127+
/**
128+
* Uses the contracts from the provided map to resolve referenced contracts.
129+
* Replaces all previously put contracts by {@link #putAdditionalContent(String, JsonObject)}.
130+
* If the same key is used also replaces the contracts set by {@link #putAdditionalContentFile(String, String)}
131+
* and {@link #setAdditionalContentFiles(Map)}.
132+
*
133+
* @param contracts A map that contains all additional contract files.
134+
* @return The builder, for a fluent interface.
135+
*/
136+
public OpenAPIContractBuilder setAdditionalContent(Map<String, JsonObject> contracts) {
137+
additionalContent.clear();
138+
for (var e : contracts.entrySet()) {
139+
putAdditionalContent(e.getKey(), e.getValue());
140+
additionalContentFiles.remove(e.getKey());
141+
}
142+
return this;
143+
}
144+
145+
/**
146+
* Builds the contract.
147+
*
148+
* @return The contract.
149+
*/
150+
public Future<OpenAPIContract> build() {
151+
if (contractFile == null && contract == null) {
152+
return Future.failedFuture(new OpenAPIContractBuilderException("Neither a contract file or a contract is set. One of them must be set."));
153+
}
154+
155+
Future<JsonObject> readContract = contractFile == null
156+
? Future.succeededFuture(contract)
157+
: Utils.readYamlOrJson(vertx, contractFile);
158+
159+
var resolvedContracts = Future
160+
.succeededFuture(additionalContent)
161+
.compose(x -> readContractFiles()
162+
.map(r -> {
163+
var all = new HashMap<>(x);
164+
all.putAll(r);
165+
return all;
166+
}));
167+
168+
return Future.all(readContract, resolvedContracts)
169+
.compose(x -> {
170+
JsonObject contract = x.resultAt(0);
171+
Map<String, JsonObject> other = x.resultAt(1);
172+
return from(contract, other);
173+
});
174+
}
175+
176+
private Future<OpenAPIContract> from(JsonObject unresolvedContract,
177+
Map<String, JsonObject> additionalContractFiles) {
178+
if (unresolvedContract == null) {
179+
return failedFuture(createInvalidContract("Spec must not be null"));
180+
}
181+
182+
OpenAPIVersion version = OpenAPIVersion.fromContract(unresolvedContract);
183+
String baseUri = "app://";
184+
185+
ContextInternal ctx = (ContextInternal) vertx.getOrCreateContext();
186+
Promise<OpenAPIContract> promise = ctx.promise();
187+
188+
version.getRepository(vertx, baseUri)
189+
.compose(repository -> {
190+
List<Future<?>> validationFutures = new ArrayList<>(additionalContractFiles.size());
191+
for (String ref : additionalContractFiles.keySet()) {
192+
// Todo: As soon a more modern Java version is used the validate part could be extracted in a private static
193+
// method and reused below.
194+
JsonObject file = additionalContractFiles.get(ref);
195+
Future<?> validationFuture = version.validateAdditionalContractFile(vertx, repository, file)
196+
.compose(v -> vertx.executeBlocking(() -> repository.dereference(ref, JsonSchema.of(ref, file))));
197+
198+
validationFutures.add(validationFuture);
199+
}
200+
return Future.all(validationFutures).map(repository);
201+
}).compose(repository ->
202+
version.validateContract(vertx, repository, unresolvedContract).compose(res -> {
203+
try {
204+
res.checkValidity();
205+
return version.resolve(vertx, repository, unresolvedContract);
206+
} catch (JsonSchemaValidationException | UnsupportedOperationException e) {
207+
return failedFuture(createInvalidContract(null, e));
208+
}
209+
})
210+
.map(resolvedSpec -> new OpenAPIContractImpl(resolvedSpec, version, repository))
211+
).recover(e -> {
212+
//Convert any non-openapi exceptions into an OpenAPIContractException
213+
if (e instanceof OpenAPIContractException) {
214+
return failedFuture(e);
215+
}
216+
217+
return failedFuture(createInvalidContract("Found issue in specification for reference: " + e.getMessage(), e));
218+
}).onComplete(promise);
219+
220+
return promise.future();
221+
}
222+
223+
private Future<Map<String, JsonObject>> readContractFiles() {
224+
if (additionalContentFiles.isEmpty()) return Future.succeededFuture(Map.of());
225+
226+
var read = new HashMap<String, JsonObject>();
227+
return Future.all(additionalContentFiles.entrySet().stream()
228+
.map(e -> Utils.readYamlOrJson(vertx, e.getValue())
229+
.map(c -> read.put(e.getKey(), c)))
230+
.collect(Collectors.toList()))
231+
.map(ign -> read);
232+
}
233+
234+
}

0 commit comments

Comments
 (0)