⚠️ EXPERIMENTAL / UNDER ACTIVE DEVELOPMENT ⚠️

AshPhoenixGenApi

An Ash Framework extension for generating PhoenixGenApi function configurations from Ash resources and domains.

AshPhoenixGenApi bridges the Ash Framework and PhoenixGenApi by allowing you to define PhoenixGenApi endpoints directly in your Ash resource and domain DSLs. It automatically generates PhoenixGenApi.Structs.FunConfig structs from your Ash actions, including type mappings, argument ordering, and configuration defaults.

Features

Installation

Add ash_phoenix_gen_api to your list of dependencies in mix.exs:

def deps do
  [
    {:ash_phoenix_gen_api, "~> 0.1.0"},
    {:ash, "~> 3.5"},
    {:phoenix_gen_api, "~> 2.1"}
  ]
end

Then fetch dependencies:

mix deps.get

Quick Start

1. Add the Resource extension

Add AshPhoenixGenApi.Resource to your Ash resources:

defmodule MyApp.Chat.DirectMessage do
  use Ash.Resource,
    domain: MyApp.Chat,
    extensions: [AshPhoenixGenApi.Resource]

  attributes do
    uuid_primary_key :id
    attribute :from_user_id, :uuid do
      public? true
    end
    attribute :to_user_id, :uuid do
      public? true
    end
    attribute :content, :string do
      public? true
      allow_nil? true
    end
    attribute :reply_to_id, :uuid do
      public? true
      allow_nil? true
    end
    attribute :file_id, :uuid do
      public? true
      allow_nil? true
    end
  end

  actions do
    create :create do
      accept [:from_user_id, :to_user_id, :content, :reply_to_id, :file_id]
    end

    read :read do
      primary? true
    end

    update :update_content do
      accept [:content]
    end

    destroy :destroy
  end

  gen_api do
    service "chat"
    nodes {ClusterHelper, :get_nodes, [:chat]}
    choose_node_mode :random
    timeout 5_000
    response_type :async
    request_info true
    version "0.0.1"

    action :create do
      request_type "send_direct_message"
      timeout 10_000
      check_permission {:arg, "from_user_id"}
    end

    action :read do
      request_type "get_conversation"
      timeout 5_000
    end

    action :update_content do
      request_type "update_content"
      response_type :sync
    end
  end
end

2. Add the Domain extension

Add AshPhoenixGenApi.Domain to your Ash domain:

defmodule MyApp.Chat do
  use Ash.Domain,
    extensions: [AshPhoenixGenApi.Domain]

  gen_api do
    service "chat"
    nodes {ClusterHelper, :get_nodes, [:chat]}
    choose_node_mode :random
    version "0.0.1"
    supporter_module MyApp.Chat.GenApiSupporter
  end

  resources do
    resource MyApp.Chat.DirectMessage
    resource MyApp.Chat.GroupMessage
  end
end

3. Use the generated supporter module

After compilation, MyApp.Chat.GenApiSupporter is auto-generated:

# Get all FunConfigs (for PhoenixGenApi pull)
MyApp.Chat.GenApiSupporter.fun_configs()
#=> [%PhoenixGenApi.Structs.FunConfig{request_type: "send_direct_message", ...}, ...]

# Get config for remote pull
MyApp.Chat.GenApiSupporter.get_config(:gateway_1)
#=> {:ok, [%PhoenixGenApi.Structs.FunConfig{...}, ...]}

# Get config version
MyApp.Chat.GenApiSupporter.get_config_version(:gateway_1)
#=> {:ok, "0.0.1"}

# Find a specific FunConfig by request_type
MyApp.Chat.GenApiSupporter.get_fun_config("send_direct_message")
#=> %PhoenixGenApi.Structs.FunConfig{request_type: "send_direct_message", ...}

# List all request types
MyApp.Chat.GenApiSupporter.list_request_types()
#=> ["send_direct_message", "get_conversation", ...]

4. Configure the gateway node

On the Phoenix gateway node, configure phoenix_gen_api in config.exs:

