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

226 - path and schema injection #227

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
/deps
erl_crash.dump
*.ez
.DS_Store
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* Improvements in SwagerUI Plug
* Update dependencies
* Bug fixes
* Add `add_module/2` function to add path and schema definitions not in controllers.

# 0.8.1

Expand Down
30 changes: 30 additions & 0 deletions examples/simple/lib/simple_web/channels/user_socket.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
defmodule SimpleWeb.UserSocket do
use Phoenix.Socket
use PhoenixSwagger

## Channels
# channel "room:*", Simple.RoomChannel
Expand Down Expand Up @@ -34,4 +35,33 @@ defmodule SimpleWeb.UserSocket do
#
# Returning `nil` makes this socket anonymous.
def id(_socket), do: nil

swagger_path(:test) do
get("/api/users/test")
summary("Test function")
description("List tests in db")
produces("application/json")
deprecated(false)

response(200, "OK", Schema.ref(:UsersResponse),
example: %{
data: [
%{
id: 1,
name: "Joe",
email: "[email protected]",
inserted_at: "2017-02-08T12:34:55Z",
updated_at: "2017-02-12T13:45:23Z"
},
%{
id: 2,
name: "Jack",
email: "[email protected]",
inserted_at: "2017-02-04T11:24:45Z",
updated_at: "2017-02-15T23:15:43Z"
}
]
}
)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
defmodule SimpleWeb.Helpers.CommonSchemas do
use PhoenixSwagger

def swagger_definitions do
%{
Error:
swagger_schema do
title("Error")
description("An error response")

properties do
success(:boolean, "Success bool")
msg(:string, "Error response", required: true)
end

example(%{
success: false,
msg: "User ID missing"
})
end
}
end

end
4 changes: 4 additions & 0 deletions examples/simple/lib/simple_web/router.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
defmodule SimpleWeb.Router do
use SimpleWeb, :router
import PhoenixSwagger
alias PhoenixSwagger.Plug.Validate

pipeline :api do
Expand All @@ -23,5 +24,8 @@ defmodule SimpleWeb.Router do
title: "Simple App"
}
}
|> add_module(SimpleWeb.Helpers.CommonSchemas)
|> add_module(SimpleWeb.UserSocket)

end
end
65 changes: 65 additions & 0 deletions examples/simple/priv/static/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,49 @@
"description": "Delete a user by ID"
}
},
"/api/users/test": {
"get": {
"tags": [
"UserSocket"
],
"summary": "Test function",
"responses": {
"200": {
"schema": {
"$ref": "#/definitions/UsersResponse"
},
"examples": {
"application/json": {
"data": [
{
"updated_at": "2017-02-12T13:45:23Z",
"name": "Joe",
"inserted_at": "2017-02-08T12:34:55Z",
"id": 1,
"email": "[email protected]"
},
{
"updated_at": "2017-02-15T23:15:43Z",
"name": "Jack",
"inserted_at": "2017-02-04T11:24:45Z",
"id": 2,
"email": "[email protected]"
}
]
}
},
"description": "OK"
}
},
"produces": [
"application/json"
],
"parameters": [],
"operationId": "SimpleWeb.UserSocket.test",
"description": "List tests in db",
"deprecated": false
}
},
"/api/users": {
"post": {
"tags": [
Expand Down Expand Up @@ -297,6 +340,28 @@
"email": "[email protected]"
},
"description": "A user of the app"
},
"Error": {
"type": "object",
"title": "Error",
"required": [
"msg"
],
"properties": {
"success": {
"type": "boolean",
"description": "Success bool"
},
"msg": {
"type": "string",
"description": "Error response"
}
},
"example": {
"success": false,
"msg": "User ID missing"
},
"description": "An error response"
}
}
}
27 changes: 27 additions & 0 deletions lib/helpers/helpers.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
defmodule PhoenixSwagger.Helpers do

def merge_definitions(definitions, swagger_map = %{definitions: existing}) do
%{swagger_map | definitions: Map.merge(existing, definitions)}
end

def merge_paths(path, swagger_map) do
paths = Map.merge(swagger_map.paths, path, &merge_conflicts/3)
%{swagger_map | paths: paths}
end

def swagger_map(swagger_map) do
Map.update(swagger_map, :definitions, %{}, &(&1))
|> Map.update(:paths, %{}, &(&1))
end

def extract_args(action) do
[
%{verb: action |> String.to_atom, path: ""}
]
end

defp merge_conflicts(_key, value1, value2) do
Map.merge(value1, value2)
end

end
20 changes: 4 additions & 16 deletions lib/mix/tasks/swagger.generate.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule Mix.Tasks.Phx.Swagger.Generate do
use Mix.Task
require Logger
alias PhoenixSwagger.Helpers, as: Helpers

