diff --git a/ROADMAP.md b/ROADMAP.md index d66f8b61..db7ed1e3 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -3,3 +3,5 @@ - Remove need for `json_render_error_v2: true` option for `CastAndValidate` - Remove `OpenApiSpex.Plug.Cast`, and rename `Cast2` to `Cast`. - Simplify interface for error rendering modules +- Pass casted request params to `conn` without breaking contract with `Conn.t` +- Remove `@derive` call in `OpenApiSpex.schema/1` diff --git a/lib/open_api_spex.ex b/lib/open_api_spex.ex index b19fa906..7a055202 100644 --- a/lib/open_api_spex.ex +++ b/lib/open_api_spex.ex @@ -169,38 +169,111 @@ defmodule OpenApiSpex do } } end + + ## Example + + This example shows the `:struct?` and `:derive?` options that may + be passed to `schema/2`: + + defmodule MyAppWeb.Schemas.User do + require OpenApiSpex + alias OpenApiSpex.Schema + + OpenApiSpex.schema( + %{ + type: :object, + properties: %{ + name: %Schema{type: :string} + } + }, + struct?: false, + derive?: false + ) + end + + ## Options + + - `:struct?` (boolean) - When false, prevents the automatic generation + of a struct definition for the schema module. + - `:derive?` (boolean) When false, prevents the automatic generation + of a `@derive` call for either `Poison.Encoder` + or `Jason.Encoder`. Using this option can + prevent "... protocol has already been consolidated ..." + compiler warnings. """ - defmacro schema(body) do + defmacro schema(body, opts \\ []) do quote do @compile {:report_warnings, false} @behaviour OpenApiSpex.Schema - @schema struct( - OpenApiSpex.Schema, - unquote(body) - |> Map.delete(:__struct__) - |> update_in([:"x-struct"], fn struct_module -> - struct_module || __MODULE__ - end) - |> update_in([:title], fn title -> - title || __MODULE__ |> Module.split() |> List.last() - end) + @schema OpenApiSpex.build_schema( + unquote(body), + Keyword.merge([module: __MODULE__], unquote(opts)) ) def schema, do: @schema if Map.get(@schema, :"x-struct") == __MODULE__ do - @derive Enum.filter([Poison.Encoder, Jason.Encoder], &Code.ensure_loaded?/1) - defstruct Schema.properties(@schema) - @type t :: %__MODULE__{} + if Keyword.get(unquote(opts), :derive?, true) do + @derive Enum.filter([Poison.Encoder, Jason.Encoder], &Code.ensure_loaded?/1) + end + + if Keyword.get(unquote(opts), :struct?, true) do + defstruct Schema.properties(@schema) + @type t :: %__MODULE__{} + end end + end + end - Map.from_struct(@schema) |> OpenApiSpex.validate_compiled_schema() + @doc """ + Build a Schema struct from the given keyword list. - # Throwing warnings to prevent runtime bugs (like in issue #144) - @schema - |> SchemaConsistency.warnings() - |> Enum.each(&IO.warn("Inconsistent schema: #{&1}", Macro.Env.stacktrace(__ENV__))) - end + This function adds functionality over defining a + schema with struct literal sytax using `%OpenApiSpex.Struct{}`: + + - When the `:module` option is given, the `:"x-struct` and `:title` + attributes of the schema will be autopopulated based on the given + module + - Validations are performed on the schema to ensure it is correct. + + ## Options + + - `:module` (module) - A module in the application that the schema + should be associated with. + """ + def build_schema(body, opts \\ []) do + module = opts[:module] || body[:"x-struct"] + + attrs = + body + |> Map.delete(:__struct__) + |> update_in([:"x-struct"], fn struct_module -> + if Keyword.get(opts, :struct?, true) do + struct_module || module + else + struct_module + end + end) + |> update_in([:title], fn title -> + title || title_from_module(module) + end) + + schema = struct(OpenApiSpex.Schema, attrs) + + Map.from_struct(schema) |> OpenApiSpex.validate_compiled_schema() + + # Throwing warnings to prevent runtime bugs (like in issue #144) + schema + |> SchemaConsistency.warnings() + |> Enum.each(&IO.warn("Inconsistent schema: #{&1}", Macro.Env.stacktrace(__ENV__))) + + schema + end + + def title_from_module(nil), do: nil + + def title_from_module(module) do + module |> Module.split() |> List.last() end @doc """ diff --git a/mix.exs b/mix.exs index f6274203..7cb7b0f6 100644 --- a/mix.exs +++ b/mix.exs @@ -57,7 +57,7 @@ defmodule OpenApiSpex.Mixfile do {:jason, "~> 1.0", optional: true}, {:plug, "~> 1.7"}, {:phoenix, "~> 1.3", only: [:dev, :test]}, - {:ex_doc, "~> 0.20", only: :dev, runtime: false}, + {:ex_doc, "~> 0.23", only: :dev, runtime: false}, {:dialyxir, "~> 0.5", only: [:dev, :test], runtime: false} ] end diff --git a/mix.lock b/mix.lock index 0888fbca..7abb01b0 100644 --- a/mix.lock +++ b/mix.lock @@ -1,12 +1,13 @@ %{ "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm", "6c32a70ed5d452c6650916555b1f96c79af5fc4bf286997f8b15f213de786f73"}, "earmark": {:hex, :earmark, "1.3.1", "73812f447f7a42358d3ba79283cfa3075a7580a3a2ed457616d6517ac3738cb9", [:mix], [], "hexpm", "000aaeff08919e95e7aea13e4af7b2b9734577b3e6a7c50ee31ee88cab6ec4fb"}, - "ex_doc": {:hex, :ex_doc, "0.20.2", "1bd0dfb0304bade58beb77f20f21ee3558cc3c753743ae0ddbb0fd7ba2912331", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "8e24fc8ff9a50b9f557ff020d6c91a03cded7e59ac3e0eec8a27e771430c7d27"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.12", "b245e875ec0a311a342320da0551da407d9d2b65d98f7a9597ae078615af3449", [:mix], [], "hexpm", "711e2cc4d64abb7d566d43f54b78f7dc129308a63bc103fbd88550d2174b3160"}, + "ex_doc": {:hex, :ex_doc, "0.23.0", "a069bc9b0bf8efe323ecde8c0d62afc13d308b1fa3d228b65bca5cf8703a529d", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f5e2c4702468b2fd11b10d39416ddadd2fcdd173ba2a0285ebd92c39827a5a16"}, "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fdf843bca858203ae1de16da2ee206f53416bbda5dc8c9e78f43243de4bc3afe"}, - "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5fbc8e549aa9afeea2847c0769e3970537ed302f93a23ac612602e805d9d1e7f"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "adf0218695e22caeda2820eaba703fa46c91820d53813a2223413da3ef4ba515"}, + "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.15.0", "98312c9f0d3730fde4049985a1105da5155bfe5c11e47bdc7406d88e01e4219b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "75ffa34ab1056b7e24844c90bfc62aaf6f3a37a15faa76b07bc5eba27e4a8b4a"}, "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"}, - "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm", "5c040b8469c1ff1b10093d3186e2e10dbe483cd73d79ec017993fb3985b8a9b3"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, "phoenix": {:hex, :phoenix, "1.4.0", "56fe9a809e0e735f3e3b9b31c1b749d4b436e466d8da627b8d82f90eaae714d2", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "22da8f659cf13d3ba73b767f66b8c389113ddf0ef7b94225cc84e94b85eac90e"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.1", "6668d787e602981f24f17a5fbb69cc98f8ab085114ebfac6cc36e10a90c8e93c", [:mix], [], "hexpm", "a3d890aaa3156d51056179dcaaadaf32b844f71656bb27c58756f2b97875c36c"}, "plug": {:hex, :plug, "1.7.1", "8516d565fb84a6a8b2ca722e74e2cd25ca0fc9d64f364ec9dbec09d33eb78ccd", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}], "hexpm", "daa5fee4209c12c3c48b05a96cf88c320b627c9575f987554dcdc1fdcdf2c15e"}, diff --git a/test/open_api/decode_test.exs b/test/open_api/decode_test.exs index 69021c58..2cd372af 100644 --- a/test/open_api/decode_test.exs +++ b/test/open_api/decode_test.exs @@ -218,7 +218,7 @@ defmodule OpenApiSpex.OpenApi.DecodeTest do } == link assert %{ - "api_key" => api_key_security_scheme, + "api_key" => _api_key_security_scheme, "petstore_auth" => petstore_auth_security_scheme } = securitySchemes @@ -387,7 +387,7 @@ defmodule OpenApiSpex.OpenApi.DecodeTest do test_conn = fetch_query_params(test_conn) - assert {:ok, validation_result} = + assert {:ok, _validation_result} = OpenApiSpex.cast_and_validate( spec, spec.paths["/example"].post, diff --git a/test/schema_construction_test.exs b/test/schema_construction_test.exs index 06d596e3..02811f64 100644 --- a/test/schema_construction_test.exs +++ b/test/schema_construction_test.exs @@ -11,5 +11,32 @@ defmodule OpenApiSpex.SchemaConstructionTest do assert %OpenApiSpex.Schema{"x-struct": module, type: schema_type} = SchemaCreate.schema() assert schema_type == :string assert module == SchemaCreate + + assert SchemaCreate.__struct__() + end + + defmodule SchemaWithoutStructDef do + require OpenApiSpex + + OpenApiSpex.schema(%{type: :string}, struct?: false) + end + + test "able to define schema without defining a struct" do + assert_raise UndefinedFunctionError, fn -> + SchemaWithoutStructDef.__struct__() + end + end + + defmodule SchemaWithoutDerive do + require OpenApiSpex + + OpenApiSpex.schema(%{type: :string}, derive?: false) + end + + test "able to define schema module without a @derive" do + assert_raise Protocol.UndefinedError, fn -> + struct = %SchemaWithoutDerive{} + Jason.encode!(struct) + end end end diff --git a/test/support/dsl_controller.ex b/test/support/dsl_controller.ex index fc68f365..9c8b8ef7 100644 --- a/test/support/dsl_controller.ex +++ b/test/support/dsl_controller.ex @@ -47,7 +47,6 @@ defmodule OpenApiSpexTest.DslController do end defmodule UsersDestroyResponse do - alias OpenApiSpex.Schema require OpenApiSpex OpenApiSpex.schema(%{ @@ -117,7 +116,7 @@ defmodule OpenApiSpexTest.DslController do no_content: {"Users destroy response", "application/json", UsersDestroyResponse} ] - def index(conn, _) do + def destroy(conn, _) do json(conn, []) end end