Skip to content

Commit

Permalink
Merge pull request #69 from Frameio/bc/router-refactor
Browse files Browse the repository at this point in the history
[PLATFORM-641] Add Rolodex.Router
  • Loading branch information
bceskavich authored Jul 22, 2019
2 parents 6671da0 + 29b130f commit 0ccd7fa
Show file tree
Hide file tree
Showing 16 changed files with 684 additions and 576 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ erl_crash.dump
# Ignore package tarball (built via "mix hex.build").
swag-*.tar

.elixir_ls
75 changes: 40 additions & 35 deletions lib/rolodex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ defmodule Rolodex do
@moduledoc """
Rolodex generates documentation for your Phoenix API.
Rolodex inspects a Phoenix Router and transforms the `@doc` annotations on your
controller actions into documentation in the format of your choosing.
Rolodex transforms the structured `@doc` annotations on your Phoenix Controller
action functions into documentation API documentation in the format of your
choosing. Rolodex ships with default support for OpenAPI 3.0 (Swagger) docs.
`Rolodex.run/1` encapsulates the full documentation generation process. When
invoked, it will:
1. Traverse your Phoenix Router
1. Traverse your Rolodex Router
2. Collect documentation data for the API endpoints exposed by your router
3. Serialize the data into a format of your choosing (e.g. Swagger JSON)
4. Write the serialized data out to a destination of your choosing.
Expand All @@ -18,25 +19,32 @@ defmodule Rolodex do
## Features and resources
- **Reusable components** - See `Rolodex.Schema` for details on how define reusable
parameter schemas. See `Rolodex.RequestBody` for details on how to use schemas
in your API request body definitions. See `Rolodex.Response` for details on
how to use schemas in your API response definitions. See `Rolodex.Headers` for
details on how to define reusable headers for your route doc annotations and your
responses.
- **Structured annotations** - See `Rolodex.Route` for details on how to format
annotations on your API route action functions for the Rolodex parser to handle
- **Reusable components** — Support for reusable parameter schemas, request
bodies, responses, and headers.
- **Structured annotations** — Standardized format for annotating your Phoenix
Controller action functions with documentation info
- **Generic serialization** - The `Rolodex.Processor` behaviour encapsulates
the basic steps needed to serialize API metadata into documentation. Rolodex
ships with a valid OpenAPI (Swagger) JSON processor (see: `Rolodex.Processors.OpenAPI`)
ships with a valid OpenAPI 3.0 (Swagger) JSON processor
(see: `Rolodex.Processors.OpenAPI`)
- **Generic writing** - The `Rolodex.Writer` behaviour encapsulates the basic
steps needed to write out formatted docs. Rolodex ships with a file writer (
see: `Rolodex.Writers.FileWriter`)
## Further reading
- `Rolodex.Router` — for defining which routes Rolodex should document
- `Rolodex.Route` — for info on how to structure your doc annotations
- `Rolodex.Schema` — for defining reusable request and response data schemas
- `Rolodex.RequestBody` — for defining rusable request body parameters
- `Rolodex.Response` — for defining reusable API responses
- `Rolodex.Headers` — for defining reusable request and response headers
- `Rolodex.Config` — for configuring your Phoenix app to use Rolodex
## High level example
# Your Phoenix router
defmodule MyRouter do
defmodule MyPhoenixRouter do
pipeline :api do
plug MyPlug
end
Expand All @@ -48,6 +56,15 @@ defmodule Rolodex do
end
end
# Your Rolodex router, which tells Rolodex which routes to document
defmodule MyRouter do
use Rolodex.Router
router MyPhoenixRouter do
get "/api/test"
end
end
# Your controller
defmodule MyController do
@doc [
Expand Down Expand Up @@ -125,11 +142,14 @@ defmodule Rolodex do
[
title: "MyApp",
description: "An example",
version: "1.0.0",
router: MyRouter
version: "1.0.0"
]
end
def render_groups_spec() do
[router: MyRouter]
end
def auth_spec() do
[
BearerAuth: [
Expand Down Expand Up @@ -282,7 +302,7 @@ defmodule Rolodex do
RenderGroupConfig,
RequestBody,
Response,
Route,
Router,
Schema
}

Expand All @@ -293,34 +313,19 @@ defmodule Rolodex do
Runs Rolodex and writes out documentation to the specified destination
"""
@spec run(Rolodex.Config.t()) :: :ok | {:error, any()}
def run(config) do
config
|> generate_routes()
|> process_render_groups(config)
end

