Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions technical-test-api/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM maven:3.9.4-amazoncorretto-17

WORKDIR /app

COPY pom.xml .

RUN mvn clean install

COPY . .

ARG DEFAULT_PORT=80

ENV PORT $DEFAULT_PORT

EXPOSE $PORT

CMD ["mvn", "spring-boot:run"]
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.mongodb.repository.config.EnableReactiveMongoRepositories;

@EnableReactiveMongoRepositories
@SpringBootApplication
public class TechnicalTestApiApplication {

public static void main(String[] args) {
SpringApplication.run(TechnicalTestApiApplication.class, args);
}

public static void main(String[] args) {
SpringApplication.run(TechnicalTestApiApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package technical.test.api.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import technical.test.api.representations.AuthorRepresentation;
import technical.test.api.representations.BookRepresentation;
import technical.test.api.services.LibraryService;

@CrossOrigin(origins = "http://localhost:8080")
@RequiredArgsConstructor
@RequestMapping("/library")
@RestController
public class LibraryController {

private final LibraryService libraryService;

@GetMapping("/authors")
public Flux<AuthorRepresentation> findAuthors(
@RequestParam(name = "firstname", required = false) final String firstname,
@RequestParam(name = "lastname", required = false) final String lastname,
@RequestParam(name = "birthdate", required = false) final Integer birthdate) {
return libraryService.findAuthorByFirstnameAndLastnameAndBirthdate(
firstname, lastname, birthdate);
}

@GetMapping("/books")
public Flux<BookRepresentation> filterBooks(
@RequestParam(name = "authorRefId", required = false) final String authorRefId,
@RequestParam(name = "yearFrom", required = false) final Integer yearFrom,
@RequestParam(name = "yearTo", required = false) final Integer yearTo) {
return libraryService.findBookByAuthorAndDateBetween(authorRefId, yearFrom, yearTo);
}

@PostMapping("/books")
public Mono<BookRepresentation> filterBooks(
@RequestParam(name = "isbn") final String isbn,
@RequestParam(name = "title") final String title,
@RequestParam(name = "releaseDateYear") final Integer releaseDateYear,
@RequestParam(name = "authorRefId") final String authorRefId) {
return libraryService.registerBook(isbn, title, releaseDateYear, authorRefId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package technical.test.api.data;

import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import technical.test.api.services.LibraryService;

@RequiredArgsConstructor
@Component
public class LoadInitialData implements CommandLineRunner {

private final LibraryService libraryService;

@Override
public void run(String... args) {
libraryService.registerBook("123456", "Title1", 2023, "victor_hugo").block();
libraryService.registerBook("12345", "Title2", 2023, "victor_hugo").block();
libraryService.registerBook("1234", "Title3", 2023, "guillaume_musso").block();
libraryService.registerBook("123", "Title4", 2023, "guillaume_musso").block();
libraryService.registerBook("12", "Title5", 2023, "guillaume_musso").block();

libraryService.registerAuthor("Victor", "Hugo", 1989).block();
libraryService.registerAuthor("Guillaume", "Musso", 1987).block();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package technical.test.api.mappers;

import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import technical.test.api.representations.AuthorRepresentation;
import technical.test.api.storage.models.Author;

@Mapper(componentModel = "spring")
public interface AuthorMapper {

AuthorMapper INSTANCE = Mappers.getMapper(AuthorMapper.class);

AuthorRepresentation toAuthorRepresentation(Author book);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package technical.test.api.mappers;

import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import technical.test.api.representations.BookRepresentation;
import technical.test.api.storage.models.Book;

@Mapper(componentModel = "spring")
public interface BookMapper {

BookMapper INSTANCE = Mappers.getMapper(BookMapper.class);

BookRepresentation toBookRepresentation(Book book);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package technical.test.api.representations;

import java.io.Serial;
import java.io.Serializable;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

@Builder
@Getter
@Setter
public class AuthorRepresentation implements Serializable {

@Serial private static final long serialVersionUID = -5828406190179381271L;

private String id;

private Integer birthdate;

private String firstname;

private String lastname;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package technical.test.api.representations;

import java.io.Serial;
import java.io.Serializable;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.Id;

@Builder
@Getter
@Setter
public class BookRepresentation implements Serializable {

@Serial private static final long serialVersionUID = -5822406190179321283L;

@Id private String id;

private String isbn;

private String title;

private Integer releaseDate;

private String authorId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package technical.test.api.services;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import technical.test.api.representations.AuthorRepresentation;
import technical.test.api.representations.BookRepresentation;

public interface LibraryService {

Mono<BookRepresentation> registerBook(
String isbn, String title, Integer releaseDate, String authorId);

Mono<AuthorRepresentation> registerAuthor(String firstName, String lastname, Integer birthDate);

Flux<BookRepresentation> findAllBooks();

Flux<BookRepresentation> findBookByAuthorAndDateBetween(
String authorId, Integer startDate, Integer endDate);

Flux<AuthorRepresentation> findAuthorByFirstnameAndLastnameAndBirthdate(
String firstname, String lastname, Integer birthdate);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package technical.test.api.services;

import java.time.Year;
import java.util.Objects;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Range;
import org.springframework.data.domain.Range.Bound;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import technical.test.api.mappers.AuthorMapper;
import technical.test.api.mappers.BookMapper;
import technical.test.api.representations.AuthorRepresentation;
import technical.test.api.representations.BookRepresentation;
import technical.test.api.storage.models.Author;
import technical.test.api.storage.models.Book;
import technical.test.api.storage.repositories.AuthorRepository;
import technical.test.api.storage.repositories.BookRepository;

@Service
public class LibraryServiceImpl implements LibraryService {

private final BookRepository bookRepository;

private final AuthorRepository authorRepository;

private final BookMapper bookMapper;

private final AuthorMapper authorMapper;

@Value("${library.release-dates.minimum-year:1800}")
private Integer minimumYear;

public LibraryServiceImpl(
BookRepository bookRepository,
AuthorRepository authorRepository,
BookMapper bookMapper,
AuthorMapper authorMapper) {
this.bookRepository = bookRepository;
this.authorRepository = authorRepository;
this.bookMapper = bookMapper;
this.authorMapper = authorMapper;
}

@Override
public Mono<BookRepresentation> registerBook(
String isbn, String title, Integer releaseDate, String authorId) {
return bookRepository
.save(
Book.builder()
.isbn(isbn)
.title(title)
.authorId(authorId)
.releaseDate(releaseDate)
.build())
.map(bookMapper::toBookRepresentation);
}

@Override
public Mono<AuthorRepresentation> registerAuthor(
String firstName, String lastname, Integer birthDate) {
return authorRepository
.save(
Author.builder()
.id(
String.join(
"_", StringUtils.lowerCase(firstName), StringUtils.lowerCase(lastname)))
.birthdate(birthDate)
.firstname(firstName)
.lastname(lastname)
.build())
.map(authorMapper::toAuthorRepresentation);
}

@Override
public Flux<BookRepresentation> findAllBooks() {
return bookRepository.findAll().map(bookMapper::toBookRepresentation);
}

@Override
public Flux<BookRepresentation> findBookByAuthorAndDateBetween(
String authorId, Integer startDate, Integer endDate) {
Range releaseDateRange = buildReleaseDateRange(startDate, endDate);
if (StringUtils.isNotEmpty(authorId)) {
return bookRepository.findByAuthorIdAndReleaseDateBetween(authorId, releaseDateRange);
}
return bookRepository.findByReleaseDateBetween(releaseDateRange);
}

@Override
public Flux<AuthorRepresentation> findAuthorByFirstnameAndLastnameAndBirthdate(
String firstname, String lastname, Integer birthdate) {
if (StringUtils.isEmpty(firstname)
&& StringUtils.isEmpty(lastname)
&& Objects.isNull(birthdate)) {
return authorRepository.findAll().map(authorMapper::toAuthorRepresentation);
}
return authorRepository
.findByFirstnameAndLastnameAndBirthdate(firstname, lastname, birthdate)
.map(authorMapper::toAuthorRepresentation);
}

private Range<Integer> buildReleaseDateRange(Integer startDate, Integer endDate) {
Range range =
Range.from(Bound.inclusive(minimumYear)).to(Bound.inclusive(Year.now().getValue()));
if (!Objects.isNull(startDate) && !Objects.isNull(endDate)) {
range = Range.from(Bound.inclusive(startDate)).to(Bound.inclusive(endDate));
} else if (Objects.isNull(startDate) && !Objects.isNull(endDate)) {
range = Range.from(Bound.inclusive(minimumYear)).to(Bound.inclusive(endDate));
} else if (!Objects.isNull(startDate)) {
range = Range.from(Bound.inclusive(startDate)).to(Bound.inclusive(Year.now().getValue()));
}
return range;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package technical.test.api.storage.models;

import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Builder
@Getter
@Setter
@Document
public class Author {

@Id private String id;

private Integer birthdate;

private String firstname;

private String lastname;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package technical.test.api.storage.models;

import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Builder
@Getter
@Setter
@Document
public class Book {

@Id private String id;

private String isbn;

private String title;

private Integer releaseDate;

private String authorId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package technical.test.api.storage.repositories;

import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import technical.test.api.storage.models.Author;

@Repository
public interface AuthorRepository extends ReactiveMongoRepository<Author, String> {

Flux<Author> findByFirstnameAndLastnameAndBirthdate(
String firstname, String lastname, Integer birthdate);
}
Loading