diff --git a/lib/open_api_spex/cast.ex b/lib/open_api_spex/cast.ex index 35e198d2..f9b155e9 100644 --- a/lib/open_api_spex/cast.ex +++ b/lib/open_api_spex/cast.ex @@ -122,11 +122,7 @@ defmodule OpenApiSpex.Cast do # Enum def cast(%__MODULE__{schema: %{enum: enum}} = ctx) when is_list(enum) do with {:ok, value} <- cast(%{ctx | schema: %{ctx.schema | enum: nil}}) do - if value in enum do - {:ok, value} - else - error(ctx, {:invalid_enum}) - end + OpenApiSpex.Cast.Enum.cast(%{ctx | value: value}) end end diff --git a/lib/open_api_spex/cast/enum.ex b/lib/open_api_spex/cast/enum.ex new file mode 100644 index 00000000..b78c146c --- /dev/null +++ b/lib/open_api_spex/cast/enum.ex @@ -0,0 +1,30 @@ +defmodule OpenApiSpex.Cast.Enum do + @moduledoc false + alias OpenApiSpex.Cast + + def cast(ctx = %Cast{schema: %{enum: enum}, value: value}) do + case Enum.find(enum, {:error, :invalid_enum}, &equivalent?(&1, value)) do + {:error, :invalid_enum} -> Cast.error(ctx, {:invalid_enum}) + found -> {:ok, found} + end + end + + defp equivalent?(x, x), do: true + + # Special case: atoms are equivalent to their stringified representation + defp equivalent?(left, right) when is_atom(left) and is_binary(right) do + to_string(left) == right + end + + # an explicit schema should be used to cast to enum of structs + defp equivalent?(_x, %_struct{}), do: false + + # Special case: Atom-keyed maps are equivalent to their string-keyed representation + defp equivalent?(left, right) when is_map(left) and is_map(right) do + Enum.all?(left, fn {k, v} -> + equivalent?(v, Map.get(right, to_string(k))) + end) + end + + defp equivalent?(_left, _right), do: false +end diff --git a/test/cast/enum_test.exs b/test/cast/enum_test.exs new file mode 100644 index 00000000..7f78df91 --- /dev/null +++ b/test/cast/enum_test.exs @@ -0,0 +1,90 @@ +defmodule OpenApiSpex.Cast.EnumTest do + use ExUnit.Case + alias OpenApiSpex.{Cast, Schema} + alias OpenApiSpex.Cast.Error + + defp cast(ctx), do: Cast.cast(ctx) + + defmodule User do + require OpenApiSpex + alias __MODULE__ + + defstruct [:age] + + def schema() do + %OpenApiSpex.Schema{ + type: :object, + required: [:age], + properties: %{ + age: %Schema{type: :integer}, + }, + enum: [%User{age: 32}, %User{age: 45}], + "x-struct": __MODULE__ + } + end + end + + describe "Enum of strings" do + setup do + {:ok, %{schema: %Schema{type: :string, enum: ["one"]}}} + end + + test "error on invalid string", %{schema: schema} do + assert {:error, [error]} = cast(schema: schema, value: "two") + assert %Error{} = error + assert error.reason == :invalid_enum + end + + test "OK on valid string", %{schema: schema} do + assert {:ok, "one"} = cast(schema: schema, value: "one") + end + end + + describe "Enum of atoms" do + setup do + {:ok, %{schema: %Schema{type: :string, enum: [:one, :two, :three]}}} + end + + test "string will be converted to atom", %{schema: schema} do + assert {:ok, :three} = cast(schema: schema, value: "three") + end + + test "error on invalid string", %{schema: schema} do + assert {:error, [error]} = cast(schema: schema, value: "four") + assert %Error{} = error + assert error.reason == :invalid_enum + end + end + + describe "Enum with explicit schema" do + test "converts string keyed map to struct" do + assert {:ok, %User{age: 32}} = cast(schema: User.schema(), value: %{"age" => 32}) + end + + test "Must be a valid enum value" do + assert {:error, [error]} = cast(schema: User.schema(), value: %{"age" => 33}) + assert %Error{} = error + assert error.reason == :invalid_enum + end + end + + describe "Enum without explicit schema" do + setup do + schema = %Schema{ + type: :object, + enum: [%{age: 55}, %{age: 66}, %{age: 77}] + } + {:ok, %{schema: schema}} + end + + test "casts from string keyed map", %{schema: schema} do + assert {:ok, %{age: 55}} = cast(value: %{"age" => 55}, schema: schema) + end + + test "value must be a valid enum value", %{schema: schema} do + assert {:error, [error]} = cast(value: %{"age" => 56}, schema: schema) + assert %Error{} = error + assert error.reason == :invalid_enum + end + end +end diff --git a/test/cast_test.exs b/test/cast_test.exs index 2d7ecd6c..5e41091a 100644 --- a/test/cast_test.exs +++ b/test/cast_test.exs @@ -191,19 +191,6 @@ defmodule OpenApiSpec.CastTest do assert Error.message_with_path(error2) == "#/3: Invalid integer. Got: string" end - - test "enum - invalid" do - schema = %Schema{type: :string, enum: ["one"]} - assert {:error, [error]} = cast(value: "two", schema: schema) - - assert %Error{} = error - assert error.reason == :invalid_enum - end - - test "enum - valid" do - schema = %Schema{type: :string, enum: ["one"]} - assert {:ok, "one"} = cast(value: "one", schema: schema) - end end describe "ok/1" do