View Javadoc
1   package com.guinetik.hexafun;
2   
3   import com.guinetik.hexafun.hexa.AdapterKey;
4   import com.guinetik.hexafun.hexa.UseCase;
5   import com.guinetik.hexafun.hexa.UseCaseKey;
6   import com.guinetik.hexafun.testing.HexaTest;
7   import com.guinetik.hexafun.testing.UseCaseTest;
8   import java.util.HashMap;
9   import java.util.Map;
10  import java.util.Set;
11  import java.util.function.Function;
12  
13  /**
14   * Core container for a Hexagonal Architecture application.
15   * Manages use cases, ports, and adapters.
16   */
17  public abstract class HexaApp {
18  
19      protected final Map<String, UseCase<?, ?>> useCases = new HashMap<>();
20      protected final Map<Class<?>, Object> ports = new HashMap<>();
21      protected final Map<String, Function<?, ?>> adapters = new HashMap<>();
22  
23      /**
24       * Create a new empty HexaApp.
25       * @return A new empty HexaApp
26       */
27      public static HexaApp create() {
28          return new HexaAppImpl();
29      }
30  
31      /**
32       * Add a use case to this HexaApp (used internally by builder).
33       */
34      public <I, O> HexaApp withUseCase(String name, UseCase<I, O> useCase) {
35          useCases.put(name, useCase);
36          return this;
37      }
38  
39      /**
40       * Add a use case using a type-safe key.
41       *
42       * <p>Example:
43       * <pre class="language-java">{@code
44       * app.withUseCase(CREATE_TASK, new CreateTaskHandler(app));
45       * }</pre>
46       *
47       * @param key The type-safe use case key
48       * @param useCase The use case implementation
49       * @param <I> Input type
50       * @param <O> Output type
51       * @return This HexaApp for chaining
52       */
53      public <I, O> HexaApp withUseCase(UseCaseKey<I, O> key, UseCase<I, O> useCase) {
54          useCases.put(key.name(), useCase);
55          return this;
56      }
57  
58      /**
59       * Register a port (output adapter) by its type.
60       * Provides type-safe dependency injection for output ports.
61       *
62       * <p>Example:
63       * <pre class="language-java">{@code
64       * app.port(TaskRepository.class, new InMemoryTaskRepository());
65       * }</pre>
66       *
67       * @param type The interface/class type to register
68       * @param impl The implementation instance
69       * @param <T> The port type
70       * @return This HexaApp for chaining
71       */
72      public <T> HexaApp port(Class<T> type, T impl) {
73          ports.put(type, impl);
74          return this;
75      }
76  
77      /**
78       * Retrieve a port by its type.
79       *
80       * <p>Example:
81       * <pre class="language-java">{@code
82       * TaskRepository repo = app.port(TaskRepository.class);
83       * }</pre>
84       *
85       * @param type The interface/class type to retrieve
86       * @param <T> The port type
87       * @return The registered implementation
88       * @throws IllegalArgumentException if no port is registered for the given type
89       */
90      @SuppressWarnings("unchecked")
91      public <T> T port(Class<T> type) {
92          T impl = (T) ports.get(type);
93          if (impl == null) {
94              throw new IllegalArgumentException(
95                  "No port registered for type: " + type.getName()
96              );
97          }
98          return impl;
99      }
100 
101     /**
102      * Check if a port is registered for the given type.
103      *
104      * @param type The interface/class type to check
105      * @return true if a port is registered, false otherwise
106      */
107     public boolean hasPort(Class<?> type) {
108         return ports.containsKey(type);
109     }
110 
111     /**
112      * Get all registered port types.
113      *
114      * @return A set of registered port types
115      */
116     public Set<Class<?>> registeredPorts() {
117         return ports.keySet();
118     }
119 
120     // ===== Adapters =====
121 
122     /**
123      * Register an adapter with a type-safe key.
124      * Adapters transform data from one type to another.
125      *
126      * <p>Example:
127      * <pre class="language-java">{@code
128      * app.withAdapter(TO_INVENTORY, req -> new InventoryCheck(req.itemId()));
129      * }</pre>
130      *
131      * @param key The type-safe adapter key
132      * @param adapter The adapter function
133      * @param <From> The source type
134      * @param <To> The target type
135      * @return This HexaApp for chaining
136      */
137     public <From, To> HexaApp withAdapter(AdapterKey<From, To> key, Function<From, To> adapter) {
138         adapters.put(key.name(), adapter);
139         return this;
140     }
141 
142     /**
143      * Register an adapter by name (used internally by builder).
144      *
145      * @param name The adapter name
146      * @param adapter The adapter function
147      * @param <From> The source type
148      * @param <To> The target type
149      * @return This HexaApp for chaining
150      */
151     public <From, To> HexaApp withAdapter(String name, Function<From, To> adapter) {
152         adapters.put(name, adapter);
153         return this;
154     }
155 
156     /**
157      * Adapt a value using a type-safe adapter key.
158      * Transforms the input from one type to another.
159      *
160      * <p>Example:
161      * <pre class="language-java">{@code
162      * InventoryCheck check = app.adapt(TO_INVENTORY, orderRequest);
163      * }</pre>
164      *
165      * @param key The type-safe adapter key
166      * @param input The value to adapt
167      * @param <From> The source type
168      * @param <To> The target type
169      * @return The adapted value
170      * @throws IllegalArgumentException if no adapter is registered with the given key
171      */
172     @SuppressWarnings("unchecked")
173     public <From, To> To adapt(AdapterKey<From, To> key, From input) {
174         Function<From, To> adapter = (Function<From, To>) adapters.get(key.name());
175         if (adapter == null) {
176             throw new IllegalArgumentException(
177                 "No adapter registered with name: " + key.name()
178             );
179         }
180         return adapter.apply(input);
181     }
182 
183     /**
184      * Check if an adapter is registered for the given key.
185      *
186      * @param key The adapter key to check
187      * @return true if an adapter is registered, false otherwise
188      */
189     public boolean hasAdapter(AdapterKey<?, ?> key) {
190         return adapters.containsKey(key.name());
191     }
192 
193     /**
194      * Get all registered adapter names.
195      *
196      * @return A set of registered adapter names
197      */
198     public Set<String> registeredAdapters() {
199         return adapters.keySet();
200     }
201 
202     /**
203      * Invoke a use case using a type-safe key.
204      * Provides compile-time type checking for input and output types.
205      *
206      * @param key The type-safe key for the use case
207      * @param input The input to the use case
208      * @param <I> The input type of the use case
209      * @param <O> The output type of the use case
210      * @return The result of the use case
211      * @throws IllegalArgumentException if no use case is registered with the given key
212      */
213     @SuppressWarnings("unchecked")
214     public <I, O> O invoke(UseCaseKey<I, O> key, I input) {
215         UseCase<I, O> useCase = (UseCase<I, O>) useCases.get(key.name());
216         if (useCase == null) {
217             throw new IllegalArgumentException(
218                 "No use case registered with name: " + key.name()
219             );
220         }
221         return useCase.apply(input);
222     }
223 
224     /**
225      * Get the names of all registered use cases.
226      * @return A set of registered use case names
227      */
228     public Set<String> registeredUseCases() {
229         return useCases.keySet();
230     }
231 
232     /**
233      * Invoke a use case by name (for internal/testing use).
234      * Prefer using {@link #invoke(UseCaseKey, Object)} for type safety.
235      *
236      * @param name The name of the use case
237      * @param input The input to the use case
238      * @param <I> The input type
239      * @param <O> The output type
240      * @return The result of the use case
241      * @throws IllegalArgumentException if no use case is registered with the given name
242      */
243     @SuppressWarnings("unchecked")
244     public <I, O> O invokeByName(String name, I input) {
245         UseCase<I, O> useCase = (UseCase<I, O>) useCases.get(name);
246         if (useCase == null) {
247             throw new IllegalArgumentException(
248                 "No use case registered with name: " + name
249             );
250         }
251         return useCase.apply(input);
252     }
253 
254     /**
255      * Start testing a use case using a type-safe key.
256      *
257      * @param key The type-safe key for the use case
258      * @param <I> Input type of the use case
259      * @param <O> Output type of the use case
260      * @return A new UseCaseTest instance
261      */
262     public <I, O> UseCaseTest<I, O> test(UseCaseKey<I, O> key) {
263         return HexaTest.forApp(this).test(key.name());
264     }
265 
266     /**
267      * Optional startup logic.
268      */
269     public void run() {
270         // optional startup logic
271     }
272 }