Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
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
Copy link
Contributor

Choose a reason for hiding this comment

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

My suggestion is to only log a warning when a Regex is used in conjunction with OTP 28, with a message to update their code to pattern: String.t.


def has_regex_pattern?(%Schema{} = schema) do
schema
|> Map.from_struct()
|> has_regex_pattern?()
end

def has_regex_pattern?(enumerable)
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
2 changes: 1 addition & 1 deletion test/controller_specs_test.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
defmodule OpenApiSpex.ControllerSpecsTest do
use ExUnit.Case, async: true
use ExUnit.Case, async: false

import ExUnit.CaptureIO

Expand Down
2 changes: 1 addition & 1 deletion test/controller_test.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
defmodule OpenApiSpex.ControllerTest do
use ExUnit.Case, async: true
use ExUnit.Case, async: false

alias OpenApiSpex.Reference
alias OpenApiSpex.Controller, as: Subject
Expand Down
Loading