refactor(project): replace mechanical constructors with Lombok

This commit is contained in:
sechmachine
2026-08-15 11:37:02 +07:00
parent b9b150ff8c
commit e5639c1461
9 changed files with 207 additions and 60 deletions
@@ -8,6 +8,7 @@ import com.lab.labtimesheet.feature.project.service.ProjectQueryService;
import com.lab.labtimesheet.feature.project.service.ProjectService;
import jakarta.validation.Valid;
import java.security.Principal;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
@@ -27,22 +28,12 @@ import org.springframework.web.bind.annotation.RequestMapping;
*/
@Controller
@RequestMapping("/projects")
@RequiredArgsConstructor
public class ProjectController {
private final ProjectQueryService pages;
private final ProjectService projects;
/**
* Creates the MVC adapter for Project queries and mutations.
*
* @param pages authorized Project read operations
* @param projects transactional Project mutation operations
*/
public ProjectController(ProjectQueryService pages, ProjectService projects) {
this.pages = pages;
this.projects = projects;
}
/**
* Lists only Projects visible to the authenticated actor and exposes Project creation only
* to Mentors.
@@ -17,10 +17,6 @@ import org.springframework.web.servlet.ModelAndView;
@ControllerAdvice(assignableTypes = ProjectController.class)
public class ProjectControllerAdvice {
/** Creates the stateless Project exception-to-view adapter. */
public ProjectControllerAdvice() {
}
/**
* Hides whether a requested Project or nested resource exists.
*
@@ -22,6 +22,8 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
/**
* JPA aggregate root for Project lifecycle, membership intervals, and leadership intervals.
@@ -32,6 +34,7 @@ import java.util.Set;
*/
@Entity
@Table(name = "projects")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ProjectEntity {
@Id
@@ -75,10 +78,6 @@ public class ProjectEntity {
@Version
private long version;
/** Constructor reserved for JPA materialization. */
protected ProjectEntity() {
}
private ProjectEntity(
long mentorUserId,
String name,
@@ -11,6 +11,8 @@ import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.Instant;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
/**
* JPA leadership interval attached to an active same-Project membership.
@@ -20,6 +22,7 @@ import java.time.Instant;
*/
@Entity
@Table(name = "project_leadership_terms")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ProjectLeadershipTermEntity {
@Id
@@ -46,10 +49,6 @@ public class ProjectLeadershipTermEntity {
@Column(name = "ended_by_mentor_user_id")
private Long endedByMentorUserId;
/** Constructor reserved for JPA materialization. */
protected ProjectLeadershipTermEntity() {
}
ProjectLeadershipTermEntity(
ProjectEntity project,
ProjectMembershipEntity membership,
@@ -11,6 +11,8 @@ import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import java.time.Instant;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
/**
* JPA membership interval linking one Intern to one Project.
@@ -20,6 +22,7 @@ import java.time.Instant;
*/
@Entity
@Table(name = "project_memberships")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ProjectMembershipEntity {
@Id
@@ -48,10 +51,6 @@ public class ProjectMembershipEntity {
@Version
private long version;
/** Constructor reserved for JPA materialization. */
protected ProjectMembershipEntity() {
}
ProjectMembershipEntity(ProjectEntity project, long internUserId, Instant joinedAt, long addedByUserId) {
this.project = project;
this.internUserId = internUserId;
@@ -15,6 +15,7 @@ import com.lab.labtimesheet.feature.project.model.dto.ProjectTaskMemberView;
import com.lab.labtimesheet.feature.project.model.entity.ProjectEntity;
import com.lab.labtimesheet.feature.project.repository.ProjectRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -26,22 +27,12 @@ import org.springframework.transaction.annotation.Transactional;
* members but expose no current Leader or active-member context.
*/
@Service
@RequiredArgsConstructor
public class ProjectQueryService {
private final ProjectRepository projects;
private final AccountService accounts;
/**
* Creates the Project read service.
*
* @param projects Project aggregate repository
* @param accounts public Account identity and internship-eligibility boundary
*/
public ProjectQueryService(ProjectRepository projects, AccountService accounts) {
this.projects = projects;
this.accounts = accounts;
}
/**
* Resolves an active authenticated account to its stable user identifier.
*
@@ -11,6 +11,7 @@ import com.lab.labtimesheet.feature.task.service.TaskQueryService;
import java.time.Clock;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -22,6 +23,7 @@ import org.springframework.transaction.annotation.Transactional;
* imports their repositories or entities.
*/
@Service
@RequiredArgsConstructor
public class ProjectService {
private final ProjectRepository projects;
@@ -30,28 +32,6 @@ public class ProjectService {
private final TaskQueryService taskQueries;
private final Clock clock;
/**
* Creates the Project mutation service.
*
* @param projects Project aggregate repository
* @param accounts public Account identity and eligibility boundary
* @param queries DTO-only Project query boundary reused for locked Task context
* @param taskQueries public Task activation-guard boundary
* @param clock server clock supplying persisted mutation instants
*/
public ProjectService(
ProjectRepository projects,
AccountService accounts,
ProjectQueryService queries,
TaskQueryService taskQueries,
Clock clock) {
this.projects = projects;
this.accounts = accounts;
this.queries = queries;
this.taskQueries = taskQueries;
this.clock = clock;
}
/**
* Atomically creates a planned Mentor-owned Project, eligible initial membership, and first
* leadership term. {@code saveAndFlush} exposes database invariant violations before commit.
@@ -0,0 +1,77 @@
package com.lab.labtimesheet.feature.project.repository;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.junit.jupiter.api.Test;
class ProjectLombokBoilerplateTest {
private static final Path PROJECT_SOURCE =
Path.of("src/main/java/com/lab/labtimesheet/feature/project");
@Test
void eligibleConstructorsUseTargetedLombokWithoutChangingDomainApis() throws IOException {
assertRequiredArgsConstructor("controller/ProjectController.java", "ProjectController");
assertRequiredArgsConstructor("service/ProjectService.java", "ProjectService");
assertRequiredArgsConstructor("service/ProjectQueryService.java", "ProjectQueryService");
String advice = source("exception/ProjectControllerAdvice.java");
assertThat(advice).doesNotContain("public ProjectControllerAdvice()");
assertProtectedJpaConstructor("model/entity/ProjectEntity.java", "ProjectEntity");
assertProtectedJpaConstructor(
"model/entity/ProjectLeadershipTermEntity.java", "ProjectLeadershipTermEntity");
assertProtectedJpaConstructor(
"model/entity/ProjectMembershipEntity.java", "ProjectMembershipEntity");
assertThat(source("model/entity/ProjectEntity.java"))
.contains("public static ProjectEntity plan(", "public ProjectMembershipEntity addMember(")
.doesNotContain("@Getter", "@Setter", "@Data", "@EqualsAndHashCode", "@ToString");
assertThat(source("model/entity/ProjectLeadershipTermEntity.java"))
.contains("public long internUserId()", "Instant end(Instant at, long mentorUserId)")
.doesNotContain("@Getter", "@Setter", "@Data", "@EqualsAndHashCode", "@ToString");
assertThat(source("model/entity/ProjectMembershipEntity.java"))
.contains("public long internUserId()", "public boolean isCurrent()")
.doesNotContain("@Getter", "@Setter", "@Data", "@EqualsAndHashCode", "@ToString");
for (String recordSource : List.of(
"model/ProjectInternEligibility.java",
"model/ProjectLeaderChange.java",
"model/dto/ProjectActorView.java",
"model/dto/ProjectCreateCommand.java",
"model/dto/ProjectCreateForm.java",
"model/dto/ProjectDashboardSummary.java",
"model/dto/ProjectDetail.java",
"model/dto/ProjectLeadershipTermView.java",
"model/dto/ProjectMemberForm.java",
"model/dto/ProjectMemberView.java",
"model/dto/ProjectSummary.java",
"model/dto/ProjectTaskContext.java",
"model/dto/ProjectTaskMemberView.java")) {
assertThat(source(recordSource)).contains(" record ");
}
}
private static void assertRequiredArgsConstructor(String relativePath, String typeName) throws IOException {
assertThat(source(relativePath))
.contains("import lombok.RequiredArgsConstructor;", "@RequiredArgsConstructor")
.doesNotContain("public " + typeName + "(");
}
private static void assertProtectedJpaConstructor(String relativePath, String typeName) throws IOException {
assertThat(source(relativePath))
.contains(
"import lombok.AccessLevel;",
"import lombok.NoArgsConstructor;",
"@NoArgsConstructor(access = AccessLevel.PROTECTED)")
.doesNotContain("protected " + typeName + "()");
}
private static String source(String relativePath) throws IOException {
return Files.readString(PROJECT_SOURCE.resolve(relativePath));
}
}