Skip to content

Commit

Permalink
Generate interfaces for APIs (as well as impl classes); misc. other i…
Browse files Browse the repository at this point in the history
…mprovements (OpenAPITools#51)
  • Loading branch information
tjquinno authored Sep 1, 2022
1 parent 369fcc9 commit aa18a22
Show file tree
Hide file tree
Showing 11 changed files with 532 additions and 139 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,16 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;

import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.servers.Server;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.openapitools.codegen.CliOption;
Expand All @@ -55,6 +60,8 @@ public class JavaHelidonClientCodegen extends JavaHelidonCommonCodegen {

private final Logger LOGGER = LoggerFactory.getLogger(JavaHelidonClientCodegen.class);

private static final String X_HELIDON_REQUIRED_IMPL_IMPORTS = "x-helidon-requiredImplImports";
private static final String X_HELIDON_IMPL_IMPORTS = "x-helidon-implImports";
public static final String CONFIG_KEY = "configKey";

protected String configKey = null;
Expand Down Expand Up @@ -152,6 +159,15 @@ public void addOperationToGroup(String tag, String resourcePath, Operation opera
}
}

@Override
public String apiFilename(String templateName, String tag) {
if (templateName.contains("_impl")) {
String suffix = apiTemplateFiles().get(templateName);
return apiFileFolder() + File.separator + toApiFilename(tag) + "Impl" + suffix;
}
return super.apiFilename(templateName, tag);
}

@Override
public void processOpts() {
super.processOpts();
Expand Down Expand Up @@ -187,12 +203,24 @@ public void processOpts() {
}
processSupportingFiles(modifiable, unmodifiable);
} else if (isLibrary(HELIDON_SE)) {
// TODO check for SE-specifics and supporting files used for both MP and SE
supportingFiles.clear();
supportingFiles.add(new SupportingFile("pom.mustache", "", "pom.xml"));
supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));
supportingFiles.add(new SupportingFile("ApiClient.mustache", invokerFolder.toString(), "ApiClient.java"));
supportingFiles.add(new SupportingFile("Pair.mustache", invokerFolder.toString(), "Pair.java"));
apiTemplateFiles.put("api_impl.mustache", ".java");
importMapping.put("StringJoiner", "java.util.StringJoiner");
importMapping.put("WebClientRequestHeaders", "io.helidon.webclient.WebClientRequestHeaders");
importMapping.put("Pair", invokerPackage + ".Pair");


List<SupportingFile> modifiable = new ArrayList<>();
modifiable.add(new SupportingFile("pom.mustache", "", "pom.xml"));
modifiable.add(new SupportingFile("README.mustache", "", "README.md"));

List<SupportingFile> unmodifiable = new ArrayList<>();
unmodifiable.add(new SupportingFile("ApiResponse.mustache", invokerFolder.toString(), "ApiResponse.java"));
unmodifiable.add(new SupportingFile("ApiResponseBase.mustache", invokerFolder.toString(), "ApiResponseBase.java"));
unmodifiable.add(new SupportingFile("ApiClient.mustache", invokerFolder.toString(), "ApiClient.java"));
unmodifiable.add(new SupportingFile("Pair.mustache", invokerFolder.toString(), "Pair.java"));
unmodifiable.add(new SupportingFile("ResponseType.mustache", apiFileFolder(), "ResponseType.java"));

