<p align="center">
  <img src="assets/patterns.png" alt="Patterns" width="903" height="346" />
</p>

[![Hex Version](https://img.shields.io/hexpm/v/patterns.svg)](https://hex.pm/packages/patterns)
[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/patterns/)
[![CI Status](https://github.com/minnasoft/patterns/workflows/CI/badge.svg)](https://github.com/minnasoft/patterns/actions)
[![Coverage Status](https://coveralls.io/repos/github/minnasoft/patterns/badge.svg?branch=main)](https://coveralls.io/github/minnasoft/patterns?branch=main)

> Pretty patterns for writing the Elixir you actually want to write.

Patterns is a small Elixir package for turning repeated application structure into explicit, documented APIs. It focuses on code that shows up in real projects: schema query builders, function middleware, scoped context, delegation helpers, and ETS-backed key/value utilities.

## Features

- `Patterns.Queryable` adds a consistent `query/1` and `query/2` API to Ecto schemas.
- `Patterns.Queryable.Filters` provides reusable filters for fields, associations, ordering, limits, offsets, preloads, selects, and common comparators.
- `Patterns.Middleware` wraps public or private functions with explicit middleware stacks.
- `Patterns.Delegate` provides prefixed, suffixed, and bulk delegates.
- `Patterns.Utils` provides `with_ctx/2` and `ctx/1`.
- `Patterns.Utils.ETS` wraps common ETS key/value operations with a map-shaped API.
- `Patterns.Guards` includes small reusable guards for pattern modules.
- Experimental modules provide structured errors, repo-aware keyset pagination, and a small LiveView route/page bridge.

## Queryable

Use `Patterns.Queryable` when schemas should own their query vocabulary.

```elixir
defmodule Blog.Post do
  use Ecto.Schema
  use Patterns.Queryable

  schema "posts" do
    field :title, :string
    field :views, :integer
    field :published, :boolean

    has_many :comments, Blog.Comment
  end
end
```

Callers can pass keyword or map filters:

```elixir
Blog.Post.query(title: "Hello", published: true)
Blog.Post.query(%{title: "Hello"})
```

Start from an existing query with `query/2`:

```elixir
import Ecto.Query

base_query =
  from post in Blog.Post,
    where: post.views > 10

Blog.Post.query(base_query, title: {:not, nil})
```

Built-in filters include query modifiers, equality, lists, ranges, `LIKE` shorthand, regex shorthand, negation, and association filters:

```elixir
Blog.Post.query(
  title: {:like, "^intro"},
  views: {:gte, 100},
  published: true,
  comments: [body: "Nice"],
  order_by: [desc: :views],
  limit: 10,
  preload: :comments
)
```

Association filters use schema association names. If the associated schema also implements `query/2`, nested filters are delegated to it.

```elixir
defmodule Blog.Comment do
  use Ecto.Schema
  use Patterns.Queryable

  schema "comments" do
    field :body, :string
    field :likes, :integer

    belongs_to :post, Blog.Post
  end

  @impl Patterns.Queryable
  def query(base_query, filters) do
    Enum.reduce(filters, base_query, fn
      {:popular, true}, query ->
        from binding(comment) in query,
          where: comment.likes > 5

      filter, query ->
        apply_filter(query, filter)
    end)
  end
end

Blog.Post.query(comments: [popular: true])
```

List values inside one association filter match any value on one associated row:

```elixir
Blog.Post.query(comments: [body: ["Nice", "Cool"]])
```

Nested association filters require one matching associated row per filter:

```elixir
Blog.Post.query(comments: [[body: "Nice"], [body: "Cool"]])
```

Override `base_query/0` when every query should share the same starting point:

```elixir
defmodule Blog.PublishedPost do
  use Ecto.Schema
  use Patterns.Queryable

  schema "posts" do
    field :title, :string
    field :published, :boolean
  end

  @impl Patterns.Queryable
  def base_query do
    from post in __MODULE__,
      where: post.published == true
  end
end
```

Define custom filters by implementing `query/2`. Unknown filters can be delegated back to `apply_filter/2`.

```elixir
defmodule Blog.Post do
  use Ecto.Schema
  use Patterns.Queryable

  schema "posts" do
    field :title, :string
    field :views, :integer
  end

  @impl Patterns.Queryable
  def query(base_query, filters) do
    Enum.reduce(filters, base_query, fn
      {:popular, true}, query ->
        from binding(post) in query,
          where: post.views > 10

      filter, query ->
        apply_filter(query, filter)
    end)
  end
end
```

## Middleware

Use `Patterns.Middleware` when code should run around a function call without moving that logic into the function body.

```elixir
defmodule Blog.Middlewares.NormalizePostTitle do
  use Patterns.Middleware

  @behaviour Patterns.Middleware

  @impl Patterns.Middleware
  def process([attrs], resolution) do
    attrs = Map.update!(attrs, :title, &String.trim/1)

    yield([attrs], resolution)
  end
end

defmodule Blog.Middlewares.AuthorizeEditor do
  use Patterns.Middleware

  @behaviour Patterns.Middleware

  @impl Patterns.Middleware
  def process([user, attrs], resolution) do
    if user.role == :editor do
      yield([user, attrs], resolution)
    else
      {{:error, :unauthorized}, resolution}
    end
  end
end

defmodule Blog do
  use Patterns.Middleware

  @middleware Blog.Middlewares.AuthorizeEditor
  @middleware Blog.Middlewares.NormalizePostTitle
  def create_post(user, attrs) do
    {:ok, attrs}
  end
end
```

Middleware receives the wrapped function arguments as a list and a `Patterns.Middleware.Resolution`. Calling `yield/2` continues to the next middleware or the original function. Returning `{result, resolution}` without yielding halts the stack.

Middleware can store per-call metadata on the resolution:

```elixir
defmodule Blog.Middlewares.MarkAudited do
  use Patterns.Middleware

  @behaviour Patterns.Middleware

  @impl Patterns.Middleware
  def process(args, resolution) do
    {result, resolution} = yield(args, resolution)

    {result, put_private(resolution, :audited?, true)}
  end
end
```

Middleware can also replace or wrap the super function that runs after the last middleware yields:

```elixir
defmodule Blog.Middlewares.MarkSavedPost do
  use Patterns.Middleware

  @behaviour Patterns.Middleware

  @impl Patterns.Middleware
  def process(args, resolution) do
    resolution =
      update_super(resolution, fn super ->
        fn args, resolution ->
          {:ok, post} = super.(args, resolution)

          {:ok, Map.put(post, :saved?, true)}
        end
      end)

    yield(args, resolution)
  end
end
```

## Delegate

Use `Patterns.Delegate` when one module should expose functions from another module with a prettier public name.

```elixir
defmodule MyString do
  use Patterns.Delegate

  defdelegate_all String
end

MyString.upcase("hello")
#=> "HELLO"
```

`defdelegate/2` shadows Kernel's delegate macro and accepts `:prefix` and `:suffix`.

```elixir
defmodule Components do
  use Patterns.Delegate

  alias __MODULE__

  defdelegate_all Components.Entry, prefix: true, only: [:hero, :list]
end
```

`prefix: true` uses the target module tail, so `Components.Entry.hero/1` becomes `Components.entry_hero/1`.

`defdelegate_all/2` also accepts `:only` and `:except` to keep the exported surface small.

## Utils

`with_ctx/2` and `ctx/1` provide scoped process-local context. Context is restored after the block exits, including raised, thrown, or exited blocks.

```elixir
import Patterns.Utils, only: [with_ctx: 2]

Patterns.Utils.ctx(:binding)
#=> nil

with_ctx binding: :comments do
  Patterns.Utils.ctx(:binding)
  #=> :comments
end

Patterns.Utils.ctx(:binding)
#=> nil
```

`Patterns.Queryable` uses this context to make `binding/1` target the current named binding during nested query composition.

## ETS Helpers

`Patterns.Utils.ETS` wraps common ETS operations in a small key/value API.

```elixir
alias Patterns.Utils.ETS

table = ETS.new([count: 1], access: :public)

ETS.fetch(table, :count)
#=> {:ok, 1}

ETS.get(table, :missing, :default)
#=> :default

ETS.update(table, :count, 0, &(&1 + 1))
#=> table

ETS.to_list(table)
#=> [count: 2]
```

Tables created with `new/0`, `new/1`, or `new/2` are owned by the calling process. For `:set` and `:ordered_set` tables, `put/3` replaces the value for a key. For `:bag` and `:duplicate_bag` tables, `fetch/2` and `get/3` return lists of values.

```elixir
bag = ETS.new([{:tag, :elixir}, {:tag, :ecto}], type: :bag)

ETS.get(bag, :tag)
#=> [:elixir, :ecto]
```

`update/4` is a caller-side read-modify-write operation and only supports `:set` and `:ordered_set` tables.

## Experimental APIs

Experimental modules are useful, but their internals may still change while the
public shape settles.

`Patterns.Experimental.Error` normalizes errors into one boundary struct with a
public message, stable code, optional transport status, optional public
extensions, and private diagnostic cause.

```elixir
{:error, error} = Patterns.Experimental.Error.not_found("Post")

error.message
#=> "Post not found"
```

Use `Patterns.Experimental.Error.Normalize` when a domain error or dependency
error needs custom normalization.

`Patterns.Experimental.Pagination` lets repos execute queries carrying cursor
pagination metadata as Relay-style keyset connections.

```elixir
Post
|> Patterns.Experimental.Pagination.put(%{first: 10})
|> MyApp.Repo.all()
```

The current pagination backend is not part of the public contract.

`Patterns.Experimental.Web` makes one module the incoming web request home while
keeping page shells as normal Phoenix LiveView code.

```elixir
defmodule MyApp.Router do
  use Patterns.Experimental.Web

  scope "/", MyApp do
    pipe_through :browser

    router do
      route "/", Pages.Home
      route "/posts", Pages.Entries, as: Pages.Entries.Posts
      route "/*path", Pages.Entry

      def mount(_params, _session, socket) do
        {:ok, socket}
      end

      def render(assigns) do
        ~H"""
        {Page.render(assigns)}
        """
      end
    end
  end
end
```

Module-shaped actions are route identities. `as:` names the unique route identity
when one page handles multiple routes.

Without a module argument, the generated LiveView is `Router.LiveView` under the
current Phoenix scope. Pass a module, such as `router AdminLive do`, when a
router needs more than one generated LiveView.

The generated LiveView delegates fallback `handle_event/3` and `handle_info/2`
calls to the current route page when that page exports the matching callback.
Missing events raise because the rendered client contract is broken; missing
info handlers warn and keep the socket because stale process messages can arrive
after navigation.

`Patterns.Experimental.Web.Page` pages use LiveView-shaped callbacks like
`handle_params/3` and `render/1`. Use `web: MyApp.Router` when the page should
inherit the app's HTML helpers and verified routes.

If the current scope root module exports `verified_routes/0`, its quoted routes
are injected into the generated LiveView. Apps can provide Phoenix verified
routes at their own boundary without making Patterns know about app endpoints,
routers, or static paths.

## Installation

Add `patterns` to your dependencies:

```elixir
def deps do
  [
    {:patterns, "~> 0.0.1"}
  ]
end
```

Patterns requires Elixir `~> 1.15`. `ecto` is a runtime dependency because `Patterns.Queryable` builds Ecto queries.

## Status

Patterns is early and intentionally small. The stable surface is centered on `Patterns.Queryable`, `Patterns.Middleware`, `Patterns.Delegate`, `Patterns.Utils`, `Patterns.Utils.ETS`, and `Patterns.Guards`.

## Development

Fetch dependencies and run tests:

```bash
mix deps.get
mix test
```

Run the project lint checks:

```bash
mix lint
```

The lint alias runs compilation with warnings as errors, unused dependency checks, formatting checks, Credo, and Dialyzer.

Coverage is reported through ExCoveralls:

```bash
mix coveralls
```

## License

MIT. See `LICENSE`.
