# ArchTest Cheatsheet

{: .col-2}

## Module Selection

### Glob patterns

```elixir
modules_matching("MyApp.Orders.*")      # direct children only
modules_matching("MyApp.Orders.**")     # all descendants
modules_matching("**.*Service")         # last segment ends with Service
modules_matching("**.*Service*")        # last segment contains Service
modules_matching("MyApp.**.*Repo")      # under MyApp, ends with Repo
modules_in("MyApp.Orders")             # shorthand for "MyApp.Orders.*"
all_modules()                          # every loaded module
```

### Custom predicate

```elixir
modules_satisfying(fn mod ->
  function_exported?(mod, :__schema__, 1)
end)
```

### Composing sets

```elixir
modules_matching("MyApp.**")
|> excluding("MyApp.Web.*")

modules_matching("**.*Service")
|> union(modules_matching("**.*View"))

modules_matching("MyApp.**")
|> intersection(modules_matching("**.*Schema"))
```

## Pattern Reference

| Pattern | Matches |
|---------|---------|
| `"MyApp.Orders"` | Exact match only |
| `"MyApp.Orders.*"` | Direct children (`MyApp.Orders.Order`) |
| `"MyApp.Orders.**"` | All descendants at any depth |
| `"**.*Service"` | Any module whose last segment ends with `Service` |
| `"**.*Service*"` | Any module whose last segment contains `Service` |
| `"MyApp.**.*Repo"` | Under `MyApp`, last segment ends with `Repo` |
| `"**"` | All modules |

## Dependency Rules

### Forbid

```elixir
modules_matching("MyApp.Domain.**")
|> should_not_depend_on(modules_matching("MyApp.Web.**"))
```

### Allowlist

```elixir
modules_matching("MyApp.Web.**")
|> should_only_depend_on(modules_matching("MyApp.Domain.**"))
```

### Caller restriction

```elixir
modules_matching("MyApp.Repo")
|> should_only_be_called_by(modules_matching("MyApp.Domain.**"))

modules_matching("MyApp.Repo")
|> should_not_be_called_by(modules_matching("MyApp.Web.**"))
```

### Transitive

```elixir
modules_matching("MyApp.Domain.**")
|> should_not_transitively_depend_on(modules_matching("Ecto.**"))
```

### Cycles

```elixir
modules_matching("MyApp.**") |> should_be_free_of_cycles()
```

## Naming Rules

```elixir
# Ban a naming convention
modules_matching("MyApp.**.*Manager") |> should_not_exist()

# Enforce namespace
modules_satisfying(fn m -> function_exported?(m, :__schema__, 1) end)
|> should_reside_under("MyApp.**.Schemas")

# Enforce name pattern
modules_matching("MyApp.Web.**")
|> should_have_name_matching("**.*Controller")

# Count
modules_matching("MyApp.**.*God")
|> should_have_module_count(max: 0)
```

## Behaviour / Protocol / Attribute Rules

```elixir
modules_matching("MyApp.Workers.**")
|> should_implement_behaviour(Oban.Worker)

modules_matching("MyApp.Domain.**")
|> should_not_implement_behaviour(Plug)

modules_matching("MyApp.**.*Schema")
|> should_implement_protocol(Jason.Encoder)

modules_matching("MyApp.**")
|> should_have_attribute(:moduledoc)

modules_matching("MyApp.**")
|> should_not_have_attribute_value(:deprecated, true)
```

## Function Rules

```elixir
modules_matching("MyApp.Domain.**")
|> should_export(:call, 2)

modules_matching("MyApp.Web.**")
|> should_not_export(:__impl__, 1)

modules_matching("MyApp.**")
|> should_use(Phoenix.Controller)

modules_matching("MyApp.Domain.**")
|> should_not_use(Plug)
```

## Layered Architecture

### Classic layers (top → bottom)

```elixir
define_layers(
  web:     "MyApp.Web.**",
  context: "MyApp.**",
  repo:    "MyApp.Repo.**"
)
|> enforce_direction()
```

### Onion / hexagonal (innermost first)

```elixir
define_onion(
  domain:      "MyApp.Domain.**",
  application: "MyApp.Application.**",
  adapters:    "MyApp.Adapters.**",
  web:         "MyApp.Web.**"
)
|> enforce_onion_rules()
```

