From cbb6ba1e84bbec9965007a794eab425fd6a26856 Mon Sep 17 00:00:00 2001 From: belaustegui Date: Sun, 2 Apr 2017 14:15:50 +0200 Subject: [PATCH] Validate translated conditions in compile time The macro `Trans.QueryBuilder.translated/3` now validates the translated fields before the macro expansion step. We will receive an error in compilation time when adding conditions on untranslatable fields. --- lib/trans/query_builder.ex | 25 ++++++++++++++++++++----- test/query_builder_test.exs | 27 ++++++++++++++++++++------- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/lib/trans/query_builder.ex b/lib/trans/query_builder.ex index b018678..e635c97 100644 --- a/lib/trans/query_builder.ex +++ b/lib/trans/query_builder.ex @@ -1,8 +1,13 @@ if Code.ensure_loaded?(Ecto.Query) do defmodule Trans.QueryBuilder do - defmacro translated(translatable, opts) do - generate_query(schema(translatable), field(translatable), locale(opts)) + defmacro translated(module, translatable, opts) do + with field <- field(translatable) do + Module.eval_quoted __CALLER__, [ + __validate_fields__(module, field) + ] + generate_query(schema(translatable), field, locale(opts)) + end end defp generate_query(schema, nil, locale) do @@ -21,7 +26,7 @@ if Code.ensure_loaded?(Ecto.Query) do case Keyword.fetch(opts, :locale) do {:ok, locale} when is_atom(locale) -> to_string(locale) {:ok, locale} when is_binary(locale) -> locale - _ -> error_unspecified_locale() + _ -> raise ArgumentError, mesage: "You must specify a locale for the query. For example `translated(x.field, locale: :en)`." end end @@ -31,8 +36,18 @@ if Code.ensure_loaded?(Ecto.Query) do defp field({{:., _, [_schema, field]}, _metadata, _args}), do: to_string(field) defp field(_), do: nil - defp error_unspecified_locale do - raise ArgumentError, mesage: "You must specify a locale for the query. For example `translated(x.field, locale: :en)`." + @doc false + def __validate_fields__(module, field) do + quote do + with field <- unquote(field) do + cond do + is_nil(field) -> nil + not Trans.translatable?(unquote(module), unquote(field)) -> + raise ArgumentError, message: "'#{inspect(unquote(module))}' module must declare '#{inspect(unquote(field))}' as translatable" + true -> nil + end + end + end end end diff --git a/test/query_builder_test.exs b/test/query_builder_test.exs index 0587764..f91b75f 100644 --- a/test/query_builder_test.exs +++ b/test/query_builder_test.exs @@ -17,7 +17,7 @@ defmodule QueryBuilderTest do test "should find only one article translated to ES" do count = Repo.one(from a in Article, - where: not is_nil(translated(a, locale: :es)), + where: not is_nil(translated(Article, a, locale: :es)), select: count(a.id) ) assert count == 1 @@ -25,7 +25,7 @@ defmodule QueryBuilderTest do test "should not find any article translated to DE" do count = Repo.one(from a in Article, - where: not is_nil(translated(a, locale: :de)), + where: not is_nil(translated(Article, a, locale: :de)), select: count(a.id)) assert count == 0 end @@ -33,7 +33,7 @@ defmodule QueryBuilderTest do test "should find an article by its FR title", %{translated_article: article} do matches = Repo.all(from a in Article, - where: translated(a.title, locale: :fr) == ^article.translations["fr"]["title"]) + where: translated(Article, a.title, locale: :fr) == ^article.translations["fr"]["title"]) assert Enum.count(matches) == 1 assert hd(matches).id == article.id end @@ -41,7 +41,7 @@ defmodule QueryBuilderTest do test "should not find an article by a non existant translation" do count = Repo.one(from a in Article, select: count(a.id), - where: translated(a.title, locale: :es) == "FAKE TITLE") + where: translated(Article, a.title, locale: :es) == "FAKE TITLE") assert count == 0 end @@ -54,7 +54,7 @@ defmodule QueryBuilderTest do |> Enum.join(" ") |> Kernel.<>("%") matches = Repo.all(from a in Article, - where: ilike(translated(a.body, locale: :es), ^first_words)) + where: ilike(translated(Article, a.body, locale: :es), ^first_words)) assert Enum.count(matches) == 1 assert hd(matches).id == article.id end @@ -70,7 +70,7 @@ defmodule QueryBuilderTest do |> Kernel.<>("%") count = Repo.one(from a in Article, select: count(a.id), - where: like(translated(a.body, locale: :fr), ^first_words)) + where: like(translated(Article, a.body, locale: :fr), ^first_words)) assert count == 0 end @@ -84,8 +84,21 @@ defmodule QueryBuilderTest do |> String.upcase |> Kernel.<>("%") matches = Repo.all(from a in Article, - where: ilike(translated(a.body, locale: :fr), ^first_words)) + where: ilike(translated(Article, a.body, locale: :fr), ^first_words)) assert Enum.count(matches) == 1 assert hd(matches).id == article.id end + + test "should raise when adding conditions to an untranslatable field" do + # Since the QueryBuilder errors are emitted during compilation, we do a + # little trick to delay the compilation of the query until the test + # is running, so we can catch the raised error. + query = quote do + Repo.all(from a in Article, + where: not is_nil(translated(Article, a.translations, locale: :es))) + end + assert_raise ArgumentError, fn -> + Module.eval_quoted __ENV__, query + end + end end