Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add render groups support #54

Merged
merged 1 commit into from
May 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions lib/mix/tasks/rolodex.gen.docs.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,25 @@ defmodule Mix.Tasks.Rolodex.Gen.Docs do

@doc false
def run(_args) do
IO.puts("Rolodex is compiling your docs...\n")

Application.get_all_env(:rolodex)[:module]
|> Rolodex.Config.new()
|> Rolodex.run()
|> log_result()
end

defp log_result(renders) do
renders
|> Enum.reduce([], fn
{:ok, _}, acc -> acc
{:error, err}, acc -> [err | acc]
end)
|> case do
[] -> IO.puts("Done!")
errs ->
IO.puts("Rolodex failed to compile some docs with the following errors:")
Enum.each(errs, &IO.inspect(&1))
end
end
end
73 changes: 34 additions & 39 deletions lib/rolodex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ defmodule Rolodex do
Config,
Field,
Headers,
RenderGroupConfig,
RequestBody,
Response,
Route,
Expand All @@ -288,54 +289,48 @@ defmodule Rolodex do
"""
@spec run(Rolodex.Config.t()) :: :ok | {:error, any()}
def run(config) do
generate_documentation(config)
|> write(config)
config
|> generate_routes()
|> process_render_groups(config)
end

defp write(processed, %Config{writer: writer} = config) do
with {:ok, device} <- writer.init(config),
:ok <- writer.write(device, processed),
:ok <- writer.close(device) do
:ok
else
err ->
IO.puts("Failed to write docs with error:")
IO.inspect(err)
end
defp generate_routes(%Config{router: router} = config) do
router.__routes__()
|> Enum.map(&Route.new(&1, config))
end

@doc """
Generates a list of route docs and a map of response schemas. Passes both into
the configured processor to generate the documentation JSON to be written to
file.
"""
@spec generate_documentation(Rolodex.Config.t()) :: String.t()
def generate_documentation(%Config{processor: processor} = config) do
routes = generate_routes(config)
defp process_render_groups(routes, %Config{render_groups: groups} = config) do
Enum.map(groups, &process_render_group(routes, config, &1))
end

defp process_render_group(routes, config, %RenderGroupConfig{processor: processor} = group) do
routes = filter_routes(routes, group)
refs = generate_refs(routes)
processor.process(config, routes, refs)

config
|> processor.process(routes, refs)
|> write(group)
end

@doc """
Inspects the Phoenix Router provided in your `Rolodex.Config`. Iterates
through the list of routes to generate a `Rolodex.Route` for each. It will
filter out any route(s) that match the filter(s) you provide in your config.
"""
@spec generate_routes(Rolodex.Config.t()) :: [Rolodex.Route.t()]
def generate_routes(%Config{router: router} = config) do
router.__routes__()
|> Enum.map(&Route.new(&1, config))
|> Enum.reject(&(&1 == nil || Route.matches_filter?(&1, config)))
defp filter_routes(routes, %RenderGroupConfig{filters: filters}) do
Enum.reject(routes, &(&1 == nil || Route.matches_filter?(&1, filters)))
end

@doc """
Inspects the request and response parameter data for each `Rolodex.Route`.
From these routes, it collects a unique list of `Rolodex.RequestBody`,
`Rolodex.Response`, `Rolodex.Headers`, and `Rolodex.Schema` references. The
serialized refs will be passed along to a `Rolodex.Processor` behaviour.
"""
@spec generate_refs([Rolodex.Route.t()]) :: map()
def generate_refs(routes) do
defp write(processed, %RenderGroupConfig{writer: writer, writer_opts: opts}) do
with {:ok, device} <- writer.init(opts),
:ok <- writer.write(device, processed),
:ok <- writer.close(device) do
{:ok, processed}
else
err -> {:error, err}
end
end

# Inspects the request and response parameter data for each `Rolodex.Route`.
# From these routes, it collects a unique list of `Rolodex.RequestBody`,
# `Rolodex.Response`, `Rolodex.Headers`, and `Rolodex.Schema` references. The
# serialized refs will be passed along to a `Rolodex.Processor` behaviour.
defp generate_refs(routes) do
Enum.reduce(
routes,
%{schemas: %{}, responses: %{}, request_bodies: %{}, headers: %{}},
Expand Down
100 changes: 71 additions & 29 deletions lib/rolodex/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ defmodule Rolodex.Config do
list by default:

- `spec/0` - Basic configuration for your Rolodex setup
- `render_groups_spec/0` - Definitions for render targets for your API docs. A
render group is combination of: (optional) route filters, a processor, a writer,
and options for the writer. You can specify more than one render group to create
multiple docs outputs for your API. By default, one render group will be defined
using the default values in `Rolodex.RenderGroupConfig`.
- `auth_spec/0` - Definitions for shared auth patterns to be used in routes.
Auth definitions should follow the OpenAPI pattern, but keys can use snake_case
and will be converted to camelCase for the OpenAPI target.
Expand All @@ -42,19 +47,12 @@ defmodule Rolodex.Config do
- `version` (required) - Your documentation's version
- `default_content_type` (default: "application/json") - Default content type
used for request body and response schemas
- `file_name` (default: "api.json") - The name of the output file with the processed
documentation
- `filters` (default: `:none`) - A list of maps or functions used to filter
out routes from your documentation. Filters are matched against `Rolodex.Route`
structs in `Rolodex.Route.matches_filter?/2`.
- `locale` (default: `"en"`) - Locale key to use when processing descriptions
- `pipelines` (default: `%{}`) - Map of pipeline configs. Used to set default
parameter values for all routes in a pipeline. See `Rolodex.PipelineConfig`.
- `processor` (default: `Rolodex.Processors.Swagger`) - Module implementing
the `Rolodex.Processor` behaviour
- `render_groups` (default: `%Rolodex.RenderGroupConfig{}`) - List of render
groups.
- `server_urls` (default: []) - List of base url(s) for your API paths
- `writer` (default: `Rolodex.Writers.FileWriter`) - Module implementing the
`Rolodex.Writer` behaviour to be used to write out the docs

## Full Example

Expand All @@ -67,15 +65,19 @@ defmodule Rolodex.Config do
description: "My API's description",
version: "1.0.0",
default_content_type: "application/json+api",
file_name: "api.json",
filters: :none,
locale: "en",
processor: MyProcessor,
server_urls: ["https://myapp.io"],
router: MyRouter
]
end

def render_groups_spec() do
[
[writer_opts: [file_name: "api-public.json"]],
[writer_opts: [file_name: "api-private.json"]]
]
end

def auth_spec() do
[
BearerAuth: [
Expand Down Expand Up @@ -110,51 +112,43 @@ defmodule Rolodex.Config do
end
"""

