Current section

Files

Jump to
ashton lib ashton doc.ex
Raw

lib/ashton/doc.ex

defmodule Ashton.Doc do
@moduledoc """
Automatic opt documentation, to be placed into your function docstrings
"""
alias Ashton.Schema
@document_opts Ashton.schema(
opts: [name: :string, header_depth: :int],
defaults: [name: "Opts", header_depth: 1],
describe: [
name: "The top level header for the opts documentation",
header_depth: "How many `#` to prepend before any heading"
],
constraints: [
header_depth: {&__MODULE__.positive?/1, "must be positive"}
]
)
def positive?(integer), do: integer > 0
# These opts cannot be auto-documented, so must be regenerated manually
@doc """
---
## Opts
* `name`(`:string`): The top level header for the opts documentation - Default: "Opts"
* `header_depth`(`:int`): How many `#` to prepend before any heading - Default: 1
---
"""
@spec document(schema :: Ashton.schema(), doc_opts :: Keyword.t()) :: String.t()
def document(schema, doc_opts \\ [])
def document(%Schema{opts: [], extra_keys?: false}, doc_opts) do
doc_opts = Ashton.validate!(doc_opts, @document_opts)
"---\n" <>
header(1, doc_opts[:header_depth]) <>
doc_opts[:name] <> "\n\nAccepts no options.\n" <> "---"
end
def document(%Schema{opts: [], extra_keys?: true}, doc_opts) do
doc_opts = Ashton.validate!(doc_opts, @document_opts)
"---\n" <>
header(1, doc_opts[:header_depth]) <>
doc_opts[:name] <> "\n\nAccepts any options.\n" <> "---"
end
def document(schema, doc_opts) do
doc_opts = Ashton.validate!(doc_opts, @document_opts)
prefix = "---\n" <> header(1, doc_opts[:header_depth]) <> doc_opts[:name] <> "\n\n"
{documented_opts, additional_schemas_to_document} =
document_opts(schema.opts, schema, doc_opts)
documented_opts = prefix <> documented_opts
with_extra_keys =
if schema.extra_keys? do
documented_opts <> "\nAlso accepts extra opts that are not named here.\n\n"
else
documented_opts <> "\n"
end
if Enum.empty?(additional_schemas_to_document) do
with_extra_keys <> "---"
else
with_extra_keys <>
Enum.map_join(
additional_schemas_to_document,
"\n#{header(2, doc_opts[:header_depth])}",
fn {opt, schema} ->
doc_opts =
doc_opts
|> Keyword.update!(:header_depth, &Kernel.+(&1, 1))
|> Keyword.update!(:name, fn name -> name <> " - " <> to_string(opt) end)
document(schema, doc_opts)
end
)
end
end
defp document_opts([], _, _), do: ""
defp document_opts(opts, schema, doc_opts) do
opts
|> Enum.group_by(fn opt ->
schema.annotations[opt]
end)
|> Enum.sort_by(&elem(&1, 0))
|> Enum.reduce({"", []}, fn {annotation, opts}, {doc_string, additional_schemas} ->
if annotation do
{string, new_additional_schemas} = do_document_opts(opts, schema)
{doc_string <>
"\n" <>
header(3, doc_opts[:header_depth]) <> to_string(annotation) <> "\n\n" <> string <> "\n",
additional_schemas ++ new_additional_schemas}
else
{string, new_additional_schemas} = do_document_opts(opts, schema)
{doc_string <> string <> "\n", additional_schemas ++ new_additional_schemas}
end
end)
end
defp do_document_opts(opts, schema) do
{doc_string, schemas} =
opts
|> Enum.sort_by(fn opt ->
not (opt in schema.required)
end)
|> Enum.reduce({"", []}, fn opt, {doc_string, additional_schemas} ->
string_opt = "`" <> Atom.to_string(opt) <> "`"
# TODO: This *will* fail on certain composite types that have not yet been accounted for.
{schemas_to_print_below, string_type} = describe_type(to_string(opt), schema.types[opt])
description = schema.describe[opt]
required =
if opt in schema.required do
" **Required**"
else
""
end
prefix = "* " <> string_opt <> "(`" <> string_type <> "`)"
with_description_and_type =
if description do
prefix <> required <> ": " <> description
else
prefix <> required
end
with_default =
if Keyword.has_key?(schema.defaults, opt) do
with_description_and_type <> " - Default: " <> inspect(schema.defaults[opt])
else
with_description_and_type
end
final_string =
case schema.constraints[opt] do
empty when empty in [nil, []] ->
with_default
constraints ->
described_constraints =
constraints
|> List.wrap()
|> Enum.map_join(", ", &elem(&1, 1))
with_default <> " Constraints: " <> described_constraints
end
{doc_string <> "\n" <> final_string, additional_schemas ++ schemas_to_print_below}
end)
{String.trim_leading(doc_string, "\n"), schemas}
end
defp describe_type(opt, schema = %Ashton.Schema{}) do
{[{opt, schema}], "#{opt}(see below)"}
end
defp describe_type(opt, list) when is_list(list) do
{schemas, types} =
list
|> Enum.with_index()
|> Enum.reduce({[], []}, fn {type, index}, {schemas, descriptions} ->
{new_schemas, type_description} = describe_type(opt <> "[#{index}]", type)
{schemas ++ new_schemas, descriptions ++ [type_description]}
end)
{schemas, "One of #{Enum.join(types, " | ")}"}
end
defp describe_type(_opt, type) do
{[], inspect(type)}
end
defp header(depth, header_depth_opt), do: String.duplicate("#", header_depth_opt + depth) <> " "
end