config :phoenix_gen_api, :gen_api,
  service_configs: [
    %{
      service: "chat",
      nodes: {ClusterHelper, :get_nodes, [:chat]},
      module: MyApp.Chat.GenApiSupporter,
      function: :get_config,
      args: [:gateway_1]
    }
  ]

DSL Reference

Resource DSL (gen_api)

The gen_api section is added to Ash resources when using AshPhoenixGenApi.Resource.

Section Options

Option Type Default Description
service:atom | :stringrequired Service name for routing
nodes:local | {:list, :atom} | {:tuple, [:atom, :atom, {:list, :any}]}:local Default target nodes
choose_node_mode:random | :hash | :round_robin | {:hash, string}:random Default node selection strategy
timeoutpos_integer | :infinity5000 Default timeout in milliseconds
response_type:sync | :async | :stream | :none:async Default response mode
request_info:booleantrue Default for passing request info
check_permissionfalse | :any_authenticated | {:arg, string} | {:role, [string]}false Default permission check mode
version:string"0.0.1" Default version string
retrypos_integer | {:same_node, pos_integer} | {:all_nodes, pos_integer}nil Default retry configuration

Action Entity Options

Option Type Default Description
name:atomrequired Ash action name to expose
request_type:string action name as string PhoenixGenApi request type string
timeoutpos_integer | :infinity section default Timeout in milliseconds
response_type:sync | :async | :stream | :none section default Response mode
request_info:boolean section default Whether to pass request info
check_permission see above section default Permission check mode
choose_node_mode see above section default Node selection strategy
nodes see above section default Target nodes
retry see above section default Retry configuration
version:string section default API version string
mfa{module, atom, list} auto-generated Explicit MFA tuple
arg_typesmap | nil auto-derived Explicit argument types
arg_orders[string] | nil auto-derived Explicit argument order
disabled:booleanfalse Disable this endpoint

Domain DSL (gen_api)

The gen_api section is added to Ash domains when using AshPhoenixGenApi.Domain.

Section Options

Option Type Default Description
service:atom | :string Service name (used as default for resources)
nodes see above :local Default target nodes
choose_node_mode see above :random Default node selection strategy
timeout see above 5000 Default timeout
response_type see above :async Default response mode
request_info:booleantrue Default for passing request info
check_permission see above false Default permission check mode
version:string"0.0.1" Default version string
retry see above nil Default retry configuration
supporter_module:atomrequired Module name for auto-generated supporter
define_supporter?:booleantrue Whether to auto-generate the supporter module

Type Mapping

Ash types are automatically mapped to PhoenixGenApi argument types:

Ash Type PhoenixGenApi Type
:string, Ash.Type.String:string
:ci_string, Ash.Type.CiString:string
:integer, Ash.Type.Integer:num
:float, Ash.Type.Float:num
:decimal, Ash.Type.Decimal:num
:uuid, Ash.Type.UUID:string
:uuid_v7, Ash.Type.UUIDv7:string
:boolean, Ash.Type.Boolean:string
:date, Ash.Type.Date:string
:time, Ash.Type.Time:string
:datetime, Ash.Type.DateTime:string
:utc_datetime, Ash.Type.UtcDateTime:string
:naive_datetime, Ash.Type.NaiveDateTime:string
:atom, Ash.Type.Atom:string
:map, Ash.Type.Map:string
:json, Ash.Type.Json:string
:binary, Ash.Type.Binary:string
:term, Ash.Type.Term:string
{:array, :string}{:list_string, 1000, 50}
{:array, :integer}{:list_num, 1000}
{:array, :uuid}{:list_string, 1000, 50}
{:array, :float}{:list_num, 1000}

See AshPhoenixGenApi.TypeMapper for the complete mapping and customization options.

Resolution Order

Configuration values are resolved in this order (highest priority first):

  1. Action-level explicit config — e.g., action :foo do timeout 10_000 end
  2. Resource section-level defaults — e.g., gen_api do timeout 5_000 end
  3. Domain section-level defaults — e.g., gen_api do timeout 5_000 end
  4. Built-in defaults — e.g., timeout defaults to 5000

