Skip to content

Commit b3d4f53

Browse files
feat: Add OTP 28+ compatibility with version-aware regex pattern handling
- Implement OTP version detection in schema macro - Add runtime schema compilation for OTP 28+ when regex patterns are present - Maintain module attribute optimization for non-regex schemas on all OTP versions - Add deprecation warning for regex patterns on OTP 28+ with migration guidance - Fix cast_parameters.ex to avoid regex in module attributes - Update string test to check pattern source instead of regex struct equality This ensures backward compatibility while providing a clear migration path for OTP 28+ users to move from regex patterns to string patterns.
1 parent 410f3aa commit b3d4f53

File tree

4 files changed

+94
-21
lines changed

4 files changed

+94
-21
lines changed

lib/open_api_spex.ex

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -253,32 +253,59 @@ defmodule OpenApiSpex do
253253
prevent "... protocol has already been consolidated ..."
254254
compiler warnings.
255255
"""
256+
def should_use_runtime_compilation?(body) do
257+
with true <- System.otp_release() >= "28",
258+
true <- Schema.has_regex_pattern?(body) do
259+
true
260+
else
261+
_ -> false
262+
end
263+
end
264+
256265
defmacro schema(body, opts \\ []) do
257266
quote do
258267
@compile {:report_warnings, false}
259268
@behaviour OpenApiSpex.Schema
260-
@schema OpenApiSpex.build_schema(
261-
unquote(body),
262-
Keyword.merge([module: __MODULE__], unquote(opts))
263-
)
269+
270+
schema = OpenApiSpex.build_schema(unquote(body), Keyword.merge([module: __MODULE__], unquote(opts)))
271+
272+
case OpenApiSpex.should_use_runtime_compilation?(unquote(body)) do
273+
true ->
274+
IO.warn("""
275+
[OpenApiSpex] Regex patterns in schema definitions are deprecated in OTP 28+.
276+
Consider using string patterns: pattern: "\\\\d-\\\\d" instead of pattern: ~r/\\\\d-\\\\d/
277+
""", Macro.Env.stacktrace(__ENV__))
278+
279+
def schema do
280+
OpenApiSpex.build_schema_without_validation(unquote(body), Keyword.merge([module: __MODULE__], unquote(opts)))
281+
end
282+
283+
false ->
284+
@schema schema
285+
def schema, do: @schema
286+
end
264287

265288
unless Module.get_attribute(__MODULE__, :moduledoc) do
266-
@moduledoc [@schema.title, @schema.description]
289+
@moduledoc [schema.title, schema.description]
267290
|> Enum.reject(&is_nil/1)
268291
|> Enum.join("\n\n")
269292
end
270293

271-
def schema, do: @schema
272-
273-
if Map.get(@schema, :"x-struct") == __MODULE__ do
274-
if Keyword.get(unquote(opts), :derive?, true) do
275-
@derive Enum.filter([Poison.Encoder, Jason.Encoder], &Code.ensure_loaded?/1)
276-
end
277-
278-
if Keyword.get(unquote(opts), :struct?, true) do
279-
defstruct Schema.properties(@schema)
280-
@type t :: %__MODULE__{}
281-
end
294+
case Map.get(schema, :"x-struct") == __MODULE__ do
295+
true ->
296+
case Keyword.get(unquote(opts), :derive?, true) do
297+
true -> @derive Enum.filter([Poison.Encoder, Jason.Encoder], &Code.ensure_loaded?/1)
298+
false -> nil
299+
end
300+
301+
case Keyword.get(unquote(opts), :struct?, true) do
302+
true ->
303+
defstruct Schema.properties(schema)
304+
@type t :: %__MODULE__{}
305+
false -> nil
306+
end
307+
308+
false -> nil
282309
end
283310
end
284311
end
@@ -328,6 +355,27 @@ defmodule OpenApiSpex do
328355
schema
329356
end
330357

358+
@doc false
359+
def build_schema_without_validation(body, opts \\ []) do
360+
module = opts[:module] || body[:"x-struct"]
361+
362+
attrs =
363+
body
364+
|> Map.delete(:__struct__)
365+
|> update_in([:"x-struct"], fn struct_module ->
366+
if Keyword.get(opts, :struct?, true) do
367+
struct_module || module
368+
else
369+
struct_module
370+
end
371+
end)
372+
|> update_in([:title], fn title ->
373+
title || title_from_module(module)
374+
end)
375+
376+
struct(OpenApiSpex.Schema, attrs)
377+
end
378+
331379
def title_from_module(nil), do: nil
332380

333381
def title_from_module(module) do

lib/open_api_spex/cast_parameters.ex

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ defmodule OpenApiSpex.CastParameters do
44
alias OpenApiSpex.Cast.Error
55
alias Plug.Conn
66

7-
@default_parsers %{~r/^application\/.*json.*$/ => OpenApi.json_encoder()}
7+
@doc false
8+
@spec default_content_parsers() :: %{Regex.t() => module() | function()}
9+
defp default_content_parsers do
10+
%{~r/^application\/.*json.*$/ => OpenApi.json_encoder()}
11+
end
812

913
@spec cast(Plug.Conn.t(), Operation.t(), OpenApi.t(), opts :: [OpenApiSpex.cast_opt()]) ::
1014
{:error, [Error.t()]} | {:ok, Conn.t()}
@@ -119,8 +123,8 @@ defmodule OpenApiSpex.CastParameters do
119123
conn,
120124
opts
121125
) do
122-
parsers = Map.get(ext || %{}, "x-parameter-content-parsers", %{})
123-
parsers = Map.merge(@default_parsers, parsers)
126+
custom_parsers = Map.get(ext || %{}, "x-parameter-content-parsers", %{})
127+
parsers = Map.merge(default_content_parsers(), custom_parsers)
124128

125129
conn
126130
|> get_params_by_location(

lib/open_api_spex/schema.ex

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,4 +530,24 @@ defmodule OpenApiSpex.Schema do
530530
defp default(value) do
531531
raise "Expected %Schema{}, schema module, or %Reference{}. Got: #{inspect(value)}"
532532
end
533+
534+
@doc false
535+
def has_regex_pattern?(%Schema{pattern: %Regex{}}), do: true
536+
537+
def has_regex_pattern?(%Schema{} = schema) do
538+
schema
539+
|> Map.from_struct()
540+
|> has_regex_pattern?()
541+
end
542+
543+
def has_regex_pattern?(enumerable)
544+
when not is_struct(enumerable) and (is_list(enumerable) or is_map(enumerable)) do
545+
Enum.any?(enumerable, fn
546+
{_, value} -> has_regex_pattern?(value)
547+
%Schema{} = schema -> has_regex_pattern?(schema)
548+
_ -> false
549+
end)
550+
end
551+
552+
def has_regex_pattern?(_), do: false
533553
end

test/cast/string_test.exs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@ defmodule OpenApiSpex.CastStringTest do
2222
end
2323

2424
test "string with pattern" do
25-
schema = %Schema{type: :string, pattern: ~r/\d-\d/}
25+
pattern = ~r/\d-\d/
26+
schema = %Schema{type: :string, pattern: pattern}
2627
assert cast(value: "1-2", schema: schema) == {:ok, "1-2"}
2728
assert {:error, [error]} = cast(value: "hello", schema: schema)
2829
assert error.reason == :invalid_format
2930
assert error.value == "hello"
30-
assert error.format == ~r/\d-\d/
31+
assert error.format.source == "\\d-\\d"
3132
end
3233

3334
test "string with format (date time)" do

0 commit comments

Comments
 (0)