## Modulith / Bounded Contexts

### Enforce isolation

```elixir
define_slices(
  orders:    "MyApp.Orders",
  inventory: "MyApp.Inventory",
  accounts:  "MyApp.Accounts"
)
|> allow_dependency(:orders, :accounts)
|> allow_dependency(:orders, :inventory)
|> enforce_isolation()
```

### Strict (zero cross-context deps)

```elixir
define_slices(
  core:    "MyApp.Core",
  plugins: "MyApp.Plugins"
)
|> should_not_depend_on_each_other()
```

### Cycle detection

```elixir
define_slices([...]) |> should_be_free_of_cycles()
```

## Code Conventions

```elixir
use ArchTest.Conventions

no_io_puts_in(modules_matching("MyApp.**"))
no_process_sleep_in(modules_matching("MyApp.**"))
no_application_get_env_in(modules_matching("MyApp.Domain.**"))
no_dbg_in(modules_matching("MyApp.**"))
no_raise_string_in(modules_matching("MyApp.**"))
no_plug_in(modules_matching("MyApp.Domain.**"))
all_public_functions_documented(modules_matching("MyApp.**.*Repo"))
```

## Coupling Metrics

```elixir
alias ArchTest.Metrics

# Martin metrics for a namespace
# %{Module => %{instability: 0.6, abstractness: 0.0, distance: 0.4}}
metrics = Metrics.martin("MyApp.**")

Enum.each(metrics, fn {mod, m} ->
  assert m.distance < 0.5, "#{mod} too far from main sequence"
end)

# Single module
Metrics.instability("MyApp.Orders")    # 0.0..1.0
Metrics.abstractness("MyApp.Orders")   # 0.0..1.0

# Afferent / efferent coupling counts
Metrics.afferent("MyApp.Orders")       # who depends on Orders
Metrics.efferent("MyApp.Orders")       # who Orders depends on
```

## Gradual Adoption (Freeze)

```elixir
# Wrap any rule with freeze/2 to baseline current violations
ArchTest.Freeze.freeze("domain_web_deps", fn ->
  modules_matching("MyApp.Domain.**")
  |> should_not_depend_on(modules_matching("MyApp.Web.**"))
end)
```

```sh
# Establish or update the baseline
ARCH_TEST_UPDATE_FREEZE=true mix test
```

Baseline files live in `test/arch_test_violations/`. Commit them. Delete the file when all violations are fixed.

## Setup

### Single app

```elixir
defmodule MyApp.ArchTest do
  use ExUnit.Case
  use ArchTest
end
```

### Umbrella — scope to one app

```elixir
use ArchTest, app: :my_app
```

### Configure freeze directory

```elixir
# config/test.exs
config :arch_test, freeze_store: "test/arch_violations"
```

## Igniter Generators

Add `{:igniter, "~> 0.7", only: [:dev, :test], runtime: false}`, then:

| Command | Guide | Further reading |
|---------|-------|-----------------|
| `mix igniter.install arch_test` | Basic cycle-check file | |
| `mix arch_test.gen.phoenix` | Layers + naming + conventions combined | [Phoenix directory structure](https://hexdocs.pm/phoenix/directory_structure.html) · [N-tier architecture](https://en.wikipedia.org/wiki/Multitier_architecture) |
| `mix arch_test.gen.layers` | [Layered Architecture](layered-architecture.md) | [N-tier architecture](https://en.wikipedia.org/wiki/Multitier_architecture) |
| `mix arch_test.gen.onion` | [Layered Architecture — Onion](layered-architecture.md#onion--hexagonal-architecture) | [Onion Architecture](https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/) · [Hexagonal / Ports & Adapters](https://alistair.cockburn.us/hexagonal-architecture/) |
| `mix arch_test.gen.modulith` | [Modulith / Bounded Contexts](modulith-rules.md) | [Modular Monolith Primer](https://www.kamilgrzybek.com/blog/posts/modular-monolith-primer) |
| `mix arch_test.gen.naming` | Naming Rules section above | |
| `mix arch_test.gen.conventions` | Code Conventions section above | |
| `mix arch_test.gen.freeze` | [Gradual Adoption — Freeze](freezing.md) | |
