HexaApp.java

package com.guinetik.hexafun;

import com.guinetik.hexafun.hexa.AdapterKey;
import com.guinetik.hexafun.hexa.UseCase;
import com.guinetik.hexafun.hexa.UseCaseKey;
import com.guinetik.hexafun.testing.HexaTest;
import com.guinetik.hexafun.testing.UseCaseTest;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;

/**
 * Core container for a Hexagonal Architecture application.
 * Manages use cases, ports, and adapters.
 */
public abstract class HexaApp {

    protected final Map<String, UseCase<?, ?>> useCases = new HashMap<>();
    protected final Map<Class<?>, Object> ports = new HashMap<>();
    protected final Map<String, Function<?, ?>> adapters = new HashMap<>();

    /**
     * Create a new empty HexaApp.
     * @return A new empty HexaApp
     */
    public static HexaApp create() {
        return new HexaAppImpl();
    }

    /**
     * Add a use case to this HexaApp (used internally by builder).
     */
    public <I, O> HexaApp withUseCase(String name, UseCase<I, O> useCase) {
        useCases.put(name, useCase);
        return this;
    }

    /**
     * Add a use case using a type-safe key.
     *
     * <p>Example:
     * <pre class="language-java">{@code
     * app.withUseCase(CREATE_TASK, new CreateTaskHandler(app));
     * }</pre>
     *
     * @param key The type-safe use case key
     * @param useCase The use case implementation
     * @param <I> Input type
     * @param <O> Output type
     * @return This HexaApp for chaining
     */
    public <I, O> HexaApp withUseCase(UseCaseKey<I, O> key, UseCase<I, O> useCase) {
        useCases.put(key.name(), useCase);
        return this;
    }

    /**
     * Register a port (output adapter) by its type.
     * Provides type-safe dependency injection for output ports.
     *
     * <p>Example:
     * <pre class="language-java">{@code
     * app.port(TaskRepository.class, new InMemoryTaskRepository());
     * }</pre>
     *
     * @param type The interface/class type to register
     * @param impl The implementation instance
     * @param <T> The port type
     * @return This HexaApp for chaining
     */
    public <T> HexaApp port(Class<T> type, T impl) {
        ports.put(type, impl);
        return this;
    }

    /**
     * Retrieve a port by its type.
     *
     * <p>Example:
     * <pre class="language-java">{@code
     * TaskRepository repo = app.port(TaskRepository.class);
     * }</pre>
     *
     * @param type The interface/class type to retrieve
     * @param <T> The port type
     * @return The registered implementation
     * @throws IllegalArgumentException if no port is registered for the given type
     */
    @SuppressWarnings("unchecked")
    public <T> T port(Class<T> type) {
        T impl = (T) ports.get(type);
        if (impl == null) {
            throw new IllegalArgumentException(
                "No port registered for type: " + type.getName()
            );
        }
        return impl;
    }

    /**
     * Check if a port is registered for the given type.
     *
     * @param type The interface/class type to check
     * @return true if a port is registered, false otherwise
     */
    public boolean hasPort(Class<?> type) {
        return ports.containsKey(type);
    }

    /**
     * Get all registered port types.
     *
     * @return A set of registered port types
     */
    public Set<Class<?>> registeredPorts() {
        return ports.keySet();
    }

    // ===== Adapters =====

    /**
     * Register an adapter with a type-safe key.
     * Adapters transform data from one type to another.
     *
     * <p>Example:
     * <pre class="language-java">{@code
     * app.withAdapter(TO_INVENTORY, req -> new InventoryCheck(req.itemId()));
     * }</pre>
     *
     * @param key The type-safe adapter key
     * @param adapter The adapter function
     * @param <From> The source type
     * @param <To> The target type
     * @return This HexaApp for chaining
     */
    public <From, To> HexaApp withAdapter(AdapterKey<From, To> key, Function<From, To> adapter) {
        adapters.put(key.name(), adapter);
        return this;
    }

    /**
     * Register an adapter by name (used internally by builder).
     *
     * @param name The adapter name
     * @param adapter The adapter function
     * @param <From> The source type
     * @param <To> The target type
     * @return This HexaApp for chaining
     */
    public <From, To> HexaApp withAdapter(String name, Function<From, To> adapter) {
        adapters.put(name, adapter);
        return this;
    }

    /**
     * Adapt a value using a type-safe adapter key.
     * Transforms the input from one type to another.
     *
     * <p>Example:
     * <pre class="language-java">{@code
     * InventoryCheck check = app.adapt(TO_INVENTORY, orderRequest);
     * }</pre>
     *
     * @param key The type-safe adapter key
     * @param input The value to adapt
     * @param <From> The source type
     * @param <To> The target type
     * @return The adapted value
     * @throws IllegalArgumentException if no adapter is registered with the given key
     */
    @SuppressWarnings("unchecked")
    public <From, To> To adapt(AdapterKey<From, To> key, From input) {
        Function<From, To> adapter = (Function<From, To>) adapters.get(key.name());
        if (adapter == null) {
            throw new IllegalArgumentException(
                "No adapter registered with name: " + key.name()
            );
        }
        return adapter.apply(input);
    }

    /**
     * Check if an adapter is registered for the given key.
     *
     * @param key The adapter key to check
     * @return true if an adapter is registered, false otherwise
     */
    public boolean hasAdapter(AdapterKey<?, ?> key) {
        return adapters.containsKey(key.name());
    }

    /**
     * Get all registered adapter names.
     *
     * @return A set of registered adapter names
     */
    public Set<String> registeredAdapters() {
        return adapters.keySet();
    }

    /**
     * Invoke a use case using a type-safe key.
     * Provides compile-time type checking for input and output types.
     *
     * @param key The type-safe key for the use case
     * @param input The input to the use case
     * @param <I> The input type of the use case
     * @param <O> The output type of the use case
     * @return The result of the use case
     * @throws IllegalArgumentException if no use case is registered with the given key
     */
    @SuppressWarnings("unchecked")
    public <I, O> O invoke(UseCaseKey<I, O> key, I input) {
        UseCase<I, O> useCase = (UseCase<I, O>) useCases.get(key.name());
        if (useCase == null) {
            throw new IllegalArgumentException(
                "No use case registered with name: " + key.name()
            );
        }
        return useCase.apply(input);
    }

    /**
     * Get the names of all registered use cases.
     * @return A set of registered use case names
     */
    public Set<String> registeredUseCases() {
        return useCases.keySet();
    }

    /**
     * Invoke a use case by name (for internal/testing use).
     * Prefer using {@link #invoke(UseCaseKey, Object)} for type safety.
     *
     * @param name The name of the use case
     * @param input The input to the use case
     * @param <I> The input type
     * @param <O> The output type
     * @return The result of the use case
     * @throws IllegalArgumentException if no use case is registered with the given name
     */
    @SuppressWarnings("unchecked")
    public <I, O> O invokeByName(String name, I input) {
        UseCase<I, O> useCase = (UseCase<I, O>) useCases.get(name);
        if (useCase == null) {
            throw new IllegalArgumentException(
                "No use case registered with name: " + name
            );
        }
        return useCase.apply(input);
    }

    /**
     * Start testing a use case using a type-safe key.
     *
     * @param key The type-safe key for the use case
     * @param <I> Input type of the use case
     * @param <O> Output type of the use case
     * @return A new UseCaseTest instance
     */
    public <I, O> UseCaseTest<I, O> test(UseCaseKey<I, O> key) {
        return HexaTest.forApp(this).test(key.name());
    }

    /**
     * Optional startup logic.
     */
    public void run() {
        // optional startup logic
    }
}