Feature/bael 5756 choose api (#13055)
* BAEL-5756: Rest controller * BAEL-5756: GraphQL controller (without tests) * BAEL-5756: Fix GraphQL test * BAEL-5756: Fix GraphQL test 2 * BAEL-5756: GRPC working * BAEL-5756: GRPC with Spring Boot * BAEL-5756: Books proto service * BAEL-5756: Fix grpc integration test * BAEL-5756: Refactor * BAEL-5756: Revert some changes
This commit is contained in:
parent
5f939afaf0
commit
0f3c81c248
|
@ -13,6 +13,47 @@
|
|||
<version>1.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<build>
|
||||
<extensions>
|
||||
<extension>
|
||||
<groupId>kr.motd.maven</groupId>
|
||||
<artifactId>os-maven-plugin</artifactId>
|
||||
<version>1.7.0</version>
|
||||
</extension>
|
||||
</extensions>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.xolstice.maven.plugins</groupId>
|
||||
<artifactId>protobuf-maven-plugin</artifactId>
|
||||
<version>${protobuf-plugin.version}</version>
|
||||
<configuration>
|
||||
<protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
|
||||
<pluginId>grpc-java</pluginId>
|
||||
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
<goal>compile-custom</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<properties>
|
||||
<protobuf.version>3.19.2</protobuf.version>
|
||||
<protobuf-plugin.version>0.6.1</protobuf-plugin.version>
|
||||
<grpc.version>1.43.2</grpc.version>
|
||||
<grpc.spring.version>2.13.1.RELEASE</grpc.spring.version>
|
||||
<jsonassert.version>1.5.1</jsonassert.version>
|
||||
<jakarta.annotation.version>1.3.5</jakarta.annotation.version>
|
||||
<os-maven-plugin.version>1.6.2</os-maven-plugin.version>
|
||||
<maven-war-plugin.version>3.3.2</maven-war-plugin.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
@ -34,6 +75,27 @@
|
|||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-stub</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-protobuf</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.annotation</groupId>
|
||||
<artifactId>jakarta.annotation-api</artifactId>
|
||||
<version>${jakarta.annotation.version}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.devh</groupId>
|
||||
<artifactId>grpc-spring-boot-starter</artifactId>
|
||||
<version>${grpc.spring.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
|
@ -49,6 +111,12 @@
|
|||
<artifactId>spring-graphql-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.skyscreamer</groupId>
|
||||
<artifactId>jsonassert</artifactId>
|
||||
<version>${jsonassert.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
|
@ -0,0 +1,14 @@
|
|||
package com.baeldung.chooseapi;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class ChooseApiApp {
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.setProperty("spring.profiles.default", "chooseapi");
|
||||
SpringApplication.run(ChooseApiApp.class, args);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package com.baeldung.chooseapi.controllers;
|
||||
|
||||
import com.baeldung.chooseapi.dtos.Book;
|
||||
import com.baeldung.chooseapi.services.BooksService;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
||||
|
||||
@RestController
|
||||
public class BooksControllerGraphQL {
|
||||
|
||||
private final BooksService booksService;
|
||||
|
||||
public BooksControllerGraphQL(BooksService booksService) {
|
||||
this.booksService = booksService;
|
||||
}
|
||||
|
||||
@QueryMapping
|
||||
public List<Book> books() {
|
||||
return booksService.getBooks();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.baeldung.chooseapi.controllers;
|
||||
|
||||
import com.baeldung.chooseapi.dtos.Book;
|
||||
import com.baeldung.chooseapi.services.BooksService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
public class BooksControllerRest {
|
||||
|
||||
private final BooksService booksService;
|
||||
|
||||
public BooksControllerRest(BooksService booksService) {
|
||||
this.booksService = booksService;
|
||||
}
|
||||
|
||||
@GetMapping("/rest/books")
|
||||
public List<Book> books() {
|
||||
return booksService.getBooks();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package com.baeldung.chooseapi.dtos;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class Author {
|
||||
|
||||
private final String firstName;
|
||||
private final String lastName;
|
||||
|
||||
public Author(String firstName, String lastName) {
|
||||
this.firstName = firstName;
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Author author = (Author) o;
|
||||
return firstName.equals(author.firstName) && lastName.equals(author.lastName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(firstName, lastName);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package com.baeldung.chooseapi.dtos;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class Book {
|
||||
|
||||
private final String title;
|
||||
private final Author author;
|
||||
private final int year;
|
||||
|
||||
public Book(String title, Author author, int year) {
|
||||
this.title = title;
|
||||
this.author = author;
|
||||
this.year = year;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public Author getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public int getYear() {
|
||||
return year;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Book book = (Book) o;
|
||||
return year == book.year && title.equals(book.title) && author.equals(book.author);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(title, author, year);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package com.baeldung.chooseapi.grpc;
|
||||
|
||||
import com.baeldung.chooseapi.BooksServiceGrpc.BooksServiceImplBase;
|
||||
import com.baeldung.chooseapi.BooksServiceOuterClass.BooksRequest;
|
||||
import com.baeldung.chooseapi.BooksServiceOuterClass.BooksResponse;
|
||||
|
||||
import com.baeldung.chooseapi.dtos.Book;
|
||||
import com.baeldung.chooseapi.services.BooksService;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import net.devh.boot.grpc.server.service.GrpcService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@GrpcService
|
||||
public class BooksServiceGrpc extends BooksServiceImplBase {
|
||||
|
||||
private final BooksService booksService;
|
||||
|
||||
public BooksServiceGrpc(BooksService booksService) {
|
||||
this.booksService = booksService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void books(BooksRequest request, StreamObserver<BooksResponse> responseObserver) {
|
||||
List<Book> books = booksService.getBooks();
|
||||
BooksResponse.Builder responseBuilder = BooksResponse.newBuilder();
|
||||
books.forEach(book -> responseBuilder.addBook(GrpcBooksMapper.mapBookToProto(book)));
|
||||
responseObserver.onNext(responseBuilder.build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package com.baeldung.chooseapi.grpc;
|
||||
|
||||
import com.baeldung.chooseapi.BooksServiceOuterClass;
|
||||
import com.baeldung.chooseapi.dtos.Author;
|
||||
import com.baeldung.chooseapi.dtos.Book;
|
||||
|
||||
public class GrpcBooksMapper {
|
||||
|
||||
public static BooksServiceOuterClass.BookProto mapBookToProto(Book book) {
|
||||
return BooksServiceOuterClass.BookProto.newBuilder()
|
||||
.setTitle(book.getTitle())
|
||||
.setAuthor(BooksServiceOuterClass.AuthorProto.newBuilder()
|
||||
.setFirstName(book.getAuthor().getFirstName())
|
||||
.setLastName(book.getAuthor().getLastName())
|
||||
.build())
|
||||
.setYear(book.getYear())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Book mapProtoToBook(BooksServiceOuterClass.BookProto bookProto) {
|
||||
return new Book(bookProto.getTitle(),
|
||||
new Author(bookProto.getAuthor().getFirstName(), bookProto.getAuthor().getLastName()),
|
||||
bookProto.getYear());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package com.baeldung.chooseapi.services;
|
||||
|
||||
import com.baeldung.chooseapi.dtos.Author;
|
||||
import com.baeldung.chooseapi.dtos.Book;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class BooksService {
|
||||
|
||||
private static final Author AUTHOR = new Author("Joanne", "Rowling");
|
||||
|
||||
private static final List<Book> BOOKS = new ArrayList<>(Arrays.asList(
|
||||
new Book("Philosopher's Stone", AUTHOR, 1997),
|
||||
new Book("Goblet of Fire", AUTHOR, 2000),
|
||||
new Book("Deathly Hallows", AUTHOR, 2007)
|
||||
));
|
||||
|
||||
public List<Book> getBooks() {
|
||||
return BOOKS;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
syntax = "proto3";
|
||||
package com.baeldung.chooseapi;
|
||||
|
||||
message BooksRequest {}
|
||||
|
||||
message AuthorProto {
|
||||
string firstName = 1;
|
||||
string lastName = 2;
|
||||
}
|
||||
|
||||
message BookProto {
|
||||
string title = 1;
|
||||
AuthorProto author = 2;
|
||||
int32 year = 3;
|
||||
}
|
||||
|
||||
message BooksResponse {
|
||||
repeated BookProto book = 1;
|
||||
}
|
||||
|
||||
service BooksService {
|
||||
rpc books(BooksRequest) returns (BooksResponse);
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
server:
|
||||
port: 8082
|
||||
|
||||
spring:
|
||||
graphql:
|
||||
graphiql:
|
||||
enabled: true
|
||||
schema:
|
||||
locations: classpath:chooseapi/
|
|
@ -0,0 +1,14 @@
|
|||
type Author {
|
||||
firstName: String!
|
||||
lastName: String!
|
||||
}
|
||||
|
||||
type Book {
|
||||
title: String!
|
||||
year: Int!
|
||||
author: Author!
|
||||
}
|
||||
|
||||
type Query {
|
||||
books: [Book]
|
||||
}
|
|
@ -43,7 +43,6 @@ input ProductUpdateModel {
|
|||
stock: Int
|
||||
}
|
||||
|
||||
|
||||
# The Root Query for the application
|
||||
type Query {
|
||||
products(size: Int, page: Int): [Product]!
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
package com.baeldung.chooseapi.controllers;
|
||||
|
||||
import com.baeldung.chooseapi.ChooseApiApp;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.graphql.test.tester.HttpGraphQlTester;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = ChooseApiApp.class)
|
||||
@ActiveProfiles("chooseapi")
|
||||
class BooksControllerGraphQLIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private HttpGraphQlTester graphQlTester;
|
||||
|
||||
@Test
|
||||
void givenBooksServiceThatReturnThreeBooks_whenCallingGraphQLEndpoint_thenThreeBooksAreReturned() throws Exception {
|
||||
String document = "query { books { title year author { firstName lastName }}}";
|
||||
Path expectedResponse = Paths.get("src/test/resources/graphql-test/books_expected_response.json");
|
||||
String expectedJson = new String(Files.readAllBytes(expectedResponse));
|
||||
|
||||
this.graphQlTester.document(document)
|
||||
.execute()
|
||||
.path("books")
|
||||
.matchesJson(expectedJson);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package com.baeldung.chooseapi.controllers;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
class BooksControllerRestIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
void givenBooksServiceThatReturnThreeBooks_whenCallingRestEndpoint_thenThreeBooksAreReturned() throws Exception {
|
||||
Path expectedResponse = Paths.get("src/test/resources/graphql-test/books_expected_response.json");
|
||||
String expectedJson = new String(Files.readAllBytes(expectedResponse));
|
||||
|
||||
this.mockMvc.perform(get("/rest/books"))
|
||||
.andDo(print())
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().json(expectedJson));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package com.baeldung.chooseapi.grpc;
|
||||
|
||||
import com.baeldung.chooseapi.dtos.Book;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import net.devh.boot.grpc.client.inject.GrpcClient;
|
||||
import org.json.JSONException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.skyscreamer.jsonassert.JSONAssert;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import com.baeldung.chooseapi.BooksServiceOuterClass.BooksRequest;
|
||||
import com.baeldung.chooseapi.BooksServiceOuterClass.BooksResponse;
|
||||
|
||||
import com.baeldung.chooseapi.BooksServiceGrpc.BooksServiceBlockingStub;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SpringBootTest(properties = {
|
||||
"grpc.server.inProcessName=test", // Enable inProcess server
|
||||
"grpc.server.port=-1", // Disable external server
|
||||
"grpc.client.inProcess.address=in-process:test" // Configure the client to connect to the inProcess server
|
||||
})
|
||||
@SpringJUnitConfig(GrpcIntegrationTestConfig.class)
|
||||
@DirtiesContext // Ensures that the grpc-server is properly shutdown after each test
|
||||
class BooksServiceGrpcIntegrationTest {
|
||||
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@GrpcClient("inProcess")
|
||||
private BooksServiceBlockingStub booksServiceGrpc;
|
||||
|
||||
@Test
|
||||
@DirtiesContext
|
||||
void givenBooksServiceThatReturnThreeBooks_whenCallingGrpcEndpoint_thenThreeBooksAreReturned() throws IOException, JSONException {
|
||||
Path expectedResponse = Paths.get("src/test/resources/graphql-test/books_expected_response.json");
|
||||
String expectedJson = new String(Files.readAllBytes(expectedResponse));
|
||||
|
||||
BooksRequest request = BooksRequest.newBuilder().build();
|
||||
BooksResponse response = booksServiceGrpc.books(request);
|
||||
|
||||
List<Book> books = response.getBookList().stream()
|
||||
.map(GrpcBooksMapper::mapProtoToBook)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
JSONAssert.assertEquals(objectMapper.writeValueAsString(books), expectedJson, true);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package com.baeldung.chooseapi.grpc;
|
||||
|
||||
import com.baeldung.chooseapi.services.BooksService;
|
||||
import net.devh.boot.grpc.client.autoconfigure.GrpcClientAutoConfiguration;
|
||||
import net.devh.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration;
|
||||
import net.devh.boot.grpc.server.autoconfigure.GrpcServerFactoryAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@ImportAutoConfiguration({
|
||||
GrpcServerAutoConfiguration.class, // Create required server beans
|
||||
GrpcServerFactoryAutoConfiguration.class, // Select server implementation
|
||||
GrpcClientAutoConfiguration.class}) // Support @GrpcClient annotation
|
||||
class GrpcIntegrationTestConfig {
|
||||
|
||||
@Bean
|
||||
BooksService booksService() {
|
||||
return new BooksService();
|
||||
}
|
||||
|
||||
@Bean
|
||||
BooksServiceGrpc booksServiceGrpc() {
|
||||
return new BooksServiceGrpc(booksService());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
[
|
||||
{
|
||||
"title": "Philosopher's Stone",
|
||||
"year": 1997,
|
||||
"author": {
|
||||
"firstName": "Joanne",
|
||||
"lastName": "Rowling"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Goblet of Fire",
|
||||
"year": 2000,
|
||||
"author": {
|
||||
"firstName": "Joanne",
|
||||
"lastName": "Rowling"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Deathly Hallows",
|
||||
"year": 2007,
|
||||
"author": {
|
||||
"firstName": "Joanne",
|
||||
"lastName": "Rowling"
|
||||
}
|
||||
}
|
||||
]
|
Loading…
Reference in New Issue