diff --git a/lib/open_api_spex/controller_specs.ex b/lib/open_api_spex/controller_specs.ex index d1595579..5578e089 100644 --- a/lib/open_api_spex/controller_specs.ex +++ b/lib/open_api_spex/controller_specs.ex @@ -366,15 +366,50 @@ defmodule OpenApiSpex.ControllerSpecs do end end + @operation_permitted_keys [ + :callbacks, + :description, + :deprecated, + :external_docs, + :operation_id, + :parameters, + :request_body, + :responses, + :security, + :summary, + :tags + ] + @doc """ Define an Operation for a controller action. See `OpenApiSpex.ControllerSpecs` for usage and examples. + + Permitted operation keys: + + #{Enum.map_join(@operation_permitted_keys, "\n", fn k -> "* `#{k}`" end)} + * keys prefixed by `x-` for extensions """ def operation_spec(_module, _action, nil = _spec), do: nil def operation_spec(_module, _action, false = _spec), do: nil def operation_spec(module, action, spec) do + validation_result = + spec + |> Enum.reject(fn {key, _val} -> extension_key?(key) end) + |> Keyword.validate(@operation_permitted_keys) + + case validation_result do + {:ok, _spec} -> + :ok + + {:error, unknown_keys} -> + raise ArgumentError, + "Unknown keys given to operation/2: #{inspect(unknown_keys)}. " <> + "Allowed keys are: #{inspect(@operation_permitted_keys)}, " <> + "and keys starting with 'x-'." + end + spec = Map.new(spec) shared_tags = Module.get_attribute(module, :shared_tags, []) |> List.flatten() @@ -386,9 +421,7 @@ defmodule OpenApiSpex.ControllerSpecs do extensions = spec - |> Enum.filter(fn {key, _val} -> - is_atom(key) && String.starts_with?(to_string(key), "x-") - end) + |> Enum.filter(fn {key, _val} -> extension_key?(key) end) |> Map.new(fn {key, value} -> {to_string(key), value} end) %Operation{ @@ -406,4 +439,8 @@ defmodule OpenApiSpex.ControllerSpecs do extensions: extensions } end + + defp extension_key?(key) do + is_atom(key) && String.starts_with?(to_string(key), "x-") + end end diff --git a/test/controller_specs_test.exs b/test/controller_specs_test.exs index 7ef0f786..eff3b1de 100644 --- a/test/controller_specs_test.exs +++ b/test/controller_specs_test.exs @@ -7,7 +7,7 @@ defmodule OpenApiSpex.ControllerSpecsTest do alias OpenApiSpexTest.DslController alias OpenApiSpexTest.DslControllerOperationStructs - describe "operation/1" do + describe "operation/2" do test "supports :parameters" do assert %OpenApiSpex.Operation{ responses: %{}, @@ -157,5 +157,39 @@ defmodule OpenApiSpex.ControllerSpecsTest do assert %OpenApiSpex.Operation{extensions: %{"x-foo" => "bar"}} = DslController.open_api_operation(:index) end + + test "raises when unknown key is provided" do + msg = + "Unknown keys given to operation/2: [:unknown]. Allowed keys are: " <> + "[:callbacks, :description, :deprecated, :external_docs, :operation_id, :parameters, " <> + ":request_body, :responses, :security, :summary, :tags], and keys starting with 'x-'." + + assert_raise ArgumentError, msg, fn -> + Code.eval_string(""" + defmodule TestController do + use OpenApiSpex.ControllerSpecs + + operation :index, + summary: "Users index", + parameters: [ + username: [ + in: :query, + description: "Filter by username", + type: :string + ] + ], + responses: [ + ok: {"Users index response", "application/json", UsersIndexResponse} + ], + unknown: "value", + "x-foo": "bar" + + def index(conn, _) do + json(conn, []) + end + end + """) + end + end end end