Skip to content

Commit ff65bac

Browse files
committed
Add moqu and wiremock module
1 parent adf964d commit ff65bac

File tree

52 files changed

+2068
-1
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+2068
-1
lines changed

client/deployment/pom.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
<version.org.openapitools>7.8.0</version.org.openapitools>
1515
<version.org.slf4j>2.0.16</version.org.slf4j>
1616
<version.com.github.jknack>4.3.1</version.com.github.jknack>
17-
<version.io.swagger.parser>2.1.22</version.io.swagger.parser>
1817
</properties>
1918

2019
<dependencies>

moqu/core/pom.xml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<parent>
7+
<groupId>io.quarkiverse.openapi.generator</groupId>
8+
<artifactId>quarkus-openapi-generator-moqu-parent</artifactId>
9+
<version>3.0.0-SNAPSHOT</version>
10+
</parent>
11+
12+
<artifactId>quarkus-openapi-generator-moqu-core</artifactId>
13+
<name>Quarkus - Openapi Generator - Moqu - Core</name>
14+
15+
<properties>
16+
<commons.io.version>2.16.1</commons.io.version>
17+
</properties>
18+
19+
<dependencies>
20+
<dependency>
21+
<groupId>io.swagger.parser.v3</groupId>
22+
<artifactId>swagger-parser</artifactId>
23+
<version>${version.io.swagger.parser}</version>
24+
</dependency>
25+
<dependency>
26+
<groupId>org.assertj</groupId>
27+
<artifactId>assertj-core</artifactId>
28+
</dependency>
29+
<dependency>
30+
<groupId>org.junit.jupiter</groupId>
31+
<artifactId>junit-jupiter-api</artifactId>
32+
<scope>test</scope>
33+
</dependency>
34+
<dependency>
35+
<groupId>org.junit.jupiter</groupId>
36+
<artifactId>junit-jupiter-params</artifactId>
37+
<scope>test</scope>
38+
</dependency>
39+
<dependency>
40+
<groupId>org.junit.jupiter</groupId>
41+
<artifactId>junit-jupiter-engine</artifactId>
42+
<scope>test</scope>
43+
</dependency>
44+
<dependency>
45+
<groupId>commons-io</groupId>
46+
<artifactId>commons-io</artifactId>
47+
<version>${commons.io.version}</version>
48+
</dependency>
49+
<dependency>
50+
<groupId>org.jboss.logmanager</groupId>
51+
<artifactId>jboss-logmanager</artifactId>
52+
</dependency>
53+
</dependencies>
54+
</project>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package io.quarkiverse.openapi.moqu;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collections;
5+
import java.util.List;
6+
import java.util.Objects;
7+
8+
import io.quarkiverse.openapi.moqu.model.RequestResponsePair;
9+
10+
/**
11+
* Represents a collection of request-response pairs, providing methods to access
12+
* these pairs in an immutable list.
13+
*/
14+
public class Moqu {
15+
16+
private List<RequestResponsePair> requestResponsePairs = new ArrayList<>();
17+
18+
/**
19+
* Constructs a {@code Moqu} instance with the provided list of request-response pairs.
20+
*
21+
* @param requestResponsePairs the list of {@link RequestResponsePair} objects to initialize
22+
* the collection. Must not be {@code null}.
23+
* @throws NullPointerException if {@code requestResponsePairs} is null.
24+
*/
25+
public Moqu(List<RequestResponsePair> requestResponsePairs) {
26+
this.requestResponsePairs = Objects.requireNonNull(requestResponsePairs);
27+
}
28+
29+
/**
30+
* Returns an unmodifiable list of request-response pairs.
31+
*
32+
* @return an immutable list of {@link RequestResponsePair}.
33+
*/
34+
public List<RequestResponsePair> getRequestResponsePairs() {
35+
return Collections.unmodifiableList(requestResponsePairs);
36+
}
37+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package io.quarkiverse.openapi.moqu;
2+
3+
/**
4+
* {@link MoquImporter} aims to convert a specification into a {@link Moqu} model.
5+
* It provides a method to parse the content, typically from an OpenAPI specification,
6+
* and generate a corresponding {@link Moqu} instance.
7+
*/
8+
public interface MoquImporter {
9+
10+
/**
11+
* Parses the provided OpenAPI content and generates a new {@link Moqu} instance.
12+
*
13+
* @param content the OpenAPI content as a string, which will be parsed into a {@link Moqu} model.
14+
* @return a new {@link Moqu} instance based on the provided content.
15+
*/
16+
Moqu parse(String content);
17+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package io.quarkiverse.openapi.moqu;
2+
3+
import java.util.List;
4+
5+
/**
6+
* A generic interface for mapping a {@link Moqu} instance to a list of objects of type {@code T}.
7+
*
8+
* @param <T> the type of objects to which the {@link Moqu} instance will be mapped.
9+
*/
10+
public interface MoquMapper<T> {
11+
12+
/**
13+
* Maps the given {@link Moqu} instance to a list of objects of type {@code T}.
14+
*
15+
* @param moqu the {@link Moqu} instance to be mapped.
16+
* @return a list of mapped objects of type {@code T}.
17+
*/
18+
List<T> map(Moqu moqu);
19+
}
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
package io.quarkiverse.openapi.moqu;
2+
3+
import static io.swagger.v3.parser.util.SchemaTypeUtil.INTEGER_TYPE;
4+
import static io.swagger.v3.parser.util.SchemaTypeUtil.OBJECT_TYPE;
5+
import static io.swagger.v3.parser.util.SchemaTypeUtil.STRING_TYPE;
6+
7+
import java.util.ArrayList;
8+
import java.util.Collections;
9+
import java.util.HashMap;
10+
import java.util.List;
11+
import java.util.Map;
12+
import java.util.Objects;
13+
import java.util.Optional;
14+
import java.util.Set;
15+
import java.util.stream.Collectors;
16+
17+
import org.slf4j.Logger;
18+
import org.slf4j.LoggerFactory;
19+
20+
import com.google.common.base.Strings;
21+
import com.google.common.collect.ArrayListMultimap;
22+
import com.google.common.collect.Multimap;
23+
24+
import io.quarkiverse.openapi.moqu.model.Header;
25+
import io.quarkiverse.openapi.moqu.model.Request;
26+
import io.quarkiverse.openapi.moqu.model.RequestResponsePair;
27+
import io.quarkiverse.openapi.moqu.model.Response;
28+
import io.swagger.v3.oas.models.OpenAPI;
29+
import io.swagger.v3.oas.models.Operation;
30+
import io.swagger.v3.oas.models.PathItem;
31+
import io.swagger.v3.oas.models.examples.Example;
32+
import io.swagger.v3.oas.models.media.MediaType;
33+
import io.swagger.v3.oas.models.media.Schema;
34+
import io.swagger.v3.oas.models.parameters.Parameter;
35+
import io.swagger.v3.oas.models.responses.ApiResponse;
36+
import io.swagger.v3.parser.OpenAPIV3Parser;
37+
import io.swagger.v3.parser.core.models.SwaggerParseResult;
38+
39+
public class OpenAPIMoquImporter implements MoquImporter {
40+
41+
private static final Logger LOGGER = LoggerFactory.getLogger(OpenAPIMoquImporter.class);
42+
private static final String HTTP_HEADER_ACCEPT = "Accept";
43+
private static final String REFERENCE_PREFIX = "#/components/schemas/";
44+
45+
@Override
46+
public Moqu parse(String content) {
47+
48+
SwaggerParseResult swaggerParseResult = new OpenAPIV3Parser().readContents(content);
49+
50+
if (LOGGER.isDebugEnabled()) {
51+
for (String message : swaggerParseResult.getMessages()) {
52+
LOGGER.debug("[context:SwaggerParseResult] {}", message);
53+
}
54+
}
55+
56+
OpenAPI openAPI = swaggerParseResult.getOpenAPI();
57+
58+
if (Objects.isNull(openAPI)) {
59+
throw new IllegalArgumentException("Cannot parse OpenAPI V3 content: " + content);
60+
}
61+
62+
return new Moqu(
63+
getRequestResponsePairs(openAPI));
64+
}
65+
66+
private List<RequestResponsePair> getRequestResponsePairs(OpenAPI openAPI) {
67+
Map<Request, Response> requestResponsePairs = new HashMap<>();
68+
69+
Map<String, Schema> localSchemas = getSchemas(openAPI);
70+
71+
Set<Map.Entry<String, PathItem>> entries = Optional.ofNullable(openAPI.getPaths())
72+
.orElseThrow(IllegalArgumentException::new)
73+
.entrySet();
74+
75+
for (Map.Entry<String, PathItem> entry : entries) {
76+
77+
for (Map.Entry<PathItem.HttpMethod, Operation> httpMethodOperation : entry.getValue().readOperationsMap()
78+
.entrySet()) {
79+
80+
if (!Objects.isNull(httpMethodOperation.getValue().getResponses())) {
81+
82+
Set<Map.Entry<String, ApiResponse>> statusApiResponses = httpMethodOperation.getValue().getResponses()
83+
.entrySet();
84+
85+
for (Map.Entry<String, ApiResponse> statusApiResponse : statusApiResponses) {
86+
87+
if (Objects.isNull(statusApiResponse.getValue())) {
88+
continue;
89+
}
90+
91+
Map<String, Multimap<String, String>> examplesOnPath = extractParameters(httpMethodOperation.getValue(),
92+
ParameterType.PATH);
93+
94+
requestResponsePairs.putAll(getContentRequestResponsePairs(statusApiResponse, examplesOnPath,
95+
httpMethodOperation.getKey(), entry.getKey(), localSchemas));
96+
}
97+
}
98+
}
99+
}
100+
101+
return requestResponsePairs.entrySet().stream().map(entry -> new RequestResponsePair(entry.getKey(), entry.getValue()))
102+
.collect(Collectors.toList());
103+
}
104+
105+
private Map<String, Schema> getSchemas(OpenAPI openAPI) {
106+
if (openAPI.getComponents() == null) {
107+
return Map.of();
108+
}
109+
return Objects.requireNonNullElse(openAPI.getComponents().getSchemas(), Map.of());
110+
}
111+
112+
private int tryGetStatusCode(Map.Entry<String, ApiResponse> statusApiResponse) {
113+
try {
114+
return Integer.parseInt(statusApiResponse.getKey());
115+
} catch (NumberFormatException e) {
116+
throw new IllegalArgumentException("Invalid status code: " + statusApiResponse.getKey());
117+
}
118+
}
119+
120+
private Map<String, Multimap<String, String>> extractParameters(Operation operation, ParameterType parameterType) {
121+
List<Parameter> parameters = Optional.ofNullable(operation.getParameters()).orElse(Collections.emptyList());
122+
Map<String, Multimap<String, String>> finalParameters = new HashMap<>();
123+
124+
for (Parameter parameter : parameters) {
125+
if (isEligibleForExtraction(parameter, parameterType)) {
126+
127+
Set<String> exampleNames = parameter.getExamples().keySet();
128+
for (String exampleName : exampleNames) {
129+
130+
Example example = parameter.getExamples().get(exampleName);
131+
132+
Object object = example.getValue();
133+
String value = resolveContent(object);
134+
finalParameters.computeIfAbsent(exampleName,
135+
k -> ArrayListMultimap.create()).put(parameter.getName(), value);
136+
}
137+
}
138+
}
139+
140+
return finalParameters;
141+
}
142+
143+
private boolean isEligibleForExtraction(Parameter parameter, ParameterType type) {
144+
return parameter.getIn().equals(type.value()) && !Objects.isNull(parameter.getExamples());
145+
}
146+
147+
private Map<Request, Response> getContentRequestResponsePairs(Map.Entry<String, ApiResponse> statusApiResponse,
148+
Map<String, Multimap<String, String>> parametersOnPath, PathItem.HttpMethod httpMethod, String url,
149+
Map<String, Schema> localSchemas) {
150+
Map<Request, Response> requestResponseMap = new HashMap<>();
151+
152+
ApiResponse apiResponse = statusApiResponse.getValue();
153+
154+
int statusCode = tryGetStatusCode(statusApiResponse);
155+
156+
for (Map.Entry<String, MediaType> entry : apiResponse.getContent().entrySet()) {
157+
String contentType = entry.getKey();
158+
MediaType mediaType = entry.getValue();
159+
Map<String, Example> examples = Optional.ofNullable(mediaType.getExamples()).orElse(Collections.emptyMap());
160+
161+
examples.forEach((exampleName, example) -> {
162+
163+
String content = resolveContent(localSchemas, example);
164+
165+
Response response = new Response(
166+
exampleName,
167+
mediaType,
168+
statusCode,
169+
content,
170+
List.of());
171+
172+
Multimap<String, String> onPath = parametersOnPath.get(exampleName);
173+
List<io.quarkiverse.openapi.moqu.model.Parameter> reqParams = new ArrayList<>();
174+
175+
if (onPath != null) {
176+
for (Map.Entry<String, String> paramEntry : onPath.entries()) {
177+
io.quarkiverse.openapi.moqu.model.Parameter parameter = new io.quarkiverse.openapi.moqu.model.Parameter(
178+
paramEntry.getKey(),
179+
paramEntry.getValue(),
180+
ParameterType.PATH);
181+
reqParams.add(parameter);
182+
}
183+
}
184+
185+
List<io.quarkiverse.openapi.moqu.model.Parameter> parameters = reqParams.stream()
186+
.filter(reqParam -> reqParam.where().equals(ParameterType.PATH)).toList();
187+
String finalUrl = resolveUrlParameters(url, parameters);
188+
Request request = new Request(
189+
finalUrl,
190+
httpMethod.name(),
191+
exampleName,
192+
new Header(HTTP_HEADER_ACCEPT, Set.of(contentType)),
193+
reqParams);
194+
requestResponseMap.put(request, response);
195+
});
196+
}
197+
198+
return requestResponseMap;
199+
}
200+
201+
private String resolveContent(Map<String, Schema> localSchemas, Example example) {
202+
if (!Strings.isNullOrEmpty(example.get$ref())) {
203+
return resolveRef(example.get$ref(), localSchemas);
204+
} else {
205+
return resolveContent(example.getValue());
206+
}
207+
}
208+
209+
private String resolveUrlParameters(String url, List<io.quarkiverse.openapi.moqu.model.Parameter> parameters) {
210+
for (io.quarkiverse.openapi.moqu.model.Parameter parameter : parameters) {
211+
String placeholder = "{%s}".formatted(parameter.key());
212+
url = url.replace(placeholder, parameter.value());
213+
}
214+
return url;
215+
}
216+
217+
private String resolveRef(String ref, Map<String, Schema> localSchemas) {
218+
if (!ref.startsWith(REFERENCE_PREFIX)) {
219+
throw new IllegalArgumentException(
220+
"There is no support for external $ref schemas. Please, configure the %s as local schema"
221+
.formatted(ref));
222+
}
223+
224+
String refName = ref.substring(REFERENCE_PREFIX.length(), ref.length());
225+
226+
Schema schema = localSchemas.get(refName);
227+
228+
if (schema == null) {
229+
throw new IllegalArgumentException("Schema not found: " + refName);
230+
}
231+
232+
return generateResponseBodyFromRefSchema(schema);
233+
}
234+
235+
private String resolveContent(Object object) {
236+
if (object instanceof String) {
237+
return (String) object;
238+
}
239+
if (object instanceof Integer) {
240+
return String.valueOf((Integer) object);
241+
}
242+
throw new IllegalArgumentException("Object is not a String");
243+
}
244+
245+
private static String generateResponseBodyFromRefSchema(final Schema<?> schema) {
246+
String schemaType = Optional.ofNullable(schema.getType()).orElse(OBJECT_TYPE);
247+
return switch (schemaType) {
248+
case STRING_TYPE, INTEGER_TYPE -> (String) schema.getExample();
249+
case OBJECT_TYPE -> SchemaReader.readObjectExample(schema);
250+
default -> "";
251+
};
252+
}
253+
}

0 commit comments

Comments
 (0)