defmodule AshPhoenixGenApi.Domain do @moduledoc """ Ash Domain extension for PhoenixGenApi configuration. This extension provides domain-level configuration for PhoenixGenApi, including default service settings and automatic generation of a "supporter" module that aggregates FunConfigs from all resources in the domain. The generated supporter module implements the `PhoenixGenApi` client config interface (`get_config/1`, `get_config_version/1`), allowing gateway nodes to pull API configurations from service nodes. ## Usage Add the extension 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 timeout 5_000 response_type :async request_info true version "0.0.1" supporter_module MyApp.Chat.GenApiSupporter end resources do resource MyApp.Chat.DirectMessage resource MyApp.Chat.GroupMessage end end This will generate `MyApp.Chat.GenApiSupporter` with: defmodule MyApp.Chat.GenApiSupporter do @moduledoc \"\"\" Auto-generated PhoenixGenApi supporter module for MyApp.Chat. Aggregates FunConfigs from all resources in the domain. \"\"\" alias PhoenixGenApi.Structs.FunConfig def get_config(remote_id) do {:ok, fun_configs()} end def get_config_version(remote_id) do {:ok, "0.0.1"} end def fun_configs do MyApp.Chat.DirectMessage.__ash_phoenix_gen_api_fun_configs__() ++ MyApp.Chat.GroupMessage.__ash_phoenix_gen_api_fun_configs__() end def list_request_types do fun_configs() |> Enum.map(& &1.request_type) end def get_fun_config(request_type) do fun_configs() |> Enum.find(&(&1.request_type == request_type)) end end ## Domain-Level Defaults Domain-level settings serve as defaults for all resources in the domain that use `AshPhoenixGenApi.Resource`. Each resource can override these defaults in its own `gen_api` section. Resolution order for any setting: 1. Resource action-level (e.g., `action :foo do timeout 10_000 end`) 2. Resource section-level (e.g., `gen_api do timeout 5_000 end`) 3. Domain section-level (this extension, e.g., `gen_api do timeout 5_000 end`) 4. Built-in defaults (e.g., timeout defaults to 5000) ## Gateway Node Configuration On the Phoenix gateway node, configure the supporter module 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] } ] ## Active Push Configuration In addition to the pull-based model (where the gateway pulls config from service nodes), you can configure the supporter module to **actively push** its configuration to gateway nodes. Set `push_nodes` to specify which gateway nodes to push to: gen_api do service "chat" supporter_module MyApp.Chat.GenApiSupporter version "0.0.1" push_nodes [:"gateway1@host", :"gateway2@host"] # Or use an MFA tuple for runtime resolution: # push_nodes {ClusterHelper, :get_gateway_nodes, []} end This adds the following functions to the generated supporter module: - `build_push_config/0` - Builds a `PushConfig` struct from the domain config - `push_to_gateway/2` - Pushes config to a specific gateway node - `push_on_startup/2` - Pushes config on application startup - `verify_on_gateway/2` - Verifies config version on a gateway node - `resolve_push_nodes/0` - Resolves `push_nodes` at runtime - `push_to_configured_nodes/1` - Pushes to all configured push_nodes Example usage during application startup: def start(_type, _args) do # ... start supervision tree, then: MyApp.Chat.GenApiSupporter.push_to_configured_nodes() # Or push to a specific node: MyApp.Chat.GenApiSupporter.push_on_startup(:"gateway1@host") end """ @gen_api %Spark.Dsl.Section{ name: :gen_api, describe: """ Configure PhoenixGenApi at the domain level. Domain-level settings serve as defaults for all resources in the domain that use `AshPhoenixGenApi.Resource`. Each resource can override these defaults in its own `gen_api` section. The `supporter_module` option defines the name of the module that will be auto-generated to aggregate FunConfigs from all resources. This module implements the PhoenixGenApi client config interface. """, examples: [ """ 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" supporter_module MyApp.Chat.GenApiSupporter end """, """ # Minimal configuration gen_api do service "chat" supporter_module MyApp.Chat.GenApiSupporter end """ ], schema: [ service: [ type: :any, doc: """ The service name for this domain's API endpoints. This serves as the default for all resources in the domain. Accepts a string or atom. Example: `"chat"`, `"user_service"`, `:notification` """ ], nodes: [ type: :any, default: :local, doc: """ Default target nodes for all resources in this domain. Can be: - A list of node atoms: `[:"node1@host", :"node2@host"]` - An MFA tuple that returns a node list at runtime: `{ClusterHelper, :get_nodes, [:chat]}` - `:local` - Execute on the local node (default) """ ], choose_node_mode: [ type: :any, default: :random, doc: """ Default node selection strategy for all resources in this domain. - `:random` - Select a random node (default) - `:hash` - Hash-based selection using request_type - `{:hash, key}` - Hash-based selection using the specified argument key - `:round_robin` - Round-robin across nodes """ ], timeout: [ type: :any, default: 5_000, doc: """ Default timeout in milliseconds for all resources in this domain. Individual resources and actions can override this. Accepts a positive integer or `:infinity`. """ ], response_type: [ type: :atom, default: :async, doc: """ Default response mode for all resources in this domain. - `:sync` - Client waits for the result - `:async` - Client receives an ack, then the result later (default) - `:stream` - Client receives streamed chunks - `:none` - Fire and forget """ ], request_info: [ type: :boolean, default: true, doc: """ Default for whether to pass request info (user_id, device_id, request_id) as the last argument to the MFA function for all resources in this domain. """ ], check_permission: [ type: :any, default: false, doc: """ Default permission check mode for all resources in this domain. - `false` - No permission check (default) - `:any_authenticated` - Requires a valid user_id - `{:arg, "arg_name"}` - The specified argument must match user_id - `{:role, ["admin"]}` - User must have one of the listed roles """ ], permission_callback: [ type: :any, default: nil, doc: """ Default permission callback MFA for all resources in this domain. When set, takes precedence over `check_permission`. Accepts `{Module, :function, []}` or `nil`. The callback function receives `request_type` (string) and `args` (map) as arguments and returns `true` (continue) or `false` (permission denied). The callback function signature: @callback check_permission(request_type :: String.t(), args :: map()) :: boolean() Example callback: def check_permission(request_type, args) do case request_type do "delete_user" -> args["role"] == "admin" "update_profile" -> args["user_id"] == args["target_user_id"] _ -> true end end When both `permission_callback` and `check_permission` are set, `permission_callback` takes precedence and is stored as `{:callback, {Module, :function, []}}` in the FunConfig's `check_permission` field. Defaults to `nil`. """ ], version: [ type: :string, default: "0.0.1", doc: """ Default version string for all resources in this domain. Used for PhoenixGenApi API versioning. """ ], retry: [ type: :any, doc: """ Default retry configuration for all resources in this domain. - `nil` - No retry (default) - A positive number `n` - Equivalent to `{:all_nodes, n}` - `{:same_node, n}` - Retry on the same node(s) - `{:all_nodes, n}` - Retry across all available nodes """ ], supporter_module: [ type: :atom, required: true, doc: """ The name of the module to generate that will serve as the PhoenixGenApi supporter for this domain. This module will be auto-generated with functions: - `get_config/1` - Returns `{:ok, fun_configs()}` for PhoenixGenApi pull - `get_config_version/1` - Returns `{:ok, version}` for version checking - `fun_configs/0` - Returns the aggregated list of FunConfig structs - `list_request_types/0` - Returns all available request type strings - `get_fun_config/1` - Returns a specific FunConfig by request_type Example: `MyApp.Chat.GenApiSupporter` """ ], define_supporter?: [ type: :boolean, default: true, doc: """ Whether to auto-generate the supporter module. Set to `false` if you want to define the supporter module manually. When `false`, the extension will still collect FunConfigs from resources but will not generate the supporter module. You can use `AshPhoenixGenApi.Domain.Info.fun_configs/1` to get the aggregated FunConfigs and build your own supporter module. """ ], push_nodes: [ type: :any, default: nil, doc: """ Target gateway nodes to push config to. Can be: - A list of node atoms: `[:"gateway1@host", :"gateway2@host"]` - An MFA tuple that returns a node list at runtime: `{ClusterHelper, :get_gateway_nodes, []}` - `nil` - No push nodes configured (default) When set, the generated supporter module will include functions to actively push its configuration to the specified gateway nodes. """ ], push_on_startup: [ type: :boolean, default: false, doc: """ Whether to automatically push config to the configured `push_nodes` on application startup. When `true`, the supporter module's `push_on_startup/2` function can be called during application startup to push the config to gateway nodes. Note: you still need to hook this into your application's supervision tree or startup sequence manually. """ ], result_encoder: [ type: :any, default: :struct, doc: """ Default result encoding mode for all resources in this domain. Determines how the result returned from the action MFA call is encoded before being returned to the caller. - `:struct` — Return the Ash resource struct as-is (default) - `:map` — Convert the Ash resource struct to a map containing only public fields (using `Ash.Resource.Info.public_fields/1` to filter; falls back to `Map.from_struct/1` for non-Ash-resource structs) - `{Module, :function, args}` — Custom encoder MFA. The function receives the result as its first argument, followed by `args`, and must return the encoded result. Individual resources and actions can override this with their own `result_encoder` option. For `:map` encoding, Ash resource structs are converted to maps containing only their public fields (attributes, calculations, aggregates, relationships). Lists of structs are mapped accordingly. Non-Ash-resource structs fall back to `Map.from_struct/1`. For custom MFA encoders, the function receives the result and must return the encoded value. Defaults to `:struct`. """ ] ] } use Spark.Dsl.Extension, verifiers: [ AshPhoenixGenApi.Verifiers.VerifyDomainConfig ], transformers: [ AshPhoenixGenApi.Transformers.DefineDomainSupporter ], sections: [@gen_api] end