[BAEL-7525] Article code
This commit is contained in:
parent
ee27de7b46
commit
866af9970f
|
@ -19,6 +19,11 @@
|
|||
<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>
|
||||
|
@ -50,8 +55,12 @@
|
|||
<inputSpec>${project.basedir}/src/main/resources/api/quotes.yaml</inputSpec>
|
||||
<generatorName>spring</generatorName>
|
||||
<supportingFilesToGenerate>ApiUtil.java</supportingFilesToGenerate>
|
||||
<templateResourcePath>src/main/resources/templates/JavaSpring</templateResourcePath>
|
||||
<globalProperties>
|
||||
<debugOpenAPI>true</debugOpenAPI>
|
||||
</globalProperties>
|
||||
<configOptions>
|
||||
<dateLibrary>custom</dateLibrary>
|
||||
<dateLibrary>java8</dateLibrary>
|
||||
<openApiNullable>false</openApiNullable>
|
||||
<delegatePattern>true</delegatePattern>
|
||||
<apiPackage>com.baeldung.tutorials.openapi.quotes.api</apiPackage>
|
||||
|
|
|
@ -2,10 +2,12 @@ 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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package com.baeldung.tutorials.openapi.quotes.controller;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.baeldung.tutorials.openapi.quotes.api.OrdersApiDelegate;
|
||||
|
||||
@Component
|
||||
public class OrdersApiImpl implements OrdersApiDelegate {
|
||||
}
|
|
@ -1,9 +1,14 @@
|
|||
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;
|
||||
|
@ -11,9 +16,11 @@ 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) {
|
||||
public QuotesApiImpl(BrokerService broker, Clock clock) {
|
||||
this.broker = broker;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
|
||||
|
@ -29,14 +36,10 @@ public class QuotesApiImpl implements QuotesApiDelegate {
|
|||
|
||||
var price = broker.getSecurityPrice(symbol);
|
||||
|
||||
if ( price.isPresent()) {
|
||||
var quote = new QuoteResponse();
|
||||
quote.setSymbol(symbol);
|
||||
quote.setPrice(price.get());
|
||||
quote.setPrice(price);
|
||||
quote.setTimestamp(OffsetDateTime.now(clock));
|
||||
return ResponseEntity.ok(quote);
|
||||
}
|
||||
else {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,23 +7,26 @@ 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 Map<String,BigDecimal> securities = new HashMap<>();
|
||||
private final Logger log = LoggerFactory.getLogger(BrokerService.class);
|
||||
|
||||
private final Random rnd = new Random();
|
||||
|
||||
|
||||
public BrokerService() {
|
||||
Random rnd = new Random();
|
||||
securities.put("GOOG", BigDecimal.valueOf(rnd.nextDouble() * 1000.00).setScale(4, RoundingMode.DOWN));
|
||||
securities.put("AA", BigDecimal.valueOf(rnd.nextDouble() * 1000.00).setScale(4, RoundingMode.DOWN));
|
||||
securities.put("BAEL", BigDecimal.valueOf(rnd.nextDouble() * 1000.00).setScale(4, RoundingMode.DOWN));
|
||||
}
|
||||
|
||||
|
||||
public Optional<BigDecimal> getSecurityPrice(@NonNull String symbol) {
|
||||
return Optional.ofNullable(securities.get(symbol));
|
||||
public BigDecimal getSecurityPrice(@NonNull String symbol) {
|
||||
log.info("getSecurityPrice: {}", symbol);
|
||||
// Just a mock value
|
||||
return BigDecimal.valueOf(100.0 + rnd.nextDouble()*100.0);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,8 @@ paths:
|
|||
- quotes
|
||||
summary: Get current quote for a security
|
||||
operationId: getQuote
|
||||
x-spring-cacheable:
|
||||
name: get-quotes
|
||||
security:
|
||||
- ApiKey:
|
||||
- Quotes.Read
|
||||
|
@ -30,57 +32,13 @@ paths:
|
|||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/QuoteResponse'
|
||||
/orders/{symbol}:
|
||||
post:
|
||||
tags:
|
||||
- orders
|
||||
operationId: createOrder
|
||||
security:
|
||||
- ApiKey:
|
||||
- Orders.Write
|
||||
summary: Places a new order to buy a security
|
||||
parameters:
|
||||
- name: symbol
|
||||
in: path
|
||||
required: true
|
||||
description: Security's symbol
|
||||
schema:
|
||||
type: string
|
||||
pattern: '[A-Z0-9]'
|
||||
requestBody:
|
||||
required: true
|
||||
description: Order info
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: '#'#/components/schemas/OrderRequest'
|
||||
responses:
|
||||
'201':
|
||||
description: Order accepted
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: '#/components/schemas/OrderResponse'
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
ApiKey:
|
||||
type: apiKey
|
||||
in: header
|
||||
name: X-API-KEY
|
||||
|
||||
schemas:
|
||||
OrderType:
|
||||
type: string
|
||||
enum:
|
||||
- BUY
|
||||
- SELL
|
||||
OrderStatus:
|
||||
type: string
|
||||
enum:
|
||||
- PENDING
|
||||
- REJECTED
|
||||
- FULFILLED
|
||||
QuoteResponse:
|
||||
description: Quote response
|
||||
type: object
|
||||
|
@ -91,40 +49,6 @@ components:
|
|||
price:
|
||||
type: number
|
||||
description: Quote value
|
||||
OrderRequest:
|
||||
description: Buy/Sell order details
|
||||
type: object
|
||||
properties:
|
||||
clientRef:
|
||||
timestamp:
|
||||
type: string
|
||||
orderType:
|
||||
$ref: '#/components/schemas/OrderType'
|
||||
symbol:
|
||||
type: string
|
||||
description: security's symbol
|
||||
quantity:
|
||||
type: number
|
||||
price:
|
||||
type: number
|
||||
OrderResponse:
|
||||
description: Buy/Sell order response
|
||||
properties:
|
||||
clientRef:
|
||||
type: string
|
||||
orderType:
|
||||
$ref: '#/components/schemas/OrderType'
|
||||
symbol:
|
||||
type: string
|
||||
description: security's symbol
|
||||
quantity:
|
||||
type: number
|
||||
price:
|
||||
type: number
|
||||
orderStatus:
|
||||
$ref: '#/components/schemas/OrderStatus'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
format: date-time
|
|
@ -1,4 +1,5 @@
|
|||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
org.springframework: DEBUG
|
||||
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,43 @@
|
|||
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 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 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