View Javadoc
1   package com.guinetik.hexafun.examples.tasks;
2   
3   import static com.guinetik.hexafun.examples.tasks.TaskInputs.*;
4   import static com.guinetik.hexafun.examples.tasks.TaskUseCases.*;
5   
6   import com.guinetik.hexafun.HexaApp;
7   import com.guinetik.hexafun.HexaFun;
8   import com.guinetik.hexafun.fun.Result;
9   import java.util.List;
10  
11  /**
12   * Task application demonstrating the port registry feature.
13   *
14   * <p>Key concepts demonstrated:
15   * <ul>
16   *   <li>Port registration: {@code .withPort(TaskRepository.class, impl)}</li>
17   *   <li>Port retrieval: {@code app.port(TaskRepository.class)}</li>
18   *   <li>Use cases using ports for persistence</li>
19   *   <li>Separation of domain logic from infrastructure</li>
20   * </ul>
21   *
22   * <p>The repository is injected as a port, allowing easy swapping
23   * between in-memory (for testing) and persistent implementations.
24   */
25  public class TaskApp {
26  
27      private final HexaApp app;
28  
29      /**
30       * Create a TaskApp with the given repository implementation.
31       */
32      public TaskApp(TaskRepository repository) {
33          this.app = HexaFun.dsl()
34              // Register the repository as a port
35              .withPort(TaskRepository.class, repository)
36              // CREATE: validate title, then save
37              .useCase(CREATE)
38              .validate(TaskValidators::validateCreateTitle)
39              .validate(TaskValidators::validateCreateTitleLength)
40              .handle(input -> {
41                  Task task = Task.create(input.title(), input.description());
42                  return Result.ok(repository.save(task));
43              })
44              // START: validate ID, find task, move to DOING
45              .useCase(START)
46              .validate(TaskValidators::validateStartTaskId)
47              .handle(input ->
48                  repository
49                      .findById(input.taskId())
50                      .map(task -> Result.ok(repository.save(task.start())))
51                      .orElse(Result.fail("Task not found: " + input.taskId()))
52              )
53              // COMPLETE: validate ID, find task, move to DONE
54              .useCase(COMPLETE)
55              .validate(TaskValidators::validateCompleteTaskId)
56              .handle(input ->
57                  repository
58                      .findById(input.taskId())
59                      .map(task -> Result.ok(repository.save(task.complete())))
60                      .orElse(Result.fail("Task not found: " + input.taskId()))
61              )
62              // UPDATE: validate ID and title, find and update
63              .useCase(UPDATE)
64              .validate(TaskValidators::validateUpdateTaskId)
65              .validate(TaskValidators::validateUpdateTitle)
66              .handle(input ->
67                  repository
68                      .findById(input.taskId())
69                      .map(task -> {
70                          Task updated = task
71                              .withTitle(input.title())
72                              .withDescription(input.description());
73                          return Result.ok(repository.save(updated));
74                      })
75                      .orElse(Result.fail("Task not found: " + input.taskId()))
76              )
77              // DELETE: validate ID, delete from repo
78              .useCase(DELETE)
79              .validate(TaskValidators::validateDeleteTaskId)
80              .handle(input -> Result.ok(repository.delete(input.taskId())))
81              // FIND: validate ID, look up in repo
82              .useCase(FIND)
83              .validate(TaskValidators::validateFindTaskId)
84              .handle(input ->
85                  repository
86                      .findById(input.taskId())
87                      .map(Result::ok)
88                      .orElse(Result.fail("Task not found: " + input.taskId()))
89              )
90              // LIST: no validation needed, just return all
91              .useCase(LIST)
92              .handle(input -> repository.findAll())
93              .build();
94      }
95  
96      /**
97       * Create a TaskApp with an in-memory repository.
98       */
99      public static TaskApp withInMemoryRepo() {
100         return new TaskApp(new InMemoryTaskRepository());
101     }
102 
103     // ----- Public API -----
104 
105     public Result<Task> createTask(String title, String description) {
106         return app.invoke(CREATE, new CreateTask(title, description));
107     }
108 
109     public Result<Task> startTask(String taskId) {
110         return app.invoke(START, new StartTask(taskId));
111     }
112 
113     public Result<Task> completeTask(String taskId) {
114         return app.invoke(COMPLETE, new CompleteTask(taskId));
115     }
116 
117     public Result<Task> updateTask(
118         String taskId,
119         String title,
120         String description
121     ) {
122         return app.invoke(UPDATE, new UpdateTask(taskId, title, description));
123     }
124 
125     public Result<Boolean> deleteTask(String taskId) {
126         return app.invoke(DELETE, new DeleteTask(taskId));
127     }
128 
129     public Result<Task> findTask(String taskId) {
130         return app.invoke(FIND, new FindTask(taskId));
131     }
132 
133     public List<Task> listTasks() {
134         return app.invoke(LIST, null);
135     }
136 
137     /**
138      * Get the underlying HexaApp (for testing/introspection).
139      */
140     public HexaApp getApp() {
141         return app;
142     }
143 
144     /**
145      * Get the repository port directly.
146      */
147     public TaskRepository getRepository() {
148         return app.port(TaskRepository.class);
149     }
150 
151     // ----- Demo -----
152 
153     public static void main(String[] args) {
154         System.out.println("=== TaskApp Demo ===\n");
155 
156         // Create app with in-memory repository
157         TaskApp taskApp = TaskApp.withInMemoryRepo();
158 
159         // Show that the port is registered
160         System.out.println(
161             "Registered ports: " + taskApp.getApp().registeredPorts()
162         );
163         System.out.println(
164             "Has TaskRepository: " +
165                 taskApp.getApp().hasPort(TaskRepository.class)
166         );
167         System.out.println();
168 
169         // Create some tasks
170         System.out.println("Creating tasks...");
171         Result<Task> task1 = taskApp.createTask(
172             "Learn HexaFun",
173             "Study the fluent DSL"
174         );
175         Result<Task> task2 = taskApp.createTask(
176             "Write tests",
177             "Add comprehensive tests"
178         );
179         Result<Task> task3 = taskApp.createTask(
180             "Deploy app",
181             "Push to production"
182         );
183 
184         System.out.println("Created: " + task1.get().title());
185         System.out.println("Created: " + task2.get().title());
186         System.out.println("Created: " + task3.get().title());
187         System.out.println();
188 
189         // List all tasks
190         System.out.println("All tasks:");
191         taskApp
192             .listTasks()
193             .forEach(t ->
194                 System.out.println(
195                     "  - [" + (t.completed() ? "X" : " ") + "] " + t.title()
196                 )
197             );
198         System.out.println();
199 
200         // Complete a task
201         String taskId = task1.get().id();
202         System.out.println("Completing task: " + task1.get().title());
203         taskApp.completeTask(taskId);
204         System.out.println();
205 
206         // List again to see the change
207         System.out.println("Tasks after completion:");
208         taskApp
209             .listTasks()
210             .forEach(t ->
211                 System.out.println(
212                     "  - [" + (t.completed() ? "X" : " ") + "] " + t.title()
213                 )
214             );
215         System.out.println();
216 
217         // Try validation - empty title
218         System.out.println("Trying to create task with empty title...");
219         Result<Task> invalid = taskApp.createTask("", "No title");
220         System.out.println(
221             "Result: " +
222                 (invalid.isSuccess() ? "OK" : "FAILED: " + invalid.error())
223         );
224         System.out.println();
225 
226         // Try validation - title too long
227         System.out.println("Trying to create task with very long title...");
228         String longTitle = "A".repeat(101);
229         Result<Task> tooLong = taskApp.createTask(longTitle, "Too long");
230         System.out.println(
231             "Result: " +
232                 (tooLong.isSuccess() ? "OK" : "FAILED: " + tooLong.error())
233         );
234         System.out.println();
235 
236         // Find a task
237         System.out.println("Finding task by ID: " + taskId);
238         Result<Task> found = taskApp.findTask(taskId);
239         System.out.println(
240             "Found: " +
241                 found.get().title() +
242                 " (completed: " +
243                 found.get().completed() +
244                 ")"
245         );
246         System.out.println();
247 
248         // Delete a task
249         System.out.println("Deleting task: " + task3.get().title());
250         taskApp.deleteTask(task3.get().id());
251         System.out.println("Tasks remaining: " + taskApp.listTasks().size());
252     }
253 }