From 9d6e4325912919db63b58abe6caefa0aa569484e Mon Sep 17 00:00:00 2001 From: fatimamohamed Date: Wed, 29 May 2019 01:47:27 -0500 Subject: [PATCH 1/3] path and schema injection --- .DS_Store | Bin 0 -> 6148 bytes .../lib/simple_web/channels/user_socket.ex | 30 +++++++ .../controllers/helpers/common_schemas.ex | 24 ++++++ examples/simple/lib/simple_web/router.ex | 4 + examples/simple/priv/static/swagger.json | 65 +++++++++++++++ lib/helpers/misc_helpers.ex | 16 ++++ lib/mix/tasks/swagger.generate.ex | 20 +---- lib/phoenix_swagger.ex | 75 ++++++++++++++++++ 8 files changed, 218 insertions(+), 16 deletions(-) create mode 100644 .DS_Store create mode 100644 examples/simple/lib/simple_web/controllers/helpers/common_schemas.ex create mode 100644 lib/helpers/misc_helpers.ex diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 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/misc_helpers.ex b/lib/helpers/misc_helpers.ex new file mode 100644 index 0000000..100409d --- /dev/null +++ b/lib/helpers/misc_helpers.ex @@ -0,0 +1,16 @@ +defmodule Helpers.MiscHelpers 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 + + 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 c64bdcb..42187e1 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 Helpers.MiscHelpers, as: Helpers @recursive true @@ -118,7 +119,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 @@ -129,8 +130,8 @@ defmodule Mix.Tasks.Phx.Swagger.Generate do Code.ensure_compiled?(controller) -> %{ controller: controller, + path: path |> format_path, swagger_fun: swagger_fun, - path: format_path(path), verb: verb } true -> @@ -143,7 +144,6 @@ defmodule Mix.Tasks.Phx.Swagger.Generate do nil end - defp format_path(path) do Regex.replace(~r/:([^\/]+)/, path, "{\\1}") end @@ -156,15 +156,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) @@ -202,14 +193,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 ce5618e..96a91a1 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 Helpers.MiscHelpers, as: Helpers @moduledoc """ The PhoenixSwagger module provides macros for defining swagger operations and schemas. @@ -201,6 +202,80 @@ 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(struct, module) do + functions = module.__info__(:functions) + + paths = get_paths(functions, struct, module) + + Enum.map(functions, fn {action, _arg} -> build_schemas(action, module) end) + |> Enum.filter(&!is_nil(&1)) + |> Enum.reduce(swagger_map(paths), &Helpers.merge_definitions/2) + end + + def 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(functions, struct, module) do + Enum.map(functions, fn {action, _arg} -> build_path(action, module) end) + |> Enum.filter(&!is_nil(&1)) + |> Enum.reduce(swagger_map(struct), &Helpers.merge_paths/2) + end + + def swagger_map(swagger_map) do + Map.update(swagger_map, :definitions, %{}, &(&1)) + |> Map.update(:paths, %{}, &(&1)) + end + + defp build_path(function, module) do #room for improvement here for sure, comments? + case is_path?(function) do + {true, action} -> + apply(module, function, extract_args(action)) + {false, _action} -> + nil + end + end + + defp is_path?(function) do #room for improvement here for sure, comments? + funct_string = + function + |> Atom.to_string + + contains = + funct_string + |> String.contains?("swagger_path") + + {contains, String.replace(funct_string, "swagger_path_", "")} + end + + defp extract_args(action) do + [ + %{verb: action |> String.to_atom, 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 From 25d537a53c781de4d199ac26d024d795a32b2c20 Mon Sep 17 00:00:00 2001 From: fatimamohamed Date: Thu, 6 Jun 2019 17:01:52 -0500 Subject: [PATCH 2/3] fixes --- .DS_Store | Bin 6148 -> 0 bytes .gitignore | 1 + CHANGELOG.md | 1 + lib/helpers/{misc_helpers.ex => helpers.ex} | 13 +++- lib/mix/tasks/swagger.generate.ex | 4 +- lib/phoenix_swagger.ex | 62 ++++++++------------ mix.exs | 2 +- mix.lock | 2 +- test/validator_test.exs | 15 +++-- 9 files changed, 53 insertions(+), 47 deletions(-) delete mode 100644 .DS_Store rename lib/helpers/{misc_helpers.ex => helpers.ex} (59%) diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 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 diff --git a/lib/mix/tasks/swagger.generate.ex b/lib/mix/tasks/swagger.generate.ex index 42187e1..e309675 100644 --- a/lib/mix/tasks/swagger.generate.ex +++ b/lib/mix/tasks/swagger.generate.ex @@ -1,7 +1,7 @@ defmodule Mix.Tasks.Phx.Swagger.Generate do use Mix.Task require Logger - alias Helpers.MiscHelpers, as: Helpers + alias PhoenixSwagger.Helpers, as: Helpers @recursive true @@ -130,7 +130,7 @@ defmodule Mix.Tasks.Phx.Swagger.Generate do Code.ensure_compiled?(controller) -> %{ controller: controller, - path: path |> format_path, + path: format_path(path), swagger_fun: swagger_fun, verb: verb } diff --git a/lib/phoenix_swagger.ex b/lib/phoenix_swagger.ex index 96a91a1..523c2e4 100644 --- a/lib/phoenix_swagger.ex +++ b/lib/phoenix_swagger.ex @@ -3,7 +3,7 @@ defmodule PhoenixSwagger do use Application alias PhoenixSwagger.Path alias PhoenixSwagger.Path.PathObject - alias Helpers.MiscHelpers, as: Helpers + alias PhoenixSwagger.Helpers, as: Helpers @moduledoc """ The PhoenixSwagger module provides macros for defining swagger operations and schemas. @@ -218,62 +218,50 @@ defmodule PhoenixSwagger do |> add_module(SimpleWeb.UserSocket) """ - def add_module(struct, module) do + def add_module(swagger_map, module) do functions = module.__info__(:functions) - paths = get_paths(functions, struct, module) + 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(swagger_map(paths), &Helpers.merge_definitions/2) + |> Enum.reduce(Helpers.swagger_map(swagger_map), &Helpers.merge_definitions/2) end - - def build_schemas(function, module) do + 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(functions, struct, module) do + 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(swagger_map(struct), &Helpers.merge_paths/2) + |> Enum.reduce(Helpers.swagger_map(swagger_map), &Helpers.merge_paths/2) end - - def swagger_map(swagger_map) do - Map.update(swagger_map, :definitions, %{}, &(&1)) - |> Map.update(:paths, %{}, &(&1)) - end - - defp build_path(function, module) do #room for improvement here for sure, comments? - case is_path?(function) do - {true, action} -> - apply(module, function, extract_args(action)) - {false, _action} -> - nil + 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 #room for improvement here for sure, comments? - funct_string = - function - |> Atom.to_string - - contains = - funct_string - |> String.contains?("swagger_path") - - {contains, String.replace(funct_string, "swagger_path_", "")} + defp is_path?(function) do + function + |> Atom.to_string + |> String.contains?("swagger_path") end - - defp extract_args(action) do - [ - %{verb: action |> String.to_atom, path: ""} - ] + defp get_action(function) do + function + |> Atom.to_string + |> String.replace("swagger_path_", "") end @doc false diff --git a/mix.exs b/mix.exs index 26eb63b..9bc9699 100644 --- a/mix.exs +++ b/mix.exs @@ -55,7 +55,7 @@ defmodule PhoenixSwagger.Mixfile do defp deps do [ {:poison, "~> 2.2 or ~> 3.0"}, - {:ex_json_schema, "~> 0.5", optional: true}, + {:ex_json_schema, "~> 0.5.0", optional: true}, {:plug, "~> 1.4"}, {:ex_doc, "~> 0.18", only: :dev, runtime: false}, {:dialyxir, "~> 0.5", only: :dev, runtime: false} diff --git a/mix.lock b/mix.lock index d37ddaf..ac303db 100644 --- a/mix.lock +++ b/mix.lock @@ -2,7 +2,7 @@ "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, "earmark": {:hex, :earmark, "1.1.1", "433136b7f2e99cde88b745b3a0cfc3fbc81fe58b918a09b40fce7f00db4d8187", [:mix], [], "hexpm"}, "ex_doc": {:hex, :ex_doc, "0.18.3", "f4b0e4a2ec6f333dccf761838a4b253d75e11f714b85ae271c9ae361367897b7", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, - "ex_json_schema": {:hex, :ex_json_schema, "0.5.6", "9a96edd51a00fc005e12a773a7a79e507a77855e69fcb3d9a02f7f39c3265c03", [:mix], [], "hexpm"}, + "ex_json_schema": {:hex, :ex_json_schema, "0.5.8", "9758444f560ebf5e1c5cb37d41a122707939f75136c7a79c4ff4ca6ad5283fb9", [:mix], [], "hexpm"}, "mime": {:hex, :mime, "1.2.0", "78adaa84832b3680de06f88f0997e3ead3b451a440d183d688085be2d709b534", [:mix], [], "hexpm"}, "plug": {:hex, :plug, "1.5.0", "224b25b4039bedc1eac149fb52ed456770b9678bbf0349cdd810460e1e09195b", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, diff --git a/test/validator_test.exs b/test/validator_test.exs index 4df424c..d30610d 100644 --- a/test/validator_test.exs +++ b/test/validator_test.exs @@ -62,16 +62,21 @@ defmodule ValidatorTest do assert :ok = Validator.validate("/get/pets/{id}", %{"id" => 1}) assert {:error, :resource_not_exists} = Validator.validate("/pets", %{"id" => 1}) assert {:error, - [{"Required property id was not present.", "#"}, - {"Required property pet was not present.", "#"}], "/post/pets"} = Validator.validate("/post/pets", %{}) + [ + {"Required property id was not present.", "#"}, + {"Required property pet was not present.", "#"} + ], "/post/pets"} + = Validator.validate("/post/pets", %{}) assert {:error, [{"Type mismatch. Expected Integer but got String.", "#/id"}, {"Required property pet was not present.", "#"}], "/post/pets"} = Validator.validate("/post/pets", %{"id" => "wrong_id"}) assert {:error, "Required property pet was not present.", "#"} = Validator.validate("/post/pets", %{"id" => 1}) assert {:error, - [{"Required property name was not present.", "#/pet"}, - {"Required property tag was not present.", "#/pet"}], "/post/pets"} - = Validator.validate("/post/pets", %{"id" => 1, "pet" => %{}}) + [ + {"Required property name was not present.", "#/pet"}, + {"Required property tag was not present.", "#/pet"} + ], "/post/pets"} + = Validator.validate("/post/pets", %{"id" => 1, "pet" => %{}}) assert {:error, "Required property tag was not present.", "#/pet"} = Validator.validate("/post/pets", %{"id" => 1, "pet" => %{"name" => "pet_name"}}) assert :ok = Validator.validate("/post/pets", %{"id" => 1, "pet" => %{"name" => "pet_name", "tag" => "pet_tag"}}) From 05b3c0e98fd5c121fa0c7c0f5ab26f8e56ee9552 Mon Sep 17 00:00:00 2001 From: Fatima Mohamed Date: Sat, 18 Apr 2020 03:47:53 -0500 Subject: [PATCH 3/3] update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c55916..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 @@ -16,7 +17,6 @@ * Add `nullable:` option to `JsonSchema.relationship` function * Handle `x-nullable` schemas in `SchemaTest` * Add `deprecated` flag for operations - * Add `add_module/2` function to add path and schema definitions not in controllers. # 0.8.0