# Spec Coverage

## Common @spec Patterns

### Basic Types

```elixir
@spec greet(String.t()) :: String.t()
def greet(name), do: "Hello, #{name}!"

@spec count_items(list()) :: non_neg_integer()
def count_items(items), do: length(items)

@spec enabled?() :: boolean()
def enabled?, do: Application.get_env(:my_app, :enabled, false)
```

### Union Types

```elixir
@spec fetch_account(integer()) :: {:ok, Account.t()} | {:error, :not_found | :suspended}
def fetch_account(id) do
  case Repo.get(Account, id) do
    nil -> {:error, :not_found}
    %Account{status: :suspended} = account -> {:error, :suspended}
    account -> {:ok, account}
  end
end
```

### Custom Types

```elixir
@type id :: pos_integer()
@type reason :: :not_found | :unauthorized | :timeout
@type result :: {:ok, t()} | {:error, reason()}

@spec find(id()) :: result()
def find(id), do: # ...
```

### Keyword Options

```elixir
@type option :: {:timeout, pos_integer()} | {:retries, non_neg_integer()}

@spec request(String.t(), [option()]) :: {:ok, Response.t()} | {:error, term()}
def request(url, opts \\ []) do
  timeout = Keyword.get(opts, :timeout, 5_000)
  retries = Keyword.get(opts, :retries, 3)
  # ...
end
```

### When Clauses

```elixir
@spec transform(list(a), (a -> b)) :: list(b) when a: term(), b: term()
def transform(items, func), do: Enum.map(items, func)

@spec wrap(value) :: [value] when value: term()
def wrap(value), do: [value]
```

## @type and @typedoc

Use custom types to name domain concepts and reduce repetition.

```elixir
defmodule MyApp.Shipping do
  @typedoc "Weight in grams."
  @type weight :: non_neg_integer()

  @typedoc "A geographic coordinate pair."
  @type coordinates :: {latitude :: float(), longitude :: float()}

  @typedoc "Shipping status throughout the delivery lifecycle."
  @type status :: :pending | :in_transit | :delivered | :returned

  @spec estimate_cost(weight(), coordinates(), coordinates()) :: {:ok, Decimal.t()}
  def estimate_cost(weight, origin, destination) do
    # ...
  end
end
```

Benefits:
- `weight()` communicates intent better than `non_neg_integer()`
- `status()` centralizes valid values -- add a new status in one place
- `@typedoc` appears in ExDoc, making types self-documenting

## When @spec Is Required

All public exported functions should have @spec. This includes:

```elixir
defmodule MyApp.Accounts do
  # Required: public function
  @spec create_user(map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
  def create_user(attrs), do: # ...

  # Required: public function with default args
  @spec list_users(keyword()) :: [User.t()]
  def list_users(opts \\ []), do: # ...

  # Required: public function used as callback
  @spec child_spec(keyword()) :: Supervisor.child_spec()
  def child_spec(opts), do: # ...
end
```

## When @spec Is Optional

```elixir
defmodule MyApp.Accounts do
  # Optional: private function
  defp normalize_email(email), do: String.downcase(email)

  # Optional: macro-generated functions (e.g., Ecto schema fields)
  # These are generated by `schema` and `field` macros

  # Optional: @impl true callbacks where the behaviour defines the spec
  @impl true
  def handle_call(:ping, _from, state), do: {:reply, :pong, state}
end
```

## Common @spec Mistakes

### Overly Broad term() or any()

```elixir
# BAD - term() hides what the function actually accepts
@spec process(term()) :: term()
def process(%Order{} = order), do: # ...

# GOOD - spec reflects the actual types
@spec process(Order.t()) :: {:ok, Receipt.t()} | {:error, String.t()}
def process(%Order{} = order), do: # ...
```

Using `term()` or `any()` defeats the purpose of specs. If you know the type, declare it.

### Missing Union Branches

```elixir
# BAD - forgets the nil case from Repo.get
@spec find_user(integer()) :: {:ok, User.t()}
def find_user(id) do
  case Repo.get(User, id) do
    nil -> {:error, :not_found}  # Not reflected in spec!
    user -> {:ok, user}
  end
end

# GOOD - all return paths represented
@spec find_user(integer()) :: {:ok, User.t()} | {:error, :not_found}
def find_user(id) do
  case Repo.get(User, id) do
    nil -> {:error, :not_found}
    user -> {:ok, user}
  end
end
```

### Not Using Custom Types for Repeated Patterns

```elixir
# BAD - same tuple pattern repeated across many functions
@spec create(map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
@spec update(User.t(), map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
@spec delete(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}

# GOOD - define a type once, reuse it
@type changeset_result :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}

@spec create(map()) :: changeset_result()
@spec update(User.t(), map()) :: changeset_result()
@spec delete(User.t()) :: changeset_result()
```

### Specs That Don't Match Function Clauses

```elixir
# BAD - spec says it only accepts String.t() but function also handles nil
@spec normalize(String.t()) :: String.t()
def normalize(nil), do: ""
def normalize(value), do: String.trim(value)

# GOOD - spec covers all clauses
@spec normalize(String.t() | nil) :: String.t()
def normalize(nil), do: ""
def normalize(value), do: String.trim(value)
```

### Using list() When a Specific Element Type Is Known

```elixir
# BAD - list() tells the caller nothing about contents
@spec active_users() :: list()
def active_users, do: Repo.all(from u in User, where: u.active == true)

# GOOD - caller knows what's in the list
@spec active_users() :: [User.t()]
def active_users, do: Repo.all(from u in User, where: u.active == true)
```

## Dialyzer-Friendly Specs

Dialyzer uses @spec to perform success typing analysis. Specs that help Dialyzer catch real bugs:

```elixir
# Dialyzer can catch callers passing wrong types
@spec send_notification(User.t(), String.t()) :: :ok | {:error, :delivery_failed}
def send_notification(%User{email: email}, message) do
  # ...
end

# Dialyzer can verify pattern match exhaustiveness
@type role :: :admin | :editor | :viewer

@spec permissions(role()) :: [atom()]
def permissions(:admin), do: [:read, :write, :delete]
def permissions(:editor), do: [:read, :write]
def permissions(:viewer), do: [:read]
# Dialyzer warns if a new role is added to the type but not handled here
```

Tips for Dialyzer compatibility:
- Avoid `@spec function() :: no_return()` unless the function truly never returns (e.g., raises always)
- Use `String.t()` instead of `binary()` for text data -- they're equivalent to Dialyzer but `String.t()` communicates intent
- Declare `@opaque` types when internal representation should not leak to callers

## Review Questions

1. Do all public functions have @spec?
2. Do specs accurately reflect all return paths (including error tuples)?
3. Are custom @type definitions used for repeated patterns?
4. Are specs specific enough to catch real bugs (no unnecessary term() or any())?
