diff --git a/boat-maven-plugin/README.md b/boat-maven-plugin/README.md index 52d2e551e..3197e9d3c 100644 --- a/boat-maven-plugin/README.md +++ b/boat-maven-plugin/README.md @@ -60,6 +60,36 @@ Same with `generate` but with opinionated defaults for Spring +## boat:generate-spring-boot-embedded-webhooks + +Same with `generate-spring-boot-embedded` but with opinionated defaults for Webhooks +It will generate webhook interfaces with prehook and posthook request mapping for each endpoint in the OpenAPI Specs. + + + ${project.build.directory}/generated-sources/openapi + true + boat-webhooks + true + false + false + ${project.basedir}/../api/product-service-api/src/main/resources/openapi.yaml + + java8 + true + true + true + false + true + true + false + com.backbase.product.api.service.v2 + com.backbase.product.api.service.v2.model + true + true + false + + + ## boat:generate-rest-template-embedded Same with `generate` but with opinionated defaults for Rest Template Client diff --git a/boat-maven-plugin/src/main/java/com/backbase/oss/boat/GenerateSpringBootEmbeddedWebhookMojo.java b/boat-maven-plugin/src/main/java/com/backbase/oss/boat/GenerateSpringBootEmbeddedWebhookMojo.java new file mode 100644 index 000000000..ccc9c4124 --- /dev/null +++ b/boat-maven-plugin/src/main/java/com/backbase/oss/boat/GenerateSpringBootEmbeddedWebhookMojo.java @@ -0,0 +1,26 @@ +package com.backbase.oss.boat; + +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Mojo; + +import java.util.Collection; +import java.util.Set; + +/** + * Generating Server Stubs using Spring Boot. + */ +@Mojo(name = "generate-spring-boot-embedded-webhooks", threadSafe = true) +public class GenerateSpringBootEmbeddedWebhookMojo extends AbstractGenerateMojo { + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + getLog().info("Generating Server Stubs using Spring Boot Webhooks"); + execute("boat-webhooks", "boat-webhooks", true, false, false); + } + + @Override + protected Collection getGeneratorSpecificSupportingFiles() { + return Set.of("BigDecimalCustomSerializer.java", "WebhookResponse.java", "ServletContent.java", "PosthookRequest.java", "PrehookRequest.java"); + } +} diff --git a/boat-scaffold/src/main/java/com/backbase/oss/codegen/java/BoatWebhooksCodeGen.java b/boat-scaffold/src/main/java/com/backbase/oss/codegen/java/BoatWebhooksCodeGen.java new file mode 100644 index 000000000..8ee2fdf65 --- /dev/null +++ b/boat-scaffold/src/main/java/com/backbase/oss/codegen/java/BoatWebhooksCodeGen.java @@ -0,0 +1,319 @@ +package com.backbase.oss.codegen.java; + +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.parameters.Parameter; +import io.swagger.v3.oas.models.servers.Server; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.openapitools.codegen.CodegenConstants; +import org.openapitools.codegen.CodegenModel; +import org.openapitools.codegen.CodegenOperation; +import org.openapitools.codegen.CodegenParameter; +import org.openapitools.codegen.CodegenProperty; +import org.openapitools.codegen.CliOption; +import org.openapitools.codegen.SupportingFile; +import org.openapitools.codegen.config.GlobalSettings; +import org.openapitools.codegen.languages.SpringCodegen; +import org.openapitools.codegen.templating.mustache.IndentedLambda; +import org.openapitools.codegen.utils.ModelUtils; + +import java.io.File; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import static com.backbase.oss.codegen.java.BoatCodeGenUtils.getCollectionCodegenValue; +import static org.apache.commons.lang3.StringUtils.isEmpty; +import static org.openapitools.codegen.utils.StringUtils.camelize; + +@Slf4j +public class BoatWebhooksCodeGen extends SpringCodegen { + public static final String NAME = "boat-webhooks"; + + public static final String USE_CLASS_LEVEL_BEAN_VALIDATION = "useClassLevelBeanValidation"; + public static final String ADD_SERVLET_REQUEST = "addServletRequest"; + public static final String ADD_BINDING_RESULT = "addBindingResult"; + public static final String USE_LOMBOK_ANNOTATIONS = "useLombokAnnotations"; + public static final String USE_WITH_MODIFIERS = "useWithModifiers"; + public static final String USE_PROTECTED_FIELDS = "useProtectedFields"; + public static final String MUSTACHE_EXTENSION =".mustache"; + public static final String JAVA_EXTENSION =".java"; + + + /** + * Add @Validated to class-level Api interfaces. Defaults to false + */ + @Setter + @Getter + protected boolean useClassLevelBeanValidation; + + /** + * Adds a HttpServletRequest object to the API definition method. + */ + @Setter + @Getter + protected boolean addServletRequest; + + /** + * Adds BindingResult to API interface method if @validate is used + */ + @Setter + @Getter + protected boolean addBindingResult; + + /** + * Add Lombok to class-level Api models. Defaults to false + */ + @Setter + @Getter + protected boolean useLombokAnnotations; + + + /** + * Whether to use {@code with} prefix for pojos modifiers. + */ + @Setter + @Getter + protected boolean useWithModifiers; + + @Setter + @Getter + protected boolean useProtectedFields; + + public BoatWebhooksCodeGen() { + super(); + log.info("BoatWebhooksCodeGen constructor called. NAME: {}", NAME); + this.embeddedTemplateDir = this.templateDir = NAME; + this.openapiNormalizer.put("REF_AS_PARENT_IN_ALLOF", "true"); + + this.cliOptions.add(CliOption.newBoolean(USE_CLASS_LEVEL_BEAN_VALIDATION, + "Add @Validated to class-level Api interfaces.", this.useClassLevelBeanValidation)); + this.cliOptions.add(CliOption.newBoolean(ADD_SERVLET_REQUEST, + "Adds a HttpServletRequest object to the API definition method.", this.addServletRequest)); + this.cliOptions.add(CliOption.newBoolean(ADD_BINDING_RESULT, + "Adds a Binding result as method perimeter. Only implemented if @validate is being used.", + this.addBindingResult)); + this.cliOptions.add(CliOption.newBoolean(USE_LOMBOK_ANNOTATIONS, + "Add Lombok to class-level Api models. Defaults to false.", this.useLombokAnnotations)); + this.cliOptions.add(CliOption.newBoolean(USE_WITH_MODIFIERS, + "Whether to use \"with\" prefix for POJO modifiers.", this.useWithModifiers)); + this.cliOptions.add(CliOption.newString(USE_PROTECTED_FIELDS, + "Whether to use protected visibility for model fields")); + supportedLibraries.put(NAME, "Boat Webhooks codegen"); + this.apiNameSuffix = "Api"; + this.apiNamePrefix = "Webhook"; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String toApiName(String name) { + if (name.isEmpty()) { + name = "default"; + } + + name = sanitizeName(name); + + return camelize(this.apiNamePrefix + "_" + name + "_" + this.apiNameSuffix); + } + + @Override + public void processOpts() { + super.processOpts(); + log.info("BoatWebhooksCodeGen processOpts called. Adding supporting files and properties."); + + // Whether it's using ApiUtil or not. + // cases: + // ApiUtil.java present or not + // true or false + final String supFiles = GlobalSettings.getProperty(CodegenConstants.SUPPORTING_FILES); + final boolean useApiUtil = supFiles != null && (supFiles.isEmpty() + ? needApiUtil() // set to empty by true + : supFiles.contains("ApiUtil.java")); // set by + + if (!useApiUtil) { + this.supportingFiles + .removeIf(sf -> "apiUtil.mustache".equals(sf.getTemplateFile())); + } + writePropertyBack("useApiUtil", useApiUtil); + final var serializerTemplate = "BigDecimalCustomSerializer"; + this.supportingFiles.add(new SupportingFile( + serializerTemplate + MUSTACHE_EXTENSION, + (sourceFolder + File.separator + modelPackage).replace(".", File.separator), + serializerTemplate + JAVA_EXTENSION + )); + final var webhookResponseTemplate = "WebhookResponse"; + this.supportingFiles.add(new SupportingFile(webhookResponseTemplate + MUSTACHE_EXTENSION, + (sourceFolder + File.separator + modelPackage).replace(".", File.separator), + webhookResponseTemplate + JAVA_EXTENSION)); + final var servletContentTemplate = "ServletContent"; + this.supportingFiles.add(new SupportingFile(servletContentTemplate + MUSTACHE_EXTENSION, + (sourceFolder + File.separator + modelPackage).replace(".", File.separator), + servletContentTemplate + JAVA_EXTENSION)); + final var posthookRequestTemplate = "PosthookRequest"; + this.supportingFiles.add(new SupportingFile(posthookRequestTemplate + MUSTACHE_EXTENSION, + (sourceFolder + File.separator + modelPackage).replace(".", File.separator), + posthookRequestTemplate + JAVA_EXTENSION)); + final var prehookRequestTemplate = "PrehookRequest"; + this.supportingFiles.add(new SupportingFile(prehookRequestTemplate + MUSTACHE_EXTENSION, + (sourceFolder + File.separator + modelPackage).replace(".", File.separator), + prehookRequestTemplate + JAVA_EXTENSION)); + this.additionalProperties.put("indent4", new IndentedLambda(4, " ", true, true)); + this.additionalProperties.put("newLine4", new BoatSpringCodeGen.NewLineIndent(4, " ")); + this.additionalProperties.put("indent8", new IndentedLambda(8, " ", true, true)); + this.additionalProperties.put("newLine8", new BoatSpringCodeGen.NewLineIndent(8, " ")); + this.additionalProperties.put("toOneLine", new BoatSpringCodeGen.FormatToOneLine()); + this.additionalProperties.put("trimAndIndent4", new BoatSpringCodeGen.TrimAndIndent(4, " ")); + } + + private boolean needApiUtil() { + return this.apiTemplateFiles.containsKey("api.mustache") + && this.apiTemplateFiles.containsKey("apiDelegate.mustache"); + } + + /* + * Overridden to be able to override the private replaceBeanValidationCollectionType method. + */ + @Override + public CodegenParameter fromParameter(Parameter parameter, Set imports) { + CodegenParameter codegenParameter = super.fromParameter(parameter, imports); + if (!isListOrSet(codegenParameter)) { + return new BoatSpringCodegenParameter(codegenParameter); + } else { + codegenParameter.datatypeWithEnum = replaceBeanValidationCollectionType(codegenParameter.items, codegenParameter.datatypeWithEnum); + codegenParameter.dataType = replaceBeanValidationCollectionType(codegenParameter.items, codegenParameter.dataType); + return new BoatSpringCodegenParameter(codegenParameter); + } + } + + /* + * Overridden to be able to override the private replaceBeanValidationCollectionType method. + */ + @Override + public CodegenProperty fromProperty(String name, Schema p, boolean required, boolean schemaIsFromAdditionalProperties) { + CodegenProperty codegenProperty = super.fromProperty(name, p, required, schemaIsFromAdditionalProperties); + if (!isListOrSet(codegenProperty)) { + return new BoatSpringCodegenProperty(codegenProperty); + } else { + codegenProperty.datatypeWithEnum = replaceBeanValidationCollectionType(codegenProperty.items, codegenProperty.datatypeWithEnum); + codegenProperty.dataType = replaceBeanValidationCollectionType(codegenProperty.items, codegenProperty.dataType); + return new BoatSpringCodegenProperty(codegenProperty); + } + } + + /** + * "overridden" to fix invalid code when the data type is a collection of a fully qualified classname. + * eg. Set<@Valid com.backbase.dbs.arrangement.commons.model.TranslationItemDto> + * + * @param codegenProperty + * @param dataType + * @return + */ + String replaceBeanValidationCollectionType(CodegenProperty codegenProperty, String dataType) { + if (!useBeanValidation || isEmpty(dataType) || !codegenProperty.isModel || isResponseType(codegenProperty)) { + return dataType; + } + String result = dataType; + if (!dataType.contains("@Valid")) { + result = dataType.replace("<", "<@Valid "); + } + // Use a safer regex to avoid catastrophic backtracking + Matcher m = Pattern.compile("^([^<]+<)(@Valid) ([a-z\\.]+)([A-Z].*)(>)$").matcher(dataType); + if (m.matches()) { + // Set<@Valid com.backbase.dbs.arrangement.commons.model.TranslationItemDto> + result = m.group(1) + m.group(3) + m.group(2) + " " + m.group(4) + m.group(5); + } + return result; + } + + // Copied, but not modified + private static boolean isListOrSet(CodegenProperty codegenProperty) { + return codegenProperty.isContainer && !codegenProperty.isMap; + } + + // Copied, but not modified + private static boolean isListOrSet(CodegenParameter codegenParameter) { + return codegenParameter.isContainer && !codegenParameter.isMap; + } + + // Copied, but not modified + private static boolean isResponseType(CodegenProperty codegenProperty) { + return codegenProperty.baseName.toLowerCase(Locale.ROOT).contains("response"); + } + + /** + This method has been overridden in order to add a parameter to codegen operation for adding HttpServletRequest to + the service interface. There is a relevant httpServletParam.mustache file. + */ + @Override + public CodegenOperation fromOperation(String path, String httpMethod, Operation operation, List servers) { + if (operation.getExtensions() == null) { + operation.setExtensions(new LinkedHashMap<>()); + } + final CodegenOperation codegenOperation = super.fromOperation(path, httpMethod, operation, servers); + // Remove the standard body parameter (if it exists) --- + // This prevents the generator's default logic from inserting its own request body. + codegenOperation.allParams.removeIf(p -> p.isBodyParam); + if (this.addServletRequest) { + final CodegenParameter codegenParameter = new CodegenParameter(); + codegenParameter.paramName = "httpServletRequest"; + codegenOperation.allParams.add(codegenParameter); + } + if (codegenOperation.returnType != null) { + codegenOperation.returnType = codegenOperation.returnType.replace("@Valid", ""); + } + return codegenOperation; + } + + @Override + public String toDefaultValue(CodegenProperty cp, Schema schema) { + final Schema referencedSchema = ModelUtils.getReferencedSchema(this.openAPI, schema); + return getCollectionCodegenValue(cp, referencedSchema, containerDefaultToNull, instantiationTypes()) + .map(BoatCodeGenUtils.CodegenValueType::getValue) + .orElseGet(() -> super.toDefaultValue(cp, referencedSchema)); + } + + @Override + public void postProcessModelProperty(CodegenModel model, CodegenProperty property) { + + super.postProcessModelProperty(model, property); + + if (shouldSerializeBigDecimalAsString(property)) { + property.vendorExtensions.put("x-extra-annotation", "@JsonSerialize(using = BigDecimalCustomSerializer.class)"); + model.imports.add("BigDecimalCustomSerializer"); + model.imports.add("JsonSerialize"); + } + } + + private boolean shouldSerializeBigDecimalAsString(CodegenProperty property) { + return (serializeBigDecimalAsString && ("decimal".equalsIgnoreCase(property.baseType) || "bigdecimal".equalsIgnoreCase(property.baseType))) + || (isApiStringFormattedAsNumber(property) && !isDataTypeString(property)); + } + + private boolean isApiStringFormattedAsNumber(CodegenProperty property) { + return "string".equalsIgnoreCase(property.openApiType) && "number".equalsIgnoreCase(property.dataFormat); + } + + private boolean isDataTypeString(CodegenProperty property) { + return Stream.of(property.baseType, property.dataType, property.datatypeWithEnum) + .anyMatch("string"::equalsIgnoreCase); + } + + @Override + public void postProcessParameter(CodegenParameter p) { + super.postProcessParameter(p); + if (p.isContainer && !this.reactive) { + p.baseType = p.dataType.replaceAll("^([^<]+)<.+>$", "$1"); + } + } + +} diff --git a/boat-scaffold/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig b/boat-scaffold/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig index 56c1e55dc..548b9ea0f 100644 --- a/boat-scaffold/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig +++ b/boat-scaffold/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig @@ -5,3 +5,4 @@ com.backbase.oss.codegen.angular.BoatAngularGenerator com.backbase.oss.codegen.marina.BoatMarinaGenerator org.openapitools.codegen.languages.BoatSwift5Codegen org.openapitools.codegen.languages.BoatAndroidClientCodegen +com.backbase.oss.codegen.java.BoatWebhooksCodeGen diff --git a/boat-scaffold/src/main/templates/boat-webhooks/BigDecimalCustomSerializer.mustache b/boat-scaffold/src/main/templates/boat-webhooks/BigDecimalCustomSerializer.mustache new file mode 100644 index 000000000..a6dc40f30 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/BigDecimalCustomSerializer.mustache @@ -0,0 +1,19 @@ +package {{modelPackage}}; + +import com.fasterxml.jackson.databind.ser.std.ToStringSerializerBase; +import java.math.BigDecimal; + +public class BigDecimalCustomSerializer extends ToStringSerializerBase { + + public BigDecimalCustomSerializer() { + super(BigDecimal.class); + } + + @Override + public String valueToString(Object value) { + if (value instanceof BigDecimal) { + return ((BigDecimal) value).toPlainString(); + } + return (value == null) ? null : value.toString(); + } +} diff --git a/boat-scaffold/src/main/templates/boat-webhooks/PosthookRequest.mustache b/boat-scaffold/src/main/templates/boat-webhooks/PosthookRequest.mustache new file mode 100644 index 000000000..fe600385f --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/PosthookRequest.mustache @@ -0,0 +1,70 @@ +package {{modelPackage}}; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Predefined request object for posthook endpoints containing API response and the request details. + */ +public class PosthookRequest extends ServletContent { + + private PrehookRequest requestDetails; + + public PosthookRequest body(String body) { + this.setBody(body); + return this; + } + + public PosthookRequest headers(Map> headers) { + this.setHeaders(headers); + return this; + } + + public PosthookRequest parameters(Map parameters) { + this.setParameters(parameters); + return this; + } + + public PosthookRequest requestDetails(PrehookRequest requestDetails) { + this.requestDetails = requestDetails; + return this; + } + + public PrehookRequest getRequestDetails() { + return requestDetails; + } + + public void setRequestDetails(PrehookRequest requestDetails) { + this.requestDetails = requestDetails; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PosthookRequest posthookRequest = (PosthookRequest) o; + return super.equals(o) && Objects.equals(this.requestDetails, posthookRequest.requestDetails); + } + + @Override + public int hashCode() { + return super.hashCode() + Objects.hash(requestDetails); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class PosthookRequest {\n"); + sb.append(" body: ").append(toIndentedString(getBody())).append("\n"); + sb.append(" headers: ").append(toIndentedString(getHeaders())).append("\n"); + sb.append(" parameters: ").append(toIndentedString(getParameters())).append("\n"); + sb.append(" requestDetails: ").append(toIndentedString(requestDetails)).append("\n"); + sb.append("}"); + return sb.toString(); + } +} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/PrehookRequest.mustache b/boat-scaffold/src/main/templates/boat-webhooks/PrehookRequest.mustache new file mode 100644 index 000000000..23be71d98 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/PrehookRequest.mustache @@ -0,0 +1,70 @@ +package {{modelPackage}}; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Predefined request object for prehook endpoints. + */ +public class PrehookRequest extends ServletContent { + + private Map pathVariables; + + public PrehookRequest body(String body) { + this.setBody(body); + return this; + } + + public PrehookRequest headers(Map> headers) { + this.setHeaders(headers); + return this; + } + + public PrehookRequest parameters(Map parameters) { + this.setParameters(parameters); + return this; + } + + public PrehookRequest pathVariables(Map pathVariables) { + this.pathVariables = pathVariables; + return this; + } + + public Map getPathVariables() { + return pathVariables; + } + + public void setPathVariables(Map pathVariables) { + this.pathVariables = pathVariables; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PrehookRequest prehookRequest = (PrehookRequest) o; + return super.equals(o) && Objects.equals(this.pathVariables, prehookRequest.pathVariables); + } + + @Override + public int hashCode() { + return super.hashCode() + Objects.hash(pathVariables); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class PrehookRequest {\n"); + sb.append(" body: ").append(toIndentedString(getBody())).append("\n"); + sb.append(" headers: ").append(toIndentedString(getHeaders())).append("\n"); + sb.append(" parameters: ").append(toIndentedString(getParameters())).append("\n"); + sb.append(" pathVariables: ").append(toIndentedString(pathVariables)).append("\n"); + sb.append("}"); + return sb.toString(); + } +} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/README.mustache b/boat-scaffold/src/main/templates/boat-webhooks/README.mustache new file mode 100644 index 000000000..37911ef59 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/README.mustache @@ -0,0 +1,81 @@ +{{^interfaceOnly}}# OpenAPI generated server + +Spring Boot Webhooks Server + +## Overview +This server was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. +By using the [OpenAPI-Spec](https://openapis.org), you can easily generate a server stub. +This is an example of building a OpenAPI-enabled server in Java using the SpringBoot framework. +{{#springFoxDocumentationProvider}} + +The underlying library integrating OpenAPI to Spring Boot is [springfox](https://github.com/springfox/springfox). +Springfox will generate an OpenAPI v2 (fka Swagger RESTful API Documentation Specification) specification based on the +generated Controller and Model classes. The specification is available to download using the following url: +http://localhost:{{serverPort}}/v2/api-docs/ + +**HEADS-UP**: Springfox is deprecated for removal in version 6.0.0 of openapi-generator. The project seems to be no longer +maintained (last commit is of Oct 14, 2020). It works with Spring Boot 2.5.x but not with 2.6. Spring Boot 2.5 is +supported until 2022-05-19. Users of openapi-generator should migrate to the springdoc documentation provider which is, +as an added bonus, OpenAPI v3 compatible. + +{{/springFoxDocumentationProvider}} + +{{#springDocDocumentationProvider}} + +The underlying library integrating OpenAPI to Spring Boot is [springdoc](https://springdoc.org). +Springdoc will generate an OpenAPI v3 specification based on the generated Controller and Model classes. +The specification is available to download using the following url: +http://localhost:{{serverPort}}/v3/api-docs/ +{{/springDocDocumentationProvider}} +{{#sourceDocumentationProvider}} + +The OpenAPI specification used to generate this project is available to download using the following url: +http://localhost:{{serverPort}}/openapi.json +{{/sourceDocumentationProvider}} + +Start your server as a simple java application +{{#useSwaggerUI}} + +You can view the api documentation in swagger-ui by pointing to +http://localhost:{{serverPort}}/swagger-ui.html + +{{/useSwaggerUI}} +Change default port value in application.properties{{/interfaceOnly}}{{#interfaceOnly}} +# OpenAPI generated API stub + +Spring Framework stub + + +## Overview +This code was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. +By using the [OpenAPI-Spec](https://openapis.org), you can easily generate an API stub. +This is an example of building API stub interfaces in Java using the Spring framework. + +The stubs generated can be used in your existing Spring-MVC or Spring-Boot application to create controller endpoints +by adding ```@Controller``` classes that implement the interface. Eg: +```java +@Controller +public class PetController implements PetApi { +// implement all PetApi methods +} +``` + +You can also use the interface to create [Spring-Cloud Feign clients](http://projects.spring.io/spring-cloud/spring-cloud.html#spring-cloud-feign-inheritance).Eg: +```java +@FeignClient(name="pet", url="http://petstore.swagger.io/v2") +public interface PetClient extends PetApi { + +} +``` +{{/interfaceOnly}} +{{#virtualService}} + + +## Virtualan : + +You can view Virtualan UI by pointing to +http://localhost:8080//virtualan-ui.html + +How to use guide available in the Virtualan wiki +https://github.com/virtualansoftware/virtualan/wiki +{{/virtualService}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/ServletContent.mustache b/boat-scaffold/src/main/templates/boat-webhooks/ServletContent.mustache new file mode 100644 index 000000000..b5e502c0b --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/ServletContent.mustache @@ -0,0 +1,104 @@ +package {{modelPackage}}; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public abstract class ServletContent { + private String body; + private Map> headers; + private Map parameters; + + public ServletContent() { + } + + /** + * Create copy. + * + * @param content other instance + */ + public ServletContent(ServletContent content) { + if (content != null) { + this.body = content.getBody(); + this.headers = content.getHeaders(); + this.parameters = content.getParameters(); + } + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public Map> getHeaders() { + return headers; + } + + public void setHeaders(Map> headers) { + this.headers = headers; + } + + /** + * Add a header overwriting existing. + * + * @param name header name + * @param value header value + */ + public void putHeader(String name, String value) { + if (this.headers == null) { + this.headers = new HashMap<>(); + } + this.headers.put(name, List.of(value)); + } + + public Map getParameters() { + return parameters; + } + + public void setParameters(Map parameters) { + this.parameters = parameters; + } + + /** + * Add a parameter overwriting existing. + * + * @param name parameter name + * @param value parameter value + */ + public void putParameter(String name, String value) { + if (this.parameters == null) { + this.parameters = new HashMap<>(); + } + this.parameters.put(name, new String[]{value}); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || !(o instanceof ServletContent servletContent)) { + return false; + } + return Objects.equals(this.body, servletContent.body) && + Objects.equals(this.headers, servletContent.headers) && + Objects.equals(this.parameters, servletContent.parameters); + } + + @Override + public int hashCode() { + return Objects.hash(body, headers, parameters); + } + + + protected String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/WebhookResponse.mustache b/boat-scaffold/src/main/templates/boat-webhooks/WebhookResponse.mustache new file mode 100644 index 000000000..b3930b038 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/WebhookResponse.mustache @@ -0,0 +1,75 @@ +package {{modelPackage}}; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + + +public class WebhookResponse extends ServletContent { + + private int httpStatusCode; + + public WebhookResponse() { + } + + public WebhookResponse(ServletContent content) { + super(content); + } + + public WebhookResponse httpStatusCode(int httpStatusCode) { + this.httpStatusCode = httpStatusCode; + return this; + } + + public int getHttpStatusCode() { + return httpStatusCode; + } + + public void setHttpStatusCode(int httpStatusCode) { + this.httpStatusCode = httpStatusCode; + } + + public WebhookResponse body(String body) { + this.setBody(body); + return this; + } + + public WebhookResponse headers(Map> headers) { + this.setHeaders(headers); + return this; + } + + public WebhookResponse parameters(Map parameters) { + this.setParameters(parameters); + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + WebhookResponse webhookResponse = (WebhookResponse) o; + return super.equals(o) && Objects.equals(this.httpStatusCode, webhookResponse.httpStatusCode); + } + + @Override + public int hashCode() { + return super.hashCode() + Objects.hash(httpStatusCode); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class WebhookResponse {\n"); + sb.append(" body: ").append(toIndentedString(getBody())).append("\n"); + sb.append(" headers: ").append(toIndentedString(getHeaders())).append("\n"); + sb.append(" parameters: ").append(toIndentedString(getParameters())).append("\n"); + sb.append(" httpStatusCode: ").append(toIndentedString(this.httpStatusCode)).append("\n"); + sb.append("}"); + return sb.toString(); + } +} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/additionalEnumTypeAnnotations.mustache b/boat-scaffold/src/main/templates/boat-webhooks/additionalEnumTypeAnnotations.mustache new file mode 100644 index 000000000..dbb6a373f --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/additionalEnumTypeAnnotations.mustache @@ -0,0 +1,3 @@ +{{#additionalEnumTypeAnnotations}} +{{{.}}} +{{/additionalEnumTypeAnnotations}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/additionalModelTypeAnnotations.mustache b/boat-scaffold/src/main/templates/boat-webhooks/additionalModelTypeAnnotations.mustache new file mode 100644 index 000000000..f4871c02c --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/additionalModelTypeAnnotations.mustache @@ -0,0 +1,2 @@ +{{#additionalModelTypeAnnotations}}{{{.}}} +{{/additionalModelTypeAnnotations}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/additionalOneOfTypeAnnotations.mustache b/boat-scaffold/src/main/templates/boat-webhooks/additionalOneOfTypeAnnotations.mustache new file mode 100644 index 000000000..283f8f91e --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/additionalOneOfTypeAnnotations.mustache @@ -0,0 +1,2 @@ +{{#additionalOneOfTypeAnnotations}}{{{.}}} +{{/additionalOneOfTypeAnnotations}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/allowableValues.mustache b/boat-scaffold/src/main/templates/boat-webhooks/allowableValues.mustache new file mode 100644 index 000000000..6aa973a6a --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/allowableValues.mustache @@ -0,0 +1 @@ +{{#allowableValues}}allowableValues ={{#swagger2AnnotationLibrary}} { {{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}} }{{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}} "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{#-last}}{{/-last}}{{/values}}"{{/swagger1AnnotationLibrary}}{{/allowableValues}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/api.mustache b/boat-scaffold/src/main/templates/boat-webhooks/api.mustache new file mode 100644 index 000000000..8c7583158 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/api.mustache @@ -0,0 +1,399 @@ +/* +Boat Generator configuration: + useBeanValidation: {{useBeanValidation}} + useOptional: {{useOptional}} + addServletRequest: {{addServletRequest}} + addBindingResult: {{addBindingResult}} + useLombokAnnotations: {{useLombokAnnotations}} + openApiNullable: {{openApiNullable}} + useSetForUniqueItems: {{useSetForUniqueItems}} + useWithModifiers: {{useWithModifiers}} +*/ +package {{package}}; + +{{#imports}}import {{import}}; +{{/imports}} +{{#swagger2AnnotationLibrary}} +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +{{/swagger2AnnotationLibrary}} +{{#swagger1AnnotationLibrary}} +import io.swagger.annotations.*; +{{/swagger1AnnotationLibrary}} +{{#jdk8-no-delegate}} +{{#virtualService}} +import io.virtualan.annotation.ApiVirtual; +import io.virtualan.annotation.VirtualService; +{{/virtualService}} +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +{{/jdk8-no-delegate}} +import org.springframework.http.ResponseEntity; +{{#useBeanValidation}} +import org.springframework.validation.annotation.Validated; +{{/useBeanValidation}} +{{#useSpringController}} +import org.springframework.stereotype.Controller; +{{/useSpringController}} +import org.springframework.web.bind.annotation.*; +{{#jdk8-no-delegate}} +{{^reactive}} +import org.springframework.web.context.request.NativeWebRequest; +{{/reactive}} +{{/jdk8-no-delegate}} +import org.springframework.web.multipart.MultipartFile; +{{#reactive}} +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import org.springframework.http.codec.multipart.Part; +{{/reactive}} + +{{#useBeanValidation}} +{{#useJakartaEe}} +import jakarta.validation.Valid; +import jakarta.validation.constraints.*; +{{/useJakartaEe}} +{{^useJakartaEe}} +import javax.validation.Valid; +import javax.validation.constraints.*; +{{/useJakartaEe}} +{{/useBeanValidation}} +{{#addServletRequest}} +{{^useJakartaEe}} +import javax.servlet.http.HttpServletRequest; +{{/useJakartaEe}} +{{#useJakartaEe}} +import jakarta.servlet.http.HttpServletRequest; +{{/useJakartaEe}} +{{/addServletRequest}} +{{#addBindingResult}} +import org.springframework.validation.BindingResult; +{{/addBindingResult}} +import java.util.List; +import java.util.Map; +{{#jdk8-no-delegate}} +import java.util.Optional; +{{/jdk8-no-delegate}} +{{^jdk8-no-delegate}} +{{#useOptional}} +import java.util.Optional; +{{/useOptional}} +{{/jdk8-no-delegate}} +{{#async}} +import java.util.concurrent.CompletableFuture; +{{/async}} +{{#useJakartaEe}} +import jakarta.annotation.Generated; +{{/useJakartaEe}} +{{^useJakartaEe}} +import javax.annotation.Generated; +{{/useJakartaEe}} +import {{modelPackage}}.WebhookResponse; +import {{modelPackage}}.PrehookRequest; +import {{modelPackage}}.PosthookRequest; +{{>generatedAnnotation}} +{{#useBeanValidation}} +{{#useClassLevelBeanValidation}} +@Validated +{{/useClassLevelBeanValidation}} +{{/useBeanValidation}} +{{#useSpringController}} +@Controller +{{/useSpringController}} +{{#swagger2AnnotationLibrary}} +@io.swagger.v3.oas.annotations.tags.Tag(name = "{{{baseName}}}", description = {{#tagDescription}}"{{{.}}}"{{/tagDescription}}{{^tagDescription}}"the {{{baseName}}} API"{{/tagDescription}}) +{{/swagger2AnnotationLibrary}} +{{#swagger1AnnotationLibrary}} +@Api(value = "{{{baseName}}}", description = {{#tagDescription}}"{{{.}}}"{{/tagDescription}}{{^tagDescription}}"the {{{baseName}}} API"{{/tagDescription}}) +{{/swagger1AnnotationLibrary}} +{{#operations}} +{{#virtualService}} +@VirtualService +{{/virtualService}} +{{#useRequestMappingOnInterface}} +{{=<% %>=}} +@RequestMapping("${openapi.<%title%>.base-path:<%>defaultBasePath%>}") +<%={{ }}=%> +{{/useRequestMappingOnInterface}} +public interface {{classname}} { +{{#jdk8-default-interface}} + {{^isDelegate}} + {{^reactive}} + + default Optional getRequest() { + return Optional.empty(); + } + {{/reactive}} + {{/isDelegate}} + {{#isDelegate}} + + default {{classname}}Delegate getDelegate() { + return new {{classname}}Delegate() {}; + } + {{/isDelegate}} +{{/jdk8-default-interface}} +{{#operation}} + + /** + * {{httpMethod}} {{{path}}}{{#summary}} : {{.}}{{/summary}} + {{#notes}} + * {{.}} + {{/notes}} + * + {{#allParams}} + * @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}} + {{/allParams}} + * @return {{#responses}}{{message}} (status code {{code}}){{^-last}} + * or {{/-last}}{{/responses}} + {{#isDeprecated}} + * @deprecated + {{/isDeprecated}} + {{#externalDocs}} + * {{description}} + * @see {{summary}} Documentation + {{/externalDocs}} + */ + {{#isDeprecated}} + @Deprecated + {{/isDeprecated}} + {{#virtualService}} + @ApiVirtual + {{/virtualService}} + {{#swagger2AnnotationLibrary}} + @Operation( + operationId = "{{{operationId}}}", + {{#summary}} + summary = "{{{.}}}", + {{/summary}} + {{#notes}} + description = "{{{.}}}", + {{/notes}} + {{#vendorExtensions.x-tags.size}} + tags = { {{#vendorExtensions.x-tags}}"{{tag}}"{{^-last}}, {{/-last}}{{/vendorExtensions.x-tags}} }, + {{/vendorExtensions.x-tags.size}} + responses = { + {{#responses}} + @ApiResponse(responseCode = "{{{code}}}", description = "{{{message}}}"{{#baseType}}, content = { + {{#produces}} + @Content(mediaType = "{{{mediaType}}}"){{^-last}},{{/-last}} + {{/produces}} + }{{/baseType}}){{^-last}},{{/-last}} + {{/responses}} + }{{#hasAuthMethods}}, + security = { + {{#authMethods}} + @SecurityRequirement(name = "{{name}}"{{#isOAuth}}, scopes={ {{#scopes}}"{{scope}}"{{^-last}}, {{/-last}}{{/scopes}} }{{/isOAuth}}){{^-last}},{{/-last}} + {{/authMethods}} + }{{/hasAuthMethods}} + ) + {{/swagger2AnnotationLibrary}} + {{#swagger1AnnotationLibrary}} + @ApiOperation( + {{#vendorExtensions.x-tags.size}} + tags = { {{#vendorExtensions.x-tags}}"{{tag}}"{{^-last}}, {{/-last}}{{/vendorExtensions.x-tags}} }, + {{/vendorExtensions.x-tags.size}} + value = "{{{summary}}}", + nickname = "{{{operationId}}}", + notes = "{{{notes}}}"{{#returnBaseType}}, + response = {{{.}}}.class{{/returnBaseType}}{{#returnContainer}}, + responseContainer = "{{{.}}}"{{/returnContainer}}{{#hasAuthMethods}}, + authorizations = { + {{#authMethods}} + {{#isOAuth}} + @Authorization(value = "{{name}}", scopes = { + {{#scopes}} + @AuthorizationScope(scope = "{{scope}}", description = "{{description}}"){{^-last}},{{/-last}} + {{/scopes}} + }){{^-last}},{{/-last}} + {{/isOAuth}} + {{^isOAuth}} + @Authorization(value = "{{name}}"){{^-last}},{{/-last}} + {{/isOAuth}} + {{/authMethods}} }{{/hasAuthMethods}} + ) + @ApiResponses({ + {{#responses}} + @ApiResponse(code = {{{code}}}, message = "{{{message}}}"{{#baseType}}, response = {{{.}}}.class{{/baseType}}{{#containerType}}, responseContainer = "{{{.}}}"{{/containerType}}){{^-last}},{{/-last}} + {{/responses}} + }) + {{/swagger1AnnotationLibrary}} + {{#implicitHeadersParams.0}} + {{#swagger2AnnotationLibrary}} + @Parameters({ + {{#implicitHeadersParams}} + {{>paramDoc}}{{^-last}},{{/-last}} + {{/implicitHeadersParams}} + }) + {{/swagger2AnnotationLibrary}} + {{#swagger1AnnotationLibrary}} + @ApiImplicitParams({ + {{#implicitHeadersParams}} + {{>implicitHeader}}{{^-last}},{{/-last}} + {{/implicitHeadersParams}} + }) + {{/swagger1AnnotationLibrary}} + {{/implicitHeadersParams.0}} + @RequestMapping( + method = RequestMethod.POST, + value = "/service-api/prehook/{{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}}{{{path}}}"{{#singleContentTypes}}{{#hasProduces}}, + produces = "{{{vendorExtensions.x-accepts}}}"{{/hasProduces}}{{#hasConsumes}}, + consumes = "{{{vendorExtensions.x-content-type}}}"{{/hasConsumes}}{{/singleContentTypes}}{{^singleContentTypes}}{{#hasProduces}}, + produces = { {{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}} }{{/hasProduces}}{{#hasConsumes}}, + consumes = { {{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}} }{{/hasConsumes}}{{/singleContentTypes}} + ) + {{#jdk8-default-interface}}default {{/jdk8-default-interface}}ResponseEntity prehook{{#lambda.pascalcase}}{{operationId}}{{/lambda.pascalcase}}( + @Valid @RequestBody PrehookRequest prehookRequest{{#allParams.0}}, {{/allParams.0}}{{#allParams}}{{#toOneLine}}{{>queryParams}}{{/toOneLine}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{>cookieParams}}{{>httpServletParam}}{{^-last}}, + {{/-last}}{{/allParams}}{{#reactive}}{{#hasParams}}, + {{/hasParams}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true){{/swagger2AnnotationLibrary}}{{#springFoxDocumentationProvider}}@ApiIgnore{{/springFoxDocumentationProvider}} final ServerWebExchange exchange{{/reactive}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}}, + {{/hasParams}}{{#springFoxDocumentationProvider}}@ApiIgnore {{/springFoxDocumentationProvider}}{{#springDocDocumentationProvider}}@ParameterObject {{/springDocDocumentationProvider}}final Pageable pageable{{/vendorExtensions.x-spring-paginated}} + ){{#unhandledException}} throws Exception{{/unhandledException}}{{^jdk8-default-interface}};{{/jdk8-default-interface}}{{#jdk8-default-interface}} { + {{#delegate-method}} + return {{operationId}}({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#reactive}}{{#hasParams}}, {{/hasParams}}exchange{{/reactive}}{{#vendorExtensions.x-spring-paginated}}, pageable{{/vendorExtensions.x-spring-paginated}}); + {{/delegate-method}} + } + + {{/jdk8-default-interface}} + + + + +{{/operation}} + + +{{#operation}} + + /** + * {{httpMethod}} {{{path}}}{{#summary}} : {{.}}{{/summary}} + {{#notes}} + * {{.}} + {{/notes}} + * + {{#allParams}} + * @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}} + {{/allParams}} + * @return {{#responses}}{{message}} (status code {{code}}){{^-last}} + * or {{/-last}}{{/responses}} + {{#isDeprecated}} + * @deprecated + {{/isDeprecated}} + {{#externalDocs}} + * {{description}} + * @see {{summary}} Documentation + {{/externalDocs}} + */ + {{#isDeprecated}} + @Deprecated + {{/isDeprecated}} + {{#virtualService}} + @ApiVirtual + {{/virtualService}} + {{#swagger2AnnotationLibrary}} + @Operation( + operationId = "{{{operationId}}}", + {{#summary}} + summary = "{{{.}}}", + {{/summary}} + {{#notes}} + description = "{{{.}}}", + {{/notes}} + {{#vendorExtensions.x-tags.size}} + tags = { {{#vendorExtensions.x-tags}}"{{tag}}"{{^-last}}, {{/-last}}{{/vendorExtensions.x-tags}} }, + {{/vendorExtensions.x-tags.size}} + responses = { + {{#responses}} + @ApiResponse(responseCode = "{{{code}}}", description = "{{{message}}}"{{#baseType}}, content = { + {{#produces}} + @Content(mediaType = "{{{mediaType}}}"){{^-last}},{{/-last}} + {{/produces}} + }{{/baseType}}){{^-last}},{{/-last}} + {{/responses}} + }{{#hasAuthMethods}}, + security = { + {{#authMethods}} + @SecurityRequirement(name = "{{name}}"{{#isOAuth}}, scopes={ {{#scopes}}"{{scope}}"{{^-last}}, {{/-last}}{{/scopes}} }{{/isOAuth}}){{^-last}},{{/-last}} + {{/authMethods}} + }{{/hasAuthMethods}} + ) + {{/swagger2AnnotationLibrary}} + {{#swagger1AnnotationLibrary}} + @ApiOperation( + {{#vendorExtensions.x-tags.size}} + tags = { {{#vendorExtensions.x-tags}}"{{tag}}"{{^-last}}, {{/-last}}{{/vendorExtensions.x-tags}} }, + {{/vendorExtensions.x-tags.size}} + value = "{{{summary}}}", + nickname = "{{{operationId}}}", + notes = "{{{notes}}}"{{#returnBaseType}}, + response = {{{.}}}.class{{/returnBaseType}}{{#returnContainer}}, + responseContainer = "{{{.}}}"{{/returnContainer}}{{#hasAuthMethods}}, + authorizations = { + {{#authMethods}} + {{#isOAuth}} + @Authorization(value = "{{name}}", scopes = { + {{#scopes}} + @AuthorizationScope(scope = "{{scope}}", description = "{{description}}"){{^-last}},{{/-last}} + {{/scopes}} + }){{^-last}},{{/-last}} + {{/isOAuth}} + {{^isOAuth}} + @Authorization(value = "{{name}}"){{^-last}},{{/-last}} + {{/isOAuth}} + {{/authMethods}} }{{/hasAuthMethods}} + ) + @ApiResponses({ + {{#responses}} + @ApiResponse(code = {{{code}}}, message = "{{{message}}}"{{#baseType}}, response = {{{.}}}.class{{/baseType}}{{#containerType}}, responseContainer = "{{{.}}}"{{/containerType}}){{^-last}},{{/-last}} + {{/responses}} + }) + {{/swagger1AnnotationLibrary}} + {{#implicitHeadersParams.0}} + {{#swagger2AnnotationLibrary}} + @Parameters({ + {{#implicitHeadersParams}} + {{>paramDoc}}{{^-last}},{{/-last}} + {{/implicitHeadersParams}} + }) + {{/swagger2AnnotationLibrary}} + {{#swagger1AnnotationLibrary}} + @ApiImplicitParams({ + {{#implicitHeadersParams}} + {{>implicitHeader}}{{^-last}},{{/-last}} + {{/implicitHeadersParams}} + }) + {{/swagger1AnnotationLibrary}} + {{/implicitHeadersParams.0}} + + + @RequestMapping( + method = RequestMethod.POST, + value = "/service-api/posthook/{{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}}{{{path}}}"{{#singleContentTypes}}{{#hasProduces}}, + produces = "{{{vendorExtensions.x-accepts}}}"{{/hasProduces}}{{#hasConsumes}}, + consumes = "{{{vendorExtensions.x-content-type}}}"{{/hasConsumes}}{{/singleContentTypes}}{{^singleContentTypes}}{{#hasProduces}}, + produces = { {{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}} }{{/hasProduces}}{{#hasConsumes}}, + consumes = { {{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}} }{{/hasConsumes}}{{/singleContentTypes}} + ) + {{#jdk8-default-interface}}default {{/jdk8-default-interface}}ResponseEntity posthook{{#lambda.pascalcase}}{{operationId}}{{/lambda.pascalcase}}( + @RequestBody PosthookRequest posthookRequest{{#allParams.0}}, {{/allParams.0}}{{#allParams}}{{#toOneLine}}{{>queryParams}}{{/toOneLine}}{{>pathParams}}{{>headerParams}}{{>formParams}}{{>cookieParams}}{{>httpServletParam}}{{^-last}}, + {{/-last}} {{/allParams}}{{#reactive}}{{#hasParams}}, + {{/hasParams}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true){{/swagger2AnnotationLibrary}}{{#springFoxDocumentationProvider}}@ApiIgnore{{/springFoxDocumentationProvider}} final ServerWebExchange exchange{{/reactive}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}}, + {{/hasParams}}{{#springFoxDocumentationProvider}}@ApiIgnore {{/springFoxDocumentationProvider}}{{#springDocDocumentationProvider}}@ParameterObject {{/springDocDocumentationProvider}}final Pageable pageable{{/vendorExtensions.x-spring-paginated}} + ){{#unhandledException}} throws Exception{{/unhandledException}}{{^jdk8-default-interface}};{{/jdk8-default-interface}}{{#jdk8-default-interface}} { + {{#isDelegate}} + return {{operationId}}({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#reactive}}{{#hasParams}}, {{/hasParams}}exchange{{/reactive}}{{#vendorExtensions.x-spring-paginated}}, pageable{{/vendorExtensions.x-spring-paginated}}); + {{/isDelegate}} + } + + {{/jdk8-default-interface}} + + + + +{{/operation}} + +} +{{/operations}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/beanValidation.mustache b/boat-scaffold/src/main/templates/boat-webhooks/beanValidation.mustache new file mode 100644 index 000000000..e427a43a0 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/beanValidation.mustache @@ -0,0 +1 @@ +{{#required}}{{^isReadOnly}}@NotNull {{/isReadOnly}}{{/required}}{{#isContainer}}{{^isPrimitiveType}}{{^isEnum}}@Valid {{/isEnum}}{{/isPrimitiveType}}{{/isContainer}}{{^isContainer}}{{^isPrimitiveType}}@Valid {{/isPrimitiveType}}{{/isContainer}}{{>beanValidationCore}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/beanValidationBodyParams.mustache b/boat-scaffold/src/main/templates/boat-webhooks/beanValidationBodyParams.mustache new file mode 100644 index 000000000..6f4e70f06 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/beanValidationBodyParams.mustache @@ -0,0 +1 @@ +{{^useOptional}}{{>beanValidationCore}}{{/useOptional}}{{#useOptional}}{{#required}}{{>beanValidationCore}}{{/required}}{{/useOptional}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/beanValidationCore.mustache b/boat-scaffold/src/main/templates/boat-webhooks/beanValidationCore.mustache new file mode 100644 index 000000000..7faf96bc9 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/beanValidationCore.mustache @@ -0,0 +1,24 @@ +{{#pattern}}{{^isByteArray}}@Pattern(regexp = "{{{pattern}}}") {{/isByteArray}}{{/pattern}}{{! +minLength && maxLength set +}}{{#minLength}}{{#maxLength}}@Size(min = {{minLength}}, max = {{maxLength}}) {{/maxLength}}{{/minLength}}{{! +minLength set, maxLength not +}}{{#minLength}}{{^maxLength}}@Size(min = {{minLength}}) {{/maxLength}}{{/minLength}}{{! +minLength not set, maxLength set +}}{{^minLength}}{{#maxLength}}@Size(max = {{.}}) {{/maxLength}}{{/minLength}}{{! +@Size: minItems && maxItems set +}}{{#minItems}}{{#maxItems}}@Size(min = {{minItems}}, max = {{maxItems}}) {{/maxItems}}{{/minItems}}{{! +@Size: minItems set, maxItems not +}}{{#minItems}}{{^maxItems}}@Size(min = {{minItems}}) {{/maxItems}}{{/minItems}}{{! +@Size: minItems not set && maxItems set +}}{{^minItems}}{{#maxItems}}@Size(max = {{.}}) {{/maxItems}}{{/minItems}}{{! +@Email: useBeanValidation set && isEmail && java8 set +}}{{#useBeanValidation}}{{#isEmail}}{{#java8}}@org.hibernate.validator.constraints.Email{{/java8}}{{/isEmail}}{{/useBeanValidation}}{{! +@Email: performBeanValidation set && isEmail && not java8 set +}}{{#performBeanValidation}}{{#isEmail}}{{^java8}}@jakarta.validation.constraints.Email{{/java8}}{{/isEmail}}{{/performBeanValidation}}{{! +check for integer or long / all others=decimal type with @Decimal* +isInteger set +}}{{#isInteger}}{{#minimum}}@Min({{.}}) {{/minimum}}{{#maximum}}@Max({{.}}) {{/maximum}}{{/isInteger}}{{! +isLong set +}}{{#isLong}}{{#minimum}}@Min({{.}}L) {{/minimum}}{{#maximum}}@Max({{.}}L) {{/maximum}}{{/isLong}}{{! +Not Integer, not Long => we have a decimal value! +}}{{^isInteger}}{{^isLong}}{{#minimum}}@DecimalMin({{#exclusiveMinimum}}value = {{/exclusiveMinimum}}"{{minimum}}"{{#exclusiveMinimum}}, inclusive = false{{/exclusiveMinimum}}) {{/minimum}}{{#maximum}}@DecimalMax({{#exclusiveMaximum}}value = {{/exclusiveMaximum}}"{{maximum}}"{{#exclusiveMaximum}}, inclusive = false{{/exclusiveMaximum}}) {{/maximum}}{{/isLong}}{{/isInteger}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/beanValidationPathParams.mustache b/boat-scaffold/src/main/templates/boat-webhooks/beanValidationPathParams.mustache new file mode 100644 index 000000000..051bd53c0 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/beanValidationPathParams.mustache @@ -0,0 +1 @@ +{{! PathParam is always required, no @NotNull necessary }}{{>beanValidationCore}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/beanValidationQueryParams.mustache b/boat-scaffold/src/main/templates/boat-webhooks/beanValidationQueryParams.mustache new file mode 100644 index 000000000..a2f19f774 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/beanValidationQueryParams.mustache @@ -0,0 +1 @@ +{{#required}}@NotNull {{/required}}{{^useOptional}}{{>beanValidationCore}}{{/useOptional}}{{#useOptional}}{{#required}}{{>beanValidationCore}}{{/required}}{{/useOptional}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/bodyParams.mustache b/boat-scaffold/src/main/templates/boat-webhooks/bodyParams.mustache new file mode 100644 index 000000000..42ca93d70 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/bodyParams.mustache @@ -0,0 +1 @@ +{{#isBodyParam}}{{>paramDoc}}{{#useBeanValidation}} @Valid{{>beanValidationBodyParams}}{{/useBeanValidation}} @RequestBody{{^required}}(required = false){{/required}} {{^reactive}}{{{dataType}}}{{/reactive}}{{#reactive}}{{^isArray}}Mono<{{{dataType}}}>{{/isArray}}{{#isArray}}Flux<{{{baseType}}}>{{/isArray}}{{/reactive}} {{paramName}}{{#useBeanValidation}}{{#addBindingResult}}, BindingResult bindingResult{{/addBindingResult}}{{/useBeanValidation}}{{/isBodyParam}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/collectionDataType.mustache b/boat-scaffold/src/main/templates/boat-webhooks/collectionDataType.mustache new file mode 100644 index 000000000..b6993f0d9 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/collectionDataType.mustache @@ -0,0 +1,23 @@ +{{#openApiNullable}} + {{#isNullable}}JsonNullable<{{/isNullable}} +{{/openApiNullable}} +{{! apply collection type e.g. Set, List }} +{{{baseType}}}< +{{#useBeanValidation}} + {{#items}} + {{#isPrimitiveType}} + {{>beanValidationCore}} + {{/isPrimitiveType}} + {{^isPrimitiveType}} + {{packageName}}@Valid + {{/isPrimitiveType}} + {{/items}} +{{/useBeanValidation}} +{{^useBeanValidation}} +{{items.packageName}} +{{/useBeanValidation}} +{{! apply collection item type }} +{{{items.simpleName}}}> +{{#openApiNullable}} + {{#isNullable}}>{{/isNullable}} +{{/openApiNullable}} diff --git a/boat-scaffold/src/main/templates/boat-webhooks/collectionDataTypeParam.mustache b/boat-scaffold/src/main/templates/boat-webhooks/collectionDataTypeParam.mustache new file mode 100644 index 000000000..623a82ca2 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/collectionDataTypeParam.mustache @@ -0,0 +1,22 @@ +{{#isArray}} + {{#useBeanValidation}} + {{#uniqueItems}}java.util.Set{{/uniqueItems}} + {{^uniqueItems}}java.util.List{{/uniqueItems}}< + {{#items}} + {{#isPrimitiveType}} + {{>beanValidationCore}} {{{dataType}}}> + {{/isPrimitiveType}} + {{^isPrimitiveType}} + {{packageName}}@Valid {{simpleName}}> + {{/isPrimitiveType}} + {{/items}} + {{/useBeanValidation}} + {{^useBeanValidation}} + {{#uniqueItems}}java.util.Set{{/uniqueItems}} + {{^uniqueItems}}java.util.List{{/uniqueItems}} + <{{{items.dataType}}}> + {{/useBeanValidation}} +{{/isArray}} +{{^isArray}} + {{{dataType}}} +{{/isArray}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/converter.mustache b/boat-scaffold/src/main/templates/boat-webhooks/converter.mustache new file mode 100644 index 000000000..a331ded63 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/converter.mustache @@ -0,0 +1,34 @@ +package {{configPackage}}; + +{{#models}} + {{#model}} + {{#isEnum}} +import {{modelPackage}}.{{name}}; + {{/isEnum}} + {{/model}} +{{/models}} + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; + +@Configuration +public class EnumConverterConfiguration { + +{{#models}} +{{#model}} +{{#isEnum}} + @Bean + Converter<{{{dataType}}}, {{name}}> {{classVarName}}Converter() { + return new Converter<{{{dataType}}}, {{name}}>() { + @Override + public {{name}} convert({{{dataType}}} source) { + return {{name}}.fromValue(source); + } + }; + } +{{/isEnum}} +{{/model}} +{{/models}} + +} diff --git a/boat-scaffold/src/main/templates/boat-webhooks/cookieParams.mustache b/boat-scaffold/src/main/templates/boat-webhooks/cookieParams.mustache new file mode 100644 index 000000000..74a837988 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/cookieParams.mustache @@ -0,0 +1 @@ +{{#isCookieParam}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{>paramDoc}} @CookieValue(name = "{{baseName}}"{{^required}}, required = false{{/required}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{>dateTimeParam}} {{>optionalDataType}} {{paramName}}{{/isCookieParam}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/dateTimeParam.mustache b/boat-scaffold/src/main/templates/boat-webhooks/dateTimeParam.mustache new file mode 100644 index 000000000..5f4f3a264 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/dateTimeParam.mustache @@ -0,0 +1 @@ +{{#isDate}} @DateTimeFormat(iso = DateTimeFormat.ISO.DATE){{/isDate}}{{#isDateTime}} @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME){{/isDateTime}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/enumClass.mustache b/boat-scaffold/src/main/templates/boat-webhooks/enumClass.mustache new file mode 100644 index 000000000..0c8de0641 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/enumClass.mustache @@ -0,0 +1,50 @@ + /** + * {{^description}}Gets or Sets {{{name}}}{{/description}}{{{description}}} + */ + {{>additionalEnumTypeAnnotations}}public enum {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}} { + {{#gson}} + {{#allowableValues}} + {{#enumVars}} + @SerializedName({{#isInteger}}"{{/isInteger}}{{#isDouble}}"{{/isDouble}}{{#isLong}}"{{/isLong}}{{#isFloat}}"{{/isFloat}}{{{value}}}{{#isInteger}}"{{/isInteger}}{{#isDouble}}"{{/isDouble}}{{#isLong}}"{{/isLong}}{{#isFloat}}"{{/isFloat}}) + {{{name}}}({{{value}}}){{^-last}}, + {{/-last}}{{#-last}};{{/-last}} + {{/enumVars}} + {{/allowableValues}} + {{/gson}} + {{^gson}} + {{#allowableValues}} + {{#enumVars}} + {{{name}}}({{{value}}}){{^-last}}, + {{/-last}}{{#-last}};{{/-last}} + {{/enumVars}} + {{/allowableValues}} + {{/gson}} + + private final {{{dataType}}} value; + + {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}({{{dataType}}} value) { + this.value = value; + } + + {{#jackson}} + @JsonValue + {{/jackson}} + public {{{dataType}}} getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + @JsonCreator + public static {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} fromValue({{{dataType}}} value) { + for ({{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} b : {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}.values()) { + if (b.value.equals(value)) { + return b; + } + } + {{#isNullable}}return null;{{/isNullable}}{{^isNullable}}throw new IllegalArgumentException("Unexpected value '" + value + "'");{{/isNullable}} + } + } \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/enumOuterClass.mustache b/boat-scaffold/src/main/templates/boat-webhooks/enumOuterClass.mustache new file mode 100644 index 000000000..5528f1cef --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/enumOuterClass.mustache @@ -0,0 +1,51 @@ +{{#jackson}} +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +{{/jackson}} + +/** + * {{^description}}Gets or Sets {{{name}}}{{/description}}{{{description}}} + */ +{{>additionalEnumTypeAnnotations}} +{{>generatedAnnotation}} +public enum {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} { + {{#gson}} + {{#allowableValues}}{{#enumVars}} + @SerializedName({{#isInteger}}"{{/isInteger}}{{#isDouble}}"{{/isDouble}}{{#isLong}}"{{/isLong}}{{#isFloat}}"{{/isFloat}}{{{value}}}{{#isInteger}}"{{/isInteger}}{{#isDouble}}"{{/isDouble}}{{#isLong}}"{{/isLong}}{{#isFloat}}"{{/isFloat}}) + {{{name}}}({{{value}}}){{^-last}}, + {{/-last}}{{#-last}};{{/-last}}{{/enumVars}}{{/allowableValues}} + {{/gson}} + {{^gson}} + {{#allowableValues}}{{#enumVars}} + {{{name}}}({{{value}}}){{^-last}}, + {{/-last}}{{#-last}};{{/-last}}{{/enumVars}}{{/allowableValues}} + {{/gson}} + + private final {{{dataType}}} value; + + {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}({{{dataType}}} value) { + this.value = value; + } + + {{#jackson}} + @JsonValue + {{/jackson}} + public {{{dataType}}} getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + @JsonCreator + public static {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} fromValue({{{dataType}}} value) { + for ({{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} b : {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}.values()) { + if (b.value.equals(value)) { + return b; + } + } + {{#isNullable}}return null;{{/isNullable}}{{^isNullable}}throw new IllegalArgumentException("Unexpected value '" + value + "'");{{/isNullable}} + } +} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/formParams.mustache b/boat-scaffold/src/main/templates/boat-webhooks/formParams.mustache new file mode 100644 index 000000000..505e5909e --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/formParams.mustache @@ -0,0 +1 @@ +{{#isFormParam}}{{^isFile}}{{>paramDoc}}{{#isMultipart}} @RequestPart(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}) {{>dateTimeParam}} {{{dataType}}} {{paramName}}{{/isMultipart}}{{^isMultipart}}{{#useBeanValidation}} @Valid{{/useBeanValidation}} @RequestParam(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}){{>dateTimeParam}} {{{dataType}}} {{paramName}}{{/isMultipart}}{{/isFile}}{{#isFile}}{{>paramDoc}} @RequestPart(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}) {{#isArray}}List<{{/isArray}}{{#reactive}}Flux{{/reactive}}{{^reactive}}MultipartFile{{/reactive}}{{#isArray}}>{{/isArray}} {{paramName}}{{/isFile}}{{/isFormParam}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/generatedAnnotation.mustache b/boat-scaffold/src/main/templates/boat-webhooks/generatedAnnotation.mustache new file mode 100644 index 000000000..2f8ef3059 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/generatedAnnotation.mustache @@ -0,0 +1 @@ +@Generated(value = "{{generatorClass}}"{{^hideGenerationTimestamp}}, date = "{{generatedDate}}"{{/hideGenerationTimestamp}}) \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/headerParams.mustache b/boat-scaffold/src/main/templates/boat-webhooks/headerParams.mustache new file mode 100644 index 000000000..b9589c147 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/headerParams.mustache @@ -0,0 +1 @@ +{{#isHeaderParam}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{>paramDoc}} @RequestHeader(value = "{{baseName}}", required = {{#required}}true{{/required}}{{^required}}false{{/required}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{>dateTimeParam}} {{>optionalDataType}} {{paramName}}{{/isHeaderParam}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/httpServletParam.mustache b/boat-scaffold/src/main/templates/boat-webhooks/httpServletParam.mustache new file mode 100644 index 000000000..4c25519e3 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/httpServletParam.mustache @@ -0,0 +1 @@ +{{#isHttpServletRequest}}{{^reactive}}{{#addServletRequest}} HttpServletRequest httpServletRequest{{/addServletRequest}}{{/reactive}}{{/isHttpServletRequest}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/implicitHeader.mustache b/boat-scaffold/src/main/templates/boat-webhooks/implicitHeader.mustache new file mode 100644 index 000000000..0453940ce --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/implicitHeader.mustache @@ -0,0 +1 @@ +{{#isHeaderParam}}@ApiImplicitParam(name = "{{{baseName}}}", value = "{{{description}}}", {{#required}}required = true,{{/required}} dataType = "{{{dataType}}}", paramType = "header"){{/isHeaderParam}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/mapDataType.mustache b/boat-scaffold/src/main/templates/boat-webhooks/mapDataType.mustache new file mode 100644 index 000000000..5f7a2196b --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/mapDataType.mustache @@ -0,0 +1,21 @@ +{{#openApiNullable}} + {{#isNullable}}JsonNullable<{{/isNullable}} +{{/openApiNullable}} +{{! apply map type with String as key type }} +{{{baseType}}}beanValidationCore}} + {{/isPrimitiveType}} + {{^isPrimitiveType}} + @Valid + {{/isPrimitiveType}} + {{/items}} +{{/useBeanValidation}} +{{! apply map value type with closing bracket }} +{{{items.datatypeWithEnum}}}> +{{#openApiNullable}} + {{#isNullable}}>{{/isNullable}} +{{/openApiNullable}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/methodBody.mustache b/boat-scaffold/src/main/templates/boat-webhooks/methodBody.mustache new file mode 100644 index 000000000..a7ccd54c4 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/methodBody.mustache @@ -0,0 +1,47 @@ +{{^reactive}} +{{#examples}} + {{#-first}} + {{#async}} +return CompletableFuture.supplyAsync(()-> { + {{/async}}getRequest().ifPresent(request -> { +{{#async}} {{/async}} for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) { + {{/-first}} +{{#async}} {{/async}}{{^async}} {{/async}} if (mediaType.isCompatibleWith(MediaType.valueOf("{{{contentType}}}"))) { +{{#async}} {{/async}}{{^async}} {{/async}} String exampleString = {{>exampleString}}; +{{#async}} {{/async}}{{^async}} {{/async}} ApiUtil.setExampleResponse(request, "{{{contentType}}}", exampleString); +{{#async}} {{/async}}{{^async}} {{/async}} break; +{{#async}} {{/async}}{{^async}} {{/async}} } + {{#-last}} +{{#async}} {{/async}}{{^async}} {{/async}} } +{{#async}} {{/async}} }); +{{#async}} {{/async}} return new ResponseEntity<>({{#returnSuccessCode}}HttpStatus.valueOf({{{statusCode}}}){{/returnSuccessCode}}{{^returnSuccessCode}}HttpStatus.NOT_IMPLEMENTED{{/returnSuccessCode}}); + {{#async}} + }, Runnable::run); + {{/async}} + {{/-last}} +{{/examples}} +{{^examples}} +return {{#async}}CompletableFuture.completedFuture({{/async}}new ResponseEntity<>({{#returnSuccessCode}}HttpStatus.OK{{/returnSuccessCode}}{{^returnSuccessCode}}HttpStatus.NOT_IMPLEMENTED{{/returnSuccessCode}}){{#async}}){{/async}}; +{{/examples}} +{{/reactive}} +{{#reactive}} +Mono result = Mono.empty(); + {{#examples}} + {{#-first}} + exchange.getResponse().setStatusCode({{#returnSuccessCode}}HttpStatus.valueOf({{{statusCode}}}){{/returnSuccessCode}}{{^returnSuccessCode}}HttpStatus.NOT_IMPLEMENTED{{/returnSuccessCode}}); + for (MediaType mediaType : exchange.getRequest().getHeaders().getAccept()) { + {{/-first}} + if (mediaType.isCompatibleWith(MediaType.valueOf("{{{contentType}}}"))) { + String exampleString = {{>exampleString}}; + result = ApiUtil.getExampleResponse(exchange, mediaType, exampleString); + break; + } + {{#-last}} + } + {{/-last}} + {{/examples}} +{{^examples}} + exchange.getResponse().setStatusCode({{#returnSuccessCode}}HttpStatus.OK{{/returnSuccessCode}}{{^returnSuccessCode}}HttpStatus.NOT_IMPLEMENTED{{/returnSuccessCode}}); +{{/examples}} + return result{{#allParams}}{{#isBodyParam}}{{^isArray}}{{#paramName}}.then({{.}}){{/paramName}}{{/isArray}}{{#isArray}}{{#paramName}}.thenMany({{.}}){{/paramName}}{{/isArray}}{{/isBodyParam}}{{/allParams}}.then(Mono.empty()); +{{/reactive}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/model.mustache b/boat-scaffold/src/main/templates/boat-webhooks/model.mustache new file mode 100644 index 000000000..896d93104 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/model.mustache @@ -0,0 +1,83 @@ +/* +Boat Generator configuration: + useBeanValidation: {{useBeanValidation}} + useOptional: {{useOptional}} + addServletRequest: {{addServletRequest}} + useLombokAnnotations: {{useLombokAnnotations}} + openApiNullable: {{openApiNullable}} + useSetForUniqueItems: {{useSetForUniqueItems}} + useWithModifiers: {{useWithModifiers}} + useJakartaEe: {{useJakartaEe}} + useSpringBoot3: {{useSpringBoot3}} +*/ +package {{package}}; + +import java.net.URI; +import java.util.Objects; +{{#imports}}import {{import}}; +{{/imports}} +{{#openApiNullable}} +import org.openapitools.jackson.nullable.JsonNullable; +{{/openApiNullable}} +{{#serializableModel}} +import java.io.Serializable; +{{/serializableModel}} +import java.time.OffsetDateTime; +{{#useBeanValidation}} +{{#useJakartaEe}} +import jakarta.validation.Valid; +import jakarta.validation.constraints.*; +{{/useJakartaEe}} +{{^useJakartaEe}} +import javax.validation.Valid; +import javax.validation.constraints.*; +{{/useJakartaEe}} +{{/useBeanValidation}} +{{^useBeanValidation}} +{{#useJakartaEe}} +import jakarta.validation.constraints.*; +{{/useJakartaEe}} +{{^useJakartaEe}} +import javax.validation.constraints.*; +{{/useJakartaEe}} +{{/useBeanValidation}} +{{#performBeanValidation}} +import org.hibernate.validator.constraints.*; +{{/performBeanValidation}} +{{#jackson}} +{{#withXml}} +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +{{/withXml}} +{{/jackson}} +{{#swagger2AnnotationLibrary}} +import io.swagger.v3.oas.annotations.media.Schema; +{{/swagger2AnnotationLibrary}} + +{{#withXml}} +import javax.xml.bind.annotation.*; +{{/withXml}} +{{^parent}} +{{#hateoas}} +import org.springframework.hateoas.RepresentationModel; +{{/hateoas}} +{{/parent}} + +import java.util.*; +{{#useJakartaEe}} +import jakarta.annotation.Generated; +{{/useJakartaEe}} +{{^useJakartaEe}} +import javax.annotation.Generated; +{{/useJakartaEe}} + +{{#models}} +{{#model}} +{{#isEnum}} +{{>enumOuterClass}} +{{/isEnum}} +{{^isEnum}} +{{#vendorExtensions.x-is-one-of-interface}}{{>oneof_interface}}{{/vendorExtensions.x-is-one-of-interface}}{{^vendorExtensions.x-is-one-of-interface}}{{>pojo}}{{/vendorExtensions.x-is-one-of-interface}} +{{/isEnum}} +{{/model}} +{{/models}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/nullableDataType.mustache b/boat-scaffold/src/main/templates/boat-webhooks/nullableDataType.mustache new file mode 100644 index 000000000..ba9bb9463 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/nullableDataType.mustache @@ -0,0 +1 @@ +{{#openApiNullable}}{{#isNullable}}JsonNullable<{{{datatypeWithEnum}}}>{{/isNullable}}{{^isNullable}}{{{datatypeWithEnum}}}{{/isNullable}}{{/openApiNullable}}{{^openApiNullable}}{{{datatypeWithEnum}}}{{/openApiNullable}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/oneof_interface.mustache b/boat-scaffold/src/main/templates/boat-webhooks/oneof_interface.mustache new file mode 100644 index 000000000..679fe3d88 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/oneof_interface.mustache @@ -0,0 +1,13 @@ +{{>additionalOneOfTypeAnnotations}} +{{#withXml}} +{{>xmlAnnotation}} +{{/withXml}} +{{#discriminator}} +{{>typeInfoAnnotation}} +{{/discriminator}} +{{>generatedAnnotation}} +public interface {{classname}}{{#vendorExtensions.x-implements}}{{#-first}} extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} { + {{#discriminator}} + public {{propertyType}} {{propertyGetter}}(); + {{/discriminator}} +} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/openapiDocumentationConfig.mustache b/boat-scaffold/src/main/templates/boat-webhooks/openapiDocumentationConfig.mustache new file mode 100644 index 000000000..bae72f1cc --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/openapiDocumentationConfig.mustache @@ -0,0 +1,89 @@ +package {{configPackage}}; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import org.springframework.web.util.UriComponentsBuilder; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.Contact; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.paths.Paths; +import springfox.documentation.spring.web.paths.RelativePathProvider; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +{{#useOptional}} +import java.util.Optional; +{{/useOptional}} +{{#useJakartaEe}} +import jakarta.annotation.Generated; +import jakarta.servlet.ServletContext; +{{/useJakartaEe}} +{{^useJakartaEe}} +import javax.annotation.Generated; +import javax.servlet.ServletContext; +{{/useJakartaEe}} + +{{>generatedAnnotation}} +@Configuration +@EnableSwagger2 +public class OpenAPIDocumentationConfig { + + ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("{{appName}}") + .description("{{{appDescription}}}") + .license("{{licenseInfo}}") + .licenseUrl("{{licenseUrl}}") + .termsOfServiceUrl("{{infoUrl}}") + .version("{{appVersion}}") + .contact(new Contact("","", "{{infoEmail}}")) + .build(); + } + + @Bean +{{=<% %>=}} + public Docket customImplementation(ServletContext servletContext, @Value("${openapi.<%title%>.base-path:<%>defaultBasePath%>}") String basePath) { +<%={{ }}=%> + return new Docket(DocumentationType.SWAGGER_2) + .select() + .apis(RequestHandlerSelectors.basePackage("{{apiPackage}}")) + .build() + .pathProvider(new BasePathAwareRelativePathProvider(servletContext, basePath)) + .directModelSubstitute(java.time.LocalDate.class, java.sql.Date.class) + .directModelSubstitute(java.time.OffsetDateTime.class, java.util.Date.class) + {{#joda}} + .directModelSubstitute(org.joda.time.LocalDate.class, java.sql.Date.class) + .directModelSubstitute(org.joda.time.DateTime.class, java.util.Date.class) + {{/joda}} + {{#useOptional}} + .genericModelSubstitutes(Optional.class) + {{/useOptional}} + .apiInfo(apiInfo()); + } + + class BasePathAwareRelativePathProvider extends RelativePathProvider { + private String basePath; + + public BasePathAwareRelativePathProvider(ServletContext servletContext, String basePath) { + super(servletContext); + this.basePath = basePath; + } + + @Override + protected String applicationPath() { + return Paths.removeAdjacentForwardSlashes(UriComponentsBuilder.fromPath(super.applicationPath()).path(basePath).build().toString()); + } + + @Override + public String getOperationPath(String operationPath) { + UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromPath("/"); + return Paths.removeAdjacentForwardSlashes( + uriComponentsBuilder.path(operationPath.replaceFirst("^" + basePath, "")).build().toString()); + } + } + +} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/optionalDataType.mustache b/boat-scaffold/src/main/templates/boat-webhooks/optionalDataType.mustache new file mode 100644 index 000000000..ec9df1a76 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/optionalDataType.mustache @@ -0,0 +1,13 @@ +{{#toOneLine}} + {{#useOptional}} + {{#required}} + {{>collectionDataTypeParam}} + {{/required}} + {{^required}} + Optional<{{>collectionDataTypeParam}}> + {{/required}} + {{/useOptional}} + {{^useOptional}} + {{>collectionDataTypeParam}} + {{/useOptional}} +{{/toOneLine}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/paramDoc.mustache b/boat-scaffold/src/main/templates/boat-webhooks/paramDoc.mustache new file mode 100644 index 000000000..7c0c65eb1 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/paramDoc.mustache @@ -0,0 +1 @@ +{{#swagger2AnnotationLibrary}}@Parameter(name = "{{{baseName}}}", description = "{{{description}}}"{{#required}}, required = true{{/required}}){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, {{> allowableValues }}{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/swagger1AnnotationLibrary}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/pathParams.mustache b/boat-scaffold/src/main/templates/boat-webhooks/pathParams.mustache new file mode 100644 index 000000000..16888518c --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/pathParams.mustache @@ -0,0 +1 @@ +{{#isPathParam}}{{#useBeanValidation}}{{>beanValidationPathParams}}{{/useBeanValidation}}{{>paramDoc}} @PathVariable("{{baseName}}"){{>dateTimeParam}} {{>optionalDataType}} {{paramName}}{{/isPathParam}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/pojo.mustache b/boat-scaffold/src/main/templates/boat-webhooks/pojo.mustache new file mode 100644 index 000000000..ed55202ae --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/pojo.mustache @@ -0,0 +1,369 @@ +/** +* {{description}}{{^description}}{{classname}}{{/description}} +*/ +{{>additionalModelTypeAnnotations}} +{{#description}} + {{#swagger1AnnotationLibrary}} + @ApiModel(description = "{{{description}}}") + {{/swagger1AnnotationLibrary}} + {{#swagger2AnnotationLibrary}} + @Schema({{#name}}name = "{{name}}", {{/name}}description = "{{{description}}}") + {{/swagger2AnnotationLibrary}} +{{/description}} +{{#discriminator}} + {{>typeInfoAnnotation}} +{{/discriminator}} +{{#jackson}} + {{#isClassnameSanitized}} + {{^hasDiscriminatorWithNonEmptyMapping}} + @JsonTypeName("{{name}}") + {{/hasDiscriminatorWithNonEmptyMapping}} + {{/isClassnameSanitized}} +{{/jackson}} +{{#withXml}} + {{>xmlAnnotation}} +{{/withXml}} +{{>generatedAnnotation}} +{{#useLombokAnnotations}} +@lombok.EqualsAndHashCode(onlyExplicitlyIncluded = true, doNotUseGetters = true{{#parent}}, callSuper = true{{/parent}}) +@lombok.ToString(onlyExplicitlyIncluded = true, doNotUseGetters = true{{#parent}}, callSuper = true{{/parent}}) +{{/useLombokAnnotations}} +{{#vendorExtensions.x-class-extra-annotation}} + {{{vendorExtensions.x-class-extra-annotation}}} +{{/vendorExtensions.x-class-extra-annotation}} +public class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}}{{^parent}}{{#hateoas}} extends RepresentationModel<{{classname}}> {{/hateoas}}{{/parent}}{{#vendorExtensions.x-implements}}{{#-first}} implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} { +{{#serializableModel}} + + private static final long serialVersionUID = 1L; +{{/serializableModel}} +{{#vars}} + + {{#isEnum}} + {{^isContainer}} + {{>enumClass}} + {{/isContainer}} + {{#isContainer}} + {{#mostInnerItems}} + {{>enumClass}} + {{/mostInnerItems}} + {{/isContainer}} + {{/isEnum}} + {{#jackson}} + {{#withXml}} + @JacksonXmlProperty({{#isXmlAttribute}}isAttribute = true, {{/isXmlAttribute}}{{#xmlNamespace}}namespace="{{.}}", {{/xmlNamespace}}localName = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}") + {{/withXml}} + {{/jackson}} + {{#gson}} + @SerializedName("{{baseName}}") + {{/gson}} + {{#useLombokAnnotations}} + @lombok.Getter{{#swagger1AnnotationLibrary}}(onMethod_ = @ApiModelProperty({{#example}}example = "{{{example}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}")){{/swagger1AnnotationLibrary}} + @lombok.Setter + @lombok.EqualsAndHashCode.Include + @lombok.ToString.Include + {{#vendorExtensions.x-extra-annotation}} + {{#indent4}}{{{vendorExtensions.x-extra-annotation}}}{{/indent4}}{{/vendorExtensions.x-extra-annotation}} + {{/useLombokAnnotations}} + {{#vendorExtensions.x-field-extra-annotation}} + {{{vendorExtensions.x-field-extra-annotation}}} + {{/vendorExtensions.x-field-extra-annotation}} + {{#trimAndIndent4}} + {{#isContainer}} + {{#useBeanValidation}} + @Valid + {{#required}} + @NotNull + {{/required}} + {{>beanValidationCore}} + {{/useBeanValidation}} + {{#isMap}} + {{#openApiNullable}} + private {{#toOneLine}}{{>mapDataType}}{{/toOneLine}} {{name}}{{#isNullable}} = JsonNullable.undefined(){{/isNullable}}{{^isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/isNullable}}; + {{/openApiNullable}} + {{^openApiNullable}} + private {{#toOneLine}}{{>mapDataType}}{{/toOneLine}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}; + {{/openApiNullable}} + {{/isMap}} + {{^isMap}} + {{#isArray}} + {{#openApiNullable}} + private {{#toOneLine}}{{>collectionDataType}}{{/toOneLine}} {{name}}{{#isNullable}} = JsonNullable.undefined(){{/isNullable}}{{^isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/isNullable}}; + {{/openApiNullable}} + {{^openApiNullable}} + private {{#toOneLine}}{{>collectionDataType}}{{/toOneLine}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}; + {{/openApiNullable}} + {{/isArray}} + {{^isArray}} + {{#openApiNullable}} + private {{>nullableDataType}} {{name}}{{#isNullable}} = JsonNullable.undefined(){{/isNullable}}{{^isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/isNullable}}; + {{/openApiNullable}} + {{^openApiNullable}} + private {{>nullableDataType}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}; + {{/openApiNullable}} + {{/isArray}} + {{/isMap}} + {{/isContainer}} + {{^isContainer}} + {{#isDate}} + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) + {{/isDate}} + {{#isDateTime}} + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + {{/isDateTime}} + {{#openApiNullable}} + {{#required}} + @NotNull + {{/required}} + {{#useBeanValidation}} + @Valid + {{#required}} + @NotNull + {{/required}} + {{>beanValidationCore}} + {{/useBeanValidation}} + private {{>nullableDataType}} {{name}}{{#isNullable}} = JsonNullable.undefined(){{/isNullable}}{{^isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/isNullable}}; + {{/openApiNullable}} + {{^openApiNullable}} + {{#useBeanValidation}} + @Valid + {{#required}} + @NotNull + {{/required}} + {{>beanValidationCore}} + {{/useBeanValidation}} + private {{>nullableDataType}} {{name}}{{#isNullable}} = null{{/isNullable}}{{^isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/isNullable}}; + {{/openApiNullable}} + {{/isContainer}} + {{/trimAndIndent4}} +{{/vars}} +{{#generatedConstructorWithRequiredArgs}} + {{#hasRequired}} + + public {{classname}}() { + super(); + } + + /** + * Constructor with only required parameters + */ + public {{classname}}({{#requiredVars}}{{{datatypeWithEnum}}} {{name}}{{^-last}}, {{/-last}}{{/requiredVars}}) { + {{#parent}} + super({{#parentRequiredVars}}{{name}}{{^-last}}, {{/-last}}{{/parentRequiredVars}}); + {{/parent}} + {{#vars}} + {{#required}} + {{#openApiNullable}} + this.{{name}} = {{#isNullable}}JsonNullable.of({{name}}){{/isNullable}}{{^isNullable}}{{name}}{{/isNullable}}; + {{/openApiNullable}} + {{^openApiNullable}} + this.{{name}} = {{name}}; + {{/openApiNullable}} + {{/required}} + {{/vars}} + } + {{/hasRequired}} +{{/generatedConstructorWithRequiredArgs}} +{{#vars}} + + {{! begin feature: fluent setter methods }} + public {{classname}} {{#useWithModifiers}}with{{nameInCamelCase}}{{/useWithModifiers}}{{^useWithModifiers}}{{name}}{{/useWithModifiers}}({{{datatypeWithEnum}}} {{name}}) { + {{#openApiNullable}} + this.{{name}} = {{#isNullable}}JsonNullable.of({{name}}){{/isNullable}}{{^isNullable}}{{name}}{{/isNullable}}; + {{/openApiNullable}} + {{^openApiNullable}} + this.{{name}} = {{name}}; + {{/openApiNullable}} + return this; + } + {{#isArray}} + + public {{classname}} add{{nameInCamelCase}}Item({{{items.datatypeWithEnum}}} {{name}}Item) { + {{#openApiNullable}} + if (this.{{name}} == null{{#isNullable}} || !this.{{name}}.isPresent(){{/isNullable}}) { + this.{{name}} = {{#isNullable}}JsonNullable.of({{/isNullable}}{{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>(){{/defaultValue}}{{#isNullable}}){{/isNullable}}; + } + this.{{name}}{{#isNullable}}.get(){{/isNullable}}.add({{name}}Item); + {{/openApiNullable}} + {{^openApiNullable}} + if (this.{{name}} == null) { + this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>(){{/defaultValue}}; + } + this.{{name}}.add({{name}}Item); + {{/openApiNullable}} + return this; + } + {{/isArray}} + {{#isMap}} + + {{#openApiNullable}} + {{#isNullable}} + public {{classname}} put{{nameInCamelCase}}Item(String key, {{{items.datatypeWithEnum}}} {{name}}Item) { + {{^required}} + if (this.{{name}} == null) { + this.{{name}} = JsonNullable.of({{{defaultValue}}}{{^defaultValue}}new HashMap<>(){{/defaultValue}}); + } + {{/required}} + this.{{name}}.get().put(key, {{name}}Item); + return this; + } + {{/isNullable}} + {{^isNullable}} + public {{classname}} put{{nameInCamelCase}}Item(String key, {{{items.datatypeWithEnum}}} {{name}}Item) { + {{^required}} + + if (this.{{name}} == null) { + this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new HashMap<>(){{/defaultValue}}; + } + {{/required}} + this.{{name}}.put(key, {{name}}Item); + return this; + } + {{/isNullable}} + {{/openApiNullable}} + {{^openApiNullable}} + public {{classname}} put{{nameInCamelCase}}Item(String key, {{{items.datatypeWithEnum}}} {{name}}Item) { + {{^required}} + + if (this.{{name}} == null) { + this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new HashMap<>(){{/defaultValue}}; + } + {{/required}} + this.{{name}}.put(key, {{name}}Item); + return this; + } + {{/openApiNullable}} + {{/isMap}} + {{! end feature: fluent setter methods }} + {{! begin feature: getter and setter }} + +{{^useLombokAnnotations}} + /** + {{#description}} + * {{{.}}} + {{/description}} + {{^description}} + * Get {{name}} + {{/description}} + {{#minimum}} + * minimum: {{.}} + {{/minimum}} + {{#maximum}} + * maximum: {{.}} + {{/maximum}} + * @return {{name}} + */ + {{#vendorExtensions.x-extra-annotation}} + {{{vendorExtensions.x-extra-annotation}}} + {{/vendorExtensions.x-extra-annotation}} + {{#useBeanValidation}} + {{>beanValidation}} + {{/useBeanValidation}} + {{^useBeanValidation}} + {{#required}}@NotNull{{/required}} + {{/useBeanValidation}} + {{#swagger2AnnotationLibrary}} + @Schema(name = "{{{baseName}}}"{{#isReadOnly}}, accessMode = Schema.AccessMode.READ_ONLY{{/isReadOnly}}{{#example}}, example = "{{{.}}}"{{/example}}{{#description}}, description = "{{{.}}}"{{/description}}, requiredMode = {{#required}}Schema.RequiredMode.REQUIRED{{/required}}{{^required}}Schema.RequiredMode.NOT_REQUIRED{{/required}}) + {{/swagger2AnnotationLibrary}} + {{#swagger1AnnotationLibrary}} + @ApiModelProperty({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}") + {{/swagger1AnnotationLibrary}} + {{#jackson}} + @JsonProperty("{{baseName}}") + {{/jackson}} + public {{>nullableDataType}} {{getter}}() { + return {{name}}; + } + + {{#vendorExtensions.x-setter-extra-annotation}} + {{{vendorExtensions.x-setter-extra-annotation}}} + {{/vendorExtensions.x-setter-extra-annotation}} + public void {{setter}}({{>nullableDataType}} {{name}}) { + this.{{name}} = {{name}}; + } + {{/useLombokAnnotations}} + {{! end feature: getter and setter }} + {{/vars}} + {{#parentVars}} + +{{! begin feature: fluent setter methods for inherited properties }} + public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) { + super.{{setter}}({{name}}); + return this; + } + {{#isArray}} + + public {{classname}} add{{nameInCamelCase}}Item({{{items.datatypeWithEnum}}} {{name}}Item) { + super.add{{nameInCamelCase}}Item({{name}}Item); + return this; + } + {{/isArray}} + {{#isMap}} + + public {{classname}} put{{nameInCamelCase}}Item(String key, {{{items.datatypeWithEnum}}} {{name}}Item) { + super.put{{nameInCamelCase}}Item(key, {{name}}Item); + return this; + } + {{/isMap}} +{{! end feature: fluent setter methods for inherited properties }} +{{/parentVars}} + +{{^useLombokAnnotations}} + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + }{{#hasVars}} + {{classname}} {{classVarName}} = ({{classname}}) o; + return {{#vars}}{{#vendorExtensions.x-is-jackson-optional-nullable}}equalsNullable(this.{{name}}, {{classVarName}}.{{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{#isByteArray}}Arrays{{/isByteArray}}{{^isByteArray}}Objects{{/isByteArray}}.equals(this.{{name}}, {{classVarName}}.{{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^-last}} && + {{/-last}}{{/vars}}{{#parent}} && + super.equals(o){{/parent}};{{/hasVars}}{{^hasVars}} + return true;{{/hasVars}} + } + {{#vendorExtensions.x-jackson-optional-nullable-helpers}} + + private static boolean equalsNullable(JsonNullable a, JsonNullable b) { + return a == b || (a != null && b != null && a.isPresent() && b.isPresent() && Objects.deepEquals(a.get(), b.get())); + } + {{/vendorExtensions.x-jackson-optional-nullable-helpers}} + + @Override + public int hashCode() { + return Objects.hash({{#vars}}{{#vendorExtensions.x-is-jackson-optional-nullable}}hashCodeNullable({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{^isByteArray}}{{name}}{{/isByteArray}}{{#isByteArray}}Arrays.hashCode({{name}}){{/isByteArray}}{{/vendorExtensions.x-is-jackson-optional-nullable}}{{^-last}}, {{/-last}}{{/vars}}{{#parent}}{{#hasVars}}, {{/hasVars}}super.hashCode(){{/parent}}); + } + {{#vendorExtensions.x-jackson-optional-nullable-helpers}} + + private static int hashCodeNullable(JsonNullable a) { + if (a == null) { + return 1; + } + return a.isPresent() ? Arrays.deepHashCode(new Object[]{a.get()}) : 31; + } + {{/vendorExtensions.x-jackson-optional-nullable-helpers}} + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class {{classname}} {\n"); + {{#parent}} + sb.append(" ").append(toIndentedString(super.toString())).append("\n"); + {{/parent}} + {{#vars}}sb.append(" {{name}}: ").append(toIndentedString({{name}})).append("\n"); + {{/vars}}sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + {{/useLombokAnnotations}} +} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/pom.mustache b/boat-scaffold/src/main/templates/boat-webhooks/pom.mustache new file mode 100644 index 000000000..9e792f14e --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/pom.mustache @@ -0,0 +1,281 @@ + + 4.0.0 + {{groupId}} + {{artifactId}} + jar + {{artifactId}} + {{artifactVersion}} + + 1.8 + ${java.version} + ${java.version} + UTF-8 + {{#springFoxDocumentationProvider}} + 2.9.2 + {{/springFoxDocumentationProvider}} + {{#springDocDocumentationProvider}} + 1.6.14 + {{/springDocDocumentationProvider}} + {{^springFoxDocumentationProvider}} + {{^springDocDocumentationProvider}} + {{#swagger1AnnotationLibrary}} + 1.6.6 + {{/swagger1AnnotationLibrary}} + {{#swagger2AnnotationLibrary}} + }2.2.7 + {{/swagger2AnnotationLibrary}} + {{/springDocDocumentationProvider}} + {{/springFoxDocumentationProvider}} + {{#virtualService}} + 2.5.2 + {{/virtualService}} + {{#useSwaggerUI}} + 4.15.5 + {{/useSwaggerUI}} + +{{#parentOverridden}} + + {{{parentGroupId}}} + {{{parentArtifactId}}} + {{{parentVersion}}} + +{{/parentOverridden}} +{{^parentOverridden}} + + org.springframework.boot + spring-boot-starter-parent + {{#springFoxDocumentationProvider}}2.5.14{{/springFoxDocumentationProvider}}{{^springFoxDocumentationProvider}}2.7.6{{/springFoxDocumentationProvider}} + + +{{/parentOverridden}} + + src/main/java + {{^interfaceOnly}} + + + org.springframework.boot + spring-boot-maven-plugin + {{#classifier}} + + {{{classifier}}} + + {{/classifier}} + + {{#apiFirst}} + + org.openapitools + openapi-generator-maven-plugin + {{{generatorVersion}}} + + + + generate + + + src/main/resources/openapi.yaml + spring + {{{apiPackage}}} + {{{modelPackage}}} + false + {{#modelNamePrefix}} + {{{.}}} + {{/modelNamePrefix}} + {{#modelNameSuffix}} + {{{.}}} + {{/modelNameSuffix}} + + {{#configOptions}} + <{{left}}>{{right}} + {{/configOptions}} + + + + + + {{/apiFirst}} + + {{/interfaceOnly}} + + + {{#useJakartaEe}} + + jakarta.persistence + jakarta.persistence-api + 3.1.0 + + + jakarta.servlet + jakarta.servlet-api + 6.0.0 + + + jakarta.annotation + jakarta.annotation-api + 2.1.1 + + + jakarta.validation + jakarta.validation-api + 3.0.2 + + {{/useJakartaEe}} + {{^useJakartaEe}} + + javax.persistence + javax.persistence-api + 2.2 + + + javax.servlet + javax.servlet-api + 4.0.1 + + {{/useJakartaEe}} + {{#useLombokAnnotations}} + + org.projectlombok + lombok + 1.18.30 + + {{/useLombokAnnotations}} + + org.springframework.boot + spring-boot-starter-web{{#reactive}}flux{{/reactive}} + + + org.springframework.data + spring-data-commons + + {{#springDocDocumentationProvider}} + + {{#useSwaggerUI}} + + org.springdoc + springdoc-openapi-{{#reactive}}webflux-{{/reactive}}ui + ${springdoc.version} + + {{/useSwaggerUI}} + {{^useSwaggerUI}} + + org.springdoc + springdoc-openapi-{{#reactive}}webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-core + ${springdoc.version} + + {{/useSwaggerUI}} + {{/springDocDocumentationProvider}} + {{#springFoxDocumentationProvider}} + + + io.springfox + springfox-swagger2 + ${springfox.version} + + {{/springFoxDocumentationProvider}} + {{#useSwaggerUI}} + {{^springDocDocumentationProvider}} + + org.webjars + swagger-ui + ${swagger-ui.version} + + + org.webjars + webjars-locator-core + + {{/springDocDocumentationProvider}} + {{/useSwaggerUI}} + {{^springFoxDocumentationProvider}} + {{^springDocDocumentationProvider}} + {{#swagger1AnnotationLibrary}} + + io.swagger + swagger-annotations + ${swagger-annotations.version} + + {{/swagger1AnnotationLibrary}} + {{#swagger2AnnotationLibrary}} + + io.swagger.core.v3 + swagger-annotations + ${swagger-annotations.version} + + {{/swagger2AnnotationLibrary}} + {{/springDocDocumentationProvider}} + {{/springFoxDocumentationProvider}} + + + com.google.code.findbugs + jsr305 + 3.0.2 + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + {{#withXml}} + + + jakarta.xml.bind + jakarta.xml.bind-api + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + + {{/withXml}} + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + {{#joda}} + + com.fasterxml.jackson.datatype + jackson-datatype-joda + + {{/joda}} + {{#openApiNullable}} + + org.openapitools + jackson-databind-nullable + 0.2.2 + + {{/openApiNullable}} +{{#useBeanValidation}} + + + org.springframework.boot + spring-boot-starter-validation + +{{/useBeanValidation}} +{{#virtualService}} + + + io.virtualan + virtualan-plugin + ${virtualan.version} + + + + org.hsqldb + hsqldb + + +{{/virtualService}} +{{#hateoas}} + + + org.springframework.boot + spring-boot-starter-hateoas + +{{/hateoas}} + + com.fasterxml.jackson.core + jackson-databind + + + org.springframework.boot + spring-boot-starter-test + test + + + diff --git a/boat-scaffold/src/main/templates/boat-webhooks/queryParams.mustache b/boat-scaffold/src/main/templates/boat-webhooks/queryParams.mustache new file mode 100644 index 000000000..6b78fb343 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/queryParams.mustache @@ -0,0 +1 @@ +{{#isQueryParam}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{>paramDoc}}{{#useBeanValidation}} @Valid{{/useBeanValidation}}{{^isModel}} @RequestParam(value = {{#isMap}}""{{/isMap}}{{^isMap}}"{{baseName}}"{{/isMap}}{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/isModel}}{{>dateTimeParam}} {{>optionalDataType}} {{paramName}}{{/isQueryParam}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/responseType.mustache b/boat-scaffold/src/main/templates/boat-webhooks/responseType.mustache new file mode 100644 index 000000000..305f28e6d --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/responseType.mustache @@ -0,0 +1 @@ +{{^vendorExtensions.x-sse}}{{#reactive}}{{#useResponseEntity}}MonoreturnTypes}}>>{{/useResponseEntity}}{{^useResponseEntity}}{{#isArray}}Flux{{/isArray}}{{^isArray}}Mono{{/isArray}}<{{>returnTypes}}>{{/useResponseEntity}}{{/reactive}}{{^reactive}}{{#responseWrapper}}{{.}}<{{/responseWrapper}}{{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{>returnTypes}}{{#useResponseEntity}}>{{/useResponseEntity}}{{#responseWrapper}}>{{/responseWrapper}}{{/reactive}}{{/vendorExtensions.x-sse}}{{#vendorExtensions.x-sse}}{{#isArray}}Flux{{/isArray}}{{^isArray}}Mono{{/isArray}}<{{>returnTypes}}>{{/vendorExtensions.x-sse}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/returnTypes.mustache b/boat-scaffold/src/main/templates/boat-webhooks/returnTypes.mustache new file mode 100644 index 000000000..0d2b380d7 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/returnTypes.mustache @@ -0,0 +1 @@ +{{#isMap}}Map{{/isMap}}{{#isArray}}{{#reactive}}Flux{{/reactive}}{{^reactive}}{{{returnContainer}}}{{/reactive}}<{{{returnType}}}>{{/isArray}}{{^returnContainer}}{{{returnType}}}{{/returnContainer}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/springdocDocumentationConfig.mustache b/boat-scaffold/src/main/templates/boat-webhooks/springdocDocumentationConfig.mustache new file mode 100644 index 000000000..467d92155 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/springdocDocumentationConfig.mustache @@ -0,0 +1,54 @@ +package {{configPackage}}; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.security.SecurityScheme; + +@Configuration +public class SpringDocConfiguration { + + @Bean + OpenAPI apiInfo() { + return new OpenAPI() + .info( + new Info(){{#appName}} + .title("{{appName}}"){{/appName}} + .description("{{{appDescription}}}"){{#termsOfService}} + .termsOfService("{{termsOfService}}"){{/termsOfService}}{{#openAPI}}{{#info}}{{#contact}} + .contact( + new Contact(){{#infoName}} + .name("{{infoName}}"){{/infoName}}{{#infoUrl}} + .url("{{infoUrl}}"){{/infoUrl}}{{#infoEmail}} + .email("{{infoEmail}}"){{/infoEmail}} + ){{/contact}}{{#license}} + .license( + new License() + {{#licenseInfo}}.name("{{licenseInfo}}") + {{/licenseInfo}}{{#licenseUrl}}.url("{{licenseUrl}}") + {{/licenseUrl}} + ){{/license}}{{/info}}{{/openAPI}} + .version("{{appVersion}}") + ){{#hasAuthMethods}} + .components( + new Components(){{#authMethods}} + .addSecuritySchemes("{{name}}", new SecurityScheme(){{#isBasic}} + .type(SecurityScheme.Type.HTTP) + .scheme("{{scheme}}"){{#bearerFormat}} + .bearerFormat("{{bearerFormat}}"){{/bearerFormat}}{{/isBasic}}{{#isApiKey}} + .type(SecurityScheme.Type.APIKEY){{#isKeyInHeader}} + .in(SecurityScheme.In.HEADER){{/isKeyInHeader}}{{#isKeyInQuery}} + .in(SecurityScheme.In.QUERY){{/isKeyInQuery}}{{#isKeyInCookie}} + .in(SecurityScheme.In.COOKIE){{/isKeyInCookie}} + .name("{{keyParamName}}"){{/isApiKey}}{{#isOAuth}} + .type(SecurityScheme.Type.OAUTH2){{/isOAuth}} + ){{/authMethods}} + ){{/hasAuthMethods}} + ; + } +} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/typeInfoAnnotation.mustache b/boat-scaffold/src/main/templates/boat-webhooks/typeInfoAnnotation.mustache new file mode 100644 index 000000000..4f49c4a0f --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/typeInfoAnnotation.mustache @@ -0,0 +1,22 @@ +{{#jackson}} +{{#discriminator.mappedModels}} +{{#-first}} +@JsonIgnoreProperties( + value = "{{{discriminator.propertyBaseName}}}", // ignore manually set {{{discriminator.propertyBaseName}}}, it will be automatically generated by Jackson during serialization + allowGetters = true, // allows the {{{discriminator.propertyBaseName}}} to be set during serialization + allowSetters = true // allows the {{{discriminator.propertyBaseName}}} to be set during deserialization +) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "{{{discriminator.propertyBaseName}}}", visible = true) +@JsonSubTypes({ +{{/-first}} + {{^vendorExtensions.x-discriminator-value}} + @JsonSubTypes.Type(value = {{modelName}}.class, name = "{{{mappingName}}}"){{^-last}},{{/-last}} + {{/vendorExtensions.x-discriminator-value}} + {{#vendorExtensions.x-discriminator-value}} + @JsonSubTypes.Type(value = {{modelName}}.class, name = "{{{vendorExtensions.x-discriminator-value}}}"){{^-last}},{{/-last}} + {{/vendorExtensions.x-discriminator-value}} +{{#-last}} +}) +{{/-last}} +{{/discriminator.mappedModels}} +{{/jackson}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/validatedDataType.mustache b/boat-scaffold/src/main/templates/boat-webhooks/validatedDataType.mustache new file mode 100644 index 000000000..44add65e8 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/validatedDataType.mustache @@ -0,0 +1,16 @@ +{{#useBeanValidation}}{{! + }}{{#isContainer}}{{! + }}{{#isMapContainer}}{{! + }}{{{baseType}}}beanValidationCore}}{{{dataType}}}{{/items}}>{{! + }}{{/isMapContainer}}{{! + }}{{^isMapContainer}}{{! + }}{{#reactive}}{{{dataType}}}{{/reactive}}{{^reactive}}{{{baseType}}}<{{#items}}{{>beanValidationCore}}{{{dataType}}}{{/items}}>{{/reactive}}{{! + }}{{/isMapContainer}}{{! + }}{{/isContainer}}{{! + }}{{^isContainer}}{{! + }}{{>beanValidationCore}} {{{dataType}}}{{! + }}{{/isContainer}}{{! +}}{{/useBeanValidation}}{{! +}}{{^useBeanValidation}}{{! + }}{{{dataType}}}{{! +}}{{/useBeanValidation}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/validatedDataTypeWithEnum.mustache b/boat-scaffold/src/main/templates/boat-webhooks/validatedDataTypeWithEnum.mustache new file mode 100644 index 000000000..a2165f996 --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/validatedDataTypeWithEnum.mustache @@ -0,0 +1,16 @@ +{{#useBeanValidation}}{{! + }}{{#isContainer}}{{! + }}{{#isMapContainer}}{{! + }}{{{baseType}}}beanValidationCore}}{{{datatypeWithEnum}}}{{/items}}>{{! + }}{{/isMapContainer}}{{! + }}{{^isMapContainer}}{{! + }}{{#reactive}}{{{dataType}}}{{/reactive}}{{^reactive}}{{{baseType}}}<{{#items}}{{>beanValidationCore}}{{{datatypeWithEnum}}}{{/items}}>{{/reactive}}{{! + }}{{/isMapContainer}}{{! + }}{{/isContainer}}{{! + }}{{^isContainer}}{{! + }}{{>beanValidationCore}} {{{datatypeWithEnum}}}{{! + }}{{/isContainer}}{{! +}}{{/useBeanValidation}}{{! +}}{{^useBeanValidation}}{{! + }}{{{datatypeWithEnum}}}{{! +}}{{/useBeanValidation}} \ No newline at end of file diff --git a/boat-scaffold/src/main/templates/boat-webhooks/xmlAnnotation.mustache b/boat-scaffold/src/main/templates/boat-webhooks/xmlAnnotation.mustache new file mode 100644 index 000000000..a9e6fb0fa --- /dev/null +++ b/boat-scaffold/src/main/templates/boat-webhooks/xmlAnnotation.mustache @@ -0,0 +1,7 @@ +{{#withXml}} +{{#jackson}} +@JacksonXmlRootElement({{#xmlNamespace}}namespace="{{.}}", {{/xmlNamespace}}localName = "{{xmlName}}{{^xmlName}}{{classname}}{{/xmlName}}") +{{/jackson}} +@XmlRootElement({{#xmlNamespace}}namespace="{{.}}", {{/xmlNamespace}}name = "{{xmlName}}{{^xmlName}}{{classname}}{{/xmlName}}") +@XmlAccessorType(XmlAccessType.FIELD) +{{/withXml}} \ No newline at end of file diff --git a/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatWebhooksCodeGenTests.java b/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatWebhooksCodeGenTests.java new file mode 100644 index 000000000..204ea2e3e --- /dev/null +++ b/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatWebhooksCodeGenTests.java @@ -0,0 +1,239 @@ +package com.backbase.oss.codegen.java; + +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.body.Parameter; +import com.samskivert.mustache.Template; +import io.swagger.parser.OpenAPIParser; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.parser.core.models.ParseOptions; +import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.openapitools.codegen.CodegenModel; +import org.openapitools.codegen.CodegenOperation; +import org.openapitools.codegen.CodegenProperty; +import org.openapitools.codegen.ClientOptInput; +import org.openapitools.codegen.DefaultGenerator; +import org.openapitools.codegen.SupportingFile; +import org.openapitools.codegen.CodegenParameter; + +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.*; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class BoatWebhooksCodeGenTests { + + static final String PROP_BASE = BoatWebhooksCodeGenTests.class.getSimpleName() + "."; + static final String TEST_OUTPUT = System.getProperty(PROP_BASE + "output", "target/boat-webhooks-codegen-tests"); + + @BeforeAll + static void before() throws IOException { + Files.createDirectories(Paths.get(TEST_OUTPUT)); + FileUtils.deleteDirectory(new File(TEST_OUTPUT)); + } + + @Test + void newLineIndent() throws IOException { + final BoatSpringCodeGen.NewLineIndent indent = new BoatSpringCodeGen.NewLineIndent(2, "_"); + final StringWriter output = new StringWriter(); + final Template.Fragment frag = mock(Template.Fragment.class); + + when(frag.execute()).thenReturn("\n Good \r\n morning, \r\n Dave "); + + indent.execute(frag, output); + + assertThat(output.toString(), equalTo(String.format("__%n__Good%n__ morning,%n__ Dave%n"))); + } + + @Test + void addServletRequestTestFromOperation() { + final BoatWebhooksCodeGen gen = new BoatWebhooksCodeGen(); + gen.addServletRequest = true; + CodegenOperation co = gen.fromOperation("/test", "POST", new Operation(), null); + assertEquals(1, co.allParams.size()); + assertEquals("httpServletRequest", co.allParams.get(0).paramName); + } + + @Test + void webhookWithCardsApi() throws IOException { + var codegen = new BoatWebhooksCodeGen(); + var input = new File("src/test/resources/boat-spring/cardsapi.yaml"); + codegen.setLibrary("spring-boot"); + codegen.setInterfaceOnly(true); + codegen.setOutputDir(TEST_OUTPUT + "/cards"); + codegen.setInputSpec(input.getAbsolutePath()); + + var openApiInput = new OpenAPIParser().readLocation(input.getAbsolutePath(), null, new ParseOptions()).getOpenAPI(); + var clientOptInput = new ClientOptInput(); + clientOptInput.config(codegen); + clientOptInput.openAPI(openApiInput); + + List files = new DefaultGenerator().opts(clientOptInput).generate(); + + File testApi = files.stream().filter(file -> file.getName().equals("WebhookClientApi.java")) + .findFirst() + .get(); + MethodDeclaration testPostMethod = StaticJavaParser.parse(testApi) + .findAll(MethodDeclaration.class) + .get(1); + + Parameter contentParam = testPostMethod.getParameterByName("prehookRequest").get(); + assertThat(contentParam.getTypeAsString(), equalTo("PrehookRequest")); + } + + @Test + void testReplaceBeanValidationCollectionType() { + var codegen = new BoatWebhooksCodeGen(); + codegen.setUseBeanValidation(true); + var codegenProperty = new CodegenProperty(); + codegenProperty.isModel = true; + codegenProperty.baseName = "request"; // not a response + + String result = codegen.replaceBeanValidationCollectionType( + codegenProperty, "Set"); + assertEquals("Set<@Valid com.backbase.dbs.arrangement.commons.model.TranslationItemDto>", result); + } + + @Test + void replaceBeanValidationCollectionType_shouldCoverAllBranches() { + BoatWebhooksCodeGen codegen = new BoatWebhooksCodeGen(); + CodegenProperty property = new CodegenProperty(); + property.isModel = true; + property.baseName = "request"; + String baseType = "Set"; + + codegen.setUseBeanValidation(false); + assertEquals(baseType, codegen.replaceBeanValidationCollectionType(property, baseType)); + + codegen.setUseBeanValidation(true); + assertEquals("", codegen.replaceBeanValidationCollectionType(property, "")); + + property.isModel = false; + assertEquals(baseType, codegen.replaceBeanValidationCollectionType(property, baseType)); + property.isModel = true; + + property.baseName = "response"; + assertEquals(baseType, codegen.replaceBeanValidationCollectionType(property, baseType)); + property.baseName = "request"; + + String noValidType = "Set"; + String expectedValidType = "Set<@Valid com.example.Model>"; + assertEquals(expectedValidType, codegen.replaceBeanValidationCollectionType(property, noValidType)); + + String regexType = "Set<@Valid com.example.Model>"; + String expectedRegexResult = "Set"; + assertEquals(expectedRegexResult, codegen.replaceBeanValidationCollectionType(property, regexType)); + + String unmatchedType = "List<@Valid Model>"; + assertEquals(unmatchedType, codegen.replaceBeanValidationCollectionType(property, unmatchedType)); + } + + @Test + void testFromParameter() { + BoatWebhooksCodeGen codeGen = new BoatWebhooksCodeGen(); + io.swagger.v3.oas.models.parameters.Parameter swaggerParam = new io.swagger.v3.oas.models.parameters.Parameter(); + swaggerParam.setName("testParam"); + Set imports = new HashSet<>(); + + // Call the method under test + var result = codeGen.fromParameter(swaggerParam, imports); + + // Assert result is not null and has expected properties + assertThat(result, notNullValue()); + assertEquals("testParam", result.baseName); + } + + @Test + void testPostProcessModelProperty_addsBigDecimalSerializerAnnotation() { + BoatWebhooksCodeGen codeGen = new BoatWebhooksCodeGen(); + // Simulate the condition for shouldSerializeBigDecimalAsString to return true + CodegenProperty property = new CodegenProperty(); + property.baseType = "BigDecimal"; + property.openApiType = "string"; + property.dataFormat = "number"; + CodegenModel model = new CodegenModel(); + model.imports = new HashSet<>(); + property.vendorExtensions = new HashMap<>(); + + codeGen.setSerializeBigDecimalAsString(true); + codeGen.postProcessModelProperty(model, property); + + assertThat(property.vendorExtensions, hasEntry("x-extra-annotation", "@JsonSerialize(using = BigDecimalCustomSerializer.class)")); + assertThat(model.imports, hasItems("BigDecimalCustomSerializer", "JsonSerialize")); + } + + @Test + void postProcessModelProperty_shouldNotAddAnnotationForNonBigDecimal() { + BoatWebhooksCodeGen codegen = new BoatWebhooksCodeGen(); + codegen.setSerializeBigDecimalAsString(true); + + CodegenModel model = new CodegenModel(); + CodegenProperty property = new CodegenProperty(); + property.baseType = "String"; + property.openApiType = "string"; + property.dataFormat = "number"; + + codegen.postProcessModelProperty(model, property); + + // Assert that no annotation is added + assertThat(property.vendorExtensions.containsKey("x-extra-annotation"), is(false)); + } + + @Test + void toApiName_shouldReturnDefaultNameForEmptyString() { + BoatWebhooksCodeGen codegen = new BoatWebhooksCodeGen(); + String result = codegen.toApiName(""); + assertEquals("WebhookDefaultApi", result); + } + + @Test + void processOpts_shouldRemoveApiUtilSupportingFileWhenNotUsed() { + BoatWebhooksCodeGen codegen = new BoatWebhooksCodeGen(); + var outputDir = TEST_OUTPUT + "/supportingFilesTest"; + codegen.supportingFiles().add(new SupportingFile("apiUtil.mustache", outputDir, "ApiUtil.java")); + codegen.additionalProperties().put("generateSupportingFiles", false); + String originalSupportingFiles = org.openapitools.codegen.config.GlobalSettings.getProperty("supportingFiles"); + try { + org.openapitools.codegen.config.GlobalSettings.setProperty("supportingFiles", ""); + codegen.processOpts(); + boolean apiUtilPresent = codegen.supportingFiles().stream() + .anyMatch(sf -> "apiUtil.mustache".equals(sf.getTemplateFile())); + assertEquals(false, apiUtilPresent); + } finally { + if (originalSupportingFiles != null) { + org.openapitools.codegen.config.GlobalSettings.setProperty("supportingFiles", originalSupportingFiles); + } else { + org.openapitools.codegen.config.GlobalSettings.clearProperty("supportingFiles"); + } + } + } + + @Test + void postProcessParameter_setsBaseTypeForContainer() { + BoatWebhooksCodeGen codeGen = new BoatWebhooksCodeGen(); + codeGen.setReactive(false); + CodegenParameter param = new CodegenParameter(); + param.isContainer = true; + param.isMap = false; + param.dataType = "List"; + param.baseType = null; + + codeGen.postProcessParameter(param); + + assertEquals("List", param.baseType, "Base type should be extracted from dataType for containers"); + } +} diff --git a/boat-scaffold/src/test/resources/boat-spring/cardsapi.yaml b/boat-scaffold/src/test/resources/boat-spring/cardsapi.yaml new file mode 100644 index 000000000..0036ebcec --- /dev/null +++ b/boat-scaffold/src/test/resources/boat-spring/cardsapi.yaml @@ -0,0 +1,89 @@ +openapi: 3.0.2 +info: + version: 1.0.0 + title: test + +paths: + /client/v3/cards: + post: + tags: + - card + operationId: postCards + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CardItem' + responses: + "201": + $ref: '#/components/responses/CardIdResponse' + get: + tags: + - card + operationId: getCards + responses: + "200": + $ref: '#/components/responses/CardsResponse' + + /client/v3/cards/{cardId}: + parameters: + - $ref: '#/components/parameters/cardIdPathParam' + get: + tags: + - card + operationId: getCardById + responses: + "200": + $ref: '#/components/responses/CardItemResponse' + +components: + + parameters: + cardIdPathParam: + name: cardId + in: path + required: true + schema: + type: string + description: The ID of the card + + schemas: + CardItem: + type: object + required: + - cardId + properties: + cardId: + type: number + name: + type: string + + CardId: + type: object + properties: + cardId: + type: number + + responses: + CardIdResponse: + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/CardId' + + CardsResponse: + description: Fetch cards + content: + application/json: + schema: + $ref: '#/components/schemas/CardItem' + + CardItemResponse: + description: Card by id + content: + application/json: + schema: + $ref: '#/components/schemas/CardItem' + +