Skip to content

Instantly share code, notes, and snippets.

@vhf
Created December 10, 2024 15:12
Show Gist options
  • Save vhf/a4a72a267eeeac540c15dde8dd3acbc3 to your computer and use it in GitHub Desktop.
Save vhf/a4a72a267eeeac540c15dde8dd3acbc3 to your computer and use it in GitHub Desktop.
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
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