-
Notifications
You must be signed in to change notification settings - Fork 83
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
52 changed files
with
2,068 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<project xmlns="http://maven.apache.org/POM/4.0.0" | ||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<modelVersion>4.0.0</modelVersion> | ||
<parent> | ||
<groupId>io.quarkiverse.openapi.generator</groupId> | ||
<artifactId>quarkus-openapi-generator-moqu-parent</artifactId> | ||
<version>3.0.0-SNAPSHOT</version> | ||
</parent> | ||
|
||
<artifactId>quarkus-openapi-generator-moqu-core</artifactId> | ||
<name>Quarkus - Openapi Generator - Moqu - Core</name> | ||
|
||
<properties> | ||
<commons.io.version>2.16.1</commons.io.version> | ||
</properties> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>io.swagger.parser.v3</groupId> | ||
<artifactId>swagger-parser</artifactId> | ||
<version>${version.io.swagger.parser}</version> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.assertj</groupId> | ||
<artifactId>assertj-core</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.junit.jupiter</groupId> | ||
<artifactId>junit-jupiter-api</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.junit.jupiter</groupId> | ||
<artifactId>junit-jupiter-params</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.junit.jupiter</groupId> | ||
<artifactId>junit-jupiter-engine</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>commons-io</groupId> | ||
<artifactId>commons-io</artifactId> | ||
<version>${commons.io.version}</version> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.jboss.logmanager</groupId> | ||
<artifactId>jboss-logmanager</artifactId> | ||
</dependency> | ||
</dependencies> | ||
</project> |
37 changes: 37 additions & 0 deletions
37
moqu/core/src/main/java/io/quarkiverse/openapi/moqu/Moqu.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package io.quarkiverse.openapi.moqu; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.Objects; | ||
|
||
import io.quarkiverse.openapi.moqu.model.RequestResponsePair; | ||
|
||
/** | ||
* Represents a collection of request-response pairs, providing methods to access | ||
* these pairs in an immutable list. | ||
*/ | ||
public class Moqu { | ||
|
||
private List<RequestResponsePair> requestResponsePairs = new ArrayList<>(); | ||
|
||
/** | ||
* Constructs a {@code Moqu} instance with the provided list of request-response pairs. | ||
* | ||
* @param requestResponsePairs the list of {@link RequestResponsePair} objects to initialize | ||
* the collection. Must not be {@code null}. | ||
* @throws NullPointerException if {@code requestResponsePairs} is null. | ||
*/ | ||
public Moqu(List<RequestResponsePair> requestResponsePairs) { | ||
this.requestResponsePairs = Objects.requireNonNull(requestResponsePairs); | ||
} | ||
|
||
/** | ||
* Returns an unmodifiable list of request-response pairs. | ||
* | ||
* @return an immutable list of {@link RequestResponsePair}. | ||
*/ | ||
public List<RequestResponsePair> getRequestResponsePairs() { | ||
return Collections.unmodifiableList(requestResponsePairs); | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
moqu/core/src/main/java/io/quarkiverse/openapi/moqu/MoquImporter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package io.quarkiverse.openapi.moqu; | ||
|
||
/** | ||
* {@link MoquImporter} aims to convert a specification into a {@link Moqu} model. | ||
* It provides a method to parse the content, typically from an OpenAPI specification, | ||
* and generate a corresponding {@link Moqu} instance. | ||
*/ | ||
public interface MoquImporter { | ||
|
||
/** | ||
* Parses the provided OpenAPI content and generates a new {@link Moqu} instance. | ||
* | ||
* @param content the OpenAPI content as a string, which will be parsed into a {@link Moqu} model. | ||
* @return a new {@link Moqu} instance based on the provided content. | ||
*/ | ||
Moqu parse(String content); | ||
} |
19 changes: 19 additions & 0 deletions
19
moqu/core/src/main/java/io/quarkiverse/openapi/moqu/MoquMapper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package io.quarkiverse.openapi.moqu; | ||
|
||
import java.util.List; | ||
|
||
/** | ||
* A generic interface for mapping a {@link Moqu} instance to a list of objects of type {@code T}. | ||
* | ||
* @param <T> the type of objects to which the {@link Moqu} instance will be mapped. | ||
*/ | ||
public interface MoquMapper<T> { | ||
|
||
/** | ||
* Maps the given {@link Moqu} instance to a list of objects of type {@code T}. | ||
* | ||
* @param moqu the {@link Moqu} instance to be mapped. | ||
* @return a list of mapped objects of type {@code T}. | ||
*/ | ||
List<T> map(Moqu moqu); | ||
} |
253 changes: 253 additions & 0 deletions
253
moqu/core/src/main/java/io/quarkiverse/openapi/moqu/OpenAPIMoquImporter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,253 @@ | ||
package io.quarkiverse.openapi.moqu; | ||
|
||
import static io.swagger.v3.parser.util.SchemaTypeUtil.INTEGER_TYPE; | ||
import static io.swagger.v3.parser.util.SchemaTypeUtil.OBJECT_TYPE; | ||
import static io.swagger.v3.parser.util.SchemaTypeUtil.STRING_TYPE; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Collections; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Objects; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
import java.util.stream.Collectors; | ||
|
||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import com.google.common.base.Strings; | ||
import com.google.common.collect.ArrayListMultimap; | ||
import com.google.common.collect.Multimap; | ||
|
||
import io.quarkiverse.openapi.moqu.model.Header; | ||
import io.quarkiverse.openapi.moqu.model.Request; | ||
import io.quarkiverse.openapi.moqu.model.RequestResponsePair; | ||
import io.quarkiverse.openapi.moqu.model.Response; | ||
import io.swagger.v3.oas.models.OpenAPI; | ||
import io.swagger.v3.oas.models.Operation; | ||
import io.swagger.v3.oas.models.PathItem; | ||
import io.swagger.v3.oas.models.examples.Example; | ||
import io.swagger.v3.oas.models.media.MediaType; | ||
import io.swagger.v3.oas.models.media.Schema; | ||
import io.swagger.v3.oas.models.parameters.Parameter; | ||
import io.swagger.v3.oas.models.responses.ApiResponse; | ||
import io.swagger.v3.parser.OpenAPIV3Parser; | ||
import io.swagger.v3.parser.core.models.SwaggerParseResult; | ||
|
||
public class OpenAPIMoquImporter implements MoquImporter { | ||
|
||
private static final Logger LOGGER = LoggerFactory.getLogger(OpenAPIMoquImporter.class); | ||
private static final String HTTP_HEADER_ACCEPT = "Accept"; | ||
private static final String REFERENCE_PREFIX = "#/components/schemas/"; | ||
|
||
@Override | ||
public Moqu parse(String content) { | ||
|
||
SwaggerParseResult swaggerParseResult = new OpenAPIV3Parser().readContents(content); | ||
|
||
if (LOGGER.isDebugEnabled()) { | ||
for (String message : swaggerParseResult.getMessages()) { | ||
LOGGER.debug("[context:SwaggerParseResult] {}", message); | ||
} | ||
} | ||
|
||
OpenAPI openAPI = swaggerParseResult.getOpenAPI(); | ||
|
||
if (Objects.isNull(openAPI)) { | ||
throw new IllegalArgumentException("Cannot parse OpenAPI V3 content: " + content); | ||
} | ||
|
||
return new Moqu( | ||
getRequestResponsePairs(openAPI)); | ||
} | ||
|
||
private List<RequestResponsePair> getRequestResponsePairs(OpenAPI openAPI) { | ||
Map<Request, Response> requestResponsePairs = new HashMap<>(); | ||
|
||
Map<String, Schema> localSchemas = getSchemas(openAPI); | ||
|
||
Set<Map.Entry<String, PathItem>> entries = Optional.ofNullable(openAPI.getPaths()) | ||
.orElseThrow(IllegalArgumentException::new) | ||
.entrySet(); | ||
|
||
for (Map.Entry<String, PathItem> entry : entries) { | ||
|
||
for (Map.Entry<PathItem.HttpMethod, Operation> httpMethodOperation : entry.getValue().readOperationsMap() | ||
.entrySet()) { | ||
|
||
if (!Objects.isNull(httpMethodOperation.getValue().getResponses())) { | ||
|
||
Set<Map.Entry<String, ApiResponse>> statusApiResponses = httpMethodOperation.getValue().getResponses() | ||
.entrySet(); | ||
|
||
for (Map.Entry<String, ApiResponse> statusApiResponse : statusApiResponses) { | ||
|
||
if (Objects.isNull(statusApiResponse.getValue())) { | ||
continue; | ||
} | ||
|
||
Map<String, Multimap<String, String>> examplesOnPath = extractParameters(httpMethodOperation.getValue(), | ||
ParameterType.PATH); | ||
|
||
requestResponsePairs.putAll(getContentRequestResponsePairs(statusApiResponse, examplesOnPath, | ||
httpMethodOperation.getKey(), entry.getKey(), localSchemas)); | ||
} | ||
} | ||
} | ||
} | ||
|
||
return requestResponsePairs.entrySet().stream().map(entry -> new RequestResponsePair(entry.getKey(), entry.getValue())) | ||
.collect(Collectors.toList()); | ||
} | ||
|
||
private Map<String, Schema> getSchemas(OpenAPI openAPI) { | ||
if (openAPI.getComponents() == null) { | ||
return Map.of(); | ||
} | ||
return Objects.requireNonNullElse(openAPI.getComponents().getSchemas(), Map.of()); | ||
} | ||
|
||
private int tryGetStatusCode(Map.Entry<String, ApiResponse> statusApiResponse) { | ||
try { | ||
return Integer.parseInt(statusApiResponse.getKey()); | ||
} catch (NumberFormatException e) { | ||
throw new IllegalArgumentException("Invalid status code: " + statusApiResponse.getKey()); | ||
} | ||
} | ||
|
||
private Map<String, Multimap<String, String>> extractParameters(Operation operation, ParameterType parameterType) { | ||
List<Parameter> parameters = Optional.ofNullable(operation.getParameters()).orElse(Collections.emptyList()); | ||
Map<String, Multimap<String, String>> finalParameters = new HashMap<>(); | ||
|
||
for (Parameter parameter : parameters) { | ||
if (isEligibleForExtraction(parameter, parameterType)) { | ||
|
||
Set<String> exampleNames = parameter.getExamples().keySet(); | ||
for (String exampleName : exampleNames) { | ||
|
||
Example example = parameter.getExamples().get(exampleName); | ||
|
||
Object object = example.getValue(); | ||
String value = resolveContent(object); | ||
finalParameters.computeIfAbsent(exampleName, | ||
k -> ArrayListMultimap.create()).put(parameter.getName(), value); | ||
} | ||
} | ||
} | ||
|
||
return finalParameters; | ||
} | ||
|
||
private boolean isEligibleForExtraction(Parameter parameter, ParameterType type) { | ||
return parameter.getIn().equals(type.value()) && !Objects.isNull(parameter.getExamples()); | ||
} | ||
|
||
private Map<Request, Response> getContentRequestResponsePairs(Map.Entry<String, ApiResponse> statusApiResponse, | ||
Map<String, Multimap<String, String>> parametersOnPath, PathItem.HttpMethod httpMethod, String url, | ||
Map<String, Schema> localSchemas) { | ||
Map<Request, Response> requestResponseMap = new HashMap<>(); | ||
|
||
ApiResponse apiResponse = statusApiResponse.getValue(); | ||
|
||
int statusCode = tryGetStatusCode(statusApiResponse); | ||
|
||
for (Map.Entry<String, MediaType> entry : apiResponse.getContent().entrySet()) { | ||
String contentType = entry.getKey(); | ||
MediaType mediaType = entry.getValue(); | ||
Map<String, Example> examples = Optional.ofNullable(mediaType.getExamples()).orElse(Collections.emptyMap()); | ||
|
||
examples.forEach((exampleName, example) -> { | ||
|
||
String content = resolveContent(localSchemas, example); | ||
|
||
Response response = new Response( | ||
exampleName, | ||
mediaType, | ||
statusCode, | ||
content, | ||
List.of()); | ||
|
||
Multimap<String, String> onPath = parametersOnPath.get(exampleName); | ||
List<io.quarkiverse.openapi.moqu.model.Parameter> reqParams = new ArrayList<>(); | ||
|
||
if (onPath != null) { | ||
for (Map.Entry<String, String> paramEntry : onPath.entries()) { | ||
io.quarkiverse.openapi.moqu.model.Parameter parameter = new io.quarkiverse.openapi.moqu.model.Parameter( | ||
paramEntry.getKey(), | ||
paramEntry.getValue(), | ||
ParameterType.PATH); | ||
reqParams.add(parameter); | ||
} | ||
} | ||
|
||
List<io.quarkiverse.openapi.moqu.model.Parameter> parameters = reqParams.stream() | ||
.filter(reqParam -> reqParam.where().equals(ParameterType.PATH)).toList(); | ||
String finalUrl = resolveUrlParameters(url, parameters); | ||
Request request = new Request( | ||
finalUrl, | ||
httpMethod.name(), | ||
exampleName, | ||
new Header(HTTP_HEADER_ACCEPT, Set.of(contentType)), | ||
reqParams); | ||
requestResponseMap.put(request, response); | ||
}); | ||
} | ||
|
||
return requestResponseMap; | ||
} | ||
|
||
private String resolveContent(Map<String, Schema> localSchemas, Example example) { | ||
if (!Strings.isNullOrEmpty(example.get$ref())) { | ||
return resolveRef(example.get$ref(), localSchemas); | ||
} else { | ||
return resolveContent(example.getValue()); | ||
} | ||
} | ||
|
||
private String resolveUrlParameters(String url, List<io.quarkiverse.openapi.moqu.model.Parameter> parameters) { | ||
for (io.quarkiverse.openapi.moqu.model.Parameter parameter : parameters) { | ||
String placeholder = "{%s}".formatted(parameter.key()); | ||
url = url.replace(placeholder, parameter.value()); | ||
} | ||
return url; | ||
} | ||
|
||
private String resolveRef(String ref, Map<String, Schema> localSchemas) { | ||
if (!ref.startsWith(REFERENCE_PREFIX)) { | ||
throw new IllegalArgumentException( | ||
"There is no support for external $ref schemas. Please, configure the %s as local schema" | ||
.formatted(ref)); | ||
} | ||
|
||
String refName = ref.substring(REFERENCE_PREFIX.length(), ref.length()); | ||
|
||
Schema schema = localSchemas.get(refName); | ||
|
||
if (schema == null) { | ||
throw new IllegalArgumentException("Schema not found: " + refName); | ||
} | ||
|
||
return generateResponseBodyFromRefSchema(schema); | ||
} | ||
|
||
private String resolveContent(Object object) { | ||
if (object instanceof String) { | ||
return (String) object; | ||
} | ||
if (object instanceof Integer) { | ||
return String.valueOf((Integer) object); | ||
} | ||
throw new IllegalArgumentException("Object is not a String"); | ||
} | ||
|
||
private static String generateResponseBodyFromRefSchema(final Schema<?> schema) { | ||
String schemaType = Optional.ofNullable(schema.getType()).orElse(OBJECT_TYPE); | ||
return switch (schemaType) { | ||
case STRING_TYPE, INTEGER_TYPE -> (String) schema.getExample(); | ||
case OBJECT_TYPE -> SchemaReader.readObjectExample(schema); | ||
default -> ""; | ||
}; | ||
} | ||
} |
Oops, something went wrong.