diff --git a/README.md b/README.md index 6ba53ef..0ce76a3 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,11 @@ The following task-specific options are available: * Description: Specifies the file name (within the output_path) of the Swagger JSON * Type: string * Default Value: "api.json" +* :pipe_through, + * Description: if pipe_through is defined only this is used. + * Type: list + * Default Value: nil + * Example: pipe_through: [:api] ### Swagger Config The following Swagger-specific options are available: @@ -224,6 +229,27 @@ The JSON output will look like: } ``` +If changeset is defined in models, like this: +```elixir + schema "users" do + field :name, :string + field :email, :string + field :bio, :string + field :number_of_pets, :integer + + timestamps + end + + @required_fields ~w(name email) + @optional_fields ~w(bio number_of_pets) + + def changeset(model, params \\ :empty) do + model + |> cast(params, @required_fields, @optional_fields) + end +``` +Changeset is used for 'required' fields in schema. + ### Converting Phoenix Routes into Swagger Paths The [Phoenix Routes](https://github.com/phoenixframework/phoenix/blob/v1.0.0/lib/phoenix/router/route.ex) that is found via the [Phoenix Router](https://github.com/phoenixframework/phoenix/blob/v1.0.0/lib/phoenix/router.ex) are converted into a [Swagger Paths Object](http://swagger.io/specification/#pathsObject), each route becoming a [Path Item](http://swagger.io/specification/#pathItemObject). The Phoenix template paths are converted into [Swagger path templates](http://swagger.io/specification/#pathTemplating) and each templated variable is converted into a [Path paramter](http://swagger.io/specification/#parametersDefinitionsObject). All path parameters are assumed to be required and are of type string (except for parameters named `id`, which are assumed to be integers). diff --git a/lib/mix/tasks/swagger.ex b/lib/mix/tasks/swagger.ex index 2dd1936..e09b26a 100644 --- a/lib/mix/tasks/swagger.ex +++ b/lib/mix/tasks/swagger.ex @@ -123,43 +123,48 @@ defmodule Mix.Tasks.Swagger do def add_routes(nil, swagger), do: swagger def add_routes([], swagger), do: swagger def add_routes([route | remaining_routes], swagger) do - swagger_path = path_from_route(String.split(route.path, "/"), nil) + pipe_through = Application.get_env(:swaggerdoc, :pipe_through, nil) + if pipe_through && route.pipe_through != pipe_through do + add_routes(remaining_routes, swagger) + else + swagger_path = path_from_route(String.split(route.path, "/"), nil) - path = swagger[:paths][swagger_path] - if path == nil do - path = %{} - end + path = swagger[:paths][swagger_path] + if path == nil do + path = %{} + end - func_name = "swaggerdoc_#{route.opts}" - verb = if route.plug != nil && Keyword.has_key?(route.plug.__info__(:functions), String.to_atom(func_name)) do - apply(route.plug, String.to_atom(func_name), []) - else - parse_default_verb(route.path) - end + func_name = "swaggerdoc_#{route.opts}" + verb = if route.plug != nil && Keyword.has_key?(route.plug.__info__(:functions), String.to_atom(func_name)) do + apply(route.plug, String.to_atom(func_name), []) + else + parse_default_verb(route.path) + end - response_schema = verb[:response_schema] - verb = Map.delete(verb, :response_schema) + response_schema = verb[:response_schema] + verb = Map.delete(verb, :response_schema) - verb_string = String.downcase("#{route.verb}") - if verb[:responses] == nil do - verb = Map.put(verb, :responses, default_responses(verb_string, response_schema)) - end + verb_string = String.downcase("#{route.verb}") + if verb[:responses] == nil do + verb = Map.put(verb, :responses, default_responses(verb_string, response_schema)) + end - if verb[:produces] == nil do - verb = Map.put(verb, :produces, Application.get_env(:swaggerdoc, :produces, [])) - end + if verb[:produces] == nil do + verb = Map.put(verb, :produces, Application.get_env(:swaggerdoc, :produces, [])) + end - if verb[:operationId] == nil do - verb = Map.put(verb, :operationId, "#{route.opts}") - end + if verb[:operationId] == nil do + verb = Map.put(verb, :operationId, "#{route.opts}") + end - if verb[:description] == nil do - verb = Map.put(verb, :description, "") - end + if verb[:description] == nil do + verb = Map.put(verb, :description, "") + end - path = Map.put(path, verb_string, verb) - paths = Map.put(swagger[:paths], swagger_path, path) - add_routes(remaining_routes, Map.put(swagger, :paths, paths)) + path = Map.put(path, verb_string, verb) + paths = Map.put(swagger[:paths], swagger_path, path) + add_routes(remaining_routes, Map.put(swagger, :paths, paths)) + end end @doc """ @@ -247,6 +252,13 @@ defmodule Mix.Tasks.Swagger do end module_json = %{"properties" => properties_json} + + if :erlang.function_exported(module, :changeset, 2) do + module_struct = module.changeset(module.__struct__, %{}) + required = required_fields module_struct.errors + module_json = Map.put(module_json, "required", required) + end + def_json = Map.put(def_json, "#{inspect module}", module_json) end @@ -274,4 +286,11 @@ defmodule Mix.Tasks.Swagger do _ -> %{"type" => "string"} end end + + def required_fields([]), do: [] + + def required_fields([head|tail]) do + {key, _msg} = head + [to_string(key)|required_fields(tail)] + end end diff --git a/mix.lock b/mix.lock index b898a08..37f21c4 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,6 @@ %{"decimal": {:hex, :decimal, "1.1.0"}, "earmark": {:hex, :earmark, "0.1.17"}, - "ecto": {:hex, :ecto, "1.0.1"}, + "ecto": {:hex, :ecto, "1.0.7"}, "ex_doc": {:hex, :ex_doc, "0.8.4"}, "meck": {:hex, :meck, "0.8.3"}, "phoenix": {:hex, :phoenix, "1.0.1"}, diff --git a/test/mix/tasks/swagger_test.exs b/test/mix/tasks/swagger_test.exs index 4dfa421..0f1ab8b 100644 --- a/test/mix/tasks/swagger_test.exs +++ b/test/mix/tasks/swagger_test.exs @@ -19,6 +19,28 @@ defmodule Mocks.UserModel do end end +defmodule Mocks.UserRequiredModel do + use Ecto.Model + + schema "users" do + field :name, :string + field :email, :string + field :bio, :string + field :number_of_pets, :integer + + timestamps + end + + @required_fields ~w(name email) + @optional_fields ~w(bio number_of_pets) + + def changeset(model, params \\ :empty) do + model + |> cast(params, @required_fields, @optional_fields) + end + +end + defmodule Mocks.SimpleRouter do def __routes__, do: [] end @@ -46,6 +68,7 @@ defmodule Mix.Tasks.Swagger.Tests do Application.delete_env(:swaggerdoc, :schemes) Application.delete_env(:swaggerdoc, :consumes) Application.delete_env(:swaggerdoc, :produces) + Application.delete_env(:swaggerdoc, :pipe_through) end setup do @@ -189,6 +212,35 @@ defmodule Mix.Tasks.Swagger.Tests do }} end + test "add_routes - route with select pipe_through" do + Application.put_env(:swaggerdoc, :pipe_through, [:api]) + route = [%PhoenixRoute{ + path: "/testing/:id", + opts: :index, + verb: "GET" + },%PhoenixRoute{ + path: "/api/v1/testing/:id", + opts: :index, + verb: "GET", + pipe_through: [:api], + }] + assert Swagger.add_routes(route, %{paths: %{}}) == %{ + paths: %{"/api/v1/testing/{id}" => + %{"get" => %{ + description: "", + operationId: "index", + parameters: [%{"description" => "", "in" => "path", "name" => "id", "required" => true, "type" => "integer"}], + produces: [], + responses: %{ + "200" => %{"description" => "Resource Content"}, + "401" => %{"description" => "Request is not authorized"}, "404" => %{"description" => "Resource not found"}, + "500" => %{"description" => "Internal Server Error"} + } + }} + }} + Application.delete_env(:swaggerdoc, :pipe_through) + end + test "add_routes - route from custom plug" do route = %PhoenixRoute{ path: "/test", @@ -304,7 +356,18 @@ defmodule Mix.Tasks.Swagger.Tests do test "build_definitions - modules but no models" do assert Swagger.build_definitions([{Mocks.DefaultPlug, ""}], %{}) == %{} - end + end + + #============================== + # required_fields tests + + test "required_fields - parse errors from struct, if errors is empty" do + assert Swagger.required_fields([]) == [] + end + + test "required_fields - parse errors from struct" do + assert Swagger.required_fields([name: "can't be blank", email: "can't be blank"]) == ["name", "email"] + end test "build_definitions - model" do assert Swagger.build_definitions([{Mocks.UserModel, ""}], %{}) == %{ @@ -319,6 +382,23 @@ defmodule Mix.Tasks.Swagger.Tests do "updated_at" => %{"format" => "date-time", "type" => "string"}} } } + end + + test "build_definitions - model support changeset (required_fields)" do + assert Swagger.build_definitions([{Mocks.UserRequiredModel, ""}], %{}) == %{ + "Mocks.UserRequiredModel" => %{ + "properties" => %{ + "bio" => %{"type" => "string"}, + "email" => %{"type" => "string"}, + "id" => %{"format" => "int64", "type" => "integer"}, + "inserted_at" => %{"format" => "date-time", "type" => "string"}, + "name" => %{"type" => "string"}, + "number_of_pets" => %{"format" => "int64", "type" => "integer"}, + "updated_at" => %{"format" => "date-time", "type" => "string"} + }, + "required" => ["name", "email"] + } + } end #============================== @@ -349,15 +429,15 @@ defmodule Mix.Tasks.Swagger.Tests do end test "convert_property_type - :Ecto.DateTime " do - assert Swagger.convert_property_type(:Ecto.DateTime ) == %{"type" => "string", "format" => "date-time"} + assert Swagger.convert_property_type(Ecto.DateTime ) == %{"type" => "string", "format" => "date-time"} end test "convert_property_type - :Ecto.Date" do - assert Swagger.convert_property_type(:Ecto.Date) ==%{"type" => "string", "format" => "date"} + assert Swagger.convert_property_type(Ecto.Date) ==%{"type" => "string", "format" => "date"} end test "convert_property_type - :Ecto.Time" do - assert Swagger.convert_property_type(:Ecto.Time) == %{"type" => "string", "format" => "date-time"} + assert Swagger.convert_property_type(Ecto.Time) == %{"type" => "string", "format" => "date-time"} end test "convert_property_type - :uuid" do @@ -390,4 +470,4 @@ defmodule Mix.Tasks.Swagger.Tests do after :meck.unload end -end \ No newline at end of file +end