Current section
Files
Jump to
Current section
Files
README.md
<p align="center">
<img src="assets/patterns.png" alt="Patterns" width="903" height="346" />
</p>
[](https://hex.pm/packages/patterns)
[](https://hexdocs.pm/patterns/)
[](https://github.com/minnasoft/patterns/actions)
[](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`.