diff --git a/README.md b/README.md new file mode 100644 index 0000000..34a746f --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +![img.png](img/img.png) + +# api-first-development i.e. Contract First Development + +## Contract first development using Swagger API + +## Dependencies + +- Java Development Kit - JDK 17 + +## Backend + +- Using: http://localhost:8080 for the project! + + +### General setup (Guide) + + +- In service see as follows: + + - Database in case of dev and prod: MyQL Server (environment variables set for project, pls. user your own!) + + - In case of test: H2 in memory database used (see application.properties for setup under test folder) + +- Environment Variables as follows: + +| Name | Value | Remark | Extra remark | +|--------------------|--------------|-------------------|----------------------------------------------------------| +| DB_PORT | 3306 | default | | +| DB_URL | no such info | pls use your own! | | +| DB_USER | root | default | pls use your own if you have set any other | +| DB_PASSWORD | no such info | pls use your own! | [MYSQL Installation Guide](https://dev.mysql.com/doc/mysql-installation-excerpt/5.7/en/) | diff --git a/api-first-development-service-api-contract/pom.xml b/api-first-development-service-api-contract/pom.xml index 4285d89..889c6be 100644 --- a/api-first-development-service-api-contract/pom.xml +++ b/api-first-development-service-api-contract/pom.xml @@ -23,13 +23,11 @@ org.springframework.boot spring-boot-starter-web - io.swagger swagger-annotations 1.6.2 - io.swagger swagger-models @@ -39,12 +37,26 @@ com.fasterxml.jackson.core jackson-annotations - javax.validation validation-api 2.0.1.Final + + io.swagger.core.v3 + swagger-annotations + 2.2.6 + + + org.apache.tomcat + tomcat-annotations-api + 9.0.16 + + + org.assertj + assertj-core + 3.23.1 + @@ -52,7 +64,7 @@ io.swagger.codegen.v3 swagger-codegen-maven-plugin - 3.0.37 + 3.0.36 diff --git a/api-first-development-service-api-contract/src/main/java/com/csaba79coder/App.java b/api-first-development-service-api-contract/src/main/java/com/csaba79coder/App.java deleted file mode 100644 index 8888bf1..0000000 --- a/api-first-development-service-api-contract/src/main/java/com/csaba79coder/App.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.csaba79coder; - -/** - * Hello world! - * - */ -public class App -{ - public static void main( String[] args ) - { - System.out.println( "Hello World!" ); - } -} diff --git a/api-first-development-service-api-contract/src/main/resources/service-contract-api.yaml b/api-first-development-service-api-contract/src/main/resources/service-contract-api.yaml index 09716d3..9a936e6 100644 --- a/api-first-development-service-api-contract/src/main/resources/service-contract-api.yaml +++ b/api-first-development-service-api-contract/src/main/resources/service-contract-api.yaml @@ -7,5 +7,364 @@ info: email: csabavadasz79@gmail.com servers: - url: 'http://localhost:8080' +tags: + - name: book + - name: log + paths: - + /books: + get: + tags: + - book + summary: Render all books + description: Show all books from database + operationId: renderAllBooks + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/BookModel' + '400': + description: Bad request + '401': + description: Unauthorized + '403': + description: Forbidden + '404': + description: Book not found + '405': + description: Validation exception + post: + tags: + - book + summary: Add a new book to the store + description: Add a new book to the store + operationId: addBook + requestBody: + description: Create a new book in the store + content: + application/json: + schema: + $ref: '#/components/schemas/NewBookModel' + required: true + responses: + '201': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/BookModel' + '405': + description: Invalid input + /books/{bookId}: + get: + tags: + - book + summary: Find book by ID + description: Returns a single book + operationId: getBookById + parameters: + - name: bookId + in: path + description: ID of book to return + required: true + schema: + format: uuid + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/BookModel' + '400': + description: Invalid ID supplied + '404': + description: Book not found + put: + tags: + - book + summary: Update an existing book + description: Update an existing book by Id + operationId: updateBook + parameters: + - name: bookId + in: path + description: ID of book to return + required: true + schema: + format: uuid + type: string + requestBody: + description: Create a new book in the store + content: + application/json: + schema: + $ref: '#/components/schemas/ModifiedBookModel' + required: true + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/BookModel' + '400': + description: Invalid ID supplied + '404': + description: Book not found + '405': + description: Validation exception + delete: + tags: + - book + summary: Deletes a book + description: delete a book + operationId: deleteBook + parameters: + - name: bookId + in: path + description: ID of book to return + required: true + schema: + format: uuid + type: string + responses: + '204': + description: successful operation + /logs: + get: + tags: + - log + summary: Render all logs + description: Show all logs from database + operationId: renderAllLogs + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/LogModel' + '400': + description: Bad request + '401': + description: Unauthorized + '403': + description: Forbidden + '404': + description: Book not found + '405': + description: Validation exception + post: + tags: + - log + summary: Add a new log to the database + description: Add a new log to the database + operationId: addLog + requestBody: + description: Create a new log in the database + content: + application/json: + schema: + $ref: '#/components/schemas/NewLogModel' + required: true + responses: + '201': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/LogModel' + '405': + description: Invalid input + +components: + schemas: + NewBookModel: + type: object + properties: + title: + description: Title of book entity + type: string + example: 'Cat Among the Pigeons' + isbn: + description: ISBN number of the book (13 characters) + type: number + example: 9780671557003 + genre: + description: Genre of the book entity + type: string + example: DETECTIVE_FICTION + enum: + - DETECTIVE_FICTION + - NOVEL + - MYSTERY + - THRILLER + - HORROR + - HISTORICAL + - ROMANCE + - WESTERN + - BILDUNGSROMAN + - SCIENCE_FICTION + - FICTION + - FANTASY + - MAGICAL_REALISM + - REALIST_LITERATURE + - OTHER + required: + - title + - isbn + - genre + BookModel: + type: object + properties: + id: + description: Id of book entity + type: string + format: uuid + example: '3a8ea9f1-1a95-4caf-932f-2f988052933b' + createdBy: + description: User id who created book entity + type: string + format: uuid + example: '3a8ea9f1-1a95-4caf-932f-2f988052933b' + updatedBy: + description: User id who updated book entity + type: string + format: uuid + example: '3a8ea9f1-1a95-4caf-932f-2f988052933b' + createdAt: + description: Creation time of book entity + type: string + format: timestamp + example: '2019-01-21T05:47:08.644' + updatedAt: + description: Updated time of book entity + type: string + format: timestamp + example: '2019-01-21T05:47:08.644' + title: + description: Title of book entity + type: string + example: 'Cat Among the Pigeons' + isbn: + description: ISBN number of the book (13 characters) + type: number + example: 9780671557003 + genre: + description: Genre of the book entity + type: string + example: DETECTIVE_FICTION + enum: + - DETECTIVE_FICTION + - NOVEL + - MYSTERY + - THRILLER + - HORROR + - HISTORICAL + - ROMANCE + - WESTERN + - BILDUNGSROMAN + - SCIENCE_FICTION + - FICTION + - FANTASY + - MAGICAL_REALISM + - REALIST_LITERATURE + - OTHER + status: + description: Status of the book + type: string + example: SOLD + enum: + - AVAILABLE + - PENDING + - SOLD + availability: + description: Status of book availability + type: string + example: AVAILABLE + enum: + - AVAILABLE + - ARCHIVE + - DELETED + ModifiedBookModel: + type: object + properties: + title: + description: Title of book entity + type: string + example: 'Cat Among the Pigeons' + isbn: + description: ISBN number of the book (13 characters) + type: number + example: 9780671557003 + genre: + description: Genre of the book entity + type: string + example: DETECTIVE_FICTION + enum: + - DETECTIVE_FICTION + - NOVEL + - MYSTERY + - THRILLER + - HORROR + - HISTORICAL + - ROMANCE + - WESTERN + - BILDUNGSROMAN + - SCIENCE_FICTION + - FICTION + - FANTASY + - MAGICAL_REALISM + - REALIST_LITERATURE + - OTHER + status: + description: Status of the book + type: string + example: SOLD + enum: + - AVAILABLE + - PENDING + - SOLD + availability: + description: Status of book availability + type: string + example: AVAILABLE + enum: + - AVAILABLE + - ARCHIVE + - DELETED + NewLogModel: + type: object + properties: + logMessage: + description: Log message + type: string + example: "{ERROR_CODE_001=Book with id: 4bc34bbf-6278-4586-9e62-429bc41edcf5 was not found}" + required: + - logMessage + LogModel: + type: object + properties: + id: + description: Id of log entity + type: string + format: uuid + example: '3a8ea9f1-1a95-4caf-932f-2f988052933b' + logMessage: + description: Log message + type: string + example: "{ERROR_CODE_001=Book with id: 4bc34bbf-6278-4586-9e62-429bc41edcf5 was not found}" + loggedAt: + description: Creation time of log + type: string + format: timestamp + example: '2019-01-21T05:47:08.644' \ No newline at end of file diff --git a/api-first-development-service/pom.xml b/api-first-development-service/pom.xml index 4aecd01..82cd69c 100644 --- a/api-first-development-service/pom.xml +++ b/api-first-development-service/pom.xml @@ -80,6 +80,18 @@ 3.8.1 test + + com.csaba79coder + api-first-development-service-api-contract + 0.0.1-SNAPSHOT + compile + + + org.modelmapper + modelmapper + 3.1.1 + + diff --git a/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/controller/BookController.java b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/controller/BookController.java new file mode 100644 index 0000000..ca4628c --- /dev/null +++ b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/controller/BookController.java @@ -0,0 +1,48 @@ +package com.csaba79coder.apifirstdevelopment.controller; + +import com.csaba79coder.api.BooksApi; +import com.csaba79coder.apifirstdevelopment.service.BookService; +import com.csaba79coder.models.BookModel; +import com.csaba79coder.models.ModifiedBookModel; +import com.csaba79coder.models.NewBookModel; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.UUID; + +@RestController +@CrossOrigin(value = "http://localhost:8080") +@RequiredArgsConstructor +public class BookController implements BooksApi { + + private final BookService bookService; + + @Override + public ResponseEntity addBook(NewBookModel body) { + return ResponseEntity.status(201).body(bookService.addNewBook(body)); + } + + @Override + public ResponseEntity deleteBook(UUID bookId) { + bookService.deleteAnExistingBookById(bookId); + return ResponseEntity.status(204).build(); + } + + @Override + public ResponseEntity getBookById(UUID bookId) { + return ResponseEntity.status(200).body(bookService.getBookById(bookId)); + } + + @Override + public ResponseEntity> renderAllBooks() { + return ResponseEntity.status(200).body(bookService.renderAllBooks()); + } + + @Override + public ResponseEntity updateBook(UUID bookId, ModifiedBookModel body) { + return null; + } +} diff --git a/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/controller/LogController.java b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/controller/LogController.java new file mode 100644 index 0000000..8833dfe --- /dev/null +++ b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/controller/LogController.java @@ -0,0 +1,28 @@ +package com.csaba79coder.apifirstdevelopment.controller; + +import com.csaba79coder.api.LogsApi; +import com.csaba79coder.apifirstdevelopment.service.LogService; +import com.csaba79coder.models.LogModel; +import com.csaba79coder.models.NewLogModel; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +public class LogController implements LogsApi { + + private final LogService logService; + + @Override + public ResponseEntity addLog(NewLogModel body) { + return null; + } + + @Override + public ResponseEntity> renderAllLogs() { + return null; + } +} diff --git a/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/entity/BaseEntity.java b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/entity/BaseEntity.java new file mode 100644 index 0000000..4da31f9 --- /dev/null +++ b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/entity/BaseEntity.java @@ -0,0 +1,39 @@ +package com.csaba79coder.apifirstdevelopment.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import lombok.Data; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.hibernate.Hibernate; + +import java.util.Objects; +import java.util.UUID; + +@MappedSuperclass +@Getter +@Setter +@ToString +@RequiredArgsConstructor +public class BaseEntity { + + @Id + @Column(name = "id", nullable = false) + private UUID id = UUID.randomUUID(); + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; + BaseEntity that = (BaseEntity) o; + return id != null && Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } +} diff --git a/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/entity/Book.java b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/entity/Book.java new file mode 100644 index 0000000..28356a6 --- /dev/null +++ b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/entity/Book.java @@ -0,0 +1,59 @@ +package com.csaba79coder.apifirstdevelopment.entity; + +import com.csaba79coder.apifirstdevelopment.value.Availability; +import com.csaba79coder.apifirstdevelopment.value.Genre; +import com.csaba79coder.apifirstdevelopment.value.Status; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jdk.jfr.Timestamp; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.hibernate.annotations.Where; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Getter +@Setter +@ToString +@NoArgsConstructor +@Where(clause = "availability != 'DELETED'") +public class Book extends BaseEntity { + + @Column(name = "created_by", nullable = false) + private UUID createdBy = UUID.fromString("3a8ea9f1-1a95-4caf-932f-2f988052933b"); + + @Column(name = "updated_by", nullable = false) + private UUID updatedBy = UUID.fromString("9e91103b-ef57-4d61-983c-28dfdd7e332a"); + + @Timestamp + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt = LocalDateTime.now(); + + @Timestamp + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt = LocalDateTime.now(); + @Column(name = "title", nullable = false) + private String title; + + @Column(name = "isbn", nullable = false, unique = true, length = 13) + private BigDecimal isbn; + + @Column(name = "genre", nullable = false) + @Enumerated(EnumType.STRING) + private Genre genre = Genre.OTHER; + + @Column(name = "status", nullable = false) + @Enumerated(EnumType.STRING) + private Status status = Status.AVAILABLE; + + @Column(name = "availability", nullable = false) + @Enumerated(EnumType.STRING) + private Availability availability = Availability.AVAILABLE; +} diff --git a/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/entity/Log.java b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/entity/Log.java new file mode 100644 index 0000000..cd5906a --- /dev/null +++ b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/entity/Log.java @@ -0,0 +1,33 @@ +package com.csaba79coder.apifirstdevelopment.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import jdk.jfr.Timestamp; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.Setter; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +@ToString +@NoArgsConstructor +@Table(name = "log") +public class Log extends BaseEntity { + + @Timestamp + @Column(name = "logged_at") + private LocalDateTime loggedAt = LocalDateTime.now(); + + @Column(name = "log_message") + private String logMessage; + + public Log(@NonNull String message) { + logMessage = message; + } +} diff --git a/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/exception/ControllerExceptionHandler.java b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/exception/ControllerExceptionHandler.java new file mode 100644 index 0000000..bc1117b --- /dev/null +++ b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/exception/ControllerExceptionHandler.java @@ -0,0 +1,32 @@ +package com.csaba79coder.apifirstdevelopment.exception; + +import com.csaba79coder.apifirstdevelopment.value.ErrorCode; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +import java.util.InputMismatchException; +import java.util.Map; +import java.util.NoSuchElementException; + +import static com.csaba79coder.apifirstdevelopment.value.ErrorCode.ERROR_CODE_001; +import static com.csaba79coder.apifirstdevelopment.value.ErrorCode.ERROR_CODE_002; + +@ControllerAdvice +public class ControllerExceptionHandler { + + @ExceptionHandler(value = {NoSuchElementException.class}) + public ResponseEntity handleNoSuchElementException(NoSuchElementException ex) { + return new ResponseEntity<>(responseBodyWithMessage(ERROR_CODE_001, ex.getMessage()), HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(value = {InputMismatchException.class}) + public ResponseEntity handleInvalidInputException(InputMismatchException ex) { + return new ResponseEntity<>(responseBodyWithMessage(ERROR_CODE_002, ex.getMessage()), HttpStatus.BAD_REQUEST); + } + + private String responseBodyWithMessage(ErrorCode code, String message) { + return Map.of(code, message).toString(); + } +} diff --git a/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/persistence/BookRepository.java b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/persistence/BookRepository.java new file mode 100644 index 0000000..38b0b42 --- /dev/null +++ b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/persistence/BookRepository.java @@ -0,0 +1,14 @@ +package com.csaba79coder.apifirstdevelopment.persistence; + +import com.csaba79coder.apifirstdevelopment.entity.Book; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.UUID; + +@Repository +public interface BookRepository extends JpaRepository { + + Optional findBookById(UUID id); +} diff --git a/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/persistence/LogRepository.java b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/persistence/LogRepository.java new file mode 100644 index 0000000..aa78135 --- /dev/null +++ b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/persistence/LogRepository.java @@ -0,0 +1,11 @@ +package com.csaba79coder.apifirstdevelopment.persistence; + +import com.csaba79coder.apifirstdevelopment.entity.Log; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.UUID; + +@Repository +public interface LogRepository extends JpaRepository { +} diff --git a/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/service/BookService.java b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/service/BookService.java new file mode 100644 index 0000000..bf60482 --- /dev/null +++ b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/service/BookService.java @@ -0,0 +1,76 @@ +package com.csaba79coder.apifirstdevelopment.service; + +import com.csaba79coder.apifirstdevelopment.entity.Book; +import com.csaba79coder.apifirstdevelopment.entity.Log; +import com.csaba79coder.apifirstdevelopment.persistence.BookRepository; +import com.csaba79coder.apifirstdevelopment.persistence.LogRepository; +import com.csaba79coder.apifirstdevelopment.util.ISBN13Validator; +import com.csaba79coder.apifirstdevelopment.util.Mapper; +import com.csaba79coder.models.BookModel; +import com.csaba79coder.models.ModifiedBookModel; +import com.csaba79coder.models.NewBookModel; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.InputMismatchException; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Slf4j +public class BookService { + private final LogRepository logRepository; + + private final BookRepository bookRepository; + private Log systemLog; + + public BookModel addNewBook(NewBookModel newModel) { + if (!ISBN13Validator.isValidISBN(newModel.getIsbn()) + || newModel.getTitle().isEmpty() || newModel.getTitle().isBlank() + || newModel.getGenre() == null) { + String message = String.format("Please represent a valid isbn input, isbn: %s is not valid!", newModel.getIsbn()); + systemLog = new Log(message); + logRepository.save(systemLog); + log.info(message); + throw new InputMismatchException(message); + } else { + return Mapper.mapBookEntityToBookModel(bookRepository.save(Mapper.mapNewBookModelToBookEntity(newModel))); + } + } + + public void deleteAnExistingBookById(UUID id) { + Book book = findById(id); + bookRepository.delete(book); + } + + public BookModel getBookById(UUID id) { + Book book = findById(id); + bookRepository.delete(book); + return null; + } + + public List renderAllBooks() { + return bookRepository.findAll() + .stream().map(Mapper::mapBookEntityToBookModel) + .collect(Collectors.toList()); + } + + public BookModel updateAnExistingBook(UUID id, ModifiedBookModel modifyBook) { + return null; + } + + private Book findById(UUID id) { + return bookRepository.findBookById(id) + .orElseThrow(() -> { + String message = String.format("Book with id: %s was not found", id); + systemLog = new Log(message); + logRepository.save(systemLog); + log.info(message); + return new NoSuchElementException(message); + }); + } +} diff --git a/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/service/LogService.java b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/service/LogService.java new file mode 100644 index 0000000..81b75af --- /dev/null +++ b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/service/LogService.java @@ -0,0 +1,12 @@ +package com.csaba79coder.apifirstdevelopment.service; + +import com.csaba79coder.apifirstdevelopment.persistence.LogRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class LogService { + + private final LogRepository logRepository; +} diff --git a/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/util/ISBN13Validator.java b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/util/ISBN13Validator.java new file mode 100644 index 0000000..2eafb00 --- /dev/null +++ b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/util/ISBN13Validator.java @@ -0,0 +1,39 @@ +package com.csaba79coder.apifirstdevelopment.util; + +import java.math.BigDecimal; + +public class ISBN13Validator { + + public static boolean isValidISBN(BigDecimal isbnLong) { + String isbn = String.valueOf(isbnLong); + if (isbn == null) { + return false; + } + if (isbn.startsWith("-")) { + return false; + } + isbn = isbn.replaceAll("-", ""); + if ( isbn.length() != 13 ) { + return false; + } + try { + int tot = 0; + for (int i = 0; i < 12; i++) { + int digit = Integer.parseInt(isbn.substring( i, i + 1 )); + tot += (i % 2 == 0) ? digit : digit * 3; + } + int checksum = 10 - (tot % 10); + if (checksum == 10) { + checksum = 0; + } + return checksum == Integer.parseInt(isbn.substring(12)); + } + catch (NumberFormatException nfe) { + return false; + } + } + + private ISBN13Validator() { + + } +} diff --git a/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/util/Mapper.java b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/util/Mapper.java new file mode 100644 index 0000000..4cda3ed --- /dev/null +++ b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/util/Mapper.java @@ -0,0 +1,28 @@ +package com.csaba79coder.apifirstdevelopment.util; + +import com.csaba79coder.apifirstdevelopment.entity.Book; +import com.csaba79coder.models.BookModel; +import com.csaba79coder.models.NewBookModel; +import org.modelmapper.ModelMapper; + +public class Mapper { + + private static final ModelMapper modelMapper = new ModelMapper(); + + public static Book mapNewBookModelToBookEntity(NewBookModel bookModel) { + Book book = new Book(); + modelMapper.map(bookModel, book); + return book; + } + + + public static BookModel mapBookEntityToBookModel(Book book) { + BookModel bookModel = new BookModel(); + modelMapper.map(book, bookModel); + return bookModel; + } + + private Mapper() { + + } +} diff --git a/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/value/Availability.java b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/value/Availability.java new file mode 100644 index 0000000..4fc22bc --- /dev/null +++ b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/value/Availability.java @@ -0,0 +1,5 @@ +package com.csaba79coder.apifirstdevelopment.value; + +public enum Availability { + AVAILABLE, ARCHIVE, DELETED +} diff --git a/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/value/ErrorCode.java b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/value/ErrorCode.java new file mode 100644 index 0000000..ab3d27e --- /dev/null +++ b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/value/ErrorCode.java @@ -0,0 +1,5 @@ +package com.csaba79coder.apifirstdevelopment.value; + +public enum ErrorCode { + ERROR_CODE_001, ERROR_CODE_002 +} diff --git a/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/value/Genre.java b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/value/Genre.java new file mode 100644 index 0000000..32bd9e3 --- /dev/null +++ b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/value/Genre.java @@ -0,0 +1,6 @@ +package com.csaba79coder.apifirstdevelopment.value; + +public enum Genre { + DETECTIVE_FICTION, NOVEL, MYSTERY, THRILLER, HORROR, HISTORICAL, ROMANCE, WESTERN, BILDUNGSROMAN, + SCIENCE_FICTION, FICTION, FANTASY, MAGICAL_REALISM, REALIST_LITERATURE, OTHER +} diff --git a/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/value/Status.java b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/value/Status.java new file mode 100644 index 0000000..29feae5 --- /dev/null +++ b/api-first-development-service/src/main/java/com/csaba79coder/apifirstdevelopment/value/Status.java @@ -0,0 +1,5 @@ +package com.csaba79coder.apifirstdevelopment.value; + +public enum Status { + AVAILABLE, PENDING, SOLD +} diff --git a/api-first-development-service/src/main/resources/application.properties b/api-first-development-service/src/main/resources/application.properties index 6cefb5f..411347f 100644 --- a/api-first-development-service/src/main/resources/application.properties +++ b/api-first-development-service/src/main/resources/application.properties @@ -1,7 +1,7 @@ -spring.liquibase.change-log= classpath:/db/changelog/db.changelog-master.xml +spring.liquibase.change-log=classpath:/db/changelog/db.changelog-master.xml spring.liquibase.enabled=true -spring.jpa.hibernate.ddl-auto=update +spring.jpa.hibernate.ddl-auto=none spring.datasource.url=jdbc:mysql://localhost:${DB_PORT}/${DB_URL}?createDatabaseIfNotExist=true&useSSL=true spring.datasource.username=${DB_USER} spring.datasource.password=${DB_PASSWORD} diff --git a/api-first-development-service/src/main/resources/db/changelog/db.changelog-master.xml b/api-first-development-service/src/main/resources/db/changelog/db.changelog-master.xml index 7ded8a3..d44691f 100644 --- a/api-first-development-service/src/main/resources/db/changelog/db.changelog-master.xml +++ b/api-first-development-service/src/main/resources/db/changelog/db.changelog-master.xml @@ -7,4 +7,8 @@ + + + + \ No newline at end of file diff --git a/api-first-development-service/src/main/resources/db/changelog/setup/book.xml b/api-first-development-service/src/main/resources/db/changelog/setup/book.xml new file mode 100644 index 0000000..2ffdd61 --- /dev/null +++ b/api-first-development-service/src/main/resources/db/changelog/setup/book.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/api-first-development-service/src/main/resources/db/changelog/setup/liquibase-config-log.xml b/api-first-development-service/src/main/resources/db/changelog/setup/log.xml similarity index 87% rename from api-first-development-service/src/main/resources/db/changelog/setup/liquibase-config-log.xml rename to api-first-development-service/src/main/resources/db/changelog/setup/log.xml index f9a70d1..71ad5e0 100644 --- a/api-first-development-service/src/main/resources/db/changelog/setup/liquibase-config-log.xml +++ b/api-first-development-service/src/main/resources/db/changelog/setup/log.xml @@ -9,10 +9,10 @@ - + - + diff --git a/api-first-development-service/src/test/java/com/csaba79coder/apifirstdevelopment/util/ISBN13ValidatorTest.java b/api-first-development-service/src/test/java/com/csaba79coder/apifirstdevelopment/util/ISBN13ValidatorTest.java new file mode 100644 index 0000000..fe53361 --- /dev/null +++ b/api-first-development-service/src/test/java/com/csaba79coder/apifirstdevelopment/util/ISBN13ValidatorTest.java @@ -0,0 +1,54 @@ +package com.csaba79coder.apifirstdevelopment.util; + +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.assertj.core.api.BDDAssertions.then; + +class ISBN13ValidatorTest { + + // https://medium.com/@stefanovskyi/unit-test-naming-conventions-dd9208eadbea + + @Test + void validISBN() { + boolean isValid = ISBN13Validator.isValidISBN(BigDecimal.valueOf(9780671557003L)); + then(isValid) + .isTrue(); + } + + @Test + void invalidISBN() { + boolean isValid = ISBN13Validator.isValidISBN(BigDecimal.valueOf(999L)); + then(isValid) + .isFalse(); + } + + @Test + void nullISBN() { + boolean isValid = ISBN13Validator.isValidISBN(null); + then(isValid) + .isFalse(); + } + + @Test + void zeroISBN() { + boolean isValid = ISBN13Validator.isValidISBN(BigDecimal.valueOf(0L)); + then(isValid) + .isFalse(); + } + + @Test + void negativeNonValidISBN() { + boolean isValid = ISBN13Validator.isValidISBN(BigDecimal.valueOf(-112L)); + then(isValid) + .isFalse(); + } + + @Test + void negativeValidISBN() { + boolean isValid = ISBN13Validator.isValidISBN(BigDecimal.valueOf(-9780425175477L)); + then(isValid) + .isFalse(); + } +} \ No newline at end of file diff --git a/api-first-development-service/src/test/java/com/csaba79coder/apifirstdevelopment/util/MapperTest.java b/api-first-development-service/src/test/java/com/csaba79coder/apifirstdevelopment/util/MapperTest.java new file mode 100644 index 0000000..061209a --- /dev/null +++ b/api-first-development-service/src/test/java/com/csaba79coder/apifirstdevelopment/util/MapperTest.java @@ -0,0 +1,56 @@ +package com.csaba79coder.apifirstdevelopment.util; + +import com.csaba79coder.apifirstdevelopment.entity.Book; +import com.csaba79coder.apifirstdevelopment.value.Genre; +import com.csaba79coder.models.BookModel; +import com.csaba79coder.models.NewBookModel; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +import static org.assertj.core.api.BDDAssertions.then; + +class MapperTest { + + @Test + void mapNewBookModelToBookEntity() { + // Given + NewBookModel newBookModel = new NewBookModel().title("Cat Among the Pigeons").isbn(BigDecimal.valueOf(9780671557003L)).genre(NewBookModel.GenreEnum.NOVEL); + Book expectedEntity = new Book(); + expectedEntity.setTitle(newBookModel.getTitle()); + expectedEntity.setIsbn(BigDecimal.valueOf(9780671557003L)); + expectedEntity.setGenre(Genre.valueOf(newBookModel.getGenre().name())); + + // When + Book book = Mapper.mapNewBookModelToBookEntity(newBookModel); + + // Then + then(book) + .usingRecursiveComparison() + .ignoringFields("id", "createdAt", "updatedAt") + .isEqualTo(expectedEntity); + } + + @Test + void mapBookEntityToBookModel() { + // Given + Book book = new Book(); + book.setTitle("Cat Among the Pigeons"); + book.setIsbn(BigDecimal.valueOf(9780671557003L)); + book.setGenre(Genre.valueOf(Genre.DETECTIVE_FICTION.name())); + + // When + BookModel bookModel = Mapper.mapBookEntityToBookModel(book); + + // Then + then(bookModel) + .usingRecursiveComparison() + .ignoringFields("createdAt", "updatedAt") + .isEqualTo(book); + then(LocalDateTime.parse(bookModel.getCreatedAt())) + .isEqualTo(book.getCreatedAt()); + then(LocalDateTime.parse(bookModel.getUpdatedAt())) + .isEqualTo(book.getUpdatedAt()); + } +} \ No newline at end of file diff --git a/api-first-development-service/src/test/resources/application.properties b/api-first-development-service/src/test/resources/application.properties index d1f87a3..e0e712f 100644 --- a/api-first-development-service/src/test/resources/application.properties +++ b/api-first-development-service/src/test/resources/application.properties @@ -1,4 +1,4 @@ -spring.liquibase.change-log= classpath:/db/changelog/db.changelog-master.xml +spring.liquibase.change-log=classpath:/db/changelog/db.changelog-master.xml spring.liquibase.enabled=true spring.datasource.url=jdbc:h2:mem:testdb diff --git a/doc/task.adoc b/doc/task.adoc new file mode 100644 index 0000000..c18b413 --- /dev/null +++ b/doc/task.adoc @@ -0,0 +1,34 @@ += Good to know + +Video with explanation for contract first development + +Part 1 - The Setup (https://www.youtube.com/watch?v=69P7p...) + +Part 2 - Editing the YAML file (https://www.youtube.com/watch?v=TyWDJ...) + +Part 3 - POST requests (https://www.youtube.com/watch?v=vVoYT...) + +Part 4 - Path Parameters (https://www.youtube.com/watch?v=TAVon...) + +''' + +Dependencies & plugin pastebin: https://pastebin.com/gdiMqhwW + +Yaml file pastebin: https://pastebin.com/GhKSVbaw + +''' + +== Liquibase changeLog + +https://www.baeldung.com/liquibase-refactor-schema-of-java-app + +''' + +== MapStruct and ModelMapper + +https://mapstruct.org/ + +https://modelmapper.org/examples/ + +https://www.baeldung.com/java-modelmapper + diff --git a/img/img.png b/img/img.png new file mode 100644 index 0000000..542c228 Binary files /dev/null and b/img/img.png differ diff --git a/pom.xml b/pom.xml index e2de91b..c2111a6 100644 --- a/pom.xml +++ b/pom.xml @@ -24,6 +24,15 @@ 17 + + + + com.csaba79coder + api-first-development + 0.0.1-SNAPSHOT + + + scm:git:git@github.com:Csaba79-coder/api-first-development HEAD