README.md

# Interact

An Elixir library for defining and running **use cases** — the application-layer business logic units described in Clean Architecture (Uncle Bob), Domain-Driven Design (*application services*), and Hexagonal Architecture (*interactors*).

> "The code should **scream** the architecture."
> — Robert C. Martin, *Screaming Architecture*

## What is a use case?

A use case is an isolated, named piece of business logic. It takes an input, does work, and produces an output. It knows nothing about HTTP, databases, queues, or any other infrastructure concern — those are the caller's problem.

In `interact`, a use case is either a module that exports an `exec/1` function or a unary function:

```elixir
defmodule MyApp.CreateOrderUseCase do
  def exec(%{user_id: user_id, items: items}) do
    # pure business logic here
    {:ok, order} = Orders.create(user_id, items)
    order
  end
end

create_order = fn %{user_id: user_id, items: items} ->
  {:ok, order} = Orders.create(user_id, items)
  order
end
```

The library handles the rest: synchronous or asynchronous execution, result delivery, concurrency control, and lifecycle tracking.

## Why?

Without use cases, business logic tends to accumulate in controllers, LiveView handlers, or context modules — mixed with HTTP parsing, database calls, and async scaffolding. This makes it hard to see what the application actually *does*.

Extracting use cases into dedicated modules makes the architecture visible at a glance, and separates the *what* (business logic) from the *how* (execution strategy).

## Installation

Add `interact` to your dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:interact, "~> 0.1"}
  ]
end
```

`interact` starts its own supervision tree automatically — no manual setup needed in your application supervisor.

## Defining a use case

A valid use case is either a module with an `exec/1` function:

```elixir
defmodule MyApp.SayHiUseCase do
  def exec(_input) do
    :hi
  end
end
```

Or a unary function:

```elixir
say_hi = fn _input -> :hi end
```

## Running a use case

Import (or alias) `Interact.UseCase` and build a pipeline:

```elixir
import Interact.UseCase

create(MyApp.SayHiUseCase)
|> input(:ignored)
|> run()
# => %Interact.UseCase{output: :hi, error: nil, ...}
```

The input can be anything accepted by the use cases (a keyword list, a list, a map, a struct, a plain value, etc. or `nil` by default).

`run/2` is a shorthand for `input/2 |> run/1`:

```elixir
create(MyApp.SayHiUseCase) |> run(:ignored)
```

Functions work too — both `fn` closures and `&Module.function/1` captures:

```elixir
create(fn x -> x + 1 end) |> run(41)
# => %Interact.UseCase{output: 42, ...}

create(&String.upcase/1) |> run("hello")
# => %Interact.UseCase{output: "HELLO", ...}
```

The returned struct always carries the full picture: `input`, `output`, `error`, `started_at`, `timedout_at`, `finished_at`.

## Asynchronous execution

By default, `run/1` blocks until the use case finishes. Add `async/1` to fire and forget:

```elixir
create(MyApp.HeavyUseCase)
|> async()
|> run(input)
# returns immediately; result arrives later via reply or callback
```

## Timeout

Add `timeout/2` to kill the underlying task if it exceeds a deadline.
The result struct is returned with `timedout_at` set to the moment the
deadline was detected. `finished_at`, `output` and `error` remain `nil`.

```elixir
uc = create(MyApp.SlowUseCase) |> timeout(5_000) |> run(params)

if uc.timedout_at do
  # use case was killed after 5 seconds
end
```

Timeout works for both synchronous and asynchronous use cases.
Callbacks and replies fire with the timed-out struct in both cases.

```elixir
create(MyApp.SlowUseCase)
|> async()
|> timeout(5_000)
|> reply(self())
|> run(params)

receive do
  %Interact.UseCase{timedout_at: t} when not is_nil(t) -> handle_timeout()
  %Interact.UseCase{output: o} when not is_nil(o)      -> handle_output(o)
  %Interact.UseCase{error: e} when not is_nil(e)       -> handle_error(e)
end
```

## Delivering results

### To a process — `reply/2`

Send the completed use case struct to one or more PIDs:

```elixir
create(MyApp.ScanNetworkUseCase)
|> async()
|> reply(self())
|> run(cidr)

receive do
  %Interact.UseCase{output: result} -> handle(result)
