Current section
Files
Jump to
Current section
Files
README.md
# Contexir

Context-oriented programming for Elixir.
Contexir lets you define layers that refine function behavior at runtime.
Layers are activated dynamically, scoped to the current BEAM process, and can be
selected explicitly or from context values.
## Installation
Add Contexir to your `mix.exs`:
```elixir
def deps do
[
{:contexir, "0.3.0"}
]
end
```
Then fetch dependencies:
```bash
mix deps.get
```
## Core Concepts
### Base Modules
Use `Contexir` in modules whose functions can be refined by layers:
```elixir
defmodule Account do
use Contexir
def withdraw(account, amount, _ctx) do
%{account | balance: account.balance - amount}
end
end
```
Contexir expects the final argument to be a context value, usually a map.
### Layers
A layer defines partial behavior for an existing function.
```elixir
import Contexir.Layer
deflayer LoggingLayer do
refine Account do
defpartial withdraw(_account, amount, _ctx), mode: :before do
IO.puts("withdrawing #{amount}")
end
defpartial withdraw(account, amount, _ctx), mode: :around do
result = continue([account, amount])
IO.puts("new balance: #{result.balance}")
result
end
defpartial withdraw(_account, _amount, _ctx), mode: :after do
IO.puts("withdrawal complete")
end
end
end
```
Supported partial modes:
| Mode | Description |
| --- | --- |
| `:around` | Wraps the next layer or primary function. Call `continue/1` to proceed. |
| `:before` | Runs before the primary function. |
| `:after` | Runs after the primary function returns. |
An `:around` partial may short-circuit the call by not calling `continue/1`.
### Dynamic Activation
Activate layers for a single call with `Contexir.with_layers/2`:
```elixir
require Contexir
Contexir.with_layers(
[LoggingLayer],
Account.withdraw(%{balance: 100}, 25, %{})
)
```
Layer activation and context are process-local. They do not automatically cross
`spawn`, `Task.async`, or other BEAM process boundaries.
## Execution Order
For active layers `[A, B]`, dispatch follows this order:
```text
A around
B around
A before
B before
primary
B after
A after
B around end
A around end
```
`:after` partials run inside-out, after the primary function returns.
## Layer Composition
Layers can include other layers:
```elixir
deflayer SecureLayer do
use_layers([Authentication, Audit])
end
```
Layers can also declare composition relationships:
```elixir
deflayer SecureCheckout do
requires(Authentication)
conflicts_with(GuestCheckout)
before(Audit)
after_layer(RateLimit)
end
```
`requires` and `conflicts_with` are validated by `Contexir.Layer.resolve/1` and
before activation through `Contexir.with_layers/2`.
```elixir
Contexir.Layer.resolve([Authentication, SecureCheckout, Audit])
#=> {:ok, [Authentication, SecureCheckout, Audit]}
```
`before` and `after_layer` are recorded as metadata and reserved for precedence
ordering. Runtime ordering from those relationships is not implemented yet.
## Layer Introspection
Use `Contexir.Layer.info/1` to inspect layer metadata:
```elixir
Contexir.Layer.info(SecureCheckout)
#=> %{
#=> module: SecureCheckout,
#=> partials: [...],
#=> includes: [],
#=> requires: [Authentication],
#=> conflicts_with: [GuestCheckout],
#=> before: [Audit],
#=> after: [RateLimit],
#=> predicate?: false
#=> }
```
## Declarative Context Activation
Use `defcontext` to select layers from context values:
```elixir
defcontext CheckoutContext do
layer(Authentication, when: & &1[:user])
layer(Audit, when: & &1[:audit?])
layer(FraudReview, when: &(&1[:risk_score] >= 70))
end
```
Then dispatch with layers selected from the context:
```elixir
Contexir.with_context(
CheckoutContext,
%{user: "alice", audit?: true, risk_score: 82},
Checkout.submit(cart, %{})
)
```
The provided context replaces the final argument of the target call.
## Context API
`Contexir.Context` exposes helpers for process-local context:
```elixir
Contexir.Context.with_context(%{request_id: "req-123"}, fn ->
Contexir.Context.get(:request_id)
Contexir.Context.put(:user, "alice")
Contexir.Context.update(:attempts, 1, &(&1 + 1))
Contexir.Context.current()
end)
```
For lower-level scope work, `Contexir.Context.with_scope/3` temporarily installs
both active layers and context.
## Task Propagation
Plain BEAM tasks do not inherit Contexir state. Use `Contexir.Task` when a task
should run with the caller's current active layers and context:
```elixir
Contexir.Context.with_scope([TraceLayer], %{request_id: "req-123"}, fn ->
task =
Contexir.Task.async(fn ->
Contexir.with_layers(
[],
Worker.run("job", Contexir.Context.current())
)
end)
Contexir.Task.await(task)
end)
```
`Contexir.Task.async/1` wraps Elixir's linked `Task.async/1`.
## Examples
The `examples/` directory contains runnable scripts:
```bash
mix run examples/basic_layers.exs
mix run examples/composition_resolution.exs
mix run examples/declarative_context.exs
mix run examples/task_propagation.exs
mix run examples/checkout_flow.exs
```
`examples/checkout_flow.exs` is the most complete example. It combines
declarative context rules, composition validation, layered dispatch, context
updates, and task propagation.
## Current Limitations
* `before` and `after_layer` relationships are metadata only; precedence
ordering and cycle detection are not implemented yet.
* Context and active layers are process-local. Use `Contexir.Task` for explicit
task propagation.
* `Contexir.explain` and telemetry integration are not implemented yet.
## License
Unlicense
## Learn More
* [Context-Oriented Programming](https://en.wikipedia.org/wiki/Context-oriented_programming)
* [Aspect-Oriented Programming](https://en.wikipedia.org/wiki/Aspect-oriented_programming)