For arg_types and arg_orders:

  1. Explicit arg_types/arg_orders on the action entity
  2. Auto-derived from the Ash action's accepted attributes and arguments

For mfa:

  1. Explicit mfa on the action entity
  2. Auto-generated as {ResourceModule, :action_name, []}

Auto-Derived Arguments

When arg_types and arg_orders are not explicitly set, the extension automatically derives them from the Ash action:

Example — given this Ash action:

actions do
  create :create do
    accept [:from_user_id, :to_user_id, :content, :reply_to_id, :file_id]
  end
end

The auto-derived arg_types and arg_orders would be:

arg_types: %{
  "from_user_id" => :string,  # UUID → :string
  "to_user_id" => :string,    # UUID → :string
  "content" => :string,       # String → :string
  "reply_to_id" => :string,   # UUID → :string
  "file_id" => :string        # UUID → :string
},
arg_orders: ["from_user_id", "to_user_id", "content", "reply_to_id", "file_id"]

Generated Supporter Module

The domain extension auto-generates a supporter module that implements the PhoenixGenApi client config interface. This module:

  1. Aggregates FunConfigs from all resources in the domain that have AshPhoenixGenApi.Resource
  2. Implements get_config/1 — Returns {:ok, fun_configs()} for PhoenixGenApi pull
  3. Implements get_config_version/1 — Returns {:ok, version} for version checking
  4. Implements fun_configs/0 — Returns the aggregated list of FunConfig structs
  5. Implements list_request_types/0 — Returns all available request type strings
  6. Implements get_fun_config/1 — Returns a specific FunConfig by request_type

The generated module matches the interface described in the PhoenixGenApi documentation for remote config pulling.

Introspection

Resource Introspection

# Check if a resource has gen_api configured
AshPhoenixGenApi.Resource.Info.has_gen_api?(MyApp.Chat.DirectMessage)
#=> true

# Get the service name
AshPhoenixGenApi.Resource.Info.gen_api_service(MyApp.Chat.DirectMessage)
#=> "chat"

# Get all action configs
AshPhoenixGenApi.Resource.Info.gen_api_actions(MyApp.Chat.DirectMessage)
#=> [%ActionConfig{name: :create, ...}, ...]

# Get a specific action config
AshPhoenixGenApi.Resource.Info.action(MyApp.Chat.DirectMessage, :create)
#=> %ActionConfig{name: :create, request_type: "send_direct_message", ...}

# Get only enabled actions
AshPhoenixGenApi.Resource.Info.enabled_actions(MyApp.Chat.DirectMessage)
#=> [%ActionConfig{disabled: false, ...}, ...]

# Get the generated FunConfig structs
AshPhoenixGenApi.Resource.Info.fun_configs(MyApp.Chat.DirectMessage)
#=> [%PhoenixGenApi.Structs.FunConfig{...}, ...]

# Get a specific FunConfig by request_type
AshPhoenixGenApi.Resource.Info.fun_config(MyApp.Chat.DirectMessage, "send_direct_message")
#=> %PhoenixGenApi.Structs.FunConfig{request_type: "send_direct_message", ...}

# Get all request types
AshPhoenixGenApi.Resource.Info.request_types(MyApp.Chat.DirectMessage)
#=> ["send_direct_message", "get_conversation", ...]

# Get effective values with fallback resolution
AshPhoenixGenApi.Resource.Info.effective_timeout(MyApp.Chat.DirectMessage, :create)
#=> 10_000
AshPhoenixGenApi.Resource.Info.effective_mfa(MyApp.Chat.DirectMessage, :create)
#=> {MyApp.Chat.DirectMessage, :create, []}

Domain Introspection

# Check if a domain has gen_api configured
AshPhoenixGenApi.Domain.Info.has_gen_api?(MyApp.Chat)
#=> true

# Get the supporter module name
AshPhoenixGenApi.Domain.Info.supporter_module(MyApp.Chat)
#=> MyApp.Chat.GenApiSupporter

# Get all resources with gen_api configured
AshPhoenixGenApi.Domain.Info.resources_with_gen_api(MyApp.Chat)
#=> [MyApp.Chat.DirectMessage, MyApp.Chat.GroupMessage]

