diff --git a/CHANGELOG.md b/CHANGELOG.md index c05bd23..5707ad6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## v0.0.12 (2022-03-07) +- Add support for array fields + ## v0.0.11 (2021-08-12) - Update dependencies - Allow custom error messages to be set on cast (#35) diff --git a/config/config.exs b/config/config.exs index 6dfa82f..8f81a0f 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,6 +1,6 @@ # This file is responsible for configuring your application # and its dependencies with the aid of the Mix.Config module. -use Mix.Config +import Config # This configuration is loaded before any dependency and is restricted # to this project. If another project depends on this project, this diff --git a/lib/waffle_ecto/schema.ex b/lib/waffle_ecto/schema.ex index b4a774e..8d0af67 100644 --- a/lib/waffle_ecto/schema.ex +++ b/lib/waffle_ecto/schema.ex @@ -79,7 +79,7 @@ defmodule Waffle.Ecto.Schema do |> convert_params_to_binary() |> Map.take(allowed_param_keys) |> check_and_apply_scope(scope, options) - |> Enum.into(%{}) + |> Map.new() end Ecto.Changeset.cast(changeset_or_data, waffle_params, allowed) @@ -90,31 +90,58 @@ 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 already uploaded files + def apply_scope(%{file_name: filename, updated_at: %NaiveDateTime{}} = existing_file, _scope, _options) + when is_binary(filename) do + existing_file + 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 diff --git a/mix.exs b/mix.exs index 30ceba4..99ad3e2 100644 --- a/mix.exs +++ b/mix.exs @@ -1,19 +1,21 @@ defmodule Waffle.Ecto.Mixfile do use Mix.Project - @version "0.0.11" + @version "0.0.12" def project do - [app: :waffle_ecto, - version: @version, - elixir: "~> 1.4", - elixirc_paths: elixirc_paths(Mix.env), - deps: deps(), - docs: docs(), - - # Hex - description: description(), - package: package()] + [ + app: :waffle_ecto, + version: @version, + elixir: "~> 1.4", + elixirc_paths: elixirc_paths(Mix.env()), + deps: deps(), + docs: docs(), + + # Hex + description: description(), + package: package() + ] end # Configuration for the OTP application @@ -27,7 +29,7 @@ defmodule Waffle.Ecto.Mixfile do # Specifies which paths to compile per environment. defp elixirc_paths(:test), do: ["lib", "test/support"] - defp elixirc_paths(_), do: ["lib"] + defp elixirc_paths(_), do: ["lib"] defp description do """ @@ -36,10 +38,12 @@ defmodule Waffle.Ecto.Mixfile do end defp package do - [maintainers: ["Boris Kuznetsov"], - licenses: ["Apache 2.0"], - links: %{"GitHub" => "https://github.com/elixir-waffle/waffle_ecto"}, - files: ~w(mix.exs README.md CHANGELOG.md lib)] + [ + maintainers: ["Boris Kuznetsov"], + licenses: ["Apache 2.0"], + links: %{"GitHub" => "https://github.com/elixir-waffle/waffle_ecto"}, + files: ~w(mix.exs README.md CHANGELOG.md lib) + ] end defp docs do @@ -51,11 +55,11 @@ defmodule Waffle.Ecto.Mixfile do defp deps do [ - {:waffle, "~> 1.0"}, - {:ecto, "~> 3.0"}, + {:waffle, "~> 1.1"}, + {:ecto, "~> 3.13"}, {:mock, "~> 0.3", only: :test}, {:ex_doc, ">= 0.23.0", only: :dev}, - {:credo, "~> 1.4", only: [:dev, :test], runtime: false} + {:credo, "~> 1.7", only: [:dev, :test], runtime: false} ] end end diff --git a/mix.lock b/mix.lock index e28e39d..678bfad 100644 --- a/mix.lock +++ b/mix.lock @@ -1,34 +1,34 @@ %{ "arc": {:hex, :arc, "0.11.0", "ac7a0cc03035317b6fef9fe94c97d7d9bd183a3e7ce1606aa0c175cfa8d1ba6d", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:ex_aws_s3, "~> 2.0", [hex: :ex_aws_s3, repo: "hexpm", optional: true]}, {:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.1", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm"}, - "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, - "certifi": {:hex, :certifi, "2.6.1", "dbab8e5e155a0763eea978c913ca280a6b544bfa115633fa20249c3d396d9493", [:rebar3], [], "hexpm", "524c97b4991b3849dd5c17a631223896272c6b0af446778ba4675a1dff53bb7e"}, - "credo": {:hex, :credo, "1.5.6", "e04cc0fdc236fefbb578e0c04bd01a471081616e741d386909e527ac146016c6", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "4b52a3e558bd64e30de62a648518a5ea2b6e3e5d2b164ef5296244753fc7eb17"}, - "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, + "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, + "credo": {:hex, :credo, "1.7.13", "126a0697df6b7b71cd18c81bc92335297839a806b6f62b61d417500d1070ff4e", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "47641e6d2bbff1e241e87695b29f617f1a8f912adea34296fb10ecc3d7e9e84f"}, + "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "earmark": {:hex, :earmark, "1.4.4", "4821b8d05cda507189d51f2caeef370cf1e18ca5d7dfb7d31e9cafe6688106a4", [:mix], [], "hexpm", "1f93aba7340574847c0f609da787f0d79efcab51b044bb6e242cae5aca9d264d"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.13", "0c98163e7d04a15feb62000e1a891489feb29f3d10cb57d4f845c405852bbef8", [:mix], [], "hexpm", "d602c26af3a0af43d2f2645613f65841657ad6efc9f0e361c3b6c06b578214ba"}, - "ecto": {:hex, :ecto, "3.6.2", "efdf52acfc4ce29249bab5417415bd50abd62db7b0603b8bab0d7b996548c2bc", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "efad6dfb04e6f986b8a3047822b0f826d9affe8e4ebdd2aeedbfcb14fd48884e"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, + "ecto": {:hex, :ecto, "3.13.4", "27834b45d58075d4a414833d9581e8b7bb18a8d9f264a21e42f653d500dbeeb5", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5ad7d1505685dfa7aaf86b133d54f5ad6c42df0b4553741a1ff48796736e88b2"}, "erlcloud": {:hex, :erlcloud, "0.9.2"}, - "ex_doc": {:hex, :ex_doc, "0.25.1", "4b736fa38dc76488a937e5ef2944f5474f3eff921de771b25371345a8dc810bc", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3200b0a69ddb2028365281fbef3753ea9e728683863d8cdaa96580925c891f67"}, - "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, - "hackney": {:hex, :hackney, "1.17.4", "99da4674592504d3fb0cfef0db84c3ba02b4508bae2dff8c0108baa0d6e0977c", [:rebar3], [{:certifi, "~>2.6.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "de16ff4996556c8548d512f4dbe22dd58a587bf3332e7fd362430a7ef3986b16"}, + "ex_doc": {:hex, :ex_doc, "0.39.1", "e19d356a1ba1e8f8cfc79ce1c3f83884b6abfcb79329d435d4bbb3e97ccc286e", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "8abf0ed3e3ca87c0847dfc4168ceab5bedfe881692f1b7c45f4a11b232806865"}, + "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, + "hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"}, "httpoison": {:hex, :httpoison, "0.13.0", "bfaf44d9f133a6599886720f3937a7699466d23bb0cd7a88b6ba011f53c6f562", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, - "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, - "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, + "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "jsx": {:hex, :jsx, "2.1.1"}, "lhttpc": {:hex, :lhttpc, "1.3.0"}, - "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.15.1", "b5888c880d17d1cc3e598f05cdb5b5a91b7b17ac4eaf5f297cb697663a1094dd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "db68c173234b07ab2a07f645a5acdc117b9f99d69ebf521821d89690ae6c6ec8"}, - "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, + "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, + "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "mock": {:hex, :mock, "0.3.7", "75b3bbf1466d7e486ea2052a73c6e062c6256fb429d6797999ab02fa32f29e03", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "4da49a4609e41fd99b7836945c26f373623ea968cfb6282742bcb94440cf7e5c"}, + "mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"}, + "mock": {:hex, :mock, "0.3.9", "10e44ad1f5962480c5c9b9fa779c6c63de9bd31997c8e04a853ec990a9d841af", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "9e1b244c4ca2551bb17bb8415eed89e40ee1308e0fbaed0a4fdfe3ec8a4adbd3"}, "mogrify": {:hex, :mogrify, "0.1.0"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, - "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, + "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm", "8f7168911120e13419e086e78d20e4d1a6776f1eee2411ac9f790af10813389f"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, - "telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, - "waffle": {:hex, :waffle, "1.1.4", "4cad1c2fd67b5c18a0e9fa9278b0de70cd1a143e8fbc73cddca248749cfa8b04", [:mix], [{:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:ex_aws_s3, "~> 2.1", [hex: :ex_aws_s3, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "f0ccb04e7e8d8529c3a885750e8014c69b20e9b9881cd80bb4a6f1ce1c1cc88f"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"}, + "waffle": {:hex, :waffle, "1.1.9", "8ce5ca9e59fa5491da67a2df57b8711d93223df3c3e5c21ad2acdedc41a0f51a", [:mix], [{:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:ex_aws_s3, "~> 2.1", [hex: :ex_aws_s3, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "307c63cfdfb4624e7c423868a128ccfcb0e5291ae73a9deecb3a10b7a3eb277c"}, } diff --git a/test/schema_test.exs b/test/schema_test.exs index 7417dab..844608c 100644 --- a/test/schema_test.exs +++ b/test/schema_test.exs @@ -1,16 +1,21 @@ defmodule WaffleTest.Ecto.Schema do + @moduledoc false use ExUnit.Case, async: false - import Mock + import ExUnit.CaptureLog + import Mock defmodule TestUser do + @moduledoc false use Ecto.Schema - import Ecto.Changeset use Waffle.Ecto.Schema + import Ecto.Changeset + schema "users" do field(:first_name, :string) field(:avatar, DummyDefinition.Type) + field(:images, {:array, DummyDefinition.Type}) end def changeset(user, params \\ :invalid) do @@ -39,6 +44,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 @@ -53,8 +65,7 @@ defmodule WaffleTest.Ecto.Schema do 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{}} -> + 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") @@ -63,9 +74,25 @@ 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{}} -> + 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") @@ -82,8 +109,7 @@ defmodule WaffleTest.Ecto.Schema do end test_with_mock "cascades custom storage error into an error", DummyDefinition, - store: fn {%{__struct__: Plug.Upload, path: "/path/to/my/file.png", filename: "file.png"}, - %TestUser{}} -> + store: fn {%{__struct__: Plug.Upload, path: "/path/to/my/file.png", filename: "file.png"}, %TestUser{}} -> {:error, "file type is invalid"} end do upload = build_upload("/path/to/my/file.png") @@ -100,8 +126,7 @@ defmodule WaffleTest.Ecto.Schema do end test_with_mock "converts changeset into schema", DummyDefinition, - store: fn {%{__struct__: Plug.Upload, path: "/path/to/my/file.png", filename: "file.png"}, - %TestUser{}} -> + 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") @@ -113,8 +138,7 @@ defmodule WaffleTest.Ecto.Schema do end test_with_mock "applies changes to schema", DummyDefinition, - store: fn {%{__struct__: Plug.Upload, path: "/path/to/my/file.png", filename: "file.png"}, - %TestUser{}} -> + 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") @@ -126,8 +150,7 @@ defmodule WaffleTest.Ecto.Schema do end test_with_mock "converts atom keys", DummyDefinition, - store: fn {%{__struct__: Plug.Upload, path: "/path/to/my/file.png", filename: "file.png"}, - %TestUser{}} -> + 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") @@ -139,8 +162,7 @@ defmodule WaffleTest.Ecto.Schema do end test_with_mock "casting nil attachments", DummyDefinition, - store: fn {%{__struct__: Plug.Upload, path: "/path/to/my/file.png", filename: "file.png"}, - %TestUser{}} -> + store: fn {%{__struct__: Plug.Upload, path: "/path/to/my/file.png", filename: "file.png"}, %TestUser{}} -> {:ok, "file.png"} end do changeset = @@ -150,8 +172,7 @@ defmodule WaffleTest.Ecto.Schema do assert nil == Ecto.Changeset.get_field(changeset, :avatar) end - test_with_mock "casting empty attachments", DummyDefinition, - store: fn {"", %TestUser{}} -> {:ok, ""} end do + test_with_mock "casting empty attachments", DummyDefinition, store: fn {"", %TestUser{}} -> {:ok, ""} end do changeset = TestUser.path_changeset(%TestUser{}, %{"avatar" => ""}) assert nil == Ecto.Changeset.get_field(changeset, :avatar) assert not called(DummyDefinition.store({"", %TestUser{}})) @@ -159,7 +180,11 @@ defmodule WaffleTest.Ecto.Schema do 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 "}) + changeset = TestUser.path_changeset(%TestUser{}, %{"avatar" => " /path/to/my/file.png "}) + + assert changeset.valid? + assert %{avatar: %{file_name: "file.png", updated_at: %NaiveDateTime{}}} = changeset.changes + assert called(DummyDefinition.store({"/path/to/my/file.png", %TestUser{}})) end @@ -167,22 +192,26 @@ defmodule WaffleTest.Ecto.Schema do store: fn {%{path: "/path/to/my/file.png", filename: "avatar.png"}, %TestUser{}} -> {:ok, "file.png"} end do - TestUser.path_changeset(%TestUser{}, %{ - "avatar" => %{path: " /path/to/my/file.png ", filename: "avatar.png"} - }) + changeset = + TestUser.path_changeset(%TestUser{}, %{ + "avatar" => %{path: " /path/to/my/file.png ", filename: "avatar.png"} + }) + + assert changeset.valid? + assert %{avatar: %{file_name: "file.png", updated_at: %NaiveDateTime{}}} = changeset.changes - assert called( - DummyDefinition.store( - {%{path: "/path/to/my/file.png", filename: "avatar.png"}, %TestUser{}} - ) - ) + assert called(DummyDefinition.store({%{path: "/path/to/my/file.png", filename: "avatar.png"}, %TestUser{}})) end test_with_mock "allow_urls => true", DummyDefinition, store: fn {"http://external.url/file.png", %TestUser{}} -> {:ok, "file.png"} end do - TestUser.url_changeset(%TestUser{}, %{"avatar" => " http://external.url/file.png "}) + changeset = TestUser.url_changeset(%TestUser{}, %{"avatar" => " http://external.url/file.png "}) + + assert changeset.valid? + assert %{avatar: %{file_name: "file.png", updated_at: %NaiveDateTime{}}} = changeset.changes + assert called(DummyDefinition.store({"http://external.url/file.png", %TestUser{}})) end @@ -190,22 +219,26 @@ defmodule WaffleTest.Ecto.Schema do store: fn {%{path: "http://external.url/file.png", filename: "avatar.png"}, %TestUser{}} -> {:ok, "file.png"} end do - TestUser.url_changeset(%TestUser{}, %{ - "avatar" => %{path: " http://external.url/file.png ", filename: "avatar.png"} - }) + changeset = + TestUser.url_changeset(%TestUser{}, %{ + "avatar" => %{path: " http://external.url/file.png ", filename: "avatar.png"} + }) - assert called( - DummyDefinition.store( - {%{path: "http://external.url/file.png", filename: "avatar.png"}, %TestUser{}} - ) - ) + assert changeset.valid? + assert %{avatar: %{file_name: "file.png", updated_at: %NaiveDateTime{}}} = changeset.changes + + assert called(DummyDefinition.store({%{path: "http://external.url/file.png", filename: "avatar.png"}, %TestUser{}})) end test_with_mock "allow_urls => true with an invalid URL", DummyDefinition, store: fn {"/path/to/my/file.png", %TestUser{}} -> {:ok, "file.png"} end do - TestUser.url_changeset(%TestUser{}, %{"avatar" => "/path/to/my/file.png"}) + changeset = TestUser.url_changeset(%TestUser{}, %{"avatar" => "/path/to/my/file.png"}) + + refute changeset.valid? + assert %{} == changeset.changes + assert not called(DummyDefinition.store({"/path/to/my/file.png", %TestUser{}})) end @@ -213,14 +246,31 @@ defmodule WaffleTest.Ecto.Schema do 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{}} - ) - ) + changeset = + TestUser.changeset(%TestUser{}, %{ + "avatar" => %{filename: "/path/to/my/file.png", binary: <<1, 2, 3>>} + }) + + assert changeset.valid? + assert %{avatar: %{file_name: "file.png", updated_at: %NaiveDateTime{}}} = changeset.changes + + assert called(DummyDefinition.store({%{filename: "/path/to/my/file.png", binary: <<1, 2, 3>>}, %TestUser{}})) + end + + test_with_mock "allow casting already uploaded files", DummyDefinition, + store: fn {"file.png", %TestUser{}} -> + {:ok, "file.png"} + end do + updated_at = ~N[2025-10-29 11:19:02.943392] + + changeset = + TestUser.changeset(%TestUser{}, %{ + "avatar" => %{file_name: "file.png", updated_at: updated_at} + }) + + assert changeset.valid? + assert changeset.changes == %{avatar: %{file_name: "file.png", updated_at: updated_at}} + + assert not called(DummyDefinition.store({%{file_name: "file.png", updated_at: updated_at}, %TestUser{}})) end end