Current section
Files
Jump to
Current section
Files
lib/doc_plug.ex
defmodule DocPlug do
@moduledoc ~S"""
A plug for automatically generating and mounting a project's documentation.
Requires that the project has documentation generation via
[ExDoc](https://github.com/elixir-lang/ex_doc) already set up.
**Note**: If the generation task given does not exist then an error will be
thrown on initialization.
## Options
- `task`: The name of the mix task to run to generate docs. Defaults to
`"docs"`.
- `from`: The directory on the filesystem the generated documentation can be
found. Defaults to `"doc"`.
- `at`: The path to serve the docs from. Defaults to `"docs/"`.
- `generate`: If the documentation should be generated on starting the
server, defaults to `true`.
"""
import Plug.Conn
alias Mix.Task
alias Plug.Conn
alias Plug.Static
# Types for plug configuration data.
@typep doc_conf :: %{from: String.t, at: String.t, task: String.t,
generate: boolean}
@typep config :: {doc_conf, %{}}
# Default values for the task -- should match ExDoc defaults.
@defaults %{from: "doc",
at: "docs",
task: "docs",
generate: true}
defmodule GenerationError do
@moduledoc ~S"""
Error caused by an invalid task used for documentation generation.
"""
defexception [:message]
@doc false
def exception(task) do
%__MODULE__{message: ~s(The task "#{task}" is not available.)}
end
end
@doc ~S"""
Initialize the plug with the given options. See the module documentation for
an overview of the options available.
"""
@spec init(Keyword.t) :: config
def init(overrides \\ []) do
opts = Enum.into(overrides, @defaults)
static_opts = Static.init(at: opts.at, from: opts.from, gzip: false)
# Determine if task name is runnable.
if opts.generate and Task.get(opts.task) == nil do
raise GenerationError, opts.task
end
generate(opts)
{opts, static_opts}
end
@doc ~S"""
Call the plug for the given connection.
Connections to the base endpoint will be redirected to the main documentation
page and the documentation contents will be served.
"""
def call(%Conn{path_info: path} = conn, {opts, static_opts}) do
at = opts.at
case path do
[^at] -> redirect(conn, at)
[^at | _rest] -> Static.call(conn, static_opts)
_ -> conn
end
end
# Generate the documentation if needed.
defp generate(%{generate: false}), do: :noop
defp generate(%{task: task, at: at}) do
:application.ensure_started(:mix)
Task.reenable(task)
Task.run(task)
end
# Redirect the user to the main documentation page.
defp redirect(conn, to) do
url = "/#{URI.encode(to)}/index.html"
body = "You are being redirected to #{url}"
conn
|> put_resp_header("location", url)
|> put_resp_content_type("text/plain")
|> send_resp(302, body)
|> halt
end
end