# Get aggregated FunConfigs from all resources
AshPhoenixGenApi.Domain.Info.fun_configs(MyApp.Chat)
#=> [%PhoenixGenApi.Structs.FunConfig{...}, ...]

# Get all request types across all resources
AshPhoenixGenApi.Domain.Info.all_request_types(MyApp.Chat)
#=> ["send_direct_message", "get_conversation", ...]

# Get a configuration summary
AshPhoenixGenApi.Domain.Info.summary(MyApp.Chat)
#=> %{
#=>   service: "chat",
#=>   version: "0.0.1",
#=>   supporter_module: MyApp.Chat.GenApiSupporter,
#=>   total_fun_configs: 5,
#=>   resources: [
#=>     %{resource: MyApp.Chat.DirectMessage, request_types: ["send_direct_message", ...]},
#=>     %{resource: MyApp.Chat.GroupMessage, request_types: ["send_group_message", ...]}
#=>   ]
#=> }

Compile-Time Verification

The extension performs compile-time verification to catch configuration errors early:

Resource Verification

Domain Verification

Example: Chat Service

Here's a complete example matching the ChatService pattern from PhoenixGenApi:

defmodule MyApp.Chat.DirectMessage do
  use Ash.Resource,
    domain: MyApp.Chat,
    extensions: [AshPhoenixGenApi.Resource]

  attributes do
    uuid_primary_key :id
    attribute :from_user_id, :uuid, public?: true
    attribute :to_user_id, :uuid, public?: true
    attribute :content, :string, public?: true, allow_nil?: true
    attribute :reply_to_id, :uuid, public?: true, allow_nil?: true
    attribute :file_id, :uuid, public?: true, allow_nil?: true
    attribute :order, :integer, public?: true
    attribute :read, :boolean, public?: true, allow_nil?: true, default: false
    attribute :deleted, :boolean, public?: true, allow_nil?: true, default: false
  end

  actions do
    create :create do
      accept [:from_user_id, :to_user_id, :content, :reply_to_id, :file_id]
    end

    read :read do
      primary? true
    end

    update :mark_read do
      accept [:read]
    end

    destroy :destroy
  end

  gen_api do
    service "chat"
    nodes {ClusterHelper, :get_nodes, [:chat]}
    choose_node_mode :random
    timeout 5_000
    response_type :async
    request_info true
    version "0.0.1"

    action :create do
      request_type "send_direct_message"
      timeout 10_000
      check_permission {:arg, "from_user_id"}
    end

    action :read do
      request_type "get_conversation"
    end

    action :mark_read do
      request_type "mark_direct_messages_as_read"
    end
  end
end

defmodule MyApp.Chat do
  use Ash.Domain,
    extensions: [AshPhoenixGenApi.Domain]

  gen_api do
    service "chat"
    nodes {ClusterHelper, :get_nodes, [:chat]}
    choose_node_mode :random
    version "0.0.1"
    supporter_module MyApp.Chat.GenApiSupporter
  end

  resources do
    resource MyApp.Chat.DirectMessage
  end
end

This generates the same FunConfig structures that were previously hand-written in ChatService.Interface.GenApi.Supporter, but now derived automatically from your Ash resource definitions.

Modules

Module Description
AshPhoenixGenApi Top-level module with documentation and helpers
AshPhoenixGenApi.Resource Resource-level DSL extension
AshPhoenixGenApi.Resource.Info Resource introspection helpers
AshPhoenixGenApi.Resource.ActionConfig Action configuration struct
AshPhoenixGenApi.Domain Domain-level DSL extension
AshPhoenixGenApi.Domain.Info Domain introspection helpers
AshPhoenixGenApi.TypeMapper Ash type to PhoenixGenApi type mapping
AshPhoenixGenApi.Transformers.DefineFunConfigs Resource transformer
AshPhoenixGenApi.Transformers.DefineDomainSupporter Domain transformer
AshPhoenixGenApi.Verifiers.VerifyActionConfigs Resource verifier
AshPhoenixGenApi.Verifiers.VerifyDomainConfig Domain verifier

License

MPL 2.0