diff --git a/lib/open_api_spex.ex b/lib/open_api_spex.ex index 9db3db9d..ae5ce0b6 100644 --- a/lib/open_api_spex.ex +++ b/lib/open_api_spex.ex @@ -184,101 +184,60 @@ defmodule OpenApiSpex do Error.message(error) end - @doc """ - Declares a struct based `OpenApiSpex.Schema` - - - defines the schema/0 callback - - ensures the schema is linked to the module by "x-struct" extension property - - defines a struct with keys matching the schema properties - - defines a @type `t` for the struct - - derives a `Jason.Encoder` and/or `Poison.Encoder` for the struct - - See `OpenApiSpex.Schema` for additional examples and details. - - ## Example - - require OpenApiSpex - - defmodule User do - OpenApiSpex.schema %{ - title: "User", - description: "A user of the app", - type: :object, - properties: %{ - id: %Schema{type: :integer, description: "User ID"}, - name: %Schema{type: :string, description: "User name", pattern: ~r/[a-zA-Z][a-zA-Z0-9_]+/}, - email: %Schema{type: :string, description: "Email address", format: :email}, - inserted_at: %Schema{type: :string, description: "Creation timestamp", format: :'date-time'}, - updated_at: %Schema{type: :string, description: "Update timestamp", format: :'date-time'} - }, - required: [:name, :email], - example: %{ - "id" => 123, - "name" => "Joe User", - "email" => "joe@gmail.com", - "inserted_at" => "2017-09-12T12:34:55Z", - "updated_at" => "2017-09-13T10:11:12Z" - } - } - 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 + @doc false + def should_use_runtime_compilation?(body) do + with true <- System.otp_release() >= "28", + true <- Schema.has_regex_pattern?(body) do + true + else + _ -> false + end + end - - `: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, opts \\ []) do quote do @compile {:report_warnings, false} @behaviour OpenApiSpex.Schema - @schema OpenApiSpex.build_schema( - unquote(body), - Keyword.merge([module: __MODULE__], unquote(opts)) - ) + + schema = OpenApiSpex.build_schema(unquote(body), Keyword.merge([module: __MODULE__], unquote(opts))) + + case OpenApiSpex.should_use_runtime_compilation?(unquote(body)) do + true -> + IO.warn(""" + [OpenApiSpex] Regex patterns in schema definitions are deprecated in OTP 28+. + Consider using string patterns: pattern: "\\\\d-\\\\d" instead of pattern: ~r/\\\\d-\\\\d/ + """, Macro.Env.stacktrace(__ENV__)) + + def schema do + OpenApiSpex.build_schema_without_validation(unquote(body), Keyword.merge([module: __MODULE__], unquote(opts))) + end + + false -> + @schema schema + def schema, do: @schema + end unless Module.get_attribute(__MODULE__, :moduledoc) do - @moduledoc [@schema.title, @schema.description] + @moduledoc [schema.title, schema.description] |> Enum.reject(&is_nil/1) |> Enum.join("\n\n") end - def schema, do: @schema - - if Map.get(@schema, :"x-struct") == __MODULE__ do - 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 + case Map.get(schema, :"x-struct") == __MODULE__ do + true -> + case Keyword.get(unquote(opts), :derive?, true) do + true -> @derive Enum.filter([Poison.Encoder, Jason.Encoder], &Code.ensure_loaded?/1) + false -> nil + end + + case Keyword.get(unquote(opts), :struct?, true) do + true -> + defstruct Schema.properties(schema) + @type t :: %__MODULE__{} + false -> nil + end + + false -> nil end end end @@ -328,6 +287,27 @@ defmodule OpenApiSpex do schema end + @doc false + def build_schema_without_validation(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) + + struct(OpenApiSpex.Schema, attrs) + end + def title_from_module(nil), do: nil def title_from_module(module) do diff --git a/lib/open_api_spex/cast_parameters.ex b/lib/open_api_spex/cast_parameters.ex index 1155cb79..7d568aaf 100644 --- a/lib/open_api_spex/cast_parameters.ex +++ b/lib/open_api_spex/cast_parameters.ex @@ -4,7 +4,11 @@ defmodule OpenApiSpex.CastParameters do alias OpenApiSpex.Cast.Error alias Plug.Conn - @default_parsers %{~r/^application\/.*json.*$/ => OpenApi.json_encoder()} + @doc false + @spec default_content_parsers() :: %{Regex.t() => module() | function()} + defp default_content_parsers do + %{~r/^application\/.*json.*$/ => OpenApi.json_encoder()} + end @spec cast(Plug.Conn.t(), Operation.t(), OpenApi.t(), opts :: [OpenApiSpex.cast_opt()]) :: {:error, [Error.t()]} | {:ok, Conn.t()} @@ -119,8 +123,8 @@ defmodule OpenApiSpex.CastParameters do conn, opts ) do - parsers = Map.get(ext || %{}, "x-parameter-content-parsers", %{}) - parsers = Map.merge(@default_parsers, parsers) + custom_parsers = Map.get(ext || %{}, "x-parameter-content-parsers", %{}) + parsers = Map.merge(default_content_parsers(), custom_parsers) conn |> get_params_by_location( diff --git a/lib/open_api_spex/schema.ex b/lib/open_api_spex/schema.ex index 5c585e13..56e5b2d2 100644 --- a/lib/open_api_spex/schema.ex +++ b/lib/open_api_spex/schema.ex @@ -530,4 +530,27 @@ defmodule OpenApiSpex.Schema do defp default(value) do raise "Expected %Schema{}, schema module, or %Reference{}. Got: #{inspect(value)}" end + + @doc false + def has_regex_pattern?(%Schema{pattern: %Regex{}}), do: true + + def has_regex_pattern?(%Schema{} = schema) do + schema + |> Map.from_struct() + |> has_regex_pattern?() + end + + def has_regex_pattern?(enumerable) when is_list(enumerable) do + Enum.any?(enumerable, &has_regex_pattern?/1) + end + + def has_regex_pattern?(enumerable) when is_map(enumerable) and not is_struct(enumerable) do + Enum.any?(enumerable, fn + {_, value} -> has_regex_pattern?(value) + %Schema{} = schema -> has_regex_pattern?(schema) + _ -> false + end) + end + + def has_regex_pattern?(_), do: false end diff --git a/test/cast/string_test.exs b/test/cast/string_test.exs index 427c616a..e28dfb96 100644 --- a/test/cast/string_test.exs +++ b/test/cast/string_test.exs @@ -22,12 +22,13 @@ defmodule OpenApiSpex.CastStringTest do end test "string with pattern" do - schema = %Schema{type: :string, pattern: ~r/\d-\d/} + pattern = ~r/\d-\d/ + schema = %Schema{type: :string, pattern: pattern} assert cast(value: "1-2", schema: schema) == {:ok, "1-2"} assert {:error, [error]} = cast(value: "hello", schema: schema) assert error.reason == :invalid_format assert error.value == "hello" - assert error.format == ~r/\d-\d/ + assert error.format.source == "\\d-\\d" end test "string with format (date time)" do