processSupportingFiles(modifiable, unmodifiable);
}
else {
LOGGER.error("Unknown library option (-l/--library): {}", getLibrary());
Expand Down Expand Up @@ -236,14 +264,65 @@ protected boolean projectFilesExist() {

@Override
public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<ModelMap> allModels) {
super.postProcessOperationsWithModels(objs, allModels);
if (isLibrary(HELIDON_MP)) {
super.postProcessOperationsWithModels(objs, allModels);
return AbstractJavaJAXRSServerCodegen.jaxrsPostProcessOperations(objs);
} else {
// TODO What do we do for SE clients?
LOGGER.warn("Might need SE-specific code here");
return AbstractJavaJAXRSServerCodegen.jaxrsPostProcessOperations(objs);
// Compute the imports to declare in the generated API impl class.
List<Map<String, String>> imports = objs.getImports();
List<Map<String, String>> implImports = new ArrayList<>(imports);

Set<String> requiredImplImportClassNames = new HashSet<>();
for (CodegenOperation op : objs.getOperations().getOperation()) {
requiredImplImportClassNames.addAll((Set) op.vendorExtensions.get(X_HELIDON_REQUIRED_IMPL_IMPORTS));
}

Set<String> missingImportClassNames = new TreeSet<>(requiredImplImportClassNames);
imports.stream()
.map(m -> m.get("classname"))
.forEach(missingImportClassNames::remove);

missingImportClassNames.forEach(c -> {
Map<String, String> singleImportMap = new HashMap<>();
singleImportMap.put("classname", c);
singleImportMap.put("import", Objects.requireNonNull(importMapping.get(c), "no mapping for " + c));
implImports.add(singleImportMap);
});

objs.put(X_HELIDON_IMPL_IMPORTS, implImports);
return objs;
}
}

@Override
public CodegenOperation fromOperation(String path, String httpMethod, Operation operation, List<Server> servers) {
CodegenOperation op = super.fromOperation(path, httpMethod, operation, servers);
// We use two templates, one for the API interface and one for the impl class.
// Add to the normal imports for this operation only those imports used in both
// the API and the impl. Create a vendor extension on the operation to record the
// additional imports needed for the implementation class.
Set<String> requiredImplImports = new TreeSet<>();
if (op.isArray) {
op.imports.add("List");
}
if (op.isMap) {
op.imports.add("Map");
}
if (op.getHasQueryParams()) {
requiredImplImports.add("ArrayList");
requiredImplImports.add("Pair");
}
if (op.getHasHeaderParams()) {
requiredImplImports.add("WebClientRequestHeaders");
}
if (op.getHasFormParams()) {
requiredImplImports.add("StringJoiner");
}
if (op.getHasCookieParams()) {
requiredImplImports.add("StringJoiner");
}
op.vendorExtensions.put(X_HELIDON_REQUIRED_IMPL_IMPORTS, requiredImplImports);
return op;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{{>licenseInfo}}
package {{invokerPackage}};

import java.util.concurrent.ExecutionException;

import io.helidon.common.GenericType;
import io.helidon.common.reactive.Single;
import io.helidon.webclient.WebClientResponse;

{{#appName}}
/**
* Generic-typed response.
*
* Return type for generated API methods.
*
* @param <T> type of the return value from the generated API method
*/
{{/appName}}
public interface ApiResponse<T> {
static <T> ApiResponse<T> create(GenericType<T> responseType, Single<WebClientResponse> webClientResponse) {
return new ApiResponseBase<>(responseType, webClientResponse);
}

/**
* @returns reactive access to the {@link WebClientResponse} describing the response from the server
*/
Single<WebClientResponse> webClientResponse();

/**
* @return reactive access to the value returned in the response from the server
*/
Single<T> result() throws ExecutionException, InterruptedException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{{>licenseInfo}}
package {{invokerPackage}};

import java.util.concurrent.ExecutionException;

import io.helidon.common.GenericType;
import io.helidon.common.reactive.Single;
import io.helidon.webclient.WebClientResponse;

{{#appName}}
/**
* Implementation of a generic-typed response.
*
* @param <T> type of the return value from the generated API method
*/
{{/appName}}
class ApiResponseBase<T> implements ApiResponse<T> {
private final Single<WebClientResponse> webClientResponse;
private final GenericType<T> responseType;
protected ApiResponseBase(GenericType<T> responseType, Single<WebClientResponse> webClientResponse) {
this.webClientResponse = webClientResponse;
this.responseType = responseType;
}

@Override
public Single<WebClientResponse> webClientResponse() {
return webClientResponse;
}

@Override
public Single<T> result() throws ExecutionException, InterruptedException {
return webClientResponse.get().content().as(responseType);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# {{appName}}

{{#appDescriptionWithNewLines}}
{{{.}}}

{{/appDescriptionWithNewLines}}

## Overview
This project was generated using the Helidon OpenAPI Generator.

The generated classes use the programming model from the Helidon WebClient implementation, primarily the `WebClient` interface and its
`WebClient.Builder` class. Refer to the Helidon WebClient documentation for complete information about them.

## Using the Generated Classes and Interfaces
The generated `ApiClient` class wraps a `WebClient` instance. Similarly, the `ApiClient.Builder` class wraps the `WebClient.Builder` class.

The generated `xxxApi` interfaces and `xxxApiImpl` classes make it very simple for your code to send requests (with input parameters) to the remote service which the OpenAPI document describes and to process the response (with output values) from the remote service.

To use the generated API, your code performs the following steps.

1. Create an instance of the `ApiClient` using its `Builder`.
2. Create an instance of a `xxxApi` it wants to access, typically by invoking `xxxApiImpl.create(ApiClient)` and passing the `ApiClient` instance just created.
3. Invoke any of the `public` methods on the `xxxApi` instance, passing the input parameters and saving the returned `Single<WebClientResponse>` object.
4. Invoke methods on the returned `Single<WebClientResponse>` to process the response and any output from it.

Browse the methods and JavaDoc on the generated classes for more information.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{{>licenseInfo}}
package {{apiPackage}};

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

import io.helidon.common.GenericType;

class ResponseType<T> {
static <T> GenericType<T> create(Type rawType, Type... typeParams) {
return typeParams.length == 0
? GenericType.create(rawType)
: GenericType.create(new ParameterizedType() {
@Override
public Type[] getActualTypeArguments() {
return typeParams;
}

@Override
public Type getRawType() {
return rawType;
}

@Override
public Type getOwnerType() {
return null;
}
});
}
}
Loading

0 comments on commit aa18a22

Please sign in to comment.