Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## v0.0.13 (2025-01-15)
- Add support for array fields

## v0.0.12 (2023-02-02)
- Update dependencies

Expand Down
69 changes: 45 additions & 24 deletions lib/waffle_ecto/schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -90,31 +90,52 @@ defmodule Waffle.Ecto.Schema do
def do_apply_changes(%{__meta__: _} = data), do: data

def check_and_apply_scope(params, scope, options) do
Enum.reduce(params, [], 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]

{field, upload = %{filename: filename, path: path}}, fields
when is_binary(filename) and is_binary(path) ->
path = String.trim(path)
upload = %{upload | path: path}
if path_allowed?(path, options), do: [{field, {upload, scope}} | fields], else: fields

# If casting a binary (path), ensure we've explicitly allowed paths
{field, path}, fields when is_binary(path) ->
path = String.trim(path)
if path_allowed?(path, options), do: [{field, {path, scope}} | fields], else: fields
params
|> Enum.reduce([], fn {field, value}, fields ->
[{field, apply_scope(value, scope, options)} | fields]
end)
|> Enum.reject(fn
{_field, :invalid} -> true
{_field, values} when is_list(values) -> Enum.any?(values, &(&1 == :invalid))
_else -> false
end)
end

# Don't wrap nil casts in the scope object
def apply_scope(nil, _scope, _options) do
nil
end

def apply_scope(values, scope, options) when is_list(values) do
Enum.map(values, &apply_scope(&1, scope, options))
end

# Allow casting Plug.Uploads
def apply_scope(%{__struct__: Plug.Upload} = upload, scope, _options) do
{upload, scope}
end

# Allow casting binary data structs
def apply_scope(%{filename: filename, binary: binary} = upload, scope, _options)
when is_binary(filename) and is_binary(binary) do
{upload, scope}
end

# If casting a binary (path), ensure we've explicitly allowed paths
def apply_scope(%{filename: filename, path: path} = upload, scope, options)
when is_binary(filename) and is_binary(path) do
path = String.trim(path)

if path_allowed?(path, options) do
{%{upload | path: path}, scope}
else
:invalid
end
end

def apply_scope(path, scope, options) when is_binary(path) do
path = String.trim(path)
if path_allowed?(path, options), do: {path, scope}, else: :invalid
end

defp path_allowed?(path, options) do
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule Waffle.Ecto.Mixfile do
use Mix.Project

@version "0.0.12"
@version "0.0.13"

def project do
[app: :waffle_ecto,
Expand Down
26 changes: 26 additions & 0 deletions test/schema_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ defmodule WaffleTest.Ecto.Schema do
schema "users" do
field(:first_name, :string)
field(:avatar, DummyDefinition.Type)
field(:images, {:array, DummyDefinition.Type})
end

def changeset(user, params \\ :invalid) do
Expand Down Expand Up @@ -39,6 +40,13 @@ defmodule WaffleTest.Ecto.Schema do
|> cast(params, ~w(first_name)a)
|> cast_attachments(params, ~w(avatar)a)
end

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

def build_upload(path) do
Expand All @@ -63,6 +71,24 @@ defmodule WaffleTest.Ecto.Schema do
%{file_name: "file.png", updated_at: _} = cs.changes.avatar
end

test_with_mock "cascades storage success with an array", DummyDefinition,
store: fn {%{__struct__: Plug.Upload, path: "/path/to/my/file.png", filename: "file.png"},
%TestUser{}} ->
{:ok, "file.png"}
end do
upload1 = build_upload("/path/to/my/file.png")
upload2 = build_upload("/path/to/my/file.png")

cs = TestUser.images_changeset(%TestUser{}, %{"images" => [upload1, upload2]})

assert cs.valid?

[
%{file_name: "file.png", updated_at: _},
%{file_name: "file.png", updated_at: _}
] = cs.changes.images
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{}} ->
Expand Down