Skip to content

Commit

Permalink
Merge pull request #56 from Frameio/develop
Browse files Browse the repository at this point in the history
Release 5/22 (v0.7.0)
  • Loading branch information
bceskavich committed May 22, 2019
2 parents 924b66f + 66f4d88 commit 20874f5
Show file tree
Hide file tree
Showing 11 changed files with 943 additions and 592 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ deps in `mix.exs`:
```elixir
def deps do
[
{:rolodex, "~> 0.6.0"}
{:rolodex, "~> 0.7.0"}
]
end
```
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
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule Rolodex.MixProject do
app: :rolodex,
name: "Rolodex",
description: "Automated docs generation",
version: "0.6.1",
version: "0.7.0",
elixir: "~> 1.7",
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
Expand Down

0 comments on commit 20874f5

Please sign in to comment.