defmodule AshJsonApi.Api.Router do
@moduledoc """
Use this module to create a router for your AshJsonApi.
To use this, create a module and do the following:
```elixir
defmodule YourRouter do
use AshJsonApi.Api.Router,
apis: [YourApi, YourOtherApi],
# these next two are optional, only add them if you want those endpoints
open_api: "/open_api",
json_schema: "/json_schema"
end
```
Then in your Phoenix router or plug pipeline, forward to this plug.
In phoenix, that looks like this:
```elixir
forward "/api", YourRouter
```
"""
defmacro __using__(opts) do
quote bind_quoted: [opts: opts] do
require Ash.Api.Info
use Plug.Router
require Ash
apis = List.wrap(opts[:api] || opts[:apis])
plug(:match)
plug(Plug.Parsers,
parsers: [:json],
pass: ["application/vnd.api+json"],
json_decoder: Jason
)
plug(:dispatch)
if apis == [] do
raise "At least one api option must be provided"
end
for api <- apis do
prefix = AshJsonApi.Api.Info.prefix(api)
resources = Ash.Api.Info.depend_on_resources(api)
resources
|> Enum.filter(&AshJsonApi.Resource.Info.type(&1))
|> Enum.each(fn resource ->
for %{
route: route,
action: action_name,
controller: controller,
method: method,
action_type: action_type,
relationship: relationship_name
} = route_struct <-
AshJsonApi.Api.Router.routes(resource) do
opts =
[
relationship: Ash.Resource.Info.public_relationship(resource, relationship_name),
action: Ash.Resource.Info.action(resource, action_name),
resource: resource,
api: api,
prefix: prefix,
route: route_struct
]
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
match(route, via: method, to: controller, init_opts: opts)
end
end)
end
schema_apis = Enum.filter(apis, &AshJsonApi.Api.Info.serve_schema?(&1))
unless Enum.empty?(schema_apis) do
match("/schema", via: :get, to: AshJsonApi.Controllers.Schema, init_opts: [apis: apis])
match("/schema.json",
via: :get,
to: AshJsonApi.Controllers.Schema,
init_opts: [apis: apis]
)
end
open_api_opts = AshJsonApi.Api.Router.open_api_opts(opts)
case Code.ensure_loaded?(OpenApiSpex) && opts[:open_api] do
falsy when falsy in [nil, false] ->
:ok
true ->
match("/open_api",
via: :get,
to: AshJsonApi.Controllers.OpenApi,
init_opts: open_api_opts
)
routes when is_list(routes) ->
for route <- routes do
match(route, via: :get, to: AshJsonApi.Controllers.OpenApi, init_opts: open_api_opts)
end
route ->
match(route, via: :get, to: AshJsonApi.Controllers.OpenApi, init_opts: open_api_opts)
end
case opts[:json_schema] do
nil ->
:ok
true ->
match("/json_schema", via: :get, to: AshJsonApi.Controllers.Schema, init_opts: opts)
routes when is_list(routes) ->
for route <- routes do
match(route, via: :get, to: AshJsonApi.Controllers.Schema, init_opts: opts)
end
route ->
match(route, via: :get, to: AshJsonApi.Controllers.Schema, init_opts: opts)
end
match(_, to: AshJsonApi.Controllers.NoRouteFound)
end
end
def open_api_opts(opts) do
opts
|> Keyword.put(:modify, Keyword.get(opts, :modify_open_api))
|> Keyword.delete(:modify_open_api)
end
@doc false
def routes(resource) do
resource
|> AshJsonApi.Resource.Info.routes()
|> Enum.sort(fn left, right ->
left_path = Path.split(left.route)
right_path = Path.split(right.route)
left_path
|> Enum.zip(right_path)
|> sorts_first?()
|> case do
:undecided ->
Enum.count(left_path) > Enum.count(right_path)
result ->
result
end
end)
end
defp sorts_first?(zipped) do
Enum.reduce_while(zipped, :undecided, fn {left_part, right_part}, :undecided ->
left_param? = String.starts_with?(left_part, ":")
right_param? = String.starts_with?(right_part, ":")
cond do
left_part == right_part ->
{:cont, :undecided}
left_param? and not right_param? ->
{:halt, false}
not left_param? and right_param? ->
{:halt, true}
true ->
{:cont, :undecided}
end
end)
end
end