Skuld.Effects.Port.Contract (skuld v0.2.3)
View SourceMacro for defining typed port contracts with defport declarations.
A Port contract defines a set of operations, generating:
- Consumer behaviour (
__MODULE__.Consumer) — plain Elixir callbacks for non-effectful implementations - Provider behaviour (
__MODULE__.Provider) — computation-returning callbacks for effectful implementations - Caller functions — typed public API returning
computation(return_type), which also satisfy the Provider behaviour - Bang variants — unwrap
{:ok, v}or dispatch Throw (when applicable) - Key helpers — for test stub matching
- Introspection —
__port_operations__/0
Consumer vs Provider
The contract generates two behaviour modules:
MyContract.Consumer— callbacks return plain values. Use for non-effectful implementations that are called viaPort.with_handler/2.MyContract.Provider— callbacks returncomputation(return_type). Use for effectful implementations that need to be wrapped withPort.Provider.
The contract module's own caller functions satisfy the Provider behaviour (they return computations that emit Port effects).
Bang Variant Generation
Bang variants (name!) are generated based on the return type:
- Auto-detect (default): If the return type contains
{:ok, T}, a bang variant is generated that unwraps{:ok, value}or dispatchesThrowon{:error, reason}. If no{:ok, T}is found, no bang is generated. bang: true: Force bang generation with standard{:ok, v}/{:error, r}unwrapping, even if the return type doesn't match the pattern.bang: false: Suppress bang generation even if the return type matches.bang: unwrap_fn: Generate a bang that first appliesunwrap_fnto the raw result (which must return{:ok, v}or{:error, r}), then unwraps.
Example
defmodule MyApp.Repository do
use Skuld.Effects.Port.Contract
# Auto-detected: has {:ok, T}, bang generated automatically
defport get_todo(tenant_id :: String.t(), id :: String.t()) ::
{:ok, Todo.t()} | {:error, term()}
# No {:ok, T} in return type, no bang generated
defport find_user(id :: String.t()) :: User.t() | nil
# Force bang with custom unwrap (nil → error, value → ok)
defport find_user(id :: String.t()) :: User.t() | nil,
bang: fn
nil -> {:error, :not_found}
user -> {:ok, user}
end
# Suppress bang even though return type has {:ok, T}
defport raw_query(sql :: String.t()) ::
{:ok, term()} | {:error, term()},
bang: false
endThis generates:
# Consumer behaviour (plain Elixir)
MyApp.Repository.Consumer
@callback get_todo(String.t(), String.t()) :: {:ok, Todo.t()} | {:error, term()}
# Provider behaviour (computation-returning)
MyApp.Repository.Provider
@callback get_todo(String.t(), String.t()) :: computation({:ok, Todo.t()} | {:error, term()})
# Caller (returns computation, satisfies Provider behaviour)
@spec get_todo(String.t(), String.t()) :: Types.computation({:ok, Todo.t()} | {:error, term()})
def get_todo(tenant_id, id)
# Bang (unwraps or throws) — auto-detected from {:ok, T}
@spec get_todo!(String.t(), String.t()) :: Types.computation(Todo.t())
def get_todo!(tenant_id, id)
# Key helper (for test stubs)
def key(:get_todo, tenant_id, id)Consumer Implementation
defmodule MyApp.Repository.Ecto do
@behaviour MyApp.Repository.Consumer
@impl true
def get_todo(tenant_id, id), do: ...
endHandler Installation
my_comp
|> Port.with_handler(%{MyApp.Repository => MyApp.Repository.Ecto})
|> Comp.run!()Provider Implementation (Effectful)
For the reverse direction — plain Elixir code calling into effectful
implementations — see Skuld.Effects.Port.Provider.
# Effectful implementation satisfies Provider behaviour
defmodule MyApp.Repository.Effectful do
@behaviour MyApp.Repository.Provider
defcomp get_todo(tenant_id, id) do
# ... effectful code returning computation(return_type)
end
end
# Provider adapter satisfies Consumer behaviour
defmodule MyApp.Repository.Effectful.Adapter do
use Skuld.Effects.Port.Provider,
contract: MyApp.Repository,
impl: MyApp.Repository.Effectful,
stack: &MyApp.Stacks.repository/1
end
Summary
Functions
Define a typed port operation.
Functions
Define a typed port operation.
Syntax
defport function_name(param :: type(), ...) :: return_type()
defport function_name(param :: type(), ...) :: return_type(), bang: optionEach defport declaration generates a caller function, @callback, key
helper, and @doc strings. A bang variant is generated based on the bang
option:
- omitted — auto-detect: generate bang only if return type contains
{:ok, T} true— force standard{:ok, v}/{:error, r}unwrappingfalse— suppress bang generationunwrap_fn— generate bang using custom unwrap function, which receives the raw result and must return{:ok, v}or{:error, r}
Default arguments (\\) are not supported — use a wrapper function instead.