Current section
Files
Jump to
Current section
Files
lib/mix/tasks/gen.api_docs.ex
# SPDX-FileCopyrightText: 2025 diffo_example contributors <https://github.com/diffo-dev/diffo_example/graphs.contributors>
#
# SPDX-License-Identifier: MIT
defmodule Mix.Tasks.Gen.ApiDocs do
@shortdoc "Generates per-domain API markdown fragments from code_interface defines"
@moduledoc """
Walks each configured domain's `code_interface` defines and writes a
markdown table fragment for inclusion in the domain doc pages.
Each fragment lists the resource sections, with one row per `define`
showing the generated Elixir function, the underlying action, the
meaningful arguments, and the action's purpose (its `description:`).
## Usage
mix gen.api_docs
Writes to:
- `documentation/domains/_access_api.md`
- `documentation/domains/_nbn_api.md`
These fragments are intended to be referenced (e.g. via `!include`
pseudo-markers or just kept open in the editor alongside the narrative
page). They're regenerated on demand so they don't drift from the
code-interface declarations.
"""
use Mix.Task
@doc_root "documentation/domains"
@domains [
{DiffoExample.Access, "_access_api.md", "Access"},
{DiffoExample.Nbn, "_nbn_api.md", "NBN"}
]
@autogen_banner """
<!--
SPDX-FileCopyrightText: 2025 diffo_example contributors <https://github.com/diffo-dev/diffo_example/graphs.contributors>
SPDX-License-Identifier: MIT
Auto-generated by `mix gen.api_docs`. Do not edit by hand.
Regenerate after changing any domain's `code_interface` defines.
-->
"""
@impl Mix.Task
def run(_args) do
Mix.Task.run("compile", [])
File.mkdir_p!(@doc_root)
Enum.each(@domains, fn {domain, file, title} ->
path = Path.join(@doc_root, file)
content = render_domain(domain, title)
File.write!(path, content)
Mix.shell().info("wrote #{path}")
end)
end
defp render_domain(domain, title) do
refs =
domain
|> Ash.Domain.Info.resource_references()
|> Enum.sort_by(&short_name(&1.resource))
body =
refs
|> Enum.map(&render_resource/1)
|> Enum.reject(&(&1 == :empty))
|> Enum.join("\n\n")
[
@autogen_banner,
"\n",
"# #{title} Domain API\n",
"\n",
"The Elixir function-call surface for each resource in the `#{inspect(domain)}` domain. ",
"Generated from the `define` declarations in the domain's `resources do` block.\n",
"\n",
body,
"\n"
]
|> IO.iodata_to_binary()
end
defp render_resource(ref) do
case ref.definitions do
[] ->
:empty
defs ->
sorted = Enum.sort_by(defs, & &1.name)
rows =
sorted
|> Enum.map(&render_row(&1, ref.resource))
|> Enum.join("\n")
"""
## #{short_name(ref.resource)}
| Function | Action | Arguments | Purpose |
|---|---|---|---|
#{rows}
"""
|> String.trim_trailing()
end
end
defp render_row(interface, resource) do
action_name = interface.action || interface.name
action = Ash.Resource.Info.action(resource, action_name)
fn_cell = "`#{interface.name}`"
action_cell = "`:#{action_name}`"
args_cell = render_args(interface, action)
purpose_cell = render_purpose(action)
"| #{fn_cell} | #{action_cell} | #{args_cell} | #{purpose_cell} |"
end
# Auto-injected by Diffo's `behaviour do create :build end` fragment —
# not part of the user-facing API surface.
@injected_build_args [:specified_by, :features, :characteristics]
defp render_args(interface, action) do
get_by_arg =
cond do
interface.get_by -> Enum.map(List.wrap(interface.get_by), &"`#{&1}`")
interface.get_by_identity -> ["`#{interface.get_by_identity}`"]
true -> []
end
accept = Enum.map(Map.get(action, :accept) || [], &"`#{&1}`")
arguments =
(Map.get(action, :arguments) || [])
|> Enum.reject(&(&1.name in @injected_build_args))
|> Enum.map(fn arg -> "`#{arg.name}` (#{type_label(arg.type)})" end)
case get_by_arg ++ accept ++ arguments do
[] -> "—"
list -> Enum.join(list, ", ")
end
end
defp render_purpose(action) do
case action.description do
nil -> "—"
"" -> "—"
desc -> desc |> String.replace("\n", " ") |> String.trim()
end
end
defp type_label({:array, type}), do: "list of #{type_label(type)}"
defp type_label(type) when is_atom(type) do
type
|> to_string()
|> String.replace_prefix("Elixir.Ash.Type.", "")
|> String.replace_prefix("Elixir.", "")
|> String.downcase()
end
defp type_label(other), do: inspect(other)
defp short_name(module) do
module |> Module.split() |> List.last()
end
end