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:
Daniel Strmecki 2022-12-03 16:00:24 +01:00 committed by GitHub
parent 5f939afaf0
commit 0f3c81c248
18 changed files with 525 additions and 1 deletions

View File

@ -13,6 +13,47 @@
<version>1.0.0-SNAPSHOT</version> <version>1.0.0-SNAPSHOT</version>
</parent> </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> <dependencies>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
@ -34,6 +75,27 @@
<groupId>com.h2database</groupId> <groupId>com.h2database</groupId>
<artifactId>h2</artifactId> <artifactId>h2</artifactId>
</dependency> </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> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
@ -49,6 +111,12 @@
<artifactId>spring-graphql-test</artifactId> <artifactId>spring-graphql-test</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.skyscreamer</groupId>
<artifactId>jsonassert</artifactId>
<version>${jsonassert.version}</version>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -0,0 +1,9 @@
server:
port: 8082
spring:
graphql:
graphiql:
enabled: true
schema:
locations: classpath:chooseapi/

View File

@ -0,0 +1,14 @@
type Author {
firstName: String!
lastName: String!
}
type Book {
title: String!
year: Int!
author: Author!
}
type Query {
books: [Book]
}

View File

@ -43,7 +43,6 @@ input ProductUpdateModel {
stock: Int stock: Int
} }
# The Root Query for the application # The Root Query for the application
type Query { type Query {
products(size: Int, page: Int): [Product]! products(size: Int, page: Int): [Product]!

View File

@ -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);
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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"
}
}
]