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 }