Merge pull request #16003 from psevestre/master
[BAEL-7524] OpenAPI Generator Customization , take 2
This commit is contained in:
commit
ebedbc6466
|
@ -112,6 +112,7 @@
|
||||||
<module>spring-boot-3-url-matching</module>
|
<module>spring-boot-3-url-matching</module>
|
||||||
<module>spring-boot-graalvm-docker</module>
|
<module>spring-boot-graalvm-docker</module>
|
||||||
<module>spring-boot-validations</module>
|
<module>spring-boot-validations</module>
|
||||||
|
<module>spring-boot-openapi</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<artifactId>spring-boot-openapi</artifactId>
|
||||||
|
<name>spring-boot-openapi</name>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
<description>OpenAPI Generator module</description>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>2.7.11</version>
|
||||||
|
<relativePath/>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- <parent>-->
|
||||||
|
<!-- <groupId>com.baeldung.spring-boot-modules</groupId>-->
|
||||||
|
<!-- <artifactId>spring-boot-modules</artifactId>-->
|
||||||
|
<!-- <version>1.0.0-SNAPSHOT</version>-->
|
||||||
|
<!-- </parent>-->
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>javax.validation</groupId>
|
||||||
|
<artifactId>validation-api</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.swagger.core.v3</groupId>
|
||||||
|
<artifactId>swagger-annotations</artifactId>
|
||||||
|
<version>${swagger-annotations.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.openapitools</groupId>
|
||||||
|
<artifactId>openapi-generator-maven-plugin</artifactId>
|
||||||
|
<version>${openapi-generator.version}</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>generate</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<skipValidateSpec>true</skipValidateSpec>
|
||||||
|
<inputSpec>${project.basedir}/src/main/resources/api/quotes.yaml</inputSpec>
|
||||||
|
<generatorName>spring</generatorName>
|
||||||
|
<supportingFilesToGenerate>ApiUtil.java</supportingFilesToGenerate>
|
||||||
|
<templateResourcePath>${project.basedir}/src/main/resources/templates/JavaSpring</templateResourcePath>
|
||||||
|
<globalProperties>
|
||||||
|
<debugOpenAPI>true</debugOpenAPI>
|
||||||
|
</globalProperties>
|
||||||
|
<configOptions>
|
||||||
|
<dateLibrary>java8</dateLibrary>
|
||||||
|
<openApiNullable>false</openApiNullable>
|
||||||
|
<delegatePattern>true</delegatePattern>
|
||||||
|
<apiPackage>com.baeldung.tutorials.openapi.quotes.api</apiPackage>
|
||||||
|
<modelPackage>com.baeldung.tutorials.openapi.quotes.api.model</modelPackage>
|
||||||
|
<documentationProvider>source</documentationProvider>
|
||||||
|
</configOptions>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
|
<maven.compiler.target>17</maven.compiler.target>
|
||||||
|
<openapi-generator.version>7.3.0</openapi-generator.version>
|
||||||
|
<swagger-annotations.version>2.2.20</swagger-annotations.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
</project>
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.baeldung.tutorials.openapi.quotes;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.cache.annotation.EnableCaching;
|
||||||
|
|
||||||
|
import com.baeldung.tutorials.openapi.quotes.service.BrokerService;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
@EnableCaching
|
||||||
|
public class QuotesApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(QuotesApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.baeldung.tutorials.openapi.quotes.config;
|
||||||
|
|
||||||
|
import java.time.Clock;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class ClockConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
Clock defaultClock() {
|
||||||
|
return Clock.systemDefaultZone();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package com.baeldung.tutorials.openapi.quotes.controller;
|
||||||
|
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
|
||||||
|
import org.springframework.cache.annotation.Cacheable;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import com.baeldung.tutorials.openapi.quotes.api.QuotesApi;
|
||||||
|
import com.baeldung.tutorials.openapi.quotes.api.QuotesApiDelegate;
|
||||||
|
import com.baeldung.tutorials.openapi.quotes.api.model.QuoteResponse;
|
||||||
|
import com.baeldung.tutorials.openapi.quotes.service.BrokerService;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class QuotesApiImpl implements QuotesApiDelegate {
|
||||||
|
private final BrokerService broker;
|
||||||
|
private final Clock clock;
|
||||||
|
|
||||||
|
public QuotesApiImpl(BrokerService broker, Clock clock) {
|
||||||
|
this.broker = broker;
|
||||||
|
this.clock = clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /quotes/{symbol} : Get current quote for a security
|
||||||
|
*
|
||||||
|
* @param symbol Security's symbol (required)
|
||||||
|
* @return OK (status code 200)
|
||||||
|
* @see QuotesApi#getQuote
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<QuoteResponse> getQuote(String symbol) {
|
||||||
|
|
||||||
|
var price = broker.getSecurityPrice(symbol);
|
||||||
|
|
||||||
|
var quote = new QuoteResponse();
|
||||||
|
quote.setSymbol(symbol);
|
||||||
|
quote.setPrice(price);
|
||||||
|
quote.setTimestamp(OffsetDateTime.now(clock));
|
||||||
|
return ResponseEntity.ok(quote);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package com.baeldung.tutorials.openapi.quotes.service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class BrokerService {
|
||||||
|
|
||||||
|
private final Logger log = LoggerFactory.getLogger(BrokerService.class);
|
||||||
|
|
||||||
|
private final Random rnd = new Random();
|
||||||
|
|
||||||
|
|
||||||
|
public BrokerService() {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public BigDecimal getSecurityPrice(@NonNull String symbol) {
|
||||||
|
log.info("getSecurityPrice: {}", symbol);
|
||||||
|
// Just a mock value
|
||||||
|
return BigDecimal.valueOf(100.0 + rnd.nextDouble()*100.0);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
openapi: 3.0.0
|
||||||
|
info:
|
||||||
|
title: Quotes API
|
||||||
|
version: 1.0.0
|
||||||
|
servers:
|
||||||
|
- description: Test server
|
||||||
|
url: http://localhost:8080
|
||||||
|
paths:
|
||||||
|
/quotes/{symbol}:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- quotes
|
||||||
|
summary: Get current quote for a security
|
||||||
|
operationId: getQuote
|
||||||
|
x-spring-cacheable:
|
||||||
|
name: get-quotes
|
||||||
|
security:
|
||||||
|
- ApiKey:
|
||||||
|
- Quotes.Read
|
||||||
|
parameters:
|
||||||
|
- name: symbol
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: Security's symbol
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
pattern: '[A-Z0-9]+'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/QuoteResponse'
|
||||||
|
components:
|
||||||
|
securitySchemes:
|
||||||
|
ApiKey:
|
||||||
|
type: apiKey
|
||||||
|
in: header
|
||||||
|
name: X-API-KEY
|
||||||
|
schemas:
|
||||||
|
QuoteResponse:
|
||||||
|
description: Quote response
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
symbol:
|
||||||
|
type: string
|
||||||
|
description: security's symbol
|
||||||
|
price:
|
||||||
|
type: number
|
||||||
|
description: Quote value
|
||||||
|
timestamp:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
|
@ -0,0 +1,5 @@
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
root: INFO
|
||||||
|
org.springframework: INFO
|
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
* Generated code: do not modify !
|
||||||
|
* Custom template with support for x-spring-cacheable extension
|
||||||
|
*/
|
||||||
|
package {{package}};
|
||||||
|
|
||||||
|
{{#imports}}import {{import}};
|
||||||
|
{{/imports}}
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
{{#useResponseEntity}}
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
{{/useResponseEntity}}
|
||||||
|
import org.springframework.web.context.request.NativeWebRequest;
|
||||||
|
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}}
|
||||||
|
import {{javaxPackage}}.validation.constraints.*;
|
||||||
|
import {{javaxPackage}}.validation.Valid;
|
||||||
|
{{/useBeanValidation}}
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
{{#async}}
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
{{/async}}
|
||||||
|
import {{javaxPackage}}.annotation.Generated;
|
||||||
|
|
||||||
|
{{#operations}}
|
||||||
|
/**
|
||||||
|
* A delegate to be called by the {@link {{classname}}Controller}}.
|
||||||
|
* Implement this interface with a {@link org.springframework.stereotype.Service} annotated class.
|
||||||
|
*/
|
||||||
|
{{>generatedAnnotation}}
|
||||||
|
public interface {{classname}}Delegate {
|
||||||
|
{{#jdk8-default-interface}}
|
||||||
|
|
||||||
|
default Optional<NativeWebRequest> getRequest() {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
{{/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 <a href="{{url}}">{{summary}} Documentation</a>
|
||||||
|
{{/externalDocs}}
|
||||||
|
* @see {{classname}}#{{operationId}}
|
||||||
|
*/
|
||||||
|
{{#isDeprecated}}
|
||||||
|
@Deprecated
|
||||||
|
{{/isDeprecated}}
|
||||||
|
{{#vendorExtensions.x-spring-cacheable}}
|
||||||
|
@org.springframework.cache.annotation.Cacheable({{#name}}"{{.}}"{{/name}}{{^name}}"default"{{/name}})
|
||||||
|
{{/vendorExtensions.x-spring-cacheable}}
|
||||||
|
{{#jdk8-default-interface}}default {{/jdk8-default-interface}}{{>responseType}} {{operationId}}({{#allParams}}{{^isFile}}{{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{{dataType}}}{{/reactive}}{{#reactive}}{{^isArray}}Mono<{{{dataType}}}>{{/isArray}}{{#isArray}}Flux<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{/isFile}}{{#isFile}}{{#isArray}}List<{{/isArray}}{{#reactive}}Flux<Part>{{/reactive}}{{^reactive}}MultipartFile{{/reactive}}{{#isArray}}>{{/isArray}}{{/isFile}} {{paramName}}{{^-last}},
|
||||||
|
{{/-last}}{{/allParams}}{{#reactive}}{{#hasParams}},
|
||||||
|
{{/hasParams}}ServerWebExchange exchange{{/reactive}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}}, {{/hasParams}}{{^hasParams}}{{#reactive}}, {{/reactive}}{{/hasParams}}final Pageable pageable{{/vendorExtensions.x-spring-paginated}}){{#unhandledException}} throws Exception{{/unhandledException}}{{^jdk8-default-interface}};{{/jdk8-default-interface}}{{#jdk8-default-interface}} {
|
||||||
|
{{>methodBody}}
|
||||||
|
}{{/jdk8-default-interface}}
|
||||||
|
|
||||||
|
{{/operation}}
|
||||||
|
}
|
||||||
|
{{/operations}}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package com.baeldung.tutorials.openapi.quotes;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||||
|
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
|
||||||
|
import com.baeldung.tutorials.openapi.quotes.api.model.QuoteResponse;
|
||||||
|
|
||||||
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||||
|
class QuotesApplicationIntegrationTest {
|
||||||
|
|
||||||
|
@LocalServerPort
|
||||||
|
private int port;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TestRestTemplate restTemplate;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void whenGetQuote_thenSuccess() {
|
||||||
|
var response = restTemplate.getForEntity("http://localhost:" + port + "/quotes/BAEL", QuoteResponse.class);
|
||||||
|
assertThat(response.getStatusCode())
|
||||||
|
.isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void whenGetQuoteMultipleTimes_thenResponseCached() {
|
||||||
|
|
||||||
|
// Call server a few times and collect responses
|
||||||
|
var quotes = IntStream.range(1, 10).boxed()
|
||||||
|
.map((i) -> restTemplate.getForEntity("http://localhost:" + port + "/quotes/BAEL", QuoteResponse.class))
|
||||||
|
.map(HttpEntity::getBody)
|
||||||
|
.collect(Collectors.groupingBy((q -> q.hashCode()), Collectors.counting()));
|
||||||
|
|
||||||
|
assertThat(quotes.size()).isEqualTo(1);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
package com.baeldung.tutorials.openapi.quotes.controller;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.context.TestConfiguration;
|
||||||
|
import org.springframework.cache.annotation.EnableCaching;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
|
||||||
|
import com.baeldung.tutorials.openapi.quotes.api.QuotesApi;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
class QuotesApiImplUnitTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private QuotesApi api;
|
||||||
|
|
||||||
|
|
||||||
|
private static Instant NOW = Instant.now();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void whenGetQuote_then_success() {
|
||||||
|
|
||||||
|
var response = api.getQuote("GOOG");
|
||||||
|
assertThat(response)
|
||||||
|
.isNotNull();
|
||||||
|
|
||||||
|
assertThat(response.getStatusCode().is2xxSuccessful())
|
||||||
|
.isTrue();
|
||||||
|
|
||||||
|
assertThat(response.getBody().getTimestamp())
|
||||||
|
.isEqualTo(OffsetDateTime.ofInstant(NOW, ZoneId.systemDefault()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@TestConfiguration
|
||||||
|
@EnableCaching
|
||||||
|
static class TestConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Primary
|
||||||
|
Clock fixedClock() {
|
||||||
|
return Clock.fixed(NOW, ZoneId.systemDefault());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue