Skip to content

feat: Add OTP 28+ compatibility with version-aware regex pattern handling #670

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 64 additions & 16 deletions lib/open_api_spex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -253,32 +253,59 @@ defmodule OpenApiSpex do
prevent "... protocol has already been consolidated ..."
compiler warnings.
"""
def should_use_runtime_compilation?(body) do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this should be a public function. It's also hijacking the @doc for defmacro schema/2.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll submit a change to fix this, but that method is called from elsewhere, so I think it needs to be public, no? Unless I'm missing something. The @doc issue is easier to fix (I think). :)

with true <- System.otp_release() >= "28",
true <- Schema.has_regex_pattern?(body) do
true
else
_ -> false
end
end

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
Expand Down Expand Up @@ -328,6 +355,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
Expand Down
10 changes: 7 additions & 3 deletions lib/open_api_spex/cast_parameters.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()}
Expand Down Expand Up @@ -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(
Expand Down
20 changes: 20 additions & 0 deletions lib/open_api_spex/schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -530,4 +530,24 @@ 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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This guard is giving a warning CI:

==> open_api_spex
Compiling 80 files (.ex)
     warning: incompatible types:

         map() !~ [dynamic()]

     in expression:

         # lib/open_api_spex/schema.ex:544
         is_list(enumerable)

     where "enumerable" was given the type map() in:

         # lib/open_api_spex/schema.ex:544
         is_map_key(enumerable, :__struct__)

     where "enumerable" was given the type [dynamic()] in:

         # lib/open_api_spex/schema.ex:544
         is_list(enumerable)

     Conflict found at
     │
 544 │       when not is_struct(enumerable) and (is_list(enumerable) or is_map(enumerable)) do
     │                                           ~
     │
     └─ lib/open_api_spex/schema.ex:544:43: OpenApiSpex.Schema.has_regex_pattern?/1

Compilation failed due to warnings while using the --warnings-as-errors option
Error: Process completed with exit code 1.

when not is_struct(enumerable) and (is_list(enumerable) or is_map(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
5 changes: 3 additions & 2 deletions test/cast/string_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading