Skip to content

Commit 08e76b4

Browse files
authored
Merge pull request #101 from open-api-spex/feature/atomify-enum-on-cast
Convert string to atom when casting enum schema
2 parents 78a1ec4 + 004abcc commit 08e76b4

File tree

4 files changed

+121
-18
lines changed

4 files changed

+121
-18
lines changed

lib/open_api_spex/cast.ex

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,7 @@ defmodule OpenApiSpex.Cast do
122122
# Enum
123123
def cast(%__MODULE__{schema: %{enum: enum}} = ctx) when is_list(enum) do
124124
with {:ok, value} <- cast(%{ctx | schema: %{ctx.schema | enum: nil}}) do
125-
if value in enum do
126-
{:ok, value}
127-
else
128-
error(ctx, {:invalid_enum})
129-
end
125+
OpenApiSpex.Cast.Enum.cast(%{ctx | value: value})
130126
end
131127
end
132128

lib/open_api_spex/cast/enum.ex

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
defmodule OpenApiSpex.Cast.Enum do
2+
@moduledoc false
3+
alias OpenApiSpex.Cast
4+
5+
def cast(ctx = %Cast{schema: %{enum: enum}, value: value}) do
6+
case Enum.find(enum, {:error, :invalid_enum}, &equivalent?(&1, value)) do
7+
{:error, :invalid_enum} -> Cast.error(ctx, {:invalid_enum})
8+
found -> {:ok, found}
9+
end
10+
end
11+
12+
defp equivalent?(x, x), do: true
13+
14+
# Special case: atoms are equivalent to their stringified representation
15+
defp equivalent?(left, right) when is_atom(left) and is_binary(right) do
16+
to_string(left) == right
17+
end
18+
19+
# an explicit schema should be used to cast to enum of structs
20+
defp equivalent?(_x, %_struct{}), do: false
21+
22+
# Special case: Atom-keyed maps are equivalent to their string-keyed representation
23+
defp equivalent?(left, right) when is_map(left) and is_map(right) do
24+
Enum.all?(left, fn {k, v} ->
25+
equivalent?(v, Map.get(right, to_string(k)))
26+
end)
27+
end
28+
29+
defp equivalent?(_left, _right), do: false
30+
end

test/cast/enum_test.exs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
defmodule OpenApiSpex.Cast.EnumTest do
2+
use ExUnit.Case
3+
alias OpenApiSpex.{Cast, Schema}
4+
alias OpenApiSpex.Cast.Error
5+
6+
defp cast(ctx), do: Cast.cast(ctx)
7+
8+
defmodule User do
9+
require OpenApiSpex
10+
alias __MODULE__
11+
12+
defstruct [:age]
13+
14+
def schema() do
15+
%OpenApiSpex.Schema{
16+
type: :object,
17+
required: [:age],
18+
properties: %{
19+
age: %Schema{type: :integer},
20+
},
21+
enum: [%User{age: 32}, %User{age: 45}],
22+
"x-struct": __MODULE__
23+
}
24+
end
25+
end
26+
27+
describe "Enum of strings" do
28+
setup do
29+
{:ok, %{schema: %Schema{type: :string, enum: ["one"]}}}
30+
end
31+
32+
test "error on invalid string", %{schema: schema} do
33+
assert {:error, [error]} = cast(schema: schema, value: "two")
34+
assert %Error{} = error
35+
assert error.reason == :invalid_enum
36+
end
37+
38+
test "OK on valid string", %{schema: schema} do
39+
assert {:ok, "one"} = cast(schema: schema, value: "one")
40+
end
41+
end
42+
43+
describe "Enum of atoms" do
44+
setup do
45+
{:ok, %{schema: %Schema{type: :string, enum: [:one, :two, :three]}}}
46+
end
47+
48+
test "string will be converted to atom", %{schema: schema} do
49+
assert {:ok, :three} = cast(schema: schema, value: "three")
50+
end
51+
52+
test "error on invalid string", %{schema: schema} do
53+
assert {:error, [error]} = cast(schema: schema, value: "four")
54+
assert %Error{} = error
55+
assert error.reason == :invalid_enum
56+
end
57+
end
58+
59+
describe "Enum with explicit schema" do
60+
test "converts string keyed map to struct" do
61+
assert {:ok, %User{age: 32}} = cast(schema: User.schema(), value: %{"age" => 32})
62+
end
63+
64+
test "Must be a valid enum value" do
65+
assert {:error, [error]} = cast(schema: User.schema(), value: %{"age" => 33})
66+
assert %Error{} = error
67+
assert error.reason == :invalid_enum
68+
end
69+
end
70+
71+
describe "Enum without explicit schema" do
72+
setup do
73+
schema = %Schema{
74+
type: :object,
75+
enum: [%{age: 55}, %{age: 66}, %{age: 77}]
76+
}
77+
{:ok, %{schema: schema}}
78+
end
79+
80+
test "casts from string keyed map", %{schema: schema} do
81+
assert {:ok, %{age: 55}} = cast(value: %{"age" => 55}, schema: schema)
82+
end
83+
84+
test "value must be a valid enum value", %{schema: schema} do
85+
assert {:error, [error]} = cast(value: %{"age" => 56}, schema: schema)
86+
assert %Error{} = error
87+
assert error.reason == :invalid_enum
88+
end
89+
end
90+
end

test/cast_test.exs

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -191,19 +191,6 @@ defmodule OpenApiSpec.CastTest do
191191

192192
assert Error.message_with_path(error2) == "#/3: Invalid integer. Got: string"
193193
end
194-
195-
test "enum - invalid" do
196-
schema = %Schema{type: :string, enum: ["one"]}
197-
assert {:error, [error]} = cast(value: "two", schema: schema)
198-
199-
assert %Error{} = error
200-
assert error.reason == :invalid_enum
201-
end
202-
203-
test "enum - valid" do
204-
schema = %Schema{type: :string, enum: ["one"]}
205-
assert {:ok, "one"} = cast(value: "one", schema: schema)
206-
end
207194
end
208195

209196
describe "ok/1" do

0 commit comments

Comments
 (0)