defmodule Skuld.Adapter.EffectfulContract do @moduledoc """ Generates an effectful behaviour from a `DoubleDown.Contract`. `use Skuld.Adapter.EffectfulContract` reads the operations from a plain DoubleDown contract and generates: * Effectful `@callback` declarations with `computation(return_type)` return types on the using module * `__callbacks__/0` — copied from the DoubleDown contract * `__port_effectful__?/0` — marker for effectful resolver auto-detection The effectful contract module *is* the effectful behaviour. Effectful implementations declare `@behaviour MyApp.Todos.Effectful`. ## Usage # Plain contract (DoubleDown) defmodule MyApp.Todos.Contract do use DoubleDown.Contract defcallback get_todo(id :: String.t()) :: {:ok, Todo.t()} | {:error, term()} end # Effectful contract (Skuld) defmodule MyApp.Todos.Effectful do use Skuld.Adapter.EffectfulContract, double_down_contract: MyApp.Todos.Contract end # Effectful facade defmodule MyApp.Todos do use Skuld.Effects.Port.EffectfulFacade, contract: MyApp.Todos.Effectful end ## Effectful Implementation defmodule MyApp.Todos.EffectfulImpl do @behaviour MyApp.Todos.Effectful def get_todo(id) do {:ok, %Todo{id: id}} end end ## Options * `:double_down_contract` (required) — the DoubleDown contract module that defines `__callbacks__/0` via `use DoubleDown.Contract`. """ @doc false defmacro __using__(opts) do double_down_contract = Keyword.fetch!(opts, :double_down_contract) quote do @skuld_double_down_contract unquote(double_down_contract) @before_compile Skuld.Adapter.EffectfulContract end end @doc false defmacro __before_compile__(env) do double_down_contract = Module.get_attribute(env.module, :skuld_double_down_contract) unless Code.ensure_loaded?(double_down_contract) do raise CompileError, description: "DoubleDown contract module #{inspect(double_down_contract)} is not loaded. " <> "Ensure it is compiled before #{inspect(env.module)}.", file: env.file, line: 0 end unless function_exported?(double_down_contract, :__callbacks__, 0) do raise CompileError, description: "#{inspect(double_down_contract)} does not define __callbacks__/0. " <> "Did you `use DoubleDown.Contract` and add `defcallback` declarations?", file: env.file, line: 0 end operations = double_down_contract.__callbacks__() callbacks = Enum.map(operations, &generate_effectful_callback/1) escaped_ops = Macro.escape(operations) quote do unquote_splicing(callbacks) @doc false def __callbacks__, do: unquote(escaped_ops) @doc false def __port_effectful__?, do: true @doc """ Declare this module as an effectful implementation of `#{inspect(unquote(env.module))}`. Generates `@behaviour`, `__port_effectful__?/0`, and compile-time callback verification. """ defmacro __using__(_opts) do contract = unquote(env.module) quote do @behaviour unquote(contract) def __port_effectful__?, do: true end end end end # ------------------------------------------------------------------- # Code Generation: Effectful @callback declarations # ------------------------------------------------------------------- defp generate_effectful_callback(%{ name: name, params: param_names, param_types: param_types, return_type: return_type }) do callback_params = Enum.zip(param_names, param_types) |> Enum.map(fn {pname, ptype} -> {:"::", [], [{pname, [], nil}, ptype]} end) comp_type = {:computation, [], [return_type]} quote do @callback unquote(name)(unquote_splicing(callback_params)) :: Skuld.Comp.Types.unquote(comp_type) end end end