diff --git a/lib/philomena/filters/filter.ex b/lib/philomena/filters/filter.ex index ef1222d7..29d542f7 100644 --- a/lib/philomena/filters/filter.ex +++ b/lib/philomena/filters/filter.ex @@ -1,9 +1,10 @@ defmodule Philomena.Filters.Filter do use Ecto.Schema import Ecto.Changeset + import PhilomenaQuery.Ecto.QueryValidator alias Philomena.Schema.TagList - alias Philomena.Schema.Search + alias Philomena.Images.Query alias Philomena.Users.User alias Philomena.Repo @@ -48,8 +49,8 @@ defmodule Philomena.Filters.Filter do |> validate_required([:name]) |> validate_my_downvotes(:spoilered_complex_str) |> validate_my_downvotes(:hidden_complex_str) - |> Search.validate_search(:spoilered_complex_str, user) - |> Search.validate_search(:hidden_complex_str, user) + |> validate_query(:spoilered_complex_str, &Query.compile(&1, user: user)) + |> validate_query(:hidden_complex_str, &Query.compile(&1, user: user)) |> unsafe_validate_unique([:user_id, :name], Repo) end diff --git a/lib/philomena/schema/search.ex b/lib/philomena/schema/search.ex deleted file mode 100644 index 819823f2..00000000 --- a/lib/philomena/schema/search.ex +++ /dev/null @@ -1,18 +0,0 @@ -defmodule Philomena.Schema.Search do - alias Philomena.Images.Query - alias PhilomenaQuery.Parse.String - import Ecto.Changeset - - def validate_search(changeset, field, user, watched \\ false) do - query = changeset |> get_field(field) |> String.normalize() - output = Query.compile(query, user: user, watch: watched) - - case output do - {:ok, _} -> - changeset - - _ -> - add_error(changeset, field, "is invalid") - end - end -end diff --git a/lib/philomena/users/user.ex b/lib/philomena/users/user.ex index 28018915..c005d92e 100644 --- a/lib/philomena/users/user.ex +++ b/lib/philomena/users/user.ex @@ -4,10 +4,11 @@ defmodule Philomena.Users.User do use Ecto.Schema import Ecto.Changeset + import PhilomenaQuery.Ecto.QueryValidator alias Philomena.Schema.TagList - alias Philomena.Schema.Search + alias Philomena.Images.Query alias Philomena.Filters.Filter alias Philomena.ArtistLinks.ArtistLink alias Philomena.Badges @@ -354,8 +355,8 @@ defmodule Philomena.Users.User do |> validate_inclusion(:images_per_page, 1..50) |> validate_inclusion(:comments_per_page, 1..100) |> validate_inclusion(:scale_large_images, ["false", "partscaled", "true"]) - |> Search.validate_search(:watched_images_query_str, user, true) - |> Search.validate_search(:watched_images_exclude_str, user, true) + |> validate_query(:watched_images_query_str, &Query.compile(&1, user: user, watch: true)) + |> validate_query(:watched_images_exclude_str, &Query.compile(&1, user: user, watch: true)) end def description_changeset(user, attrs) do diff --git a/lib/philomena_query/ecto/query_validator.ex b/lib/philomena_query/ecto/query_validator.ex new file mode 100644 index 00000000..ea0b8950 --- /dev/null +++ b/lib/philomena_query/ecto/query_validator.ex @@ -0,0 +1,69 @@ +defmodule PhilomenaQuery.Ecto.QueryValidator do + @moduledoc """ + Query string validation for Ecto. + + It enables the following usage pattern by taking a fn of the compiler: + + defmodule Filter do + import PhilomenaQuery.Ecto.QueryValidator + + # ... + + def changeset(filter, attrs, user) do + filter + |> cast(attrs, [:complex]) + |> validate_required([:complex]) + |> validate_query([:complex], with: &Query.compile(&1, user: user)) + end + end + + """ + + import Ecto.Changeset + alias PhilomenaQuery.Parse.String + + @doc """ + Validates a query string using the provided attribute(s) and compiler. + + Returns the changeset as-is, or with an `"is invalid"` error added to validated field. + + ## Examples + + # With single attribute + filter + |> cast(attrs, [:complex]) + |> validate_query(:complex, &Query.compile(&1, user: user)) + + # With list of attributes + filter + |> cast(attrs, [:spoilered_complex, :hidden_complex]) + |> validate_query([:spoilered_complex, :hidden_complex], &Query.compile(&1, user: user)) + + """ + def validate_query(changeset, attr_or_attr_list, callback) + + def validate_query(changeset, attr_list, callback) when is_list(attr_list) do + Enum.reduce(attr_list, changeset, fn attr, changeset -> + validate_query(changeset, attr, callback) + end) + end + + def validate_query(changeset, attr, callback) do + if changed?(changeset, attr) do + validate_assuming_changed(changeset, attr, callback) + else + changeset + end + end + + defp validate_assuming_changed(changeset, attr, callback) do + with value when is_binary(value) <- fetch_change!(changeset, attr) || "", + value <- String.normalize(value), + {:ok, _} <- callback.(value) do + changeset + else + _ -> + add_error(changeset, attr, "is invalid") + end + end +end