This repository has been archived by the owner on Jun 12, 2020. It is now read-only.
forked from azhi/arc_ecto
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5 from nsweeting/master
- Loading branch information
Showing
6 changed files
with
228 additions
and
180 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,3 +4,4 @@ erl_crash.dump | |
*.ez | ||
.DS_Store | ||
/doc | ||
.elixir_ls |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.