Skip to content
This repository has been archived by the owner on Jun 12, 2020. It is now read-only.

Commit

Permalink
Merge pull request #5 from nsweeting/master
Browse files Browse the repository at this point in the history
  • Loading branch information
mdlkxzmcp committed Feb 22, 2019
1 parent 4b5d7e8 commit 5032df7
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 180 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ erl_crash.dump
*.ez
.DS_Store
/doc
.elixir_ls
60 changes: 60 additions & 0 deletions lib/arc_ecto/changeset.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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]

# Allow casting binary data structs
{field, upload = %{filename: filename, binary: binary}}, fields
when is_binary(filename) and is_binary(binary) ->
[{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
19 changes: 19 additions & 0 deletions lib/arc_ecto/changeset/helpers.ex
Original file line number Diff line number Diff line change
@@ -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
76 changes: 0 additions & 76 deletions lib/arc_ecto/schema.ex

This file was deleted.

148 changes: 148 additions & 0 deletions test/changeset_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
defmodule ArcTest.Ecto.Changeset do
use ExUnit.Case, async: false
import Mock
import ExUnit.CaptureLog

defmodule TestUser do
use Ecto.Schema
import Ecto.Changeset
import Arc.Ecto.Changeset

schema "users" do
field(:first_name, :string)
field(:avatar, DummyDefinition.Type)
end

def changeset(user, params \\ :invalid) do
user
|> cast(params, ~w(first_name)a)
|> cast_attachments(params, ~w(avatar)a)
|> validate_required(:avatar)
end

def path_changeset(user, params \\ :invalid) do
user
|> cast(params, ~w(first_name)a)
|> cast_attachments(params, ~w(avatar)a, allow_paths: true)
|> validate_required(:avatar)
end

def changeset2(user, params \\ :invalid) do
user
|> cast(params, ~w(first_name)a)
|> cast_attachments(params, ~w(avatar)a)
end
end

def build_upload(path) do
%{__struct__: Plug.Upload, path: path, filename: Path.basename(path)}
end

test "supports :invalid changeset" do
cs = TestUser.changeset(%TestUser{})
assert cs.valid? == false
assert cs.changes == %{}
assert cs.errors == [avatar: {"can't be blank", [validation: :required]}]
end

test_with_mock "cascades storage success into a valid change", DummyDefinition,
store: fn {%{__struct__: Plug.Upload, path: "/path/to/my/file.png", filename: "file.png"},
%TestUser{}} ->
{:ok, "file.png"}
end do
upload = build_upload("/path/to/my/file.png")
cs = TestUser.changeset(%TestUser{}, %{"avatar" => upload})
assert cs.valid?
%{file_name: "file.png", updated_at: _} = cs.changes.avatar
end

test_with_mock "cascades storage error into an error", DummyDefinition,
store: fn {%{__struct__: Plug.Upload, path: "/path/to/my/file.png", filename: "file.png"},
%TestUser{}} ->
{:error, :invalid_file}
end do
upload = build_upload("/path/to/my/file.png")

capture_log(fn ->
cs = TestUser.changeset(%TestUser{}, %{"avatar" => upload})
assert called(DummyDefinition.store({upload, %TestUser{}}))
assert cs.valid? == false

assert cs.errors == [
avatar: {"is invalid", [type: DummyDefinition.Type, validation: :cast]}
]
end)
end

test_with_mock "converts changeset into schema", DummyDefinition,
store: fn {%{__struct__: Plug.Upload, path: "/path/to/my/file.png", filename: "file.png"},
%TestUser{}} ->
{:error, :invalid_file}
end do
upload = build_upload("/path/to/my/file.png")

capture_log(fn ->
TestUser.changeset(%TestUser{}, %{"avatar" => upload})
assert called(DummyDefinition.store({upload, %TestUser{}}))
end)
end

test_with_mock "applies changes to schema", DummyDefinition,
store: fn {%{__struct__: Plug.Upload, path: "/path/to/my/file.png", filename: "file.png"},
%TestUser{}} ->
{:error, :invalid_file}
end do
upload = build_upload("/path/to/my/file.png")

capture_log(fn ->
TestUser.changeset(%TestUser{}, %{"avatar" => upload, "first_name" => "test"})
assert called(DummyDefinition.store({upload, %TestUser{first_name: "test"}}))
end)
end

test_with_mock "converts atom keys", DummyDefinition,
store: fn {%{__struct__: Plug.Upload, path: "/path/to/my/file.png", filename: "file.png"},
%TestUser{}} ->
{:error, :invalid_file}
end do
upload = build_upload("/path/to/my/file.png")

capture_log(fn ->
TestUser.changeset(%TestUser{}, %{avatar: upload})
assert called(DummyDefinition.store({upload, %TestUser{}}))
end)
end

test_with_mock "casting nil attachments", DummyDefinition,
store: fn {%{__struct__: Plug.Upload, path: "/path/to/my/file.png", filename: "file.png"},
%TestUser{}} ->
{:ok, "file.png"}
end do
changeset =
TestUser.changeset(%TestUser{}, %{"avatar" => build_upload("/path/to/my/file.png")})

changeset = TestUser.changeset2(changeset, %{"avatar" => nil})
assert nil == Ecto.Changeset.get_field(changeset, :avatar)
end

test_with_mock "allow_paths => true", DummyDefinition,
store: fn {"/path/to/my/file.png", %TestUser{}} -> {:ok, "file.png"} end do
TestUser.path_changeset(%TestUser{}, %{"avatar" => "/path/to/my/file.png"})
assert called(DummyDefinition.store({"/path/to/my/file.png", %TestUser{}}))
end

test_with_mock "casting binary data struct attachments", DummyDefinition,
store: fn {%{filename: "/path/to/my/file.png", binary: <<1, 2, 3>>}, %TestUser{}} ->
{:ok, "file.png"}
end do
TestUser.changeset(%TestUser{}, %{
"avatar" => %{filename: "/path/to/my/file.png", binary: <<1, 2, 3>>}
})

assert called(
DummyDefinition.store(
{%{filename: "/path/to/my/file.png", binary: <<1, 2, 3>>}, %TestUser{}}
)
)
end
end
Loading

0 comments on commit 5032df7

Please sign in to comment.