@recursive true

Expand Down Expand Up @@ -129,7 +130,7 @@ defmodule Mix.Tasks.Phx.Swagger.Generate do
|> Enum.filter(&!is_nil(&1))
|> Enum.filter(&controller_function_exported?/1)
|> Enum.map(&get_swagger_path/1)
|> Enum.reduce(swagger_map, &merge_paths/2)
|> Enum.reduce(swagger_map, &Helpers.merge_paths/2)
end

defp find_swagger_path_function(route = %{opts: action, path: path, verb: verb}) when is_atom(action) do
Expand All @@ -152,15 +153,14 @@ defmodule Mix.Tasks.Phx.Swagger.Generate do
Code.ensure_compiled?(controller) ->
%{
controller: controller,
swagger_fun: swagger_fun,
path: format_path(path),
swagger_fun: swagger_fun,
verb: verb
}
true ->
Logger.warn "Warning: #{controller} module didn't load."
nil
end

end

defp format_path(path) do
Expand All @@ -175,15 +175,6 @@ defmodule Mix.Tasks.Phx.Swagger.Generate do
apply(controller, fun, [route])
end

defp merge_paths(path, swagger_map) do
paths = Map.merge(swagger_map.paths, path, &merge_conflicts/3)
%{swagger_map | paths: paths}
end

defp merge_conflicts(_key, value1, value2) do
Map.merge(value1, value2)
end

defp collect_host(swagger_map, nil), do: swagger_map
defp collect_host(swagger_map, endpoint) do
endpoint_config = Application.get_env(app_name(), endpoint)
Expand Down Expand Up @@ -221,14 +212,11 @@ defmodule Mix.Tasks.Phx.Swagger.Generate do
|> Enum.uniq()
|> Enum.filter(&function_exported?(&1, :swagger_definitions, 0))
|> Enum.map(&apply(&1, :swagger_definitions, []))
|> Enum.reduce(swagger_map, &merge_definitions/2)
|> Enum.reduce(swagger_map, &Helpers.merge_definitions/2)
end

defp find_controller(route_map) do
Module.concat([:Elixir | Module.split(route_map.plug)])
end

defp merge_definitions(definitions, swagger_map = %{definitions: existing}) do
%{swagger_map | definitions: Map.merge(existing, definitions)}
end
end
63 changes: 63 additions & 0 deletions lib/phoenix_swagger.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule PhoenixSwagger do
use Application
alias PhoenixSwagger.Path
alias PhoenixSwagger.Path.PathObject
alias PhoenixSwagger.Helpers, as: Helpers

@moduledoc """
The PhoenixSwagger module provides macros for defining swagger operations and schemas.
Expand Down Expand Up @@ -201,6 +202,68 @@ defmodule PhoenixSwagger do
end
end

@doc """
Sometimes swagger paths and schema definitions defined in non-controller modules need
to be generated in the `swagger.json` output file. The `add_module/2` function
may be used to ensure their successful addition.

## Example

%{
info: %{
version: "1.0",
title: "Simple App"
}
}
|> add_module(SimpleWeb.UserSocket)

"""
def add_module(swagger_map, module) do
functions = module.__info__(:functions)

swagger_map
|> get_paths(module, functions)
|> get_schemas(module, functions)
end

defp get_schemas(swagger_map, module, functions) do
Enum.map(functions, fn {action, _arg} -> build_schemas(action, module) end)
|> Enum.filter(&!is_nil(&1))
|> Enum.reduce(Helpers.swagger_map(swagger_map), &Helpers.merge_definitions/2)
end
defp build_schemas(function, module) do
if is_schema?(function), do: apply(module, function, [])
end
defp is_schema?(function) do
function
|> Atom.to_string
|> String.contains?("swagger_definitions")
end

defp get_paths(swagger_map, module, functions) do
Enum.map(functions, fn {action, _arg} -> build_path(action, module) end)
|> Enum.filter(&!is_nil(&1))
|> Enum.reduce(Helpers.swagger_map(swagger_map), &Helpers.merge_paths/2)
end
defp build_path(function, module) do
if is_path?(function) do
action =
function
|> get_action
apply(module, function, Helpers.extract_args(action))
end
end
defp is_path?(function) do
function
|> Atom.to_string
|> String.contains?("swagger_path")
end
defp get_action(function) do
function
|> Atom.to_string
|> String.replace("swagger_path_", "")
end

@doc false
# Add a default operationId based on model name and action if required
def ensure_operation_id(path = %PathObject{operation: %{operationId: ""}}, module, action) do
Expand Down