Contexir

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:

def deps do
[
{:contexir, "0.3.0"}
]
end

Then fetch dependencies:

mix deps.get

Core Concepts

Base Modules

Use Contexir in modules whose functions can be refined by layers:

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.

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:

ModeDescription
:aroundWraps the next layer or primary function. Call continue/1 to proceed.
:beforeRuns before the primary function.
:afterRuns 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:

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:

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:

deflayer SecureLayer do
use_layers([Authentication, Audit])
end

Layers can also declare composition relationships:

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.

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:

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:

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:

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:

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:

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:

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

License

Unlicense

Learn More