diff --git a/.gitignore b/.gitignore index 755b605..a7d0bcf 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /deps erl_crash.dump *.ez +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index 66fa8ba..11da4c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/examples/simple/lib/simple_web/channels/user_socket.ex b/examples/simple/lib/simple_web/channels/user_socket.ex index 815f721..f4597a2 100644 --- a/examples/simple/lib/simple_web/channels/user_socket.ex +++ b/examples/simple/lib/simple_web/channels/user_socket.ex @@ -1,5 +1,6 @@ defmodule SimpleWeb.UserSocket do use Phoenix.Socket + use PhoenixSwagger ## Channels # channel "room:*", Simple.RoomChannel @@ -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: "Joe6@mail.com", + inserted_at: "2017-02-08T12:34:55Z", + updated_at: "2017-02-12T13:45:23Z" + }, + %{ + id: 2, + name: "Jack", + email: "Jack7@mail.com", + inserted_at: "2017-02-04T11:24:45Z", + updated_at: "2017-02-15T23:15:43Z" + } + ] + } + ) + end end diff --git a/examples/simple/lib/simple_web/controllers/helpers/common_schemas.ex b/examples/simple/lib/simple_web/controllers/helpers/common_schemas.ex new file mode 100644 index 0000000..827ce63 --- /dev/null +++ b/examples/simple/lib/simple_web/controllers/helpers/common_schemas.ex @@ -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 diff --git a/examples/simple/lib/simple_web/router.ex b/examples/simple/lib/simple_web/router.ex index 0bd998e..3813a1e 100644 --- a/examples/simple/lib/simple_web/router.ex +++ b/examples/simple/lib/simple_web/router.ex @@ -1,5 +1,6 @@ defmodule SimpleWeb.Router do use SimpleWeb, :router + import PhoenixSwagger alias PhoenixSwagger.Plug.Validate pipeline :api do @@ -23,5 +24,8 @@ defmodule SimpleWeb.Router do title: "Simple App" } } + |> add_module(SimpleWeb.Helpers.CommonSchemas) + |> add_module(SimpleWeb.UserSocket) + end end diff --git a/examples/simple/priv/static/swagger.json b/examples/simple/priv/static/swagger.json index 436ecc1..dc6d829 100644 --- a/examples/simple/priv/static/swagger.json +++ b/examples/simple/priv/static/swagger.json @@ -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": "Joe6@mail.com" + }, + { + "updated_at": "2017-02-15T23:15:43Z", + "name": "Jack", + "inserted_at": "2017-02-04T11:24:45Z", + "id": 2, + "email": "Jack7@mail.com" + } + ] + } + }, + "description": "OK" + } + }, + "produces": [ + "application/json" + ], + "parameters": [], + "operationId": "SimpleWeb.UserSocket.test", + "description": "List tests in db", + "deprecated": false + } + }, "/api/users": { "post": { "tags": [ @@ -297,6 +340,28 @@ "email": "joe@gmail.com" }, "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" } } } \ No newline at end of file diff --git a/lib/helpers/helpers.ex b/lib/helpers/helpers.ex new file mode 100644 index 0000000..2665472 --- /dev/null +++ b/lib/helpers/helpers.ex @@ -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 diff --git a/lib/mix/tasks/swagger.generate.ex b/lib/mix/tasks/swagger.generate.ex index 7425f48..1252a5f 100644 --- a/lib/mix/tasks/swagger.generate.ex +++ b/lib/mix/tasks/swagger.generate.ex @@ -1,6 +1,7 @@ defmodule Mix.Tasks.Phx.Swagger.Generate do use Mix.Task require Logger + alias PhoenixSwagger.Helpers, as: Helpers @recursive true @@ -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 @@ -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 @@ -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) @@ -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 diff --git a/lib/phoenix_swagger.ex b/lib/phoenix_swagger.ex index 4726ee9..6845a91 100644 --- a/lib/phoenix_swagger.ex +++ b/lib/phoenix_swagger.ex @@ -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. @@ -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