alias Rolodex.PipelineConfig
alias Rolodex.{PipelineConfig, RenderGroupConfig}

import Rolodex.Utils, only: [to_struct: 2, to_map_deep: 1]

@enforce_keys [
:description,
:file_name,
:locale,
:processor,
:render_groups,
:router,
:title,
:version,
:writer
:version
]

defstruct [
:description,
:pipelines,
:render_groups,
:router,
:title,
:version,
default_content_type: "application/json",
file_name: "api.json",
filters: :none,
locale: "en",
processor: Rolodex.Processors.Swagger,
auth: %{},
server_urls: [],
writer: Rolodex.Writers.FileWriter
server_urls: []
]

@type t :: %__MODULE__{
default_content_type: binary(),
description: binary(),
file_name: binary(),
filters: [map() | (Rolodex.Route.t() -> boolean())] | :none,
locale: binary(),
pipelines: pipeline_configs() | nil,
processor: module(),
render_groups: [RenderGroupConfig.t()],
router: module(),
auth: map(),
server_urls: [binary()],
title: binary(),
version: binary(),
writer: module()
version: binary()
}

@type pipeline_configs :: %{
Expand All @@ -163,6 +157,8 @@ defmodule Rolodex.Config do

@callback spec() :: keyword() | map()
@callback pipelines_spec() :: keyword() | map()
@callback auth_spec() :: keyword() | map()
@callback render_groups_spec() :: list()

defmacro __using__(_) do
quote do
Expand All @@ -171,8 +167,12 @@ defmodule Rolodex.Config do
def spec(), do: %{}
def pipelines_spec(), do: %{}
def auth_spec(), do: %{}
def render_groups_spec(), do: [[]]

defoverridable spec: 0, pipelines_spec: 0, auth_spec: 0
defoverridable spec: 0,
pipelines_spec: 0,
auth_spec: 0,
render_groups_spec: 0
end
end

Expand All @@ -182,6 +182,7 @@ defmodule Rolodex.Config do
|> Map.new()
|> set_pipelines_config(module)
|> set_auth_config(module)
|> set_render_groups_config(module)
|> to_struct(__MODULE__)
end

Expand All @@ -193,8 +194,49 @@ defmodule Rolodex.Config do
Map.put(opts, :pipelines, pipelines)
end

def set_auth_config(opts, module),
defp set_auth_config(opts, module),
do: Map.put(opts, :auth, module.auth_spec() |> to_map_deep())

defp set_render_groups_config(opts, module) do
groups = module.render_groups_spec() |> Enum.map(&RenderGroupConfig.new/1)
Map.put(opts, :render_groups, groups)
end
end

defmodule Rolodex.RenderGroupConfig do
@moduledoc """
Configuration for a render group, a serialization target for your docs. You can
specify one or more render groups via `Rolodex.Config` to render docs output(s)
for your API.

## Options

- `filters` (default: `:none`) - A list of maps or functions used to filter
out routes from your documentation. Filters are invoked in
`Rolodex.Route.matches_filter?/2`. If the match returns true, the route will be
filtered out of the docs result for this render group.
- `processor` (default: `Rolodex.Processors.Swagger`) - Module implementing
the `Rolodex.Processor` behaviour
- `writer` (default: `Rolodex.Writers.FileWriter`) - Module implementing the
`Rolodex.Writer` behaviour to be used to write out the docs
- `writer_opts` (default: `[file_name: "api.json"]`) - Options keyword list
passed into the writer behaviour.
"""

defstruct filters: :none,
processor: Rolodex.Processors.Swagger,
writer: Rolodex.Writers.FileWriter,
writer_opts: [file_name: "api.json"]

@type t :: %__MODULE__{
filters: [map() | (Rolodex.Route.t() -> boolean())] | :none,
processor: module(),
writer: module(),
writer_opts: keyword()
}

@spec new(list() | map()) :: t()
def new(params \\ []), do: struct(__MODULE__, params)
end

defmodule Rolodex.PipelineConfig do
Expand Down
6 changes: 3 additions & 3 deletions lib/rolodex/route.ex
Original file line number Diff line number Diff line change
Expand Up @@ -402,10 +402,10 @@ defmodule Rolodex.Route do
@doc """
Checks to see if the given route matches any filter(s) stored in `Rolodex.Config`.
"""
@spec matches_filter?(t(), Rolodex.Config.t()) :: boolean()
def matches_filter?(route, config)
@spec matches_filter?(t(), any()) :: boolean()
def matches_filter?(route, filters)

def matches_filter?(route, %Config{filters: filters}) when is_list(filters) do
def matches_filter?(route, filters) when is_list(filters) do
Enum.any?(filters, fn
filter_opts when is_map(filter_opts) ->
keys = Map.keys(filter_opts)
Expand Down
21 changes: 11 additions & 10 deletions lib/rolodex/writers/file_writer.ex
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
defmodule Rolodex.Writers.FileWriter do
@behaviour Rolodex.Writer

alias Rolodex.Config

@impl Rolodex.Writer
def init(config) do
with {:ok, file_name} <- fetch_file_name(config),
def init(opts) do
with {:ok, file_name} <- fetch_file_name(opts),
{:ok, cwd} <- File.cwd(),
full_path <- Path.join([cwd, file_name]),
:ok <- File.touch(full_path) do
Expand All @@ -23,11 +21,14 @@ defmodule Rolodex.Writers.FileWriter do
File.close(io_device)
end

defp fetch_file_name(%Config{file_name: file_name}) do
case file_name do
"" -> {:error, :file_name_missing}
nil -> {:error, :file_name_missing}
path -> {:ok, path}
end
defp fetch_file_name(opts) when is_list(opts) do
opts
|> Map.new()
|> fetch_file_name()
end

defp fetch_file_name(%{file_name: name}) when is_binary(name) and name != "",
do: {:ok, name}

defp fetch_file_name(_), do: {:error, :file_name_missing}
end
2 changes: 1 addition & 1 deletion lib/rolodex/writers/writer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ defmodule Rolodex.Writer do
@doc """
Returns an open `IO.device()` for writing.
"""
@callback init(Rolodex.Config.t()) :: {:ok, IO.device()} | {:error, any}
@callback init(list() | map()) :: {:ok, IO.device()} | {:error, any}

@doc """
Closes the given `IO.device()`.
Expand Down
Loading