diff --git a/.gitignore b/.gitignore index c6f81a0..07cfb95 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ erl_crash.dump *.ez .DS_Store /doc +.elixir_ls \ No newline at end of file diff --git a/lib/arc_ecto/changeset.ex b/lib/arc_ecto/changeset.ex new file mode 100644 index 0000000..2e95f29 --- /dev/null +++ b/lib/arc_ecto/changeset.ex @@ -0,0 +1,49 @@ +defmodule Arc.Ecto.Changeset do + @spec cast_attachments( + Ecto.Schema.t() | Ecto.Changeset.t(), + :invalid | map(), + [String.t() | atom()], + Keyword.t() + ) :: Ecto.Changeset.t() + def cast_attachments(changeset_or_data, params, allowed, options \\ []) do + scope = case changeset_or_data do + %Ecto.Changeset{} -> Ecto.Changeset.apply_changes(changeset_or_data) + %{__meta__: _} -> changeset_or_data + end + + # Cast supports both atom and string keys, ensure we're matching on both. + allowed_param_keys = Enum.map(allowed, fn key -> + case key do + key when is_binary(key) -> key + key when is_atom(key) -> Atom.to_string(key) + end + end) + + arc_params = case params do + :invalid -> + :invalid + %{} -> + params + |> Arc.Ecto.Changeset.Helpers.convert_params_to_binary() + |> Map.take(allowed_param_keys) + |> Enum.reduce([], fn + # Don't wrap nil casts in the scope object + {field, nil}, fields -> [{field, nil} | fields] + + # Allow casting Plug.Uploads + {field, upload = %{__struct__: Plug.Upload}}, fields -> [{field, {upload, scope}} | fields] + + # If casting a binary (path), ensure we've explicitly allowed paths + {field, path}, fields when is_binary(path) -> + if Keyword.get(options, :allow_paths, false) do + [{field, {path, scope}} | fields] + else + fields + end + end) + |> Enum.into(%{}) + end + + Ecto.Changeset.cast(changeset_or_data, arc_params, allowed) + end +end diff --git a/lib/arc_ecto/changeset/helpers.ex b/lib/arc_ecto/changeset/helpers.ex new file mode 100644 index 0000000..a2419fd --- /dev/null +++ b/lib/arc_ecto/changeset/helpers.ex @@ -0,0 +1,19 @@ +defmodule Arc.Ecto.Changeset.Helpers do + @moduledoc false + + @doc false + def convert_params_to_binary(params) do + Enum.reduce(params, nil, fn + {key, _value}, nil when is_binary(key) -> + nil + + {key, _value}, _ when is_binary(key) -> + raise ArgumentError, "expected params to be a map with atoms or string keys, " <> + "got a map with mixed keys: #{inspect params}" + + {key, value}, acc when is_atom(key) -> + Map.put(acc || %{}, Atom.to_string(key), value) + + end) || params + end +end diff --git a/lib/arc_ecto/schema.ex b/lib/arc_ecto/schema.ex deleted file mode 100644 index 03b801f..0000000 --- a/lib/arc_ecto/schema.ex +++ /dev/null @@ -1,71 +0,0 @@ -defmodule Arc.Ecto.Schema do - defmacro __using__(_) do - quote do - import Arc.Ecto.Schema - end - end - - defmacro cast_attachments(changeset_or_data, params, allowed, options \\ []) do - quote bind_quoted: [changeset_or_data: changeset_or_data, - params: params, - allowed: allowed, - options: options] do - - # If given a changeset, apply the changes to obtain the underlying data - scope = case changeset_or_data do - %Ecto.Changeset{} -> Ecto.Changeset.apply_changes(changeset_or_data) - %{__meta__: _} -> changeset_or_data - end - - # Cast supports both atom and string keys, ensure we're matching on both. - allowed_param_keys = Enum.map(allowed, fn key -> - case key do - key when is_binary(key) -> key - key when is_atom(key) -> Atom.to_string(key) - end - end) - - arc_params = case params do - :invalid -> - :invalid - %{} -> - params - |> Arc.Ecto.Schema.convert_params_to_binary - |> Map.take(allowed_param_keys) - |> Enum.reduce([], fn - # Don't wrap nil casts in the scope object - {field, nil}, fields -> [{field, nil} | fields] - - # Allow casting Plug.Uploads - {field, upload = %{__struct__: Plug.Upload}}, fields -> [{field, {upload, scope}} | fields] - - # If casting a binary (path), ensure we've explicitly allowed paths - {field, path}, fields when is_binary(path) -> - if Keyword.get(options, :allow_paths, false) do - [{field, {path, scope}} | fields] - else - fields - end - end) - |> Enum.into(%{}) - end - - cast(changeset_or_data, arc_params, allowed) - end - end - - def convert_params_to_binary(params) do - Enum.reduce(params, nil, fn - {key, _value}, nil when is_binary(key) -> - nil - - {key, _value}, _ when is_binary(key) -> - raise ArgumentError, "expected params to be a map with atoms or string keys, " <> - "got a map with mixed keys: #{inspect params}" - - {key, value}, acc when is_atom(key) -> - Map.put(acc || %{}, Atom.to_string(key), value) - - end) || params - end -end diff --git a/test/schema_test.exs b/test/changeset_test.exs similarity index 98% rename from test/schema_test.exs rename to test/changeset_test.exs index 6e58e48..b55865e 100644 --- a/test/schema_test.exs +++ b/test/changeset_test.exs @@ -1,4 +1,4 @@ -defmodule ArcTest.Ecto.Schema do +defmodule ArcTest.Ecto.Changeset do use ExUnit.Case, async: false import Mock import ExUnit.CaptureLog @@ -6,7 +6,7 @@ defmodule ArcTest.Ecto.Schema do defmodule TestUser do use Ecto.Schema import Ecto.Changeset - use Arc.Ecto.Schema + import Arc.Ecto.Changeset schema "users" do field :first_name, :string