lib/auto_doc.ex

defmodule PhoenixAutoDoc do
  @moduledoc """
  # PhoenixAutoDoc

  PhoenixAutoDoc is a library for automatic documentation of routes in the Phoenix framework.


  ## Getting started with PhoenixAutoDoc
  For the library to work, you need to add it as a dependency in the mix.exs file:
  `{:phoenix_auto_doc, "~> 0.1.4"}`.


  After that, in the file `router.ex` you need to add the following:
      scope "/YOUR_WAY_FOR_DOCUMENTATION" do
        forward("/", AutoDoc, app: YouProjectWeb)
      end

  That's all, the library is ready to go.
  """

  use Plug.Router
  alias Plug.Conn

  alias PhoenixAutoDoc.Generator

  @template """
  <% current_data = data[module_name] |> IO.inspect() %>
  <!DOCTYPE html>
  <html lang="en">
    <head>
      <meta charset="utf-8"/>
      <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
      <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
      <title>
        <%= current_data[:title] || current_data[:module] || "Module not found" %> | PhoenixAutoDoc
      </title>
    </head>
    <body class="main_body">
      <nav>
        <%= for {current_module, i} <- Map.to_list(data) do %>
          <li class="<%= if current_module == module_name, do: "active_link", else: "" %>">
            <a href="<%= base_path %>/<%= i.module %>"><%= i.title || i.module %></a>
            <%= if i.routes != [] do %>
              <ul>
                <%= for f <- i.routes do %>
                  <li>
                    <a href="#<%= f.url %>"><span><%= f.method %></span> <%= f.url %></a>
                  </li>
                <% end %>
              </ul>
            <% end %>
          </li>
        <% end %>
      </nav>
      <main class="container">
        <section class="module_info">
          <%= if is_nil(current_data) do %>
            <h1>Module not found</h1>
          <% else %>
            <%= if current_data.documentation == nil do %>
              <h1><%= current_data.title || current_data.module %></h1>
              <span class="red">No documentation for the module</span>
            <% else %>
              <span><%= current_data.module %></span>
              <%=
                current_data.documentation
                |> String.split("\n")
                |> Earmark.as_html!()
              %>
            <% end %>
          <% end %>
        </section>
        <section class="routes_info">
          <%= if not is_nil(current_data) do %>
            <%= for i <- current_data.routes do %>
              <div class="route_container">
                <div class="route_title <%= String.downcase(i.method) %>">
                  <a name="<%= i.url %>"></a>
                  <span class="method"><%= i.method %></span>
                  <span class="url"><%= i.url %></span>
                  <%= if is_nil(i.documentation) do %>
                    <span class="red">No documentation for the route</span>
                  <% end %>
                </div>
                <div class="rout_body">
                  <%= if not is_nil(i.documentation) do %>
                    <div class="route_documentation">
                      <%=
                        i.documentation
                        |> String.split("\n")
                        |> Earmark.as_html!()
                      %>
                    </div>
                  <% end %>
                </div>
              </div>
            <% end %>
          <% end %>
        </section>
      </main>
    </body>
  </html>

  <style>
    html {
        font-size: 100%;
        font-family: Tahoma, Geneva, sans-serif;
    }

    body {
        display: grid;
        height: 100vh;
        grid-template-columns: 20rem auto;
        margin: 0;
        padding: 0;
        font-size: 1rem;
    }

    nav {
        display: block;
        min-height: 100vh;
        list-style-type: none;
        background: #303247;
        overflow: hidden;
        overflow-y: auto;
    }

    nav>li {
        display: block;
        margin-bottom: 1rem;
        padding-top: .5rem
    }

    nav li a {
      display: block;
      padding: .5rem 0 .5rem 1rem;
      text-decoration: none;
      color: #eee;
    }

    nav li ul {
      margin: 0;
      padding: 0 0 1rem 1rem;
      list-style-type: none;
    }

    nav li ul a {
      font-size: .9rem
    }

    nav li a:hover {
      text-decoration: underline;
    }

    nav .active_link {
      background: #3d3f54
    }

    nav>li:first-child {
      display: block;
      padding: 0 0 2.5rem;
      text-align: center;
      margin: 0;
      font-size: 1.5rem;
      position: relative;
    }

    nav>li:first-child::before {
      content: 'Automatic Documentation';
      display: block;
      position: relative;
      font-size: 1rem;
      color: #bbb;
      top: 3rem;
    }

    nav>li:first-child>a {
      margin: 0 !important;
      padding: 0 !important;
      border: 0 !important;
      text-decoration: none !important;
      background: transparent !important;
      font-weight: bold;
      color: #eee
    }

    .module_info span {
      font-size: .9rem;
      font-weight: bold;
      opacity: .6;
    }

    .red {
        display: block;
        color: #d25b5b;
        font-weight: bold;
    }

    main {
      width: 100%;
      max-width: 70rem;
      margin: 2rem auto;

    }

    main>h1,
    main>.module_info>h1 {
      margin: 0 0 1rem 0;
      font-size: 2.5rem;
    }

    main .module_info {
      margin-bottom: 3rem;
    }

    main .route_documentation {
      width: 90%;
      margin: 0 auto;
    }

    main .route_container {
      background: #eee;
    }

    main .rout_body {
      padding-bottom: 5%;
    }

    main .rout_body:empty {
      display: none !important;
    }

    .routes_info h1 { font-size: 1.5rem }
    .routes_info h2 { font-size: 1.4rem }
    .routes_info h3 { font-size: 1.3rem }
    .routes_info h4 { font-size: 1.2rem }
    .routes_info h5 { font-size: 1.1rem }

    pre {
        margin: 0;
        padding: 0;
        border-radius: .25rem;
    }

    pre code {
        display: block;
        margin: 0;
        padding: .5rem;
        padding-bottom: .75rem;
        background: #4d4c4c;
        color: #eaeaea;
    }

    code {
      background: #4d4c4c;
      color: #c77a22;
      display: inline-block;
      padding: 0 .2rem .1rem;
      border-radius: .2rem;
      border: .1rem solid #111;
    }

    .route_title {
      display: block;
      padding: 1rem 0 1rem 1rem;
      font-weight: bold;
      color: #fff;
    }

    .route_title.get { background: #47cb90 }
    .route_title.post { background: #62affc }
    .route_title.put { background: #fba033 }
    .route_title.delete { background: #f83e3e }
  </style>
  """

  plug(Plug.Static, at: "/", from: :phoenix_swagger)

  plug(:match)
  plug(:dispatch)

  def init(opts), do: opts

  def call(conn, app: app) do
    conn
    |> Conn.assign(:app, app)
    |> super([])
  end

  defp base_path(conn, module_name) do
    conn.request_path
    |> String.trim_trailing("/")
    |> String.replace("/#{module_name}", "")
  end

  get "/*module" do
    data = Generator.get_info(conn.assigns.app)
    module_name = current_module(conn)

    body =
      EEx.eval_string(@template,
        data: data,
        base_path: base_path(conn, module_name),
        module_name: module_name || conn.assigns.app
      )

    conn
    |> Conn.put_resp_content_type("text/html")
    |> Conn.send_resp(200, body)
  end

  defp current_module(conn) do
    if conn.params["module"] == [] do
      conn.assigns.app
    else
      [module | _] = conn.params["module"]

      String.to_atom(module)
    end
  end

  match("/*paths", do: Conn.send_resp(conn, 405, "method not allowed"))
end