defp generate_routes(%Config{router: router} = config) do
router.__routes__()
|> Enum.map(&Route.new(&1, config))
def run(%Config{render_groups: groups} = config) do
Enum.map(groups, &compile_for_group(&1, config))
end

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)
defp compile_for_group(%RenderGroupConfig{router: router, processor: processor} = group, config) do
routes = Router.build_routes(router, config)
refs = generate_refs(routes)

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

defp filter_routes(routes, %RenderGroupConfig{filters: filters}) do
Enum.reject(routes, &(&1 == nil || Route.matches_filter?(&1, filters)))
end

defp write(processed, %RenderGroupConfig{writer: writer, writer_opts: opts}) do
with {:ok, device} <- writer.init(opts),
:ok <- writer.write(device, processed),
Expand Down
34 changes: 14 additions & 20 deletions lib/rolodex/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ defmodule Rolodex.Config do
- `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,
render group is combination of: a Rolodex Router, 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`.
multiple docs outputs for your API. At least one render group specification is
required.
- `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,7 +42,6 @@ defmodule Rolodex.Config do
For `spec/0`, the following are valid options:
- `description` (required) - Description for your documentation output
- `router` (required) - `Phoenix.Router` module to inspect
- `title` (required) - Title for your documentation output
- `version` (required) - Your documentation's version
- `default_content_type` (default: "application/json") - Default content type
Expand All @@ -66,15 +65,14 @@ defmodule Rolodex.Config do
version: "1.0.0",
default_content_type: "application/json+api",
locale: "en",
server_urls: ["https://myapp.io"],
router: MyRouter
server_urls: ["https://myapp.io"]
]
end
def render_groups_spec() do
[
[writer_opts: [file_name: "api-public.json"]],
[writer_opts: [file_name: "api-private.json"]]
[router: MyRouter, writer_opts: [file_name: "api-public.json"]],
[router: MyRouter, writer_opts: [file_name: "api-private.json"]]
]
end
Expand Down Expand Up @@ -120,7 +118,6 @@ defmodule Rolodex.Config do
:description,
:locale,
:render_groups,
:router,
:title,
:version
]
Expand All @@ -129,7 +126,6 @@ defmodule Rolodex.Config do
:description,
:pipelines,
:render_groups,
:router,
:title,
:version,
default_content_type: "application/json",
Expand All @@ -144,7 +140,6 @@ defmodule Rolodex.Config do
locale: binary(),
pipelines: pipeline_configs() | nil,
render_groups: [RenderGroupConfig.t()],
router: module(),
auth: map(),
server_urls: [binary()],
title: binary(),
Expand Down Expand Up @@ -211,10 +206,7 @@ defmodule Rolodex.RenderGroupConfig do
## 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.
- `router` (required) - A `Rolodex.Router` definition
- `processor` (default: `Rolodex.Processors.OpenAPI`) - Module implementing
the `Rolodex.Processor` behaviour
- `writer` (default: `Rolodex.Writers.FileWriter`) - Module implementing the
Expand All @@ -223,13 +215,15 @@ defmodule Rolodex.RenderGroupConfig do
passed into the writer behaviour.
"""

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

@type t :: %__MODULE__{
filters: [map() | (Rolodex.Route.t() -> boolean())] | :none,
router: module(),
processor: module(),
writer: module(),
writer_opts: keyword()
Expand Down
15 changes: 10 additions & 5 deletions lib/rolodex/dsl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -166,18 +166,23 @@ defmodule Rolodex.DSL do

### Function Helpers ###

# Check the given module against the given module type
@doc """
Check the given module against the given module type
"""
@spec is_module_of_type?(module(), atom()) :: boolean()
def is_module_of_type?(mod, type) when is_atom(mod) do
try do
mod.__info__(:functions) |> Keyword.has_key?(type)
rescue
case Code.ensure_compiled(mod) do
{:module, loaded} -> function_exported?(loaded, type, 1)
_ -> false
end
end

def is_module_of_type?(_), do: false
def is_module_of_type?(_, _), do: false

# @doc """
# Serializes content body metadata
# """
# @spec to_content_body_map(function()) :: map()
def to_content_body_map(fun) do
%{
desc: fun.(:desc),
Expand Down
Loading

0 comments on commit 0ccd7fa

Please sign in to comment.