Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c36920b
Add package declaration to BookApplicationTests
codesungrape Nov 6, 2025
ce18bad
Add test setup for BookService with mocked repository
codesungrape Nov 6, 2025
98ef185
Add test for createBook success
codesungrape Nov 7, 2025
c9c366b
Add test for blank title edge case
codesungrape Nov 7, 2025
bbee690
Add test for null/invalid input tests
codesungrape Nov 7, 2025
1a78f63
Add NullPointerException when entire request is null
codesungrape Nov 7, 2025
dc7b207
Add BookService.java with createBook() and validation; TDD tests passing
codesungrape Nov 7, 2025
36e31f0
Add basic scaffolding for book request, book entity and book repository
codesungrape Nov 7, 2025
314cfeb
add failing tests for repository failures
codesungrape Nov 7, 2025
f5aec79
Add defensive check for learning purposes
codesungrape Nov 7, 2025
5e298ee
Add clearer comments
codesungrape Nov 7, 2025
624be2d
Add edgecase tests: longtitle, longSynopsis, specialChars in title
codesungrape Nov 7, 2025
1b5fa56
Refactor test to use @ParameterizedTest/CsvSource + update build.gradle
codesungrape Nov 7, 2025
0176bec
test: use @MethodSource to pass objects instead of primitives
codesungrape Nov 7, 2025
1e35f74
Clean up unused imports and comments
codesungrape Nov 11, 2025
8f669dc
fix(entity): Correct timestamp and builder defaults in Book
codesungrape Nov 11, 2025
9a294c6
Leave comment about redundant validation check
codesungrape Nov 11, 2025
0ee37ed
Clean up comments
codesungrape Nov 11, 2025
e4da445
Clean up redundant comment
codesungrape Nov 11, 2025
621eaef
Remove redundant SpringBoot config from openapi.yml
codesungrape Nov 11, 2025
3ecf354
Remove commented out code for redability and maintainability
codesungrape Nov 12, 2025
db2e5fe
Add comments for Test error to fix in next PR
codesungrape Nov 12, 2025
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
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ dependencies {
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testImplementation 'org.junit.jupiter:junit-jupiter-params'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.codesungrape.hmcts.BookAPI;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BookApiApplication {

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

}
27 changes: 27 additions & 0 deletions src/main/java/com/codesungrape/hmcts/BookAPI/dto/BookRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.codesungrape.hmcts.BookAPI.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.NotBlank;
import lombok.Value;

/**
* DTO representing the required input for creating or replacing a Book resource.
* This class mirrors the OpenAPI 'BookInput' schema.
* @Value: Makes all fields 'final' (immutable), generates constructor, getters, and equals/hashCode/toString.
* @NotBlank: Enforces the required status from your OpenAPI schema. If the field is missing or an empty string, Spring will return a 400 Bad Request.
* @JsonProperty: Jackson library - maps snake_case JSON (HMCTS rules) to camelCase Java
*/
@Value
public class BookRequest {
@NotBlank(message= "Title is required")
@JsonProperty("title")
String title;

@NotBlank(message = "Synopsis is required")
@JsonProperty("synopsis")
String synopsis;

@NotBlank(message = "Author is required")
@JsonProperty("author")
String author;
}
76 changes: 76 additions & 0 deletions src/main/java/com/codesungrape/hmcts/BookAPI/entity/Book.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.codesungrape.hmcts.BookAPI.entity;

import jakarta.persistence.*;
import java.util.UUID;
import lombok.*;
import java.time.Instant;

/**
* JPA Entity representing the Book table in PostgreSQL.
* This holds the persisted state of the resource.
* HMCTS Rule Check: IDs must be opaque strings. Using UUID for distributed ID generation.
* @Entity: Marks the class as a JPA entity - tells hibernate to map Java classes to database tables.
* @Table: Defines which database table this entity maps to. HMCTS Naming: Lowercase, singular table name is common practice.
* Lombok annotations:
* @Getter: Automatically generates getters for all fields.
* @Setter: Automatically generates setters.
* @AllArgsConstructor: Generates a no-argument constructor (required by JPA).
* JPA needs to instantiate the entity using reflection. 'PROTECTED' prevents misuse.
* @Builder: Adds a builder pattern for clean object creation.
* You can do Book.builder().title("A").author("B").build();
*/
@Entity
@Table(name = "book")
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED) // For JPA/Hibernate requirements
@AllArgsConstructor // For easy construction in tests
@Builder // For convenience in creating instances
public class Book {

@Id // Primary key of the table
@GeneratedValue(strategy = GenerationType.UUID)
@Column(name = "id", nullable = false) // maps the field to a database column named 'id' + 'nullable =false' database column cannot be NULL.
private UUID id;

@Column(name = "title", nullable = false)
private String title;

@Column(name = "synopsis", nullable = false, columnDefinition = "TEXT")
private String synopsis;

@Column(name = "author", nullable = false)
private String author;

// Soft delete - makes DELETE operations idempotent (safe to repeat)
// Soft delete - using @Builder.Default to ensure the builder
// respects this initialization if the field is not set explicitly.
@Column(name = "deleted", nullable = false)
@Builder.Default
private boolean deleted = false;

// `createdAt` is null upon object creation.
// It will be set by the `onCreate()` method right before persistence.
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;

@Column(name = "modified_at")
private java.time.Instant modifiedAt;

// --- JPA lifecycle callbacks ---
@PrePersist
protected void onCreate() {
this.createdAt = java.time.Instant.now();
}

// --- Business Logic Helper ---
// HMCTS mandates business logic in services, but a setter hook is acceptable.
// Lifecycle callback - special method runs automatically before Hibernate updates a record in the database.
@PreUpdate
protected void onUpdate() {

this.modifiedAt = java.time.Instant.now();
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.codesungrape.hmcts.BookAPI.repository;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

import com.codesungrape.hmcts.BookAPI.entity.Book;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

/**
* Repository interface for Book Entity.
* Spring Data JPA automatically provides CRUD operations based on the Entity and ID type.
*/
@Repository
public interface BookRepository extends JpaRepository<Book, UUID> {

// Custom query to find books that have NOT been soft-deleted
List<Book> findAllByDeletedFalse();

// Custom query to find a specific, non-deleted book by ID.
Optional<Book> findByIdAndDeleteFalse(UUID id);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.codesungrape.hmcts.BookAPI.service;

import com.codesungrape.hmcts.BookAPI.dto.BookRequest;
import com.codesungrape.hmcts.BookAPI.repository.BookRepository;
import org.springframework.stereotype.Service; // Marks a class as a Service Layer component.
import lombok.RequiredArgsConstructor;

import com.codesungrape.hmcts.BookAPI.entity.Book;

/**
* Service layer responsible for all business logic related to the Book resource.
*/
@Service
@RequiredArgsConstructor // Lombok creates constructor for dependency injection
public class BookService {

// Create a field to store the repo
private final BookRepository bookRepository;

// 1. CREATE Operation (POST /books)
public Book createBook(BookRequest request) {
// Validation check for business rules (e.g., uniqueness, if required)
if (request == null) {
throw new NullPointerException("BookRequest cannot be null");
}


// REVISIT: Leaving this here for now as i haven't implemented the Controller Layer yet
// The service layer is duplicating validation that already exists in the
// BookRequest DTO with @notblank annotations. Since the DTO has validation
// constraints, this manual check is redundant when Spring's validation
// framework is properly configured in the controller layer.
// Consider removing this duplication or adding a comment explaining
// why service-level validation is necessary in addition to DTO validation.
if (request.getTitle() == null || request.getTitle().isBlank()) {
throw new IllegalArgumentException("Book title cannot be null or blank");
}

// Map DTO to Entity
Book newBook = Book.builder()
.title(request.getTitle())
.author(request.getAuthor())
.synopsis(request.getSynopsis())
// ID and created_at are auto-generated by JPA/DB
.build();

Book savedBook = bookRepository.save(newBook);

// Defensive check (even though it "shouldn't" happen aka follows JPA contract)
if (savedBook == null) {
throw new IllegalStateException("Failed to save book - repository returned null");
}

return savedBook;
}
}
3 changes: 3 additions & 0 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
spring:
application:
name: BookAPI
Loading