lib/ash/api/info/livebook.ex

defmodule Ash.Api.Info.Livebook do
  @moduledoc """
  Generate a Livebook from a specified API.
  """

  # Strip Elixir off front of module
  defp module_name(module) do
    module
    |> Module.split()
    |> Enum.join(".")
  end

  # TODO: move to Ash.Resource.Info as it's also used in diagram
  defp resource_name(resource) do
    resource
    |> Ash.Resource.Info.short_name()
    |> to_string()
    |> Macro.camelize()
  end

  defp short_module(module) do
    module
    |> Module.split()
    |> List.last()
  end

  defp class_short_type({:array, t}), do: "#{short_module(t)}[]"
  defp class_short_type(t), do: short_module(t)

  def overview(apis) do
    """
    #{for api <- apis, do: api_section(api)}
    """
  end

  def api_section(api) do
    """
    # API #{module_name(api)}

    ## Class Diagram

    ```mermaid
    #{Ash.Api.Info.Diagram.mermaid_class_diagram(api) |> String.trim()}
    ```

    ## ER Diagram

    ```mermaid
    #{Ash.Api.Info.Diagram.mermaid_er_diagram(api) |> String.trim()}
    ```

    ## Resources

    #{for resource <- Ash.Api.Info.resources(api) do
      """
      - [#{resource_name(resource)}](##{resource_name(resource) |> String.downcase()})
      """
    end}
    #{for resource <- Ash.Api.Info.resources(api) do
      resource_section(resource)
    end |> Enum.join("\n")}
    """
  end

  def resource_section(resource) do
    """
    ## #{resource_name(resource)}

    #{Ash.Resource.Info.description(resource)}

    ### Attributes

    #{attr_header() |> String.trim()}
    #{for attr <- Ash.Resource.Info.attributes(resource) do
      attr_section(attr)
    end |> Enum.join("\n")}

    ### Actions

    #{action_header() |> String.trim()}
    #{for action <- Ash.Resource.Info.actions(resource) do
      action_section(resource, action)
    end |> Enum.join("\n")}
    """
  end

  def attr_header do
    """
    | Name | Type | Description |
    | ---- | ---- | ----------- |
    """
  end

  def attr_section(attr) do
    "| **#{attr.name}** | #{class_short_type(attr.type)} | #{attr.description} |"
  end

  def action_header do
    """
    | Name | Type | Input | Description |
    | ---- | ---- | ----- | ----------- |
    """
  end

  def action_section(resource, action) do
    "| **#{action.name}** | _#{action.type}_ | <ul>#{action_input_section(resource, action)}</ul> | #{action.description} |"
  end

  def action_input_section(resource, action) do
    attributes =
      if action.type == :destroy && !action.soft? do
        []
      else
        action
        |> Map.get(:accept, [])
        |> Enum.map(&Ash.Resource.Info.attribute(resource, &1))
      end

    for input <- Enum.uniq_by(action.arguments ++ attributes, & &1.name) do
      description =
        case input do
          %Ash.Resource.Attribute{} ->
            "attribute"

          %Ash.Resource.Actions.Argument{description: description} ->
            description
        end

      "<li><b>#{input.name}</b> <i>#{class_short_type(input.type)}</i> #{description}</li>"
    end
  end
end