Skip to content

Commit 8f87c0a

Browse files
authored
feature/scaffold book api and createBook service logic and tests (#1)
* Add basic scaffolding for book request, book entity and book repository * Add test setup for BookService with mocked repository * Add test for createBook success, blank title edge case, null/invalid input tests, NullPointerException when entire request is null, repository failures * Add BookService.java with createBook() and validation; TDD tests pass * Add edgecase tests: longtitle, longSynopsis, specialChars in title * Refactor test to use @ParameterizedTest/@MethodSource to pass objects instead of primitives + update build.gradle * fix(entity): Correct timestamp and builder defaults in Book - Use @PrePersist for to set timestamp at persistence time. - Use @Builder.Default for to ensure builder honors the default. * Leave comment about redundant validation check * Clean up unused imports and comments * Remove redundant SpringBoot config from openapi.yml * Add comments for Test error to fix in next PR
1 parent d5997fe commit 8f87c0a

File tree

10 files changed

+729
-0
lines changed

10 files changed

+729
-0
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ dependencies {
3333
annotationProcessor 'org.projectlombok:lombok'
3434
testImplementation 'org.springframework.boot:spring-boot-starter-test'
3535
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
36+
testImplementation 'org.junit.jupiter:junit-jupiter-params'
3637
}
3738

3839
tasks.named('test') {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.codesungrape.hmcts.BookAPI;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
6+
@SpringBootApplication
7+
public class BookApiApplication {
8+
9+
public static void main(String[] args) {
10+
SpringApplication.run(BookApiApplication.class, args);
11+
}
12+
13+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.codesungrape.hmcts.BookAPI.dto;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import jakarta.validation.constraints.NotBlank;
5+
import lombok.Value;
6+
7+
/**
8+
* DTO representing the required input for creating or replacing a Book resource.
9+
* This class mirrors the OpenAPI 'BookInput' schema.
10+
* @Value: Makes all fields 'final' (immutable), generates constructor, getters, and equals/hashCode/toString.
11+
* @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.
12+
* @JsonProperty: Jackson library - maps snake_case JSON (HMCTS rules) to camelCase Java
13+
*/
14+
@Value
15+
public class BookRequest {
16+
@NotBlank(message= "Title is required")
17+
@JsonProperty("title")
18+
String title;
19+
20+
@NotBlank(message = "Synopsis is required")
21+
@JsonProperty("synopsis")
22+
String synopsis;
23+
24+
@NotBlank(message = "Author is required")
25+
@JsonProperty("author")
26+
String author;
27+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.codesungrape.hmcts.BookAPI.entity;
2+
3+
import jakarta.persistence.*;
4+
import java.util.UUID;
5+
import lombok.*;
6+
import java.time.Instant;
7+
8+
/**
9+
* JPA Entity representing the Book table in PostgreSQL.
10+
* This holds the persisted state of the resource.
11+
* HMCTS Rule Check: IDs must be opaque strings. Using UUID for distributed ID generation.
12+
* @Entity: Marks the class as a JPA entity - tells hibernate to map Java classes to database tables.
13+
* @Table: Defines which database table this entity maps to. HMCTS Naming: Lowercase, singular table name is common practice.
14+
* Lombok annotations:
15+
* @Getter: Automatically generates getters for all fields.
16+
* @Setter: Automatically generates setters.
17+
* @AllArgsConstructor: Generates a no-argument constructor (required by JPA).
18+
* JPA needs to instantiate the entity using reflection. 'PROTECTED' prevents misuse.
19+
* @Builder: Adds a builder pattern for clean object creation.
20+
* You can do Book.builder().title("A").author("B").build();
21+
*/
22+
@Entity
23+
@Table(name = "book")
24+
@Getter
25+
@Setter
26+
@NoArgsConstructor(access = AccessLevel.PROTECTED) // For JPA/Hibernate requirements
27+
@AllArgsConstructor // For easy construction in tests
28+
@Builder // For convenience in creating instances
29+
public class Book {
30+
31+
@Id // Primary key of the table
32+
@GeneratedValue(strategy = GenerationType.UUID)
33+
@Column(name = "id", nullable = false) // maps the field to a database column named 'id' + 'nullable =false' database column cannot be NULL.
34+
private UUID id;
35+
36+
@Column(name = "title", nullable = false)
37+
private String title;
38+
39+
@Column(name = "synopsis", nullable = false, columnDefinition = "TEXT")
40+
private String synopsis;
41+
42+
@Column(name = "author", nullable = false)
43+
private String author;
44+
45+
// Soft delete - makes DELETE operations idempotent (safe to repeat)
46+
// Soft delete - using @Builder.Default to ensure the builder
47+
// respects this initialization if the field is not set explicitly.
48+
@Column(name = "deleted", nullable = false)
49+
@Builder.Default
50+
private boolean deleted = false;
51+
52+
// `createdAt` is null upon object creation.
53+
// It will be set by the `onCreate()` method right before persistence.
54+
@Column(name = "created_at", nullable = false, updatable = false)
55+
private Instant createdAt;
56+
57+
@Column(name = "modified_at")
58+
private java.time.Instant modifiedAt;
59+
60+
// --- JPA lifecycle callbacks ---
61+
@PrePersist
62+
protected void onCreate() {
63+
this.createdAt = java.time.Instant.now();
64+
}
65+
66+
// --- Business Logic Helper ---
67+
// HMCTS mandates business logic in services, but a setter hook is acceptable.
68+
// Lifecycle callback - special method runs automatically before Hibernate updates a record in the database.
69+
@PreUpdate
70+
protected void onUpdate() {
71+
72+
this.modifiedAt = java.time.Instant.now();
73+
}
74+
75+
76+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.codesungrape.hmcts.BookAPI.repository;
2+
3+
import java.util.List;
4+
import java.util.Optional;
5+
import java.util.UUID;
6+
7+
import com.codesungrape.hmcts.BookAPI.entity.Book;
8+
import org.springframework.data.jpa.repository.JpaRepository;
9+
import org.springframework.stereotype.Repository;
10+
11+
/**
12+
* Repository interface for Book Entity.
13+
* Spring Data JPA automatically provides CRUD operations based on the Entity and ID type.
14+
*/
15+
@Repository
16+
public interface BookRepository extends JpaRepository<Book, UUID> {
17+
18+
// Custom query to find books that have NOT been soft-deleted
19+
List<Book> findAllByDeletedFalse();
20+
21+
// Custom query to find a specific, non-deleted book by ID.
22+
Optional<Book> findByIdAndDeleteFalse(UUID id);
23+
24+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.codesungrape.hmcts.BookAPI.service;
2+
3+
import com.codesungrape.hmcts.BookAPI.dto.BookRequest;
4+
import com.codesungrape.hmcts.BookAPI.repository.BookRepository;
5+
import org.springframework.stereotype.Service; // Marks a class as a Service Layer component.
6+
import lombok.RequiredArgsConstructor;
7+
8+
import com.codesungrape.hmcts.BookAPI.entity.Book;
9+
10+
/**
11+
* Service layer responsible for all business logic related to the Book resource.
12+
*/
13+
@Service
14+
@RequiredArgsConstructor // Lombok creates constructor for dependency injection
15+
public class BookService {
16+
17+
// Create a field to store the repo
18+
private final BookRepository bookRepository;
19+
20+
// 1. CREATE Operation (POST /books)
21+
public Book createBook(BookRequest request) {
22+
// Validation check for business rules (e.g., uniqueness, if required)
23+
if (request == null) {
24+
throw new NullPointerException("BookRequest cannot be null");
25+
}
26+
27+
28+
// REVISIT: Leaving this here for now as i haven't implemented the Controller Layer yet
29+
// The service layer is duplicating validation that already exists in the
30+
// BookRequest DTO with @notblank annotations. Since the DTO has validation
31+
// constraints, this manual check is redundant when Spring's validation
32+
// framework is properly configured in the controller layer.
33+
// Consider removing this duplication or adding a comment explaining
34+
// why service-level validation is necessary in addition to DTO validation.
35+
if (request.getTitle() == null || request.getTitle().isBlank()) {
36+
throw new IllegalArgumentException("Book title cannot be null or blank");
37+
}
38+
39+
// Map DTO to Entity
40+
Book newBook = Book.builder()
41+
.title(request.getTitle())
42+
.author(request.getAuthor())
43+
.synopsis(request.getSynopsis())
44+
// ID and created_at are auto-generated by JPA/DB
45+
.build();
46+
47+
Book savedBook = bookRepository.save(newBook);
48+
49+
// Defensive check (even though it "shouldn't" happen aka follows JPA contract)
50+
if (savedBook == null) {
51+
throw new IllegalStateException("Failed to save book - repository returned null");
52+
}
53+
54+
return savedBook;
55+
}
56+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
spring:
2+
application:
3+
name: BookAPI

0 commit comments

Comments
 (0)