# SPDX-FileCopyrightText: 2025 diffo_example 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 """ """ @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