Repository: elixir-ecto/ecto
Pull request #4596 (2025-03-29 21:00:04)

Ecto.Enum.cast_value/3
@josevalim Is this what you meant by adding a function to Ecto to make this easier
https://github.com/phoenixframework/phoenix_html/issues/461#issuecomment-2764189902

Let me know if this is what you were thinking or any changes need to be made!

cjbottaro: Originally I was thinking to define `cast_value/3` in terms of `cast/2`, but it felt "heavy" having to call `init/1` and figuring out the options with which to call it. But maybe the "heaviness" isn't as bad as I imagine?
cjbottaro: Ahh, I figured out that you can get the params via the __changeset__() function. So now `cast_value/3` is defined in terms of `cast/2`.
cjbottaro: On really nitpicky thing... should the function be called `cast_data` instead of `cast_value`? `data` seems to be more inline with the terminology used throughout the file.
josevalim: I am happy with `cast_value` but let's see if someone has a different opinion.

Patch:
diff --git a/lib/ecto/enum.ex b/lib/ecto/enum.ex
index 3a550a80..63f1a2c9 100644
--- a/lib/ecto/enum.ex
+++ b/lib/ecto/enum.ex
@@ -279,6 +279,53 @@ defmodule Ecto.Enum do
     |> Keyword.values()
   end
 
+  @doc """
+  Casts a value from the given `schema` and `field`.
+
+  ## Examples
+
+  Assuming this schema:
+
+      defmodule MySchema do
+        use Ecto.Schema
+
+        schema "my_schema" do
+          field :my_string_enum, Ecto.Enum, values: [:foo, :bar, :baz]
+          field :my_integer_enum, Ecto.Enum, values: [foo: 1, bar: 2, baz: 5]
+        end
+      end
+
+  Then:
+
+      Ecto.Enum.cast_value(MySchema, :my_string_enum, "foo")
+      #=> {:ok, :foo}
+
+      Ecto.Enum.cast_value(MySchema, :my_string_enum, :foo)
+      #=> {:ok, :foo}
+
+      Ecto.Enum.cast_value(MySchema, :my_string_enum, "qux")
+      #=> :error
+
+      Ecto.Enum.cast_value(MySchema, :my_integer_enum, 1)
+      #=> {:ok, :foo}
+
+      Ecto.Enum.cast_value(MySchema, :my_integer_enum, :foo)
+      #=> {:ok, :foo}
+
+      Ecto.Enum.cast_value(MySchema, :my_integer_enum, 6)
+      #=> :error
+
+  `schema_or_types` can also be a types map. See `mappings/2` for more information.
+  """
+  @spec cast_value(module | map, atom, binary | atom | integer) :: {:ok, atom} | :error
+  def cast_value(schema_or_types, field, value) do
+    params = get_params(schema_or_types, field)
+    case cast(value, params) do
+      {:ok, casted_value} -> {:ok, casted_value}
+      {:error, _reason} -> :error
+    end
+  end
+
   @doc """
   Returns the mappings between values and dumped values.
 
@@ -317,25 +364,30 @@ defmodule Ecto.Enum do
 
   """
   @spec mappings(module | map, atom) :: keyword(String.t() | integer())
-  def mappings(schema_or_types, field)
-
-  def mappings(types, field) when is_map(types) do
-    case types do
-      %{^field => {:parameterized, {Ecto.Enum, %{mappings: mappings}}}} -> mappings
-      %{^field => {_, {:parameterized, {Ecto.Enum, %{mappings: mappings}}}}} -> mappings
-      %{^field => _} -> raise ArgumentError, "#{field} is not an Ecto.Enum field"
-      %{} -> raise ArgumentError, "#{field} does not exist"
-    end
+  def mappings(schema_or_types, field) do
+    get_params(schema_or_types, field)
+    |> Map.fetch!(:mappings)
   end
 
-  def mappings(schema, field) when is_atom(schema) do
+  defp get_params(schema_or_types, field)
+
+  defp get_params(schema, field) when is_atom(schema) do
     try do
       schema.__changeset__()
     rescue
       _ in UndefinedFunctionError ->
         raise ArgumentError, "#{inspect(schema)} is not an Ecto schema or types map"
     else
-      %{} = types -> mappings(types, field)
+      %{} = types -> get_params(types, field)
+    end
+  end
+
+  defp get_params(types, field) when is_map(types) do
+    case types do
+      %{^field => {:parameterized, {Ecto.Enum, params}}} -> params
+      %{^field => {_, {:parameterized, {Ecto.Enum, params}}}} -> params
+      %{^field => _} -> raise ArgumentError, "#{field} is not an Ecto.Enum field"
+      %{} -> raise ArgumentError, "#{field} does not exist"
     end
   end
 end


Test patch:
diff --git a/test/ecto/enum_test.exs b/test/ecto/enum_test.exs
index 447046c7..0ebe8af0 100644
--- a/test/ecto/enum_test.exs
+++ b/test/ecto/enum_test.exs
@@ -562,6 +562,26 @@ defmodule Ecto.EnumTest do
     end
   end
 
+  describe "cast_value/3" do
+    test "returns correct values" do
+      assert Ecto.Enum.cast_value(EnumSchema, :my_enum, "foo") == {:ok, :foo}
+      assert Ecto.Enum.cast_value(EnumSchema, :my_enum, :foo) == {:ok, :foo}
+      assert Ecto.Enum.cast_value(EnumSchema, :my_enum, "qux") == :error
+      assert Ecto.Enum.cast_value(EnumSchema, :my_enum, :qux) == :error
+
+      assert Ecto.Enum.cast_value(EnumSchema, :my_integer_enum, 1) == {:ok, :foo}
+      assert Ecto.Enum.cast_value(EnumSchema, :my_integer_enum, :foo) == {:ok, :foo}
+      assert Ecto.Enum.cast_value(EnumSchema, :my_integer_enum, 6) == :error
+      assert Ecto.Enum.cast_value(EnumSchema, :my_integer_enum, "qux") == :error
+      assert Ecto.Enum.cast_value(EnumSchema, :my_integer_enum, :qux) == :error
+
+      assert Ecto.Enum.cast_value(EnumSchema, :my_string_enum, "fooo") == {:ok, :foo}
+      assert Ecto.Enum.cast_value(EnumSchema, :my_string_enum, :foo) == {:ok, :foo}
+      assert Ecto.Enum.cast_value(EnumSchema, :my_string_enum, "quux") == :error
+      assert Ecto.Enum.cast_value(EnumSchema, :my_string_enum, :qux) == :error
+    end
+  end
+
   describe "mappings/2" do
     test "returns correct values" do
       assert Ecto.Enum.mappings(EnumSchema, :my_enum) == [foo: "foo", bar: "bar", baz: "baz"]