end
```

Particularly useful in LiveView — pass `self()` and handle the result in `handle_info/2`.

### Via a callback — `callback/2`

Run one or more functions when the use case completes:

```elixir
create(MyApp.ScanNetworkUseCase)
|> async()
|> callback(fn uc -> Logger.info("Scan finished: #{inspect(uc.output)}") end)
|> run(cidr)
```

Pass a list for multiple callbacks:

```elixir
|> callback([&log_result/1, &notify_user/1])
```

Callbacks are invoked asynchronously, even for synchronous use cases.

### Broadcasting to a PubSub

`interact` has no PubSub dependency. Use `callback/2` to broadcast through whatever PubSub your application already uses:

```elixir
create(MyApp.ScanNetworkUseCase)
|> async()
|> callback(fn uc ->
  Phoenix.PubSub.broadcast(MyApp.PubSub, "scans", {:scan_done, uc.output})
end)
|> run(cidr)
```

## Error handling

### Exceptions raised inside the use case

If `exec/1` raises, the exception is captured in the `error` field, the stacktrace in `stacktrace`, and `output` is set to `nil`. The use case struct is always returned — execution never crashes the caller.

```elixir
uc = create(fn _ -> raise "oops" end) |> run(nil)
uc.error      # => %RuntimeError{message: "oops"}
uc.stacktrace # => [...]
uc.output     # => nil
```

Callbacks and replies still fire with the failed struct, so you can handle errors uniformly:

```elixir
|> callback(fn
  %{error: nil, output: result} -> handle_success(result)
  %{error: error}               -> handle_failure(error)
end)
```

### Invocation errors

Passing a module that does not implement `exec/1` — whether it exists but lacks the function, or does not exist at all — is treated as any other runtime error: the exception is captured in `error` and `stacktrace`, and the struct is returned normally.

```elixir
# Module exists but has no exec/1
uc = create(String) |> run("hello")
uc.error  # => %UndefinedFunctionError{module: String, function: :exec, arity: 1}

# Module does not exist
uc = create(MyApp.ForgotToDefineThisUseCase) |> run(nil)
uc.error  # => %UndefinedFunctionError{...}

# Anonymous function with wrong arity
uc = create(fn x, y -> x + y end) |> run(1)
uc.error  # => %BadArityError{...}
```

### Calling pipeline functions on a started use case

Once `run/1` is called the use case is considered started. Any further
pipeline call on the same struct — `async/1`, `callback/2`,
`reply/2`, `input/2`, or `run/1` again — raises `ArgumentError` immediately.
It does not capture into `error`. Create a new struct for each execution.

```elixir
uc = create(EchoUseCase) |> run(42)
run(uc)      # => raises ArgumentError: use case already started
input(uc, 1) # => raises ArgumentError: use case already started
async(uc)    # => raises ArgumentError: use case already started
```

## Full pipeline reference

```
create(MyApp.MyUseCase)   # wraps a use case module or function
|> async()                # fire and forget (default: synchronous)
|> timeout(ms)            # kill the task after ms milliseconds (default: :infinity)
|> callback(fn_or_list)   # functions called after completion (default: [])
|> reply(pid_or_list)     # processes to receive the result struct (default: [])
|> input(data)            # input passed to the use case (default: nil)
|> run()                  # executes the use case
```

Every pipeline function accepts a module or function directly, so `create/1`
is executed internally, this means that the pipeline can start at any step:

```elixir
MyApp.MyUseCase |> async() |> run(data)
fn x -> x * 2 end |> callback(&log/1) |> run(21)
```

`run/1` (or `run/2`) must always be the last step. All steps before it are
optional and order-independent.

## Lifecycle and invariant

| **State**       | `started_at`   | `finished_at`  | `timedout_at`  | `output`   | `error`         |
|-----------------|:--------------:|:--------------:|:--------------:|:----------:|:---------------:|
| **Not started** | `nil`          | `nil`          | `nil`          | `nil`      | `nil`           |
| **Started**     | `DateTime.t()` | `nil`          | `nil`          | `nil`      | `nil`           |
| **Succeeded**   | `DateTime.t()` | `DateTime.t()` | `nil`          | `any()`    | `nil`           |
| **Failed**      | `DateTime.t()` | `DateTime.t()` | `nil`          | `nil`      | `Exception.t()` |
| **Timed out**   | `DateTime.t()` | `nil`          | `DateTime.t()` | `nil`      | `nil`           |

## Examples

The `examples/` directory contains ready-to-run use cases you can try in `iex -S mix`:

- `EchoUseCase`: returns its input unchanged
- `SayHiUseCase`: sleeps briefly, prints a greeting, returns `:hi`
- `JustRaiseUseCase`: always raises (demonstrates error capture)
- `SlowUseCase`: returns `:done` after a while

```elixir
iex> import Interact.UseCase
iex> create(EchoUseCase) |> run(42)
iex> create(SayHiUseCase) |> async() |> reply(self()) |> run(nil)
iex> flush()
```

## Roadmap

- **Persistence**: lightweight result history via ETS as a first step; full persistence as a future option
- **Restart policy**: re-run a failed use case up to N times, dependent on persistence support
- **Composition**: sequential chaining, pipes (output of one use case becomes input of the next), and parallel execution
- **Periodic use cases**: run a use case N times with a given periodicity
- **Singleton use cases**: run a use case as singleton to prevent concurrent executions
- **Process pools**: limit the number of concurrent executions of a given use case (singleton would be a particular case with one concurrent execution) or for any use case