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