Created
December 10, 2024 15:12
-
-
Save vhf/a4a72a267eeeac540c15dde8dd3acbc3 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
defmodule Foo.Helpers do | |
@doc """ | |
Validates that all keys in the input params are defined in the schema. | |
Adds an error to the changeset if any unexpected keys are found. | |
`validate_no_unused_attributes/2` accepts a list of permitted keys that might not be in the schema. | |
## Examples | |
iex> params = %{"bar" => "bar", "baz" => "baz", "unexpected" => "wow"} | |
iex> changeset = Foo.changeset(%Foo{}, params) | |
iex> changeset = validate_no_unused_attributes(changeset) | |
iex> changeset.errors | |
[unused_attributes: {"unexpected keys found in input: unexpected", []}] | |
iex> params = %{"bar" => "bar", "baz" => "baz", "expected_extra_key" => %{}} | |
iex> changeset = Foo.changeset(%Foo{}, params) | |
iex> changeset = validate_no_unused_attributes(changeset, ["expected_extra_key"]) | |
iex> changeset.valid? | |
true | |
""" | |
def validate_no_unused_attributes(%Ecto.Changeset{} = changeset, permitted_keys \\ []) | |
when is_list(permitted_keys) do | |
schema_keys = | |
changeset.data.__struct__.__schema__(:fields) | |
|> Enum.map(&Atom.to_string/1) | |
|> Enum.concat(permitted_keys) | |
|> MapSet.new() | |
input_keys = | |
changeset.params | |
|> Map.keys() | |
|> MapSet.new() | |
unused = MapSet.difference(input_keys, schema_keys) | |
if Enum.empty?(unused) do | |
changeset | |
else | |
unused_keys = MapSet.to_list(unused) |> Enum.sort() |> Enum.join(", ") | |
Ecto.Changeset.add_error( | |
changeset, | |
:unused_attributes, | |
"unexpected keys found in input: #{unused_keys}" | |
) | |
end | |
end | |
end |
This file contains hidden or 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
defmodule Foo.HelpersTest do | |
use ExUnit.Case, async: true | |
alias Foo.Helpers | |
defmodule TestSchema do | |
use Ecto.Schema | |
import Ecto.Changeset | |
schema "test_schemas" do | |
field :name, :string | |
field :age, :integer | |
timestamps() | |
end | |
def changeset(schema, params) do | |
schema | |
|> cast(params, [:name, :age]) | |
end | |
end | |
describe "validate_no_unused_attributes/1" do | |
test "returns valid changeset when all attributes are used" do | |
params = %{"name" => "John", "age" => 30} | |
changeset = | |
%TestSchema{} | |
|> TestSchema.changeset(params) | |
|> Helpers.validate_no_unused_attributes() | |
assert changeset.valid? | |
assert changeset.errors == [] | |
end | |
test "adds error when unexpected attributes are present" do | |
params = %{"name" => "John", "age" => 30, "unexpected" => "value", "extra" => "stuff"} | |
changeset = | |
%TestSchema{} | |
|> TestSchema.changeset(params) | |
|> Helpers.validate_no_unused_attributes() | |
refute changeset.valid? | |
assert {message, _} = changeset.errors[:unused_attributes] | |
assert message =~ "unexpected keys found in input: extra, unexpected" | |
end | |
test "handles empty params" do | |
params = %{} | |
changeset = | |
%TestSchema{} | |
|> TestSchema.changeset(params) | |
|> Helpers.validate_no_unused_attributes() | |
assert changeset.valid? | |
assert changeset.errors == [] | |
end | |
test "handles nil values in params" do | |
params = %{"name" => nil, "age" => nil} | |
changeset = | |
%TestSchema{} | |
|> TestSchema.changeset(params) | |
|> Helpers.validate_no_unused_attributes() | |
assert changeset.valid? | |
assert changeset.errors == [] | |
end | |
end | |
describe "validate_no_unused_attributes/2" do | |
test "allows permitted keys even if not in schema" do | |
params = %{"name" => "John", "age" => 30, "meta" => %{}, "tracking_id" => "123"} | |
changeset = | |
%TestSchema{} | |
|> TestSchema.changeset(params) | |
|> Helpers.validate_no_unused_attributes(["meta", "tracking_id"]) | |
assert changeset.valid? | |
assert changeset.errors == [] | |
end | |
test "adds error for unexpected keys while allowing permitted keys" do | |
params = %{ | |
"name" => "John", | |
"age" => 30, | |
"meta" => %{}, | |
"tracking_id" => "123", | |
"unexpected" => "value" | |
} | |
changeset = | |
%TestSchema{} | |
|> TestSchema.changeset(params) | |
|> Helpers.validate_no_unused_attributes(["meta", "tracking_id"]) | |
refute changeset.valid? | |
assert {message, _} = changeset.errors[:unused_attributes] | |
assert message =~ "unexpected keys found in input: unexpected" | |
end | |
test "handles empty permitted keys list" do | |
params = %{"name" => "John", "age" => 30} | |
changeset = | |
%TestSchema{} | |
|> TestSchema.changeset(params) | |
|> Helpers.validate_no_unused_attributes([]) | |
assert changeset.valid? | |
assert changeset.errors == [] | |
end | |
test "handles permitted keys when params are empty" do | |
params = %{} | |
changeset = | |
%TestSchema{} | |
|> TestSchema.changeset(params) | |
|> Helpers.validate_no_unused_attributes(["meta", "tracking_id"]) | |
assert changeset.valid? | |
assert changeset.errors == [] | |
end | |
test "handles timestamp fields automatically" do | |
params = %{ | |
"name" => "John", | |
"age" => 30, | |
"inserted_at" => "2024-01-01T00:00:00Z", | |
"updated_at" => "2024-01-01T00:00:00Z" | |
} | |
changeset = | |
%TestSchema{} | |
|> TestSchema.changeset(params) | |
|> Helpers.validate_no_unused_attributes() | |
assert changeset.valid? | |
assert changeset.errors == [] | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment