Last active
May 16, 2025 17:39
-
-
Save fResult/77c4341a49f8135ea8762b84b3d7fffe to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import static java.time.temporal.ChronoUnit.HOURS; | |
| import java.time.Instant; | |
| import java.util.HashMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Optional; | |
| import java.util.concurrent.atomic.AtomicLong; | |
| import java.util.function.Function; | |
| import java.util.function.Supplier; | |
| class TryTypeLevelStateTransition { | |
| private static final SpringTaskRepository repo = new SpringTaskRepository(); | |
| private static final TaskRepositoryAdapter repoAdapter = new TaskRepositoryAdapter(repo); | |
| private static final TaskManagement management = new TaskManagement(); | |
| private static final TaskUseCase useCase = new TaskUseCase(repoAdapter, management); | |
| private static final TaskController controller = new TaskController(useCase); | |
| public static void main(String[] args) { | |
| // Step 1: Create a new dinner plan task | |
| final var bodyToCreate = new TaskCreationRequest( | |
| /*title*/ "Have dinner", | |
| /*description*/ null, | |
| /*status*/ "TODO"); | |
| invokeNewTaskApi(bodyToCreate); | |
| // Step 2: Check all my current tasks | |
| invokeFindTasksAPI(); | |
| // Step 3: Add important details to our dinner plan | |
| final var bodyToPartialUpdate = new TaskUpdateRequest( | |
| /*id*/ null, | |
| /*title*/ null, | |
| /*description*/ "[NOTE!] Need to ask Trinity where we will eat for dinner", | |
| /*status*/ null, | |
| /*completedAt*/ null); | |
| invokeUpdateTaskAPI(1, bodyToPartialUpdate); | |
| // Step 4: Look up our updated dinner task | |
| invokeFindTaskByIdAPI(1); | |
| // Step 5: Move our dinner task to the "doing" state (we're planning it now) | |
| invokeAdvanceTaskAPI(1); | |
| // Error scenario: Try to advance a non-existent task | |
| invokeAdvanceTaskAPI(9999L); | |
| // Task completion flow: Complete a task and verify we can't advance it further | |
| try { | |
| // First complete the task `/api/tasks/:id/complete | |
| final var markedAsDoneTask = controller.markAsDone(111L); | |
| System.out.println("[HTTP 200] Task after mark as DONE: " + markedAsDoneTask); | |
| // Then try to advance it further (which should do nothing per domain rules) | |
| final var advancedTask = controller.advanceTask(111L); | |
| System.out.println("[HTTP 200] Task after attempting to advance beyond DONE: " + advancedTask.status()); | |
| } catch (NotFoundException e) { | |
| // Simulates @ExceptionHandler(NotFoundException.class) in @ControllerAdvice | |
| System.out.println("HTTP 404: " + e.getMessage()); | |
| } catch (IllegalStateException e) { | |
| // Simulates @ExceptionHandler(IllegalStateException.class) for domain exceptions in | |
| // @ControllerAdvice | |
| System.out.println("HTTP 422: " + e.getMessage()); | |
| } catch (Exception e) { | |
| // Simulates generic exception handler in @ControllerAdvice | |
| System.out.println("HTTP 500: Internal Server Error - " + e.getMessage()); | |
| } | |
| } | |
| private static void invokeFindTasksAPI() { | |
| try { | |
| // Invoke GET `/api/tasks` | |
| final var tasks = controller.all(); | |
| System.out.println("[HTTP 200] Successfully retrieved all tasks: " + tasks); | |
| } catch (Exception e) { | |
| // Simulates generic exception handler in @ControllerAdvice | |
| System.out.println("[HTTP 500]: Internal Server Error - " + e.getMessage()); | |
| } | |
| } | |
| private static void invokeFindTaskByIdAPI(long id) { | |
| try { | |
| // Invoke GET `/api/tasks/:id` | |
| final var task = controller.byId(id); | |
| System.out.println("[HTTP 200] Successfully retrieved task: " + task); | |
| } catch (NotFoundException e) { | |
| // Simulates @ExceptionHandler(NotFoundException.class) in @ControllerAdvice | |
| System.out.println("[HTTP 404]: " + e.getMessage()); | |
| } catch (Exception e) { | |
| // Simulates generic exception handler in @ControllerAdvice | |
| System.out.println("[HTTP 500]: Internal Server Error - " + e.getMessage()); | |
| } | |
| } | |
| private static void invokeNewTaskApi(TaskCreationRequest body) { | |
| try { | |
| // Invoke POST `/api/tasks` | |
| final var createdTask = controller.create(body); | |
| System.out.println("[HTTP 201] Successfully created task: " + createdTask); | |
| } catch (IllegalArgumentException e) { | |
| // Simulates @ExceptionHandler(IllegalArgumentException.class) in @ControllerAdvice | |
| System.out.println("[HTTP 400]: " + e.getMessage()); | |
| } catch (Exception e) { | |
| // Simulates generic exception handler in @ControllerAdvice | |
| System.out.println("[HTTP 500]: Internal Server Error - " + e.getMessage()); | |
| } | |
| } | |
| private static void invokeUpdateTaskAPI(long id, TaskUpdateRequest body) { | |
| try { | |
| // Invoke PATCH `/api/tasks/:id` | |
| final var updatedTask = controller.update(id, body); | |
| System.out.println("[HTTP 200] Successfully updated task: " + updatedTask); | |
| } catch (NotFoundException e) { | |
| // Simulates @ExceptionHandler(NotFoundException.class) in @ControllerAdvice | |
| System.out.println("[HTTP 404]: " + e.getMessage()); | |
| } catch (Exception e) { | |
| // Simulates generic exception handler in @ControllerAdvice | |
| System.out.println("[HTTP 500]: Internal Server Error - " + e.getMessage()); | |
| } | |
| } | |
| private static void invokeAdvanceTaskAPI(long id) { | |
| try { | |
| // Invoke PATCH `/api/tasks/:id/advance | |
| final var advancedTask = controller.advanceTask(id); | |
| System.out.println("[HTTP 200] Successfully advanced task: " + advancedTask); | |
| } catch (NotFoundException e) { | |
| // Simulates @ExceptionHandler(NotFoundException.class) in @ControllerAdvice | |
| System.out.println("[HTTP 404]: " + e.getMessage()); | |
| } catch (Exception e) { | |
| // Simulates generic exception handler in @ControllerAdvice | |
| System.out.println("[HTTP 500]: Internal Server Error - " + e.getMessage()); | |
| } | |
| } | |
| } | |
| // @RestController | |
| // RequestMapping("/api/v1/tasks") | |
| class TaskController { | |
| private final TaskUseCase taskUseCase; | |
| TaskController(TaskUseCase taskUse) { | |
| this.taskUseCase = taskUse; | |
| } | |
| // @GetMapping | |
| public List<TaskResponse> all() { | |
| return taskUseCase.all(); | |
| } | |
| // @GetMapping("/{id}") | |
| public TaskResponse byId(/*@PathVariable*/ long id) { | |
| return taskUseCase.byId(id); | |
| } | |
| // @PostMapping | |
| public TaskResponse create(/*@RequestBody*/ TaskCreationRequest body) { | |
| return taskUseCase.create(body); // Should return HTTP 201 if success | |
| } | |
| // @PatchMapping("/{id}") | |
| public TaskResponse update(/*@PathVariable*/ long id, /*@RequestBody*/ TaskUpdateRequest body) { | |
| return taskUseCase.update(id, body); | |
| } | |
| // @PatchMapping("/{id}/advance") | |
| public TaskResponse advanceTask(/*@PathVariable*/ long id) { | |
| return taskUseCase.advanceTask(id); | |
| } | |
| // @PatchMapping("/{id}/revert") | |
| public TaskResponse revertTask(/*@PathVariable*/ long id) { | |
| return taskUseCase.revertTask(id); | |
| } | |
| // @PatchMapping("/{id}/complete") | |
| public TaskResponse markAsDone(/*@PathVariable*/ long id) { | |
| return taskUseCase.markAsDone(id); | |
| } | |
| // @PatchMapping("/{id}/reset") | |
| public TaskResponse resetToTodo(/*@PathVariable*/ long id) { | |
| return taskUseCase.resetToTodo(id); | |
| } | |
| // @PatchMapping("/{id}/progress") | |
| public TaskResponse progress(/*@PathVariable*/ long id) { | |
| return taskUseCase.progress(id); | |
| } | |
| } | |
| class TaskUseCase { | |
| private final TaskRepository taskRepository; | |
| private final TaskManagement taskManagement; | |
| TaskUseCase(TaskRepository taskRepository, TaskManagement taskManagement) { | |
| this.taskRepository = taskRepository; | |
| this.taskManagement = taskManagement; | |
| } | |
| public List<TaskResponse> all() { | |
| return taskRepository.findAll().stream().map(TaskResponse::fromDomain).toList(); | |
| } | |
| public TaskResponse byId(Long id) { | |
| return taskRepository.findById(id) | |
| .map(TaskResponse::fromDomain) | |
| .orElseThrow(throwNotFound(id, Task.class)); | |
| } | |
| public TaskResponse create(TaskCreationRequest body) { | |
| return Optional.of(body) | |
| .map(Task::fromRequest) | |
| .map(taskRepository::save) | |
| .map(TaskResponse::fromDomain) | |
| .orElseThrow(); | |
| } | |
| public TaskResponse update(Long id, TaskUpdateRequest body) { | |
| return taskRepository.findById(id) | |
| .map(this.toTaskUpdate(body).andThen(taskRepository::save).andThen(TaskResponse::fromDomain)) | |
| .orElseThrow(throwNotFound(id, Task.class)); | |
| } | |
| public TaskResponse advanceTask(Long id) { | |
| return executeTaskOperation(id, taskManagement::advanceTask); | |
| } | |
| public TaskResponse revertTask(Long id) { | |
| return executeTaskOperation(id, taskManagement::revertTask); | |
| } | |
| public TaskResponse markAsDone(Long id) { | |
| return executeTaskOperation(id, taskManagement::markAsDone); | |
| } | |
| public TaskResponse resetToTodo(Long id) { | |
| return executeTaskOperation(id, taskManagement::resetToTodo); | |
| } | |
| public TaskResponse progress(Long id) { | |
| return executeTaskOperation(id, taskManagement::progress); | |
| } | |
| private TaskResponse executeTaskOperation(Long id, Function<Task, Task> operation) { | |
| return taskRepository.findById(id) | |
| .map(operation.andThen(taskRepository::save).andThen(TaskResponse::fromDomain)) | |
| .orElseThrow(throwNotFound(id, Task.class)); | |
| } | |
| private Function<Task, Task> toTaskUpdate(TaskUpdateRequest body) { | |
| return existingTask -> switch (existingTask) { | |
| case Todo todo -> new Todo( | |
| toUpdatedValue(todo.id(), body.id()), | |
| toUpdatedValue(todo.title(), body.title()), | |
| toUpdatedValue(todo.description(), body.description())); | |
| case Doing doing -> new Doing( | |
| toUpdatedValue(doing.id(), body.id()), | |
| toUpdatedValue(doing.title(), body.title()), | |
| toUpdatedValue(doing.description(), body.description())); | |
| case Done done -> new Done( | |
| toUpdatedValue(done.id(), body.id()), | |
| toUpdatedValue(done.title(), body.title()), | |
| toUpdatedValue(done.description(), body.description()), | |
| toUpdatedValue(done.completedAt(), body.completedAt())); | |
| }; | |
| } | |
| private <T> T toUpdatedValue(T existing, T replacer) { | |
| return Optional.ofNullable(replacer).orElse(existing); | |
| } | |
| private Supplier<NotFoundException> throwNotFound(Long resourceId, Class<?> resourceName) { | |
| return NotFoundException.toThrow( | |
| String.format("Not found %s for ID %d", resourceName.getSimpleName(), resourceId)); | |
| } | |
| } | |
| /* ================================== | |
| * ========== Domain Layer ========== | |
| * ================================== */ | |
| // ========== Domain Models ========== | |
| sealed interface Task permits Todo, Doing, Done { | |
| Long id(); | |
| String title(); | |
| String description(); | |
| String displayStatus(); | |
| static Task fromRequest(TaskCreationRequest body) { | |
| return switch (TaskState.of(body.status())) { | |
| case TODO -> new Todo(null, body.title(), body.description()); | |
| case DOING -> new Doing(null, body.title(), body.description()); | |
| case DONE -> new Done(null, body.title(), body.description(), Instant.now()); | |
| }; | |
| } | |
| static Task fromRequest(TaskUpdateRequest body) { | |
| return switch (TaskState.of(body.status())) { | |
| case TODO -> new Todo(body.id(), body.title(), body.description()); | |
| case DOING -> new Doing(body.id(), body.title(), body.description()); | |
| case DONE -> new Done(body.id(), body.title(), body.description(), Instant.now()); | |
| }; | |
| } | |
| } | |
| record Todo(Long id, String title, String description) implements Task { | |
| public Todo(String title, String description) { | |
| this(null, title, description); | |
| } | |
| @Override | |
| public String displayStatus() { | |
| return "TODO: " + title; | |
| } | |
| } | |
| record Doing(Long id, String title, String description) implements Task { | |
| public Doing(String title, String description) { | |
| this(null, title, description); | |
| } | |
| @Override | |
| public String displayStatus() { | |
| return "IN PROGRESS: " + title; | |
| } | |
| } | |
| record Done(Long id, String title, String description, Instant completedAt) implements Task { | |
| public Done(String title, String description) { | |
| this(null, title, description, Instant.now()); | |
| } | |
| public Done(Long id, String title, String description) { | |
| this(id, title, description, Instant.now()); | |
| } | |
| public Done(String title, String description, Instant completedAt) { | |
| this(null, title, description, completedAt); | |
| } | |
| @Override | |
| public String displayStatus() { | |
| return "DONE: " + title; | |
| } | |
| } | |
| enum TaskState { | |
| TODO, | |
| DOING, | |
| DONE; | |
| public static TaskState of(String state) throws IllegalStateException { | |
| final var states = Map.of("TODO", TODO, "DOING", DOING, "DONE", DONE); | |
| if (!states.containsKey(state)) throw new IllegalStateException("State '" + state + "' is not in TaskState"); | |
| return states.get(state); | |
| } | |
| } | |
| // ========== Domain Service ========== | |
| class TaskManagement { | |
| public Task advanceTask(Task task) { | |
| return switch (task) { | |
| case Todo(Long id, String title, String description) -> new Doing(id, title, description); | |
| case Doing(Long id, String title, String description) -> new Done(id, title, description); | |
| case Done ignored -> task; | |
| }; | |
| } | |
| public Task revertTask(Task task) { | |
| return switch (task) { | |
| case Done(Long id, String title, String description, Instant ignored) -> new Doing(id, title, description); | |
| case Doing(Long id, String title, String description) -> new Todo(id, title, description); | |
| case Todo ignored -> task; | |
| }; | |
| } | |
| public Task markAsDone(Task task) { | |
| if (task instanceof Done) return task; | |
| return new Done(task.id(), task.title(), task.description()); | |
| } | |
| public Task resetToTodo(Task task) { | |
| if (task instanceof Todo) return task; | |
| return new Todo(task.id(), task.title(), task.description()); | |
| } | |
| public Task progress(Task task) { | |
| if (task instanceof Doing) return task; | |
| return new Doing(task.id(), task.title(), task.description()); | |
| } | |
| } | |
| /* ======================================= | |
| * ========== Persistence Layer ========== | |
| * ======================================= */ | |
| interface TaskRepository { | |
| List<Task> findAll(); | |
| Optional<Task> findById(Long id); | |
| Task save(Task entity); | |
| } | |
| // @Repository | |
| class TaskRepositoryAdapter implements TaskRepository { | |
| private final SpringTaskRepository springRepository; | |
| public TaskRepositoryAdapter(SpringTaskRepository springRepository) { | |
| this.springRepository = springRepository; | |
| } | |
| @Override | |
| public List<Task> findAll() { | |
| return springRepository.findAll().stream().map(this::mapToDomain).toList(); | |
| } | |
| @Override | |
| public Optional<Task> findById(Long id) { | |
| return springRepository.findById(id).map(this::mapToDomain); | |
| } | |
| @Override | |
| public Task save(Task domain) { | |
| return Optional.ofNullable(domain) | |
| .map(this::mapToEntity) | |
| .map(springRepository::save) | |
| .map(this::mapToDomain) | |
| .orElseThrow(); | |
| } | |
| private Task mapToDomain(TaskEntity entity) throws IllegalStateException { | |
| return switch (TaskState.valueOf(entity.state())) { | |
| case TODO -> new Todo(entity.id(), entity.title(), entity.description()); | |
| case DOING -> new Doing(entity.id(), entity.title(), entity.description()); | |
| case DONE -> new Done(entity.id(), entity.title(), entity.description()); | |
| }; | |
| } | |
| private TaskEntity mapToEntity(Task domain) { | |
| return switch (domain) { | |
| case Todo(Long id, String title, String description) -> | |
| new TaskEntity(id, title, description, TaskState.TODO.name(), null); | |
| case Doing(Long id, String title, String description) -> | |
| new TaskEntity(id, title, description, TaskState.DOING.name(), null); | |
| case Done(Long id, String title, String description, Instant completedAt) | |
| when completedAt == null -> | |
| new TaskEntity(id, title, description, TaskState.DONE.name(), Instant.now()); | |
| case Done(Long id, String title, String description, Instant completedAt) -> | |
| new TaskEntity(id, title, description, TaskState.DONE.name(), completedAt); | |
| }; | |
| } | |
| } | |
| // interface SpringTaskRepository extends ListCrudRepository<Long, TaskEntity> {} | |
| class SpringTaskRepository { | |
| private static final AtomicLong ID_GENERATOR = new AtomicLong(0); | |
| private static final Map<Long, TaskEntity> tasks = new HashMap<>() {{ | |
| put(111L, new TaskEntity(111L, "Have Lunch", null, TaskState.TODO.name(), null)); | |
| put(555L, new TaskEntity(555L, "Read/Reply Emails", null, TaskState.DOING.name(), null)); | |
| put(999L, new TaskEntity(999L, "Attend Monthly Meeting", null, TaskState.DONE.name(), Instant.now().minus(2, HOURS))); | |
| }}; | |
| public List<TaskEntity> findAll() { | |
| return tasks.values().stream().toList(); | |
| } | |
| public Optional<TaskEntity> findById(Long id) { | |
| return Optional.ofNullable(tasks.get(id)); | |
| } | |
| public TaskEntity save(TaskEntity entity) { | |
| Optional.ofNullable(entity.id()) | |
| .ifPresentOrElse( | |
| id -> tasks.put(id, entity), | |
| () -> { | |
| final var nextID = ID_GENERATOR.addAndGet(1); | |
| final var completedAt = entity.state().equals("DONE") ? Instant.now() : null; | |
| final var entityToCreate = new TaskEntity( | |
| nextID, | |
| entity.title(), | |
| entity.description(), | |
| entity.state(), | |
| completedAt); | |
| tasks.put(nextID, entityToCreate); | |
| }); | |
| return entity; | |
| } | |
| } | |
| record TaskEntity(Long id, String title, String description, String state, Instant completedAt) { | |
| public static TaskEntity fromTaskDomain(Task domain) { | |
| return switch (domain) { | |
| case Todo(Long id, String title, String description) -> | |
| new TaskEntity(id, title, description, "TODO", null); | |
| case Doing(Long id, String title, String description) -> | |
| new TaskEntity(id, title, description, "DOING", null); | |
| case Done(Long id, String title, String description, Instant completedAt) | |
| when completedAt == null -> | |
| new TaskEntity(id, title, description, "DONE", Instant.now()); | |
| case Done(Long id, String title, String description, Instant completedAt) -> | |
| new TaskEntity(id, title, description, "DONE", completedAt); | |
| }; | |
| } | |
| } | |
| /* ================================================== | |
| * ========== DTOs (Data Transfer Objects) ========== | |
| * ================================================== */ | |
| record TaskCreationRequest(String title, String description, String status) {} | |
| record TaskUpdateRequest( | |
| Long id, String title, String description, String status, Instant completedAt) {} | |
| record TaskResponse(Long id, String title, String description, String status, Instant completedAt) { | |
| public static TaskResponse fromDomain(Task domain) { | |
| return switch (domain) { | |
| case Todo(Long id, String title, String description) -> | |
| new TaskResponse(id, title, description, TaskState.TODO.name(), null); | |
| case Doing(Long id, String title, String description) -> | |
| new TaskResponse(id, title, description, TaskState.DOING.name(), null); | |
| case Done(Long id, String title, String description, Instant completedAt) -> | |
| new TaskResponse(id, title, description, TaskState.DONE.name(), completedAt); | |
| }; | |
| } | |
| } | |
| /* ================================ | |
| * ========== Exceptions ========== | |
| * ================================ */ | |
| class NotFoundException extends RuntimeException { | |
| public NotFoundException() { | |
| super("Not found!"); | |
| } | |
| public NotFoundException(String message) { | |
| super(message); | |
| } | |
| public static Supplier<NotFoundException> toThrow(String message) { | |
| return () -> new NotFoundException(message); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment