Skuld.Effects.Port.Contract (skuld v0.2.3)

View Source

Macro 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 via Port.with_handler/2.
  • MyContract.Provider — callbacks return computation(return_type). Use for effectful implementations that need to be wrapped with Port.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 dispatches Throw on {: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 applies unwrap_fn to 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
end

This 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: ...
end

Handler 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

defport(spec, opts \\ [])

(macro)

Define a typed port operation.

Syntax

defport function_name(param :: type(), ...) :: return_type()
defport function_name(param :: type(), ...) :: return_type(), bang: option

Each 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} unwrapping
  • false — suppress bang generation
  • unwrap_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.