diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..3d8ce11 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,3 @@ +[ + inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/config/config.exs b/config/config.exs index 24912d3..933e592 100644 --- a/config/config.exs +++ b/config/config.exs @@ -30,6 +30,6 @@ use Mix.Config config :filtrex, ecto_repos: [Filtrex.Repo] -if Mix.env == :test do +if Mix.env() == :test do import_config "test.exs" end diff --git a/lib/filtrex.ex b/lib/filtrex.ex index c850a53..cc317df 100644 --- a/lib/filtrex.ex +++ b/lib/filtrex.ex @@ -19,11 +19,18 @@ defmodule Filtrex do defstruct type: nil, conditions: [], sub_filters: [], empty: false @whitelist [ - :filter, :type, :conditions, :sub_filters, - :column, :comparator, :value, :start, :end + :filter, + :type, + :conditions, + :sub_filters, + :column, + :comparator, + :value, + :start, + :end ] - @type t :: Filtrex.t + @type t :: Filtrex.t() @doc """ Parses a filter expression and returns an error or the parsed filter with @@ -35,18 +42,18 @@ defmodule Filtrex do [%Filtrex.Type.Config{type: :text, keys: ~w(title comments)}] ``` """ - @spec parse([Filtrex.Type.Config.t], Map.t) :: {:error, String.t} | {:ok, Filtrex.t} + @spec parse([Filtrex.Type.Config.t()], map) :: {:error, String.t()} | {:ok, Filtrex.t()} def parse(configs, map) do with {:ok, sanitized} <- Filtrex.Params.sanitize(map, @whitelist), {:ok, valid_structured_map} <- validate_structure(sanitized), - do: parse_validated_structure(configs, valid_structured_map) + do: parse_validated_structure(configs, valid_structured_map) end @doc """ Parses a filter expression, like `parse/2`. If any exception is raised when parsing the map, a `%Filtrex{empty: true}` struct will be returned. """ - @spec safe_parse([Filtrex.Type.Config.t], Map.t) :: Filtrex.t + @spec safe_parse([Filtrex.Type.Config.t()], map) :: Filtrex.t() def safe_parse(configs, map) do try do {:ok, filter} = parse(configs, map) @@ -65,6 +72,7 @@ defmodule Filtrex do ``` """ def parse_params(_configs, params) when params == %{}, do: {:ok, %Filtrex{empty: true}} + def parse_params(configs, params) do with {:ok, {type, params}} <- parse_params_filter_union(params), {:ok, conditions} <- Filtrex.Params.parse_conditions(configs, params), @@ -77,6 +85,7 @@ defmodule Filtrex do will be returned. """ def safe_parse_params(_configs, params) when params == %{}, do: %Filtrex{empty: true} + def safe_parse_params(configs, params) do try do {:ok, filter} = parse_params(configs, params) @@ -98,8 +107,9 @@ defmodule Filtrex do Filtrex.query(query, filter, allow_empty: true) ``` """ - @spec query(Ecto.Queryable.t, Filtrex.t, Keyword.t) :: Ecto.Query.t + @spec query(Ecto.Queryable.t(), Filtrex.t(), Keyword.t()) :: Ecto.Query.t() def query(queryable, filter, opts \\ [allow_empty: true]) + def query(queryable, %Filtrex{empty: true}, opts) do if opts[:allow_empty] do queryable @@ -113,6 +123,7 @@ defmodule Filtrex do queryable |> Filtrex.AST.build_query(filter) |> Code.eval_quoted([], __ENV__) + result end @@ -123,42 +134,53 @@ defmodule Filtrex do case map do %{filter: %{type: type}} when type not in ~w(all any none) -> {:error, "Invalid filter type '#{type}'"} + %{filter: %{conditions: conditions}} when conditions == [] or not is_list(conditions) -> {:error, "One or more conditions required to filter"} + %{filter: %{sub_filters: sub_filters}} when not is_list(sub_filters) -> {:error, "Sub-filters must be a valid list of filters"} + validated = %{filter: params} -> sub_filters = Map.get(params, :sub_filters, []) - result = Enum.reduce_while(sub_filters, {:ok, []}, fn (sub_map, {:ok, acc}) -> - case validate_structure(sub_map) do - {:ok, sub_validated} -> {:cont, {:ok, acc ++ [sub_validated]}} - {:error, error} -> {:halt, {:error, error}} - end - end) + + result = + Enum.reduce_while(sub_filters, {:ok, []}, fn sub_map, {:ok, acc} -> + case validate_structure(sub_map) do + {:ok, sub_validated} -> {:cont, {:ok, acc ++ [sub_validated]}} + {:error, error} -> {:halt, {:error, error}} + end + end) + with {:ok, validated_sub_filters} <- result, - do: {:ok, put_in(validated.filter[:sub_filters], validated_sub_filters)} + do: {:ok, put_in(validated.filter[:sub_filters], validated_sub_filters)} + _ -> {:error, "Invalid filter structure"} end end defp parse_validated_structure(configs, %{filter: params}) do - parsed_filters = Enum.reduce_while(params[:sub_filters], {:ok, []}, fn (to_parse, {:ok, acc}) -> - case parse(configs, to_parse) do - {:ok, filter} -> {:cont, {:ok, acc ++ [filter]}} - {:error, error} -> {:halt, {:error, error}} - end - end) + parsed_filters = + Enum.reduce_while(params[:sub_filters], {:ok, []}, fn to_parse, {:ok, acc} -> + case parse(configs, to_parse) do + {:ok, filter} -> {:cont, {:ok, acc ++ [filter]}} + {:error, error} -> {:halt, {:error, error}} + end + end) + with {:ok, filters} <- parsed_filters, - do: parse_conditions(configs, params[:type], params[:conditions]) - |> parse_condition_results(params[:type], filters) + do: + parse_conditions(configs, params[:type], params[:conditions]) + |> parse_condition_results(params[:type], filters) end defp parse_conditions(configs, type, conditions) do - Enum.reduce(conditions, %{errors: [], conditions: []}, fn (map, acc) -> + Enum.reduce(conditions, %{errors: [], conditions: []}, fn map, acc -> case Filtrex.Condition.parse(configs, Map.put(map, :inverse, inverse_for(type))) do {:error, error} -> update_list_in_map(acc, :errors, error) + {:ok, condition} -> update_list_in_map(acc, :conditions, condition) end @@ -168,6 +190,7 @@ defmodule Filtrex do defp parse_condition_results(%{errors: [], conditions: conditions}, type, parsed_filters) do {:ok, %Filtrex{type: type, conditions: conditions, sub_filters: parsed_filters}} end + defp parse_condition_results(%{errors: errors}, _, _) do {:error, Enum.join(errors, ", ")} end @@ -176,15 +199,17 @@ defmodule Filtrex do case Map.fetch(params, "filter_union") do {:ok, type} when type in ~w(all any none) -> {:ok, {type, Map.delete(params, "filter_union")}} + :error -> {:ok, {"all", params}} + _ -> {:error, "Invalid filter union"} end end defp inverse_for("none"), do: true - defp inverse_for(_), do: false + defp inverse_for(_), do: false defp update_list_in_map(map, key, value) do values = Map.get(map, key) diff --git a/lib/filtrex/ast.ex b/lib/filtrex/ast.ex index fc06d42..f7517ce 100644 --- a/lib/filtrex/ast.ex +++ b/lib/filtrex/ast.ex @@ -13,14 +13,16 @@ defmodule Filtrex.AST do defp build_fragments(filter) do join = logical_join(filter.type) + Enum.map(filter.conditions, &Filtrex.Encoder.encode/1) - |> fragments(join) - |> build_sub_fragments(join, filter.sub_filters) + |> fragments(join) + |> build_sub_fragments(join, filter.sub_filters) end defp build_sub_fragments(fragments, _, []), do: fragments + defp build_sub_fragments(fragments, join, sub_filters) do - Enum.reduce(sub_filters, fragments, fn (sub_filter, [expression | values]) -> + Enum.reduce(sub_filters, fragments, fn sub_filter, [expression | values] -> [sub_expression | sub_values] = build_fragments(sub_filter) [join(expression, sub_expression, join) | values ++ sub_values] end) @@ -32,14 +34,15 @@ defmodule Filtrex.AST do defp fragments(fragments, join) do Enum.reduce(fragments, ["" | []], fn - (%{expression: new_expression, values: new_values}, ["" | values]) -> + %{expression: new_expression, values: new_values}, ["" | values] -> ["(#{new_expression})" | values ++ new_values] - (%{expression: new_expression, values: new_values}, [expression | values]) -> + + %{expression: new_expression, values: new_values}, [expression | values] -> combined = "#{expression} #{join} (#{new_expression})" [combined | values ++ new_values] end) end defp logical_join("any"), do: "OR" - defp logical_join(_), do: "AND" + defp logical_join(_), do: "AND" end diff --git a/lib/filtrex/condition.ex b/lib/filtrex/condition.ex index 9d3be7f..500e34a 100644 --- a/lib/filtrex/condition.ex +++ b/lib/filtrex/condition.ex @@ -12,12 +12,20 @@ defmodule Filtrex.Condition do Filtrex.Condition.Date, Filtrex.Condition.DateTime, Filtrex.Condition.Boolean, - Filtrex.Condition.Number + Filtrex.Condition.Number, + Filtrex.Condition.UUID ] - @callback parse(Filtrex.Type.Config.t, %{inverse: boolean, column: String.t, value: any, comparator: String.t}) :: {:ok, any} | {:error, any} - @callback type :: Atom.t - @callback comparators :: [String.t] + @type t :: %__MODULE__{} + + @callback parse(Filtrex.Type.Config.t(), %{ + inverse: boolean, + column: String.t(), + value: any, + comparator: String.t() + }) :: {:ok, any} | {:error, any} + @callback type :: atom() + @callback comparators :: [String.t()] defstruct column: nil, comparator: nil, value: nil @@ -54,10 +62,14 @@ defmodule Filtrex.Condition do case condition_module(type) do nil -> {:error, "Unknown filter condition '#{type}'"} + module -> type_atom = String.to_existing_atom(type) - config = Filtrex.Type.Config.configs_for_type(configs, type_atom) + + config = + Filtrex.Type.Config.configs_for_type(configs, type_atom) |> Filtrex.Type.Config.config(options[:column]) + if config do module.parse(config, Map.delete(options, :type)) else @@ -68,21 +80,24 @@ defmodule Filtrex.Condition do @doc "Parses a params key into the condition type, column, and comparator" def param_key_type(configs, key_with_comparator) do - result = Enum.find_value(condition_modules(), fn (module) -> - Enum.find_value(module.comparators, fn (comparator) -> - normalized = "_" <> String.replace(comparator, " ", "_") - key = String.replace_trailing(key_with_comparator, normalized, "") - config = Filtrex.Type.Config.config(configs, key) - if !is_nil(config) and key in config.keys and config.type == module.type do - {:ok, module, config, key, comparator} - end + result = + Enum.find_value(condition_modules(), fn module -> + Enum.find_value(module.comparators, fn comparator -> + normalized = "_" <> String.replace(comparator, " ", "_") + key = String.replace_trailing(key_with_comparator, normalized, "") + config = Filtrex.Type.Config.config(configs, key) + + if !is_nil(config) and key in config.keys and config.type == module.type do + {:ok, module, config, key, comparator} + end + end) end) - end) + if result, do: result, else: {:error, "Unknown filter key '#{key_with_comparator}'"} end @doc "Helper method to validate that a comparator is in list" - @spec validate_comparator(atom, binary, List.t) :: {:ok, binary} | {:error, binary} + @spec validate_comparator(atom, binary, list) :: {:ok, binary} | {:error, binary} def validate_comparator(type, comparator, comparators) do if comparator in comparators do {:ok, comparator} @@ -92,9 +107,10 @@ defmodule Filtrex.Condition do end @doc "Helper method to validate whether a value is in a list" - @spec validate_in(any, List.t) :: nil | any + @spec validate_in(any, list) :: nil | any def validate_in(nil, _), do: nil def validate_in(_, nil), do: nil + def validate_in(value, list) do cond do value in list -> value @@ -103,32 +119,35 @@ defmodule Filtrex.Condition do end @doc "Helper method to validate whether a value is a binary" - @spec validate_is_binary(any) :: nil | String.t + @spec validate_is_binary(any) :: nil | String.t() def validate_is_binary(value) when is_binary(value), do: value def validate_is_binary(_), do: nil @doc "Generates an error description for a generic parse error" - @spec parse_error(any, Atom.t, Atom.t) :: String.t + @spec parse_error(any, atom, atom) :: String.t() def parse_error(value, type, filter_type) do "Invalid #{to_string(filter_type)} #{to_string(type)} '#{value}'" end @doc "Generates an error description for a parse error resulting from an invalid value type" - @spec parse_value_type_error(any, Atom.t) :: String.t + @spec parse_value_type_error(any, atom) :: String.t() def parse_value_type_error(column, filter_type) when is_binary(column) do "Invalid #{to_string(filter_type)} value for #{column}" end + def parse_value_type_error(column, filter_type) do - opts = struct(Inspect.Opts, []) - iodata = Inspect.Algebra.to_doc(column, opts) + opts = struct(Inspect.Opts, []) + + iodata = + Inspect.Algebra.to_doc(column, opts) |> Inspect.Algebra.format(opts.width) - |> Enum.join + |> Enum.join() if String.length(iodata) <= 15 do parse_value_type_error("'#{iodata}'", filter_type) else "'#{String.slice(iodata, 0..12)}...#{String.slice(iodata, -3..-1)}'" - |> parse_value_type_error(filter_type) + |> parse_value_type_error(filter_type) end end @@ -138,7 +157,7 @@ defmodule Filtrex.Condition do end defp condition_module(type) do - Enum.find(condition_modules(), fn (module) -> + Enum.find(condition_modules(), fn module -> type == to_string(module.type) end) end diff --git a/lib/filtrex/conditions/boolean.ex b/lib/filtrex/conditions/boolean.ex index 6014562..ab104df 100644 --- a/lib/filtrex/conditions/boolean.ex +++ b/lib/filtrex/conditions/boolean.ex @@ -24,8 +24,10 @@ defmodule Filtrex.Condition.Boolean do case condition do %Condition.Boolean{comparator: nil} -> {:error, parse_error(comparator, :comparator, :date)} + %Condition.Boolean{value: nil} -> {:error, parse_value_type_error(value, :boolean)} + _ -> {:ok, condition} end @@ -38,7 +40,7 @@ defmodule Filtrex.Condition.Boolean do defp validate_value(_), do: nil defimpl Filtrex.Encoder do - encoder "equals", "does not equal", "column = ?" - encoder "does not equal", "equals", "column != ?" + encoder("equals", "does not equal", "column = ?") + encoder("does not equal", "equals", "column != ?") end end diff --git a/lib/filtrex/conditions/date.ex b/lib/filtrex/conditions/date.ex index 2bc01d0..66ba97a 100644 --- a/lib/filtrex/conditions/date.ex +++ b/lib/filtrex/conditions/date.ex @@ -1,11 +1,19 @@ defmodule Filtrex.Condition.Date do use Filtrex.Condition use Timex - @string_date_comparators ["equals", "does not equal", "after", "on or after", "before", "on or before"] + + @string_date_comparators [ + "equals", + "does not equal", + "after", + "on or after", + "before", + "on or before" + ] @start_end_comparators ["between", "not between"] @comparators @string_date_comparators ++ @start_end_comparators - @type t :: Filtrex.Condition.Date.t + @type t :: Filtrex.Condition.Date.t() @moduledoc """ `Filtrex.Condition.Date` is a specific condition type for handling date filters with various comparisons. @@ -44,53 +52,58 @@ defmodule Filtrex.Condition.Date do def parse(config, %{column: column, comparator: comparator, value: value, inverse: inverse}) do with {:ok, parsed_comparator} <- validate_comparator(comparator), - {:ok, parsed_value} <- validate_value(config, parsed_comparator, value) do - {:ok, %Condition.Date{type: :date, inverse: inverse, - column: column, comparator: parsed_comparator, - value: parsed_value}} + {:ok, parsed_value} <- validate_value(config, parsed_comparator, value) do + {:ok, + %Condition.Date{ + type: :date, + inverse: inverse, + column: column, + comparator: parsed_comparator, + value: parsed_value + }} end end - defp validate_comparator(comparator) when comparator in @comparators, do: - {:ok, comparator} - defp validate_comparator(comparator), do: - {:error, parse_error(comparator, :comparator, :date)} + defp validate_comparator(comparator) when comparator in @comparators, do: {:ok, comparator} + defp validate_comparator(comparator), do: {:error, parse_error(comparator, :comparator, :date)} defp validate_value(config, comparator, value) do cond do comparator in @string_date_comparators -> Filtrex.Validator.Date.parse_string_date(config, value) + comparator in @start_end_comparators -> Filtrex.Validator.Date.parse_start_end(config, value) end end defimpl Filtrex.Encoder do - @format Filtrex.Validator.Date.format + @format Filtrex.Validator.Date.format() - encoder "after", "before", "column > ?", &default/1 - encoder "before", "after", "column < ?", &default/1 + encoder("after", "before", "column > ?", &default/1) + encoder("before", "after", "column < ?", &default/1) - encoder "on or after", "on or before", "column >= ?", &default/1 - encoder "on or before", "on or after", "column <= ?", &default/1 + encoder("on or after", "on or before", "column >= ?", &default/1) + encoder("on or before", "on or after", "column <= ?", &default/1) - encoder "between", "not between", "(column >= ?) AND (column <= ?)", fn - (%{start: start, end: end_value}) -> + encoder("between", "not between", "(column >= ?) AND (column <= ?)", fn + %{start: start, end: end_value} -> [default_value(start), default_value(end_value)] - end - encoder "not between", "between", "(column > ?) AND (column < ?)", fn - (%{start: start, end: end_value}) -> + end) + + encoder("not between", "between", "(column > ?) AND (column < ?)", fn + %{start: start, end: end_value} -> [default_value(end_value), default_value(start)] - end + end) - encoder "equals", "does not equal", "column = ?", &default/1 - encoder "does not equal", "equals", "column != ?", &default/1 + encoder("equals", "does not equal", "column = ?", &default/1) + encoder("does not equal", "equals", "column != ?", &default/1) defp default(timex_date) do {:ok, date} = Timex.format(timex_date, @format) [date] end - defp default_value(timex_date), do: default(timex_date) |> List.first + defp default_value(timex_date), do: default(timex_date) |> List.first() end end diff --git a/lib/filtrex/conditions/datetime.ex b/lib/filtrex/conditions/datetime.ex index afb84f5..e81080e 100644 --- a/lib/filtrex/conditions/datetime.ex +++ b/lib/filtrex/conditions/datetime.ex @@ -35,10 +35,15 @@ defmodule Filtrex.Condition.DateTime do def parse(config, %{column: column, comparator: comparator, value: value, inverse: inverse}) do with {:ok, parsed_comparator} <- validate_comparator(type(), comparator, @comparators), - {:ok, parsed_value} <- validate_value(config, value) do - {:ok, %__MODULE__{type: type(), inverse: inverse, - column: column, comparator: parsed_comparator, - value: parsed_value}} + {:ok, parsed_value} <- validate_value(config, value) do + {:ok, + %__MODULE__{ + type: type(), + inverse: inverse, + column: column, + comparator: parsed_comparator, + value: parsed_value + }} end end @@ -47,14 +52,14 @@ defmodule Filtrex.Condition.DateTime do end defimpl Filtrex.Encoder do - encoder "after", "before", "column > ?", &default/1 - encoder "before", "after", "column < ?", &default/1 + encoder("after", "before", "column > ?", &default/1) + encoder("before", "after", "column < ?", &default/1) - encoder "on or after", "on or before", "column >= ?", &default/1 - encoder "on or before", "on or after", "column <= ?", &default/1 + encoder("on or after", "on or before", "column >= ?", &default/1) + encoder("on or before", "on or after", "column <= ?", &default/1) - encoder "equals", "does not equal", "column = ?", &default/1 - encoder "does not equal", "equals", "column != ?", &default/1 + encoder("equals", "does not equal", "column = ?", &default/1) + encoder("does not equal", "equals", "column != ?", &default/1) defp default(timex_date) do {:ok, format} = Timex.format(timex_date, "{ISOdate} {ISOtime}") diff --git a/lib/filtrex/conditions/number.ex b/lib/filtrex/conditions/number.ex index 49dfe98..1a13a56 100644 --- a/lib/filtrex/conditions/number.ex +++ b/lib/filtrex/conditions/number.ex @@ -20,24 +20,34 @@ defmodule Filtrex.Condition.Number do def type, do: :number - def comparators, do: [ - "equals", "does not equal", - "greater than", "less than or", - "greater than or", "less than" - ] + def comparators, + do: [ + "equals", + "does not equal", + "greater than", + "less than or", + "greater than or", + "less than" + ] def parse(config, %{column: column, comparator: comparator, value: value, inverse: inverse}) do - result = with {:ok, parsed_value} <- parse_value(config.options, value), - do: %Condition.Number{type: type(), inverse: inverse, value: parsed_value, column: column, - comparator: validate_in(comparator, comparators())} + result = + with {:ok, parsed_value} <- parse_value(config.options, value), + do: %Condition.Number{ + type: type(), + inverse: inverse, + value: parsed_value, + column: column, + comparator: validate_in(comparator, comparators()) + } case result do {:error, error} -> {:error, error} + %Condition.Number{comparator: nil} -> {:error, parse_error(column, :comparator, type())} - %Condition.Number{value: nil} -> - {:error, parse_value_type_error(value, type())} + _ -> {:ok, result} end @@ -46,33 +56,39 @@ defmodule Filtrex.Condition.Number do defp parse_value(options = %{allow_decimal: true}, string) when is_binary(string) do case Float.parse(string) do {float, ""} -> parse_value(options, float) - _ -> {:error, parse_value_type_error(string, type())} + _ -> {:error, parse_value_type_error(string, type())} end end defp parse_value(options, string) when is_binary(string) do case Integer.parse(string) do {integer, ""} -> parse_value(options, integer) - _ -> {:error, parse_value_type_error(string, type())} + _ -> {:error, parse_value_type_error(string, type())} end end defp parse_value(options, float) when is_float(float) do allowed_values = options[:allowed_values] + cond do options[:allow_decimal] == false -> {:error, parse_value_type_error(float, type())} + allowed_values == nil -> {:ok, float} + Range.range?(allowed_values) -> start..final = allowed_values + if float >= start and float <= final do {:ok, float} else {:error, "Provided number value not allowed"} end + is_list(allowed_values) and float in allowed_values -> {:ok, float} + is_list(allowed_values) and float not in allowed_values -> {:error, "Provided number value not allowed"} end @@ -80,9 +96,11 @@ defmodule Filtrex.Condition.Number do defp parse_value(options, integer) when is_integer(integer) do allowed_values = options[:allowed_values] + cond do allowed_values == nil or integer in allowed_values -> {:ok, integer} + integer not in allowed_values -> {:error, "Provided number value not allowed"} end @@ -91,11 +109,11 @@ defmodule Filtrex.Condition.Number do defp parse_value(_, value), do: {:error, parse_value_type_error(value, type())} defimpl Filtrex.Encoder do - encoder "equals", "does not equal", "column = ?" - encoder "does not equal", "equals", "column != ?" - encoder "greater than", "less than or", "column > ?" - encoder "less than or", "greater than", "column <= ?" - encoder "less than", "greater than or", "column < ?" - encoder "greater than or", "less than", "column >= ?" + encoder("equals", "does not equal", "column = ?") + encoder("does not equal", "equals", "column != ?") + encoder("greater than", "less than or", "column > ?") + encoder("less than or", "greater than", "column <= ?") + encoder("less than", "greater than or", "column < ?") + encoder("greater than or", "less than", "column >= ?") end end diff --git a/lib/filtrex/conditions/text.ex b/lib/filtrex/conditions/text.ex index 97fecaf..8624d34 100644 --- a/lib/filtrex/conditions/text.ex +++ b/lib/filtrex/conditions/text.ex @@ -2,7 +2,7 @@ defmodule Filtrex.Condition.Text do use Filtrex.Condition @comparators ["equals", "does not equal", "contains", "does not contain"] - @type t :: Filtrex.Condition.Text.t + @type t :: Filtrex.Condition.Text.t() @moduledoc """ `Filtrex.Condition.Text` is a specific condition type for handling text filters with various comparisons. There are no configuration options for the date condition. @@ -35,21 +35,24 @@ defmodule Filtrex.Condition.Text do comparator: validate_in(comparator, @comparators), value: validate_is_binary(value) } + case condition do %Condition.Text{comparator: nil} -> {:error, parse_error(comparator, :comparator, :text)} + %Condition.Text{value: nil} -> {:error, parse_value_type_error(column, :text)} + _ -> {:ok, condition} end end defimpl Filtrex.Encoder do - encoder "equals", "does not equal", "column = ?" - encoder "does not equal", "equals", "column != ?" + encoder("equals", "does not equal", "column = ?") + encoder("does not equal", "equals", "column != ?") - encoder "contains", "does not contain", "lower(column) LIKE lower(?)", &(["%#{&1}%"]) - encoder "does not contain", "contains", "lower(column) NOT LIKE lower(?)", &(["%#{&1}%"]) + encoder("contains", "does not contain", "lower(column) LIKE lower(?)", &["%#{&1}%"]) + encoder("does not contain", "contains", "lower(column) NOT LIKE lower(?)", &["%#{&1}%"]) end end diff --git a/lib/filtrex/conditions/uuid.ex b/lib/filtrex/conditions/uuid.ex new file mode 100644 index 0000000..226e017 --- /dev/null +++ b/lib/filtrex/conditions/uuid.ex @@ -0,0 +1,69 @@ +defmodule Filtrex.Condition.UUID do + @moduledoc """ + Custom filter type for uuid columns. Supports "equals" and "contains" comparators. + """ + + use Filtrex.Condition + + @comparators ["equals", "does not equal", "contains", "does not contain"] + + @impl true + def type, do: :uuid + + @impl true + def comparators, do: @comparators + + @impl true + def parse(_config, %{column: column, comparator: comparator, value: value, inverse: inverse}) do + condition = %__MODULE__{ + type: :uuid, + inverse: inverse, + column: column, + comparator: validate_in(comparator, @comparators), + value: validate_is_binary(value) + } + + case condition do + %{comparator: nil} -> + {:error, parse_error(comparator, :comparator, :uuid)} + + %{value: nil} -> + {:error, parse_value_type_error(column, :uuid)} + + _ -> + {:ok, condition} + end + end + + alias Filtrex.Type.Config + + defmacro uuid(keys, opts \\ []) + + defmacro uuid(keys, opts) when is_list(keys) do + quote do + var!(configs) = + var!(configs) ++ + [ + %Filtrex.Type.Config{ + type: :uuid, + keys: Config.to_strings(unquote(keys)), + options: Enum.into(unquote(opts), %{}) + } + ] + end + end + + defmacro uuid(key, opts) do + quote do + uuid([to_string(unquote(key))], unquote(opts)) + end + end + + defimpl Filtrex.Encoder do + encoder("equals", "does not equal", "text(column) = ?") + encoder("does not equal", "equals", "text(column) != ?") + + encoder("contains", "does not contain", "text(column) ILIKE ?", &["%#{&1}%"]) + encoder("does not contain", "contains", "text(column) NOT ILIKE ?", &["%#{&1}%"]) + end +end diff --git a/lib/filtrex/config.ex b/lib/filtrex/config.ex index 8644f26..a33b8d3 100644 --- a/lib/filtrex/config.ex +++ b/lib/filtrex/config.ex @@ -61,21 +61,29 @@ defmodule Filtrex.Type.Config do end end - for module <- Filtrex.Condition.condition_modules do + for module <- Filtrex.Condition.condition_modules() do @doc "Generate a config struct for `#{to_string(module) |> String.slice(7..-1)}`" defmacro unquote(module.type)(key_or_keys, opts \\ []) + defmacro unquote(module.type)(keys, opts) when is_list(keys) do type = unquote(module.type) + quote do - var!(configs) = var!(configs) ++ - [%Filtrex.Type.Config{type: unquote(type), - keys: Filtrex.Type.Config.to_strings(unquote(keys)), - options: Enum.into(unquote(opts), %{})}] + var!(configs) = + var!(configs) ++ + [ + %Filtrex.Type.Config{ + type: unquote(type), + keys: Filtrex.Type.Config.to_strings(unquote(keys)), + options: Enum.into(unquote(opts), %{}) + } + ] end end defmacro unquote(module.type)(key, opts) do type = unquote(module.type) + quote do unquote(type)([to_string(unquote(key))], unquote(opts)) end @@ -84,11 +92,14 @@ defmodule Filtrex.Type.Config do @doc "Convert a list of mixed atoms and/or strings to a list of strings" def to_strings(keys, strings \\ []) + def to_strings([key | keys], strings) when is_atom(key) do to_strings(keys, strings ++ [to_string(key)]) end + def to_strings([key | keys], strings) when is_binary(key) do to_strings(keys, strings ++ [key]) end + def to_strings([], strings), do: strings end diff --git a/lib/filtrex/encoder.ex b/lib/filtrex/encoder.ex index fe924ed..db22a6f 100644 --- a/lib/filtrex/encoder.ex +++ b/lib/filtrex/encoder.ex @@ -15,6 +15,6 @@ defprotocol Filtrex.Encoder do """ @doc "The function that performs the encoding" - @spec encode(Filter.Condition.t) :: [String.t | [any]] + @spec encode(Filtrex.Condition.t()) :: %Filtrex.Fragment{} def encode(condition) end diff --git a/lib/filtrex/params.ex b/lib/filtrex/params.ex index b8de569..cf646da 100644 --- a/lib/filtrex/params.ex +++ b/lib/filtrex/params.ex @@ -13,34 +13,40 @@ defmodule Filtrex.Params do end defp sanitize_value(map, whitelist) when is_map(map) do - Enum.reduce_while(map, {:ok, %{}}, fn ({key, value}, {:ok, acc}) -> + Enum.reduce_while(map, {:ok, %{}}, fn {key, value}, {:ok, acc} -> cond do is_atom(key) -> case sanitize_value(value, whitelist) do {:ok, sanitized} -> {:cont, {:ok, Map.put(acc, key, sanitized)}} - error -> {:halt, error} + error -> {:halt, error} end + key in whitelist -> atom = String.to_existing_atom(key) + case sanitize_value(value, whitelist) do {:ok, sanitized} -> {:cont, {:ok, Map.put(acc, atom, sanitized)}} - error -> {:halt, error} + error -> {:halt, error} end + not is_binary(key) -> {:halt, {:error, "Invalid key. Only string keys are supported."}} + true -> {:halt, {:error, "Unknown key '#{key}'"}} end end) end + defp sanitize_value(list, whitelist) when is_list(list) do - Enum.reduce_while(list, {:ok, []}, fn (value, {:ok, acc}) -> + Enum.reduce_while(list, {:ok, []}, fn value, {:ok, acc} -> case sanitize_value(value, whitelist) do {:ok, sanitized} -> {:cont, {:ok, acc ++ [sanitized]}} - error -> {:halt, error} + error -> {:halt, error} end end) end + defp sanitize_value(value, _), do: {:ok, value} @doc "Converts parameters to a list of conditions" @@ -48,6 +54,7 @@ defmodule Filtrex.Params do Enum.reduce(params, {:ok, []}, fn {key, value}, {:ok, conditions} -> convert_and_add_condition(configs, key, value, conditions) + _, {:error, reason} -> {:error, reason} end) @@ -58,7 +65,9 @@ defmodule Filtrex.Params do {:ok, module, config, column, comparator} -> attrs = %{inverse: false, column: column, comparator: comparator, value: value} parse_and_add_condition(config, module, convert_value_in_attrs(attrs), conditions) - {:error, reason} -> {:error, reason} + + {:error, reason} -> + {:error, reason} end end @@ -77,8 +86,12 @@ defmodule Filtrex.Params do Enum.map(map, fn {key, value} when is_binary(key) -> {String.to_atom(key), value} - {key, value} -> {key, value} - end) |> Enum.into(%{}) + + {key, value} -> + {key, value} + end) + |> Enum.into(%{}) end + defp convert_value(value), do: value end diff --git a/lib/filtrex/utils/encoder.ex b/lib/filtrex/utils/encoder.ex index 8de6aee..3916263 100644 --- a/lib/filtrex/utils/encoder.ex +++ b/lib/filtrex/utils/encoder.ex @@ -18,7 +18,12 @@ defmodule Filtrex.Utils.Encoder do raw value being passed in and returns the transformed value to be injected as a value into the fragment expression. """ - defmacro encoder(comparator, reverse_comparator, expression, values_function \\ {:&, [], [[{:&, [], [1]}]]}) do + defmacro encoder( + comparator, + reverse_comparator, + expression, + values_function \\ {:&, [], [[{:&, [], [1]}]]} + ) do quote do import Filtrex.Utils.Encoder @@ -27,7 +32,7 @@ defmodule Filtrex.Utils.Encoder do end def encode(%{column: column, comparator: unquote(comparator), value: value}) do - values = + values = unquote(values_function).(value) |> intersperse_column_refs(column) @@ -52,7 +57,7 @@ defmodule Filtrex.Utils.Encoder do # => [s.title, "best", s.title, "post"] ## Background - + Ecto queries support string query fragments, but fields referenced in these fragments need to specifically reference fields, or you will get "Ambiguous column" errors for some queries. @@ -73,11 +78,11 @@ defmodule Filtrex.Utils.Encoder do def intersperse_column_refs(values, column) do column = String.to_existing_atom(column) - [quote do: s.unquote(column)] - |> Stream.cycle + [quote(do: s.unquote(column))] + |> Stream.cycle() |> Enum.take(length(values)) |> Enum.zip(values) |> Enum.map(&Tuple.to_list/1) - |> List.flatten + |> List.flatten() end end diff --git a/lib/filtrex/validators/date.ex b/lib/filtrex/validators/date.ex index b2f2519..6ce87eb 100644 --- a/lib/filtrex/validators/date.ex +++ b/lib/filtrex/validators/date.ex @@ -10,19 +10,24 @@ defmodule Filtrex.Validator.Date do def parse_string_date(config, value) when is_binary(value) do parse_format(config, value) end + def parse_string_date(config, value) do {:error, parse_value_type_error(value, config.type)} end - def parse_start_end(config, %{start: start, end: end_value}) do case {parse_format(config, start), parse_format(config, end_value)} do {{:ok, start}, {:ok, end_value}} -> {:ok, %{start: start, end: end_value}} - {{:error, error}, _} -> {:error, error} - {_, {:error, error}} -> {:error, error} + + {{:error, error}, _} -> + {:error, error} + + {_, {:error, error}} -> + {:error, error} end end + def parse_start_end(_, _) do {:error, wrap_specific_error("Both a start and end key are required.")} end @@ -32,11 +37,14 @@ defmodule Filtrex.Validator.Date do end defp parse_format(config, value) do - result = with {:ok, datetime} <- TimexParser.parse(value, config.options[:format] || @format), - {:ok, date} <- Timex.to_date(datetime), do: date + result = + with {:ok, datetime} <- TimexParser.parse(value, config.options[:format] || @format), + {:ok, date} <- Timex.to_date(datetime), + do: date + case result do {:error, error} -> {:error, wrap_specific_error(error)} - date -> {:ok, date} + date -> {:ok, date} end end end diff --git a/mix.exs b/mix.exs index b99c834..14e7858 100644 --- a/mix.exs +++ b/mix.exs @@ -4,14 +4,15 @@ defmodule Filtrex.Mixfile do def project do [ app: :filtrex, - version: "0.4.3", - elixir: "~> 1.6", + version: "0.5.0", + elixir: "~> 1.13.4", description: description(), package: package(), build_embedded: Mix.env() == :prod, start_permanent: Mix.env() == :prod, elixirc_paths: elixirc_paths(Mix.env()), deps: deps(), + dialyzer: dialyzer(), name: "Filtrex", docs: [main: "Filtrex", source_url: "https://github.com/rcdilorenzo/filtrex"] ] @@ -44,7 +45,8 @@ defmodule Filtrex.Mixfile do {:inch_ex, ">= 0.0.0", only: [:dev, :docs]}, {:plug, "~> 1.1.2", only: :test}, {:ex_machina, "~> 0.6.1", only: :test}, - {:mix_test_watch, "~> 0.3", only: :dev, runtime: false} + {:mix_test_watch, "~> 0.3", only: :dev, runtime: false}, + {:dialyxir, "~> 1.2.0", only: [:dev], runtime: false} ] end @@ -58,4 +60,12 @@ defmodule Filtrex.Mixfile do } ] end + + defp dialyzer do + [ + plt_add_deps: :app_tree, + plt_add_apps: [:ex_unit, :mix], + plt_file: {:no_warn, "priv/plts/dialyzer.plt"} + ] + end end diff --git a/mix.lock b/mix.lock index 5c75c83..29104d0 100644 --- a/mix.lock +++ b/mix.lock @@ -1,28 +1,30 @@ %{ - "certifi": {:hex, :certifi, "0.7.0", "861a57f3808f7eb0c2d1802afeaae0fa5de813b0df0979153cbafcd853ababaf", [:rebar3], [], "hexpm"}, - "combine": {:hex, :combine, "0.9.6", "8d1034a127d4cbf6924c8a5010d3534d958085575fa4d9b878f200d79ac78335", [:mix], [], "hexpm"}, - "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, - "db_connection": {:hex, :db_connection, "2.0.3", "b4e8aa43c100e16f122ccd6798cd51c48c79fd391c39d411f42b3cd765daccb0", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"}, - "decimal": {:hex, :decimal, "1.6.0", "bfd84d90ff966e1f5d4370bdd3943432d8f65f07d3bab48001aebd7030590dcc", [:mix], [], "hexpm"}, - "earmark": {:hex, :earmark, "0.2.1", "ba6d26ceb16106d069b289df66751734802777a3cbb6787026dd800ffeb850f3", [:mix], [], "hexpm"}, - "ecto": {:hex, :ecto, "3.0.4", "5d0e2b89baaa03eac37ec49f9018c39a4e2fb6501dc3ff5a839de742e171a09f", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, - "ecto_sql": {:hex, :ecto_sql, "3.0.3", "dd17f2401a69bb2ec91d5564bd259ad0bc63ee32c2cb2e616d04f1559801dba6", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.2.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, - "ex_doc": {:hex, :ex_doc, "0.12.0", "b774aabfede4af31c0301aece12371cbd25995a21bb3d71d66f5c2fe074c603f", [:mix], [{:earmark, "~> 0.2", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, - "ex_machina": {:hex, :ex_machina, "0.6.2", "2d25802d269b21ecb3df478c3609f3b162ef6d1c952d75770e0969f8971611de", [:mix], [], "hexpm"}, - "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], [], "hexpm"}, - "gettext": {:hex, :gettext, "0.13.0", "daafbddc5cda12738bb93b01d84105fe75b916a302f1c50ab9fb066b95ec9db4", [:mix], [], "hexpm"}, - "hackney": {:hex, :hackney, "1.6.3", "d489d7ca2d4323e307bedc4bfe684323a7bf773ecfd77938f3ee8074e488e140", [:mix, :rebar3], [{:certifi, "0.7.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "1.2.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, - "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], [], "hexpm"}, - "inch_ex": {:hex, :inch_ex, "0.5.5", "b63f57e281467bd3456461525fdbc9e158c8edbe603da6e3e4671befde796a3d", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, - "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, - "mix_test_watch": {:hex, :mix_test_watch, "0.4.1", "a98a84c795623f1ba020324f4354cf30e7120ba4dab65f9c2ae300f830a25f75", [:mix], [{:fs, "~> 0.9.1", [hex: :fs, repo: "hexpm", optional: false]}], "hexpm"}, - "plug": {:hex, :plug, "1.1.6", "8927e4028433fcb859e000b9389ee9c37c80eb28378eeeea31b0273350bf668b", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}], "hexpm"}, - "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, + "certifi": {:hex, :certifi, "0.7.0", "861a57f3808f7eb0c2d1802afeaae0fa5de813b0df0979153cbafcd853ababaf", [:rebar3], [], "hexpm", "f7182e85b4ece9d1371c46699793dd3dee8f2c55be3f6967a6b84b8c02bab7d2"}, + "combine": {:hex, :combine, "0.9.6", "8d1034a127d4cbf6924c8a5010d3534d958085575fa4d9b878f200d79ac78335", [:mix], [], "hexpm", "0b450698443dc9ab84cee85976752b4af1009cdf0f01da9ee8ef2550dc67c47f"}, + "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"}, + "db_connection": {:hex, :db_connection, "2.0.3", "b4e8aa43c100e16f122ccd6798cd51c48c79fd391c39d411f42b3cd765daccb0", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "1e123d79599809ca3775fd71b40574f3592a2acd85d3c41d49f5843426021f9b"}, + "decimal": {:hex, :decimal, "1.6.0", "bfd84d90ff966e1f5d4370bdd3943432d8f65f07d3bab48001aebd7030590dcc", [:mix], [], "hexpm", "bbd124e240e3ff40f407d50fced3736049e72a73d547f69201484d3a624ab569"}, + "dialyxir": {:hex, :dialyxir, "1.2.0", "58344b3e87c2e7095304c81a9ae65cb68b613e28340690dfe1a5597fd08dec37", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "61072136427a851674cab81762be4dbeae7679f85b1272b6d25c3a839aff8463"}, + "earmark": {:hex, :earmark, "0.2.1", "ba6d26ceb16106d069b289df66751734802777a3cbb6787026dd800ffeb850f3", [:mix], [], "hexpm", "c86afb8d22a5aa8315afd4257c7512011c0c9a48b0fea43af7612836b958098b"}, + "ecto": {:hex, :ecto, "3.0.4", "5d0e2b89baaa03eac37ec49f9018c39a4e2fb6501dc3ff5a839de742e171a09f", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "4f4d8d856ac3cc785015accc705d0e6eaf83cb48a27bd7b312a17bb0a68a0e85"}, + "ecto_sql": {:hex, :ecto_sql, "3.0.3", "dd17f2401a69bb2ec91d5564bd259ad0bc63ee32c2cb2e616d04f1559801dba6", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.2.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "27880891cf51c6a612e54782b274bdfcc3fd4e572cb64e4a97db311030bb6ae4"}, + "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "ex_doc": {:hex, :ex_doc, "0.12.0", "b774aabfede4af31c0301aece12371cbd25995a21bb3d71d66f5c2fe074c603f", [:mix], [{:earmark, "~> 0.2", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm", "76a04c6bb6876df8eae8ff8bf34fb1004bbead5172d31c9dfd71c4e3aed011b7"}, + "ex_machina": {:hex, :ex_machina, "0.6.2", "2d25802d269b21ecb3df478c3609f3b162ef6d1c952d75770e0969f8971611de", [:mix], [], "hexpm", "50cdca009ac7b18fde504d86358de97a2abf281433766cc3cb5100e244ae34bd"}, + "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], [], "hexpm", "9a00246e8af58cdf465ae7c48fd6fd7ba2e43300413dfcc25447ecd3bf76f0c1"}, + "gettext": {:hex, :gettext, "0.13.0", "daafbddc5cda12738bb93b01d84105fe75b916a302f1c50ab9fb066b95ec9db4", [:mix], [], "hexpm", "737c9b718f26cf8da15df9297a624b3420849c1a28f5a60251d0ee4d57a4e9c9"}, + "hackney": {:hex, :hackney, "1.6.3", "d489d7ca2d4323e307bedc4bfe684323a7bf773ecfd77938f3ee8074e488e140", [:mix, :rebar3], [{:certifi, "0.7.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "1.2.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "b2c483bc28ca6fd02b15a23e98156757b7de0dc1863427b058f46f1ad6c5cc4c"}, + "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], [], "hexpm", "1d724cdafb66397e61774ead242c9b725de7033cde8ea98fa4a91e64ac5ef5b3"}, + "inch_ex": {:hex, :inch_ex, "0.5.5", "b63f57e281467bd3456461525fdbc9e158c8edbe603da6e3e4671befde796a3d", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "9c2e35eb4189db5fe3448d1e2b98b0802a3e83a63e39e137c04d09ef1450f636"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm", "7a4c8e1115a2732a67d7624e28cf6c9f30c66711a9e92928e745c255887ba465"}, + "mix_test_watch": {:hex, :mix_test_watch, "0.4.1", "a98a84c795623f1ba020324f4354cf30e7120ba4dab65f9c2ae300f830a25f75", [:mix], [{:fs, "~> 0.9.1", [hex: :fs, repo: "hexpm", optional: false]}], "hexpm", "2b8d732595cf29b960328744fb46f5592d4b2605ce4f8bebdd12d0ab57d35a41"}, + "plug": {:hex, :plug, "1.1.6", "8927e4028433fcb859e000b9389ee9c37c80eb28378eeeea31b0273350bf668b", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}], "hexpm", "b023b28e64b7b1020829fc3afc9c95f9f134ce705da2d06da107e3cfa45cf9a4"}, + "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"}, "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"}, - "postgrex": {:hex, :postgrex, "0.14.1", "63247d4a5ad6b9de57a0bac5d807e1c32d41e39c04b8a4156a26c63bcd8a2e49", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, - "telemetry": {:hex, :telemetry, "0.2.0", "5b40caa3efe4deb30fb12d7cd8ed4f556f6d6bd15c374c2366772161311ce377", [:mix], [], "hexpm"}, - "timex": {:hex, :timex, "3.1.7", "71f9c32e13ff4860e86a314303757cc02b3ead5db6e977579a2935225ce9a666", [:mix], [{:combine, "~> 0.7", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, - "tzdata": {:hex, :tzdata, "0.5.10", "087e8dfe8c0283473115ad8ca6974b898ecb55ca5c725427a142a79593391e90", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "postgrex": {:hex, :postgrex, "0.14.1", "63247d4a5ad6b9de57a0bac5d807e1c32d41e39c04b8a4156a26c63bcd8a2e49", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "a20f189bdd5a219c484818fde18e09ace20cd15fe630a828fde70bd6efdeb23b"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm", "4f8805eb5c8a939cf2359367cb651a3180b27dfb48444846be2613d79355d65e"}, + "telemetry": {:hex, :telemetry, "0.2.0", "5b40caa3efe4deb30fb12d7cd8ed4f556f6d6bd15c374c2366772161311ce377", [:mix], [], "hexpm", "4e9071b8d1795d0f1ae00584594c3faf430c88821b69e4bd09b02e7840231f32"}, + "timex": {:hex, :timex, "3.1.7", "71f9c32e13ff4860e86a314303757cc02b3ead5db6e977579a2935225ce9a666", [:mix], [{:combine, "~> 0.7", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "67aa24c166919e49a5c3a0d8c30b235a0e36f8af6cd4cfb07612c40c39826aa4"}, + "tzdata": {:hex, :tzdata, "0.5.10", "087e8dfe8c0283473115ad8ca6974b898ecb55ca5c725427a142a79593391e90", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "01e0f97173abb274a49f95167fbcdbda3371e338bd9bcf520ccd0e59fc1d63ee"}, } diff --git a/priv/plts/dialyzer.plt b/priv/plts/dialyzer.plt new file mode 100644 index 0000000..8496d3a Binary files /dev/null and b/priv/plts/dialyzer.plt differ diff --git a/priv/plts/dialyzer.plt.hash b/priv/plts/dialyzer.plt.hash new file mode 100644 index 0000000..6db446e --- /dev/null +++ b/priv/plts/dialyzer.plt.hash @@ -0,0 +1 @@ +CAV2.¬%”h‹ó_øη¹[o \ No newline at end of file diff --git a/test/ast_test.exs b/test/ast_test.exs index 0dad544..bb69374 100644 --- a/test/ast_test.exs +++ b/test/ast_test.exs @@ -2,22 +2,42 @@ defmodule FiltrexASTTest do use ExUnit.Case use Timex - @filter %Filtrex{type: "any", conditions: [ - %Filtrex.Condition.Text{column: "title", comparator: "contains", value: "created"}, - %Filtrex.Condition.Text{column: "title", comparator: "does not equal", value: "Chris McCord"} - ], sub_filters: [ - %Filtrex{type: "all", conditions: [ - %Filtrex.Condition.Date{column: "date_column", comparator: "after", value: Timex.to_date({2016, 5, 1})}, - %Filtrex.Condition.Date{column: "date_column", comparator: "before", value: Timex.to_date({2017, 1, 1})} - ]} - ]} + @filter %Filtrex{ + type: "any", + conditions: [ + %Filtrex.Condition.Text{column: "title", comparator: "contains", value: "created"}, + %Filtrex.Condition.Text{ + column: "title", + comparator: "does not equal", + value: "Chris McCord" + } + ], + sub_filters: [ + %Filtrex{ + type: "all", + conditions: [ + %Filtrex.Condition.Date{ + column: "date_column", + comparator: "after", + value: Timex.to_date({2016, 5, 1}) + }, + %Filtrex.Condition.Date{ + column: "date_column", + comparator: "before", + value: Timex.to_date({2017, 1, 1}) + } + ] + } + ] + } test "building an ecto query expression" do ast = Filtrex.AST.build_query(Filtrex.SampleModel, @filter) expression = Macro.to_string(quote do: unquote(ast)) + assert with_newline(expression) == """ - Ecto.Query.where(Filtrex.SampleModel, [s], fragment("((lower(?) LIKE lower(?)) OR (? != ?)) OR ((? > ?) AND (? < ?))", s.title(), "%created%", s.title(), "Chris McCord", s.date_column(), "2016-05-01", s.date_column(), "2017-01-01")) - """ + Ecto.Query.where(Filtrex.SampleModel, [s], fragment("((lower(?) LIKE lower(?)) OR (? != ?)) OR ((? > ?) AND (? < ?))", s.title(), "%created%", s.title(), "Chris McCord", s.date_column(), "2016-05-01", s.date_column(), "2017-01-01")) + """ end defp with_newline(string), do: "#{string}\n" diff --git a/test/condition_test.exs b/test/condition_test.exs index 19c7257..9dd5f1f 100644 --- a/test/condition_test.exs +++ b/test/condition_test.exs @@ -1,37 +1,39 @@ defmodule FiltrexConditionTest do use ExUnit.Case - @config Filtrex.SampleModel.filtrex_config + @config Filtrex.SampleModel.filtrex_config() @text_config %Filtrex.Type.Config{keys: ["title"], options: %{}, type: :text} @date_config %Filtrex.Type.Config{keys: ["date_column"], options: %{}, type: :date} test "finding the right type of condition" do - {:ok, condition} = Filtrex.Condition.parse(@config, %{ - inverse: false, - type: "text", - column: "title", - value: "Buy Milk", - comparator: "equals" - }) + {:ok, condition} = + Filtrex.Condition.parse(@config, %{ + inverse: false, + type: "text", + column: "title", + value: "Buy Milk", + comparator: "equals" + }) + assert condition.__struct__ == Filtrex.Condition.Text end test "determining whether params key matches" do assert Filtrex.Condition.param_key_type(@config, "title_contains") == - {:ok, Filtrex.Condition.Text, @text_config, "title", "contains"} + {:ok, Filtrex.Condition.Text, @text_config, "title", "contains"} assert Filtrex.Condition.param_key_type(@config, "date_column_on_or_after") == - {:ok, Filtrex.Condition.Date, @date_config, "date_column", "on or after"} + {:ok, Filtrex.Condition.Date, @date_config, "date_column", "on or after"} assert Filtrex.Condition.param_key_type(@config, "completed_on_or_after") == - {:error, "Unknown filter key 'completed_on_or_after'"} + {:error, "Unknown filter key 'completed_on_or_after'"} assert Filtrex.Condition.param_key_type(@config, "date_column_contains") == - {:error, "Unknown filter key 'date_column_contains'"} + {:error, "Unknown filter key 'date_column_contains'"} end test "defaulting to certain comparator when none is present in params" do assert Filtrex.Condition.param_key_type(@config, "title") == - {:ok, Filtrex.Condition.Text, @text_config, "title", "equals"} + {:ok, Filtrex.Condition.Text, @text_config, "title", "equals"} end end diff --git a/test/conditions/boolean_test.exs b/test/conditions/boolean_test.exs index be46872..7f3b079 100644 --- a/test/conditions/boolean_test.exs +++ b/test/conditions/boolean_test.exs @@ -8,48 +8,52 @@ defmodule FiltrexConditionBooleanTest do test "parsing true condition" do assert Boolean.parse(@config, params("true")) == - {:ok, condition(true)} + {:ok, condition(true)} + assert Boolean.parse(@config, params(true)) == - {:ok, condition(true)} + {:ok, condition(true)} end test "parsing false/empty condition" do assert Boolean.parse(@config, params("")) == - {:ok, condition(false)} + {:ok, condition(false)} + assert Boolean.parse(@config, params("false")) == - {:ok, condition(false)} + {:ok, condition(false)} end test "throwing error for non-boolean value" do assert Boolean.parse(@config, params("blah")) == - {:error, "Invalid boolean value for blah"} + {:error, "Invalid boolean value for blah"} end test "encoding true value" do assert Filtrex.Encoder.encode(condition(true, "equals")) == - %Filtrex.Fragment{expression: "? = ?", values: [column_ref(:flag), true]} + %Filtrex.Fragment{expression: "? = ?", values: [column_ref(:flag), true]} assert Filtrex.Encoder.encode(condition(true, "does not equal")) == - %Filtrex.Fragment{expression: "? != ?", values: [column_ref(:flag), true]} + %Filtrex.Fragment{expression: "? != ?", values: [column_ref(:flag), true]} end test "encoding false value" do assert Filtrex.Encoder.encode(condition(false, "equals")) == - %Filtrex.Fragment{expression: "? = ?", values: [column_ref(:flag), false]} + %Filtrex.Fragment{expression: "? = ?", values: [column_ref(:flag), false]} assert Filtrex.Encoder.encode(condition(false, "does not equal")) == - %Filtrex.Fragment{expression: "? != ?", values: [column_ref(:flag), false]} + %Filtrex.Fragment{expression: "? != ?", values: [column_ref(:flag), false]} end defp params(value) do - %{inverse: false, - column: @column, - value: value, - comparator: "equals"} + %{inverse: false, column: @column, value: value, comparator: "equals"} end defp condition(value, comparator \\ "equals") do - %Boolean{type: :boolean, column: @column, - inverse: false, comparator: comparator, value: value} + %Boolean{ + type: :boolean, + column: @column, + inverse: false, + comparator: comparator, + value: value + } end end diff --git a/test/conditions/date_test.exs b/test/conditions/date_test.exs index f3c58a0..92832be 100644 --- a/test/conditions/date_test.exs +++ b/test/conditions/date_test.exs @@ -11,76 +11,103 @@ defmodule FiltrexConditionDateTest do test "parsing errors with binary date format" do assert Date.parse(@config, %{ - inverse: false, - column: @column, - value: "2015-09-34", - comparator: "after" - }) == {:error, "Invalid date value format: Expected `day of month` at line 1, column 9."} + inverse: false, + column: @column, + value: "2015-09-34", + comparator: "after" + }) == + {:error, "Invalid date value format: Expected `day of month` at line 1, column 9."} assert Date.parse(@config, %{ - inverse: false, - column: @column, - value: %{start: "2015-03-01"}, - comparator: "after" - }) == {:error, "Invalid date value for '%{start: \"201...1\"}'"} + inverse: false, + column: @column, + value: %{start: "2015-03-01"}, + comparator: "after" + }) == {:error, "Invalid date value for '%{start: \"201...1\"}'"} end test "parsing errors with start/end date format" do assert Date.parse(@config, %{ - inverse: false, - column: @column, - value: %{start: "2015-03-01"}, - comparator: "between" - }) == {:error, "Invalid date value format: Both a start and end key are required."} + inverse: false, + column: @column, + value: %{start: "2015-03-01"}, + comparator: "between" + }) == {:error, "Invalid date value format: Both a start and end key are required."} assert Date.parse(@config, %{ - inverse: false, - column: @column, - value: %{start: "2015-03-01", end: "2015-13-21"}, - comparator: "between" - }) == {:error, "Invalid date value format: Expected `1-2 digit month` at line 1, column 6."} + inverse: false, + column: @column, + value: %{start: "2015-03-01", end: "2015-13-21"}, + comparator: "between" + }) == + {:error, + "Invalid date value format: Expected `1-2 digit month` at line 1, column 6."} end test "specifying different date formats" do assert Date.parse(@options_config, %{ - inverse: false, - column: @column, - value: "12-29-2016", - comparator: "after" - }) == {:ok, %Filtrex.Condition.Date{column: "date_column", comparator: "after", - inverse: false, type: :date, value: Timex.to_date({2016, 12, 29})}} + inverse: false, + column: @column, + value: "12-29-2016", + comparator: "after" + }) == + {:ok, + %Filtrex.Condition.Date{ + column: "date_column", + comparator: "after", + inverse: false, + type: :date, + value: Timex.to_date({2016, 12, 29}) + }} end test "'equals' comparator" do assert Date.parse(@config, %{ - inverse: false, - column: @column, - value: "2016-05-18", - comparator: "equals" - }) |> elem(0) == :ok + inverse: false, + column: @column, + value: "2016-05-18", + comparator: "equals" + }) + |> elem(0) == :ok end test "encoding as SQL fragments for ecto" do - assert encode(Date, @column, @default, "after") == {"? > ?", [column_ref(:date_column), @default]} - assert encode(Date, @column, @default, "on or after") == {"? >= ?", [column_ref(:date_column), @default]} - assert encode(Date, @column, @default, "before") == {"? < ?", [column_ref(:date_column), @default]} - assert encode(Date, @column, @default, "on or before") == {"? <= ?", [column_ref(:date_column), @default]} + assert encode(Date, @column, @default, "after") == + {"? > ?", [column_ref(:date_column), @default]} + + assert encode(Date, @column, @default, "on or after") == + {"? >= ?", [column_ref(:date_column), @default]} + + assert encode(Date, @column, @default, "before") == + {"? < ?", [column_ref(:date_column), @default]} + + assert encode(Date, @column, @default, "on or before") == + {"? <= ?", [column_ref(:date_column), @default]} assert encode(Date, @column, %{start: @default, end: "2015-12-31"}, "between") == - {"(? >= ?) AND (? <= ?)", [column_ref(:date_column), @default, column_ref(:date_column), "2015-12-31"]} + {"(? >= ?) AND (? <= ?)", + [column_ref(:date_column), @default, column_ref(:date_column), "2015-12-31"]} assert encode(Date, @column, %{start: @default, end: "2015-12-31"}, "not between") == - {"(? > ?) AND (? < ?)", [column_ref(:date_column), "2015-12-31", column_ref(:date_column), @default]} + {"(? > ?) AND (? < ?)", + [column_ref(:date_column), "2015-12-31", column_ref(:date_column), @default]} assert encode(Date, @column, "2016-03-01", "equals") == - {"? = ?", [column_ref(:date_column), "2016-03-01"]} + {"? = ?", [column_ref(:date_column), "2016-03-01"]} assert encode(Date, @column, "2016-03-01", "does not equal") == - {"? != ?", [column_ref(:date_column), "2016-03-01"]} + {"? != ?", [column_ref(:date_column), "2016-03-01"]} end defp encode(module, column, value, comparator) do - {:ok, condition} = module.parse(@config, %{inverse: false, column: column, value: value, comparator: comparator}) + {:ok, condition} = + module.parse(@config, %{ + inverse: false, + column: column, + value: value, + comparator: comparator + }) + encoded = Filtrex.Encoder.encode(condition) {encoded.expression, encoded.values} end diff --git a/test/conditions/datetime_test.exs b/test/conditions/datetime_test.exs index 0a51527..dfb8896 100644 --- a/test/conditions/datetime_test.exs +++ b/test/conditions/datetime_test.exs @@ -15,50 +15,90 @@ defmodule FiltrexConditionDateTimeTest do test "parsing with default format" do assert DateTime.parse(@config, %{ - inverse: false, - column: @column, - value: @default, - comparator: "after" - }) == {:ok, %Filtrex.Condition.DateTime{column: @column, comparator: "after", - inverse: false, type: :datetime, value: %Elixir.DateTime{calendar: Calendar.ISO, day: 1, hour: 12, - minute: 30, month: 4, second: 45, - std_offset: 0, time_zone: "Etc/UTC", - utc_offset: 0, year: 2016, zone_abbr: "UTC", - microsecond: {0, 3}}}} + inverse: false, + column: @column, + value: @default, + comparator: "after" + }) == + {:ok, + %Filtrex.Condition.DateTime{ + column: @column, + comparator: "after", + inverse: false, + type: :datetime, + value: %Elixir.DateTime{ + calendar: Calendar.ISO, + day: 1, + hour: 12, + minute: 30, + month: 4, + second: 45, + std_offset: 0, + time_zone: "Etc/UTC", + utc_offset: 0, + year: 2016, + zone_abbr: "UTC", + microsecond: {0, 3} + } + }} end test "parsing with custom format" do assert DateTime.parse(@options_config, %{ - inverse: false, - column: @column, - value: @options_default, - comparator: "after" - }) == {:ok, %Filtrex.Condition.DateTime{column: @column, comparator: "after", - inverse: false, type: :datetime, value: Timex.to_datetime({{2016, 4, 18}, {13, 30, 45}}, "GMT")}} + inverse: false, + column: @column, + value: @options_default, + comparator: "after" + }) == + {:ok, + %Filtrex.Condition.DateTime{ + column: @column, + comparator: "after", + inverse: false, + type: :datetime, + value: Timex.to_datetime({{2016, 4, 18}, {13, 30, 45}}, "GMT") + }} end test "parsing with invalid format" do assert DateTime.parse(@config, %{ - inverse: false, - column: @column, - value: @options_default, - comparator: "after" - }) |> elem(0) == :error + inverse: false, + column: @column, + value: @options_default, + comparator: "after" + }) + |> elem(0) == :error end test "encoding as SQL fragments for ecto" do - assert encode(DateTime, @column, @default, "after") == {"? > ?", [column_ref(:datetime_column), @default_converted]} - assert encode(DateTime, @column, @default, "on or after") == {"? >= ?", [column_ref(:datetime_column), @default_converted]} + assert encode(DateTime, @column, @default, "after") == + {"? > ?", [column_ref(:datetime_column), @default_converted]} - assert encode(DateTime, @column, @default, "before") == {"? < ?", [column_ref(:datetime_column), @default_converted]} - assert encode(DateTime, @column, @default, "on or before") == {"? <= ?", [column_ref(:datetime_column), @default_converted]} + assert encode(DateTime, @column, @default, "on or after") == + {"? >= ?", [column_ref(:datetime_column), @default_converted]} - assert encode(DateTime, @column, @default, "equals") == {"? = ?", [column_ref(:datetime_column), @default_converted]} - assert encode(DateTime, @column, @default, "does not equal") == {"? != ?", [column_ref(:datetime_column), @default_converted]} + assert encode(DateTime, @column, @default, "before") == + {"? < ?", [column_ref(:datetime_column), @default_converted]} + + assert encode(DateTime, @column, @default, "on or before") == + {"? <= ?", [column_ref(:datetime_column), @default_converted]} + + assert encode(DateTime, @column, @default, "equals") == + {"? = ?", [column_ref(:datetime_column), @default_converted]} + + assert encode(DateTime, @column, @default, "does not equal") == + {"? != ?", [column_ref(:datetime_column), @default_converted]} end defp encode(module, column, value, comparator) do - {:ok, condition} = module.parse(@config, %{inverse: false, column: column, value: value, comparator: comparator}) + {:ok, condition} = + module.parse(@config, %{ + inverse: false, + column: column, + value: value, + comparator: comparator + }) + encoded = Filtrex.Encoder.encode(condition) {encoded.expression, encoded.values} end diff --git a/test/conditions/number_test.exs b/test/conditions/number_test.exs index 4fa3925..d191873 100644 --- a/test/conditions/number_test.exs +++ b/test/conditions/number_test.exs @@ -4,70 +4,68 @@ defmodule FiltrexConditionNumberTest do alias Filtrex.Condition.Number @column "age" - @config %Filtrex.Type.Config{type: :number, keys: [@column], - options: %{allowed_values: 1..100, allow_decimal: true}} + @config %Filtrex.Type.Config{ + type: :number, + keys: [@column], + options: %{allowed_values: 1..100, allow_decimal: true} + } test "parsing integer successfully" do assert Number.parse(@config, params("equals", "10")) == - {:ok, condition("equals", 10)} + {:ok, condition("equals", 10)} assert Number.parse(@config, params("equals", "1")) == - {:ok, condition("equals", 1)} + {:ok, condition("equals", 1)} assert Number.parse(@config, params("equals", 5)) == - {:ok, condition("equals", 5)} + {:ok, condition("equals", 5)} assert Number.parse(put_in(@config.options[:allow_decimal], false), params("equals", 5)) == - {:ok, condition("equals", 5)} + {:ok, condition("equals", 5)} end test "parsing float successfully" do assert Number.parse(@config, params("equals", "3.5")) == - {:ok, condition("equals", 3.5)} + {:ok, condition("equals", 3.5)} end test "parsing number errors" do assert Number.parse(@config, params("equals", "blah")) == - {:error, "Invalid number value for blah"} + {:error, "Invalid number value for blah"} assert Number.parse(@config, params("equals", "")) == - {:error, "Invalid number value for "} + {:error, "Invalid number value for "} assert Number.parse(@config, params("equals", nil)) == - {:error, "Invalid number value for 'nil'"} - + {:error, "Invalid number value for 'nil'"} assert Number.parse(put_in(@config.options[:allow_decimal], false), params("equals", "10.5")) == - {:error, "Invalid number value for 10.5"} + {:error, "Invalid number value for 10.5"} end test "validating range of allowed integer values" do assert Number.parse(@config, params("equals", "101")) == - {:error, "Provided number value not allowed"} + {:error, "Provided number value not allowed"} assert Number.parse(@config, params("equals", "-1")) == - {:error, "Provided number value not allowed"} + {:error, "Provided number value not allowed"} end test "validating range of allowed float values" do assert Number.parse(@config, params("equals", "100.5")) == - {:error, "Provided number value not allowed"} + {:error, "Provided number value not allowed"} end test "encoding 'greater than'" do assert Filtrex.Encoder.encode(condition("greater than", 10)) == - %Filtrex.Fragment{expression: "? > ?", values: [column_ref(:age), 10]} + %Filtrex.Fragment{expression: "? > ?", values: [column_ref(:age), 10]} end defp params(comparator, value) do - %{inverse: false, - column: @column, - value: value, - comparator: comparator} + %{inverse: false, column: @column, value: value, comparator: comparator} end defp condition(comparator, value) do - %Number{type: :number, column: @column, - inverse: false, comparator: comparator, value: value} + %Number{type: :number, column: @column, inverse: false, comparator: comparator, value: value} end end diff --git a/test/conditions/text_test.exs b/test/conditions/text_test.exs index f812c41..cf177e7 100644 --- a/test/conditions/text_test.exs +++ b/test/conditions/text_test.exs @@ -3,38 +3,69 @@ defmodule FiltrexConditionTextTest do import Filtrex.TestHelpers alias Filtrex.Condition.Text - @config Filtrex.SampleModel.filtrex_config + @config Filtrex.SampleModel.filtrex_config() test "parsing errors" do - assert {:error, "Invalid text value for title"} == Text.parse(@config, %{ - inverse: false, - column: "title", - value: %{}, - comparator: "equals" - }) - assert {:error, "Invalid text comparator 'between'"} == Text.parse(@config, %{ - inverse: false, - column: "title", - value: "Buy Milk", - comparator: "between" - }) + assert {:error, "Invalid text value for title"} == + Text.parse(@config, %{ + inverse: false, + column: "title", + value: %{}, + comparator: "equals" + }) + + assert {:error, "Invalid text comparator 'between'"} == + Text.parse(@config, %{ + inverse: false, + column: "title", + value: "Buy Milk", + comparator: "between" + }) end test "encoding as SQL fragments for ecto" do - {:ok, condition} = Text.parse(@config, %{inverse: false, column: "title", value: "Buy Milk", comparator: "equals"}) + {:ok, condition} = + Text.parse(@config, %{ + inverse: false, + column: "title", + value: "Buy Milk", + comparator: "equals" + }) + encoded = Filtrex.Encoder.encode(condition) assert encoded.values == [column_ref(:title), "Buy Milk"] assert encoded.expression == "? = ?" - {:ok, condition} = Text.parse(@config, %{inverse: false, column: "title", value: "Buy Milk", comparator: "does not equal"}) + {:ok, condition} = + Text.parse(@config, %{ + inverse: false, + column: "title", + value: "Buy Milk", + comparator: "does not equal" + }) + encoded = Filtrex.Encoder.encode(condition) assert encoded.expression == "? != ?" - {:ok, condition} = Text.parse(@config, %{inverse: false, column: "title", value: "Buy Milk", comparator: "contains"}) + {:ok, condition} = + Text.parse(@config, %{ + inverse: false, + column: "title", + value: "Buy Milk", + comparator: "contains" + }) + encoded = Filtrex.Encoder.encode(condition) assert encoded.expression == "lower(?) LIKE lower(?)" - {:ok, condition} = Text.parse(@config, %{inverse: false, column: "title", value: "Buy Milk", comparator: "does not contain"}) + {:ok, condition} = + Text.parse(@config, %{ + inverse: false, + column: "title", + value: "Buy Milk", + comparator: "does not contain" + }) + encoded = Filtrex.Encoder.encode(condition) assert encoded.expression == "lower(?) NOT LIKE lower(?)" assert encoded.values == [column_ref(:title), "%Buy Milk%"] diff --git a/test/config_test.exs b/test/config_test.exs index 2a894dc..a2b0be7 100644 --- a/test/config_test.exs +++ b/test/config_test.exs @@ -4,7 +4,7 @@ defmodule FiltrexTypeConfigTest do import Filtrex.Type.Config doctest Filtrex.Type.Config - @configs Filtrex.SampleModel.filtrex_config + @configs Filtrex.SampleModel.filtrex_config() test "finding whether key is allowed" do refute Config.allowed?(@configs, "blah") diff --git a/test/filtrex_test.exs b/test/filtrex_test.exs index 32f944f..83b4d1e 100644 --- a/test/filtrex_test.exs +++ b/test/filtrex_test.exs @@ -4,154 +4,209 @@ defmodule FiltrexTest do alias Factory.ConditionParams alias Factory.FilterParams - @config Filtrex.SampleModel.filtrex_config + @config Filtrex.SampleModel.filtrex_config() @tag :validate_structure test "only allows certain types" do assert FilterParams.build(:all) - |> FilterParams.type("dash-combine") - |> Filtrex.validate_structure == - {:error, "Invalid filter type 'dash-combine'"} + |> FilterParams.type("dash-combine") + |> Filtrex.validate_structure() == + {:error, "Invalid filter type 'dash-combine'"} end @tag :validate_structure test "requiring more than one condition" do assert FilterParams.build(:all) - |> Filtrex.validate_structure == - {:error, "One or more conditions required to filter"} + |> Filtrex.validate_structure() == + {:error, "One or more conditions required to filter"} end @tag :validate_structure test "validating sub-filters is a list" do assert FilterParams.build(:all) - |> FilterParams.conditions([ConditionParams.build(:text)]) - |> FilterParams.sub_filters(%{}) - |> Filtrex.validate_structure == - {:error, "Sub-filters must be a valid list of filters"} + |> FilterParams.conditions([ConditionParams.build(:text)]) + |> FilterParams.sub_filters(%{}) + |> Filtrex.validate_structure() == + {:error, "Sub-filters must be a valid list of filters"} end @tag :validate_structure test "validating sub-filters recursively" do assert FilterParams.build(:all) - |> FilterParams.conditions([ConditionParams.build(:text)]) - |> FilterParams.sub_filters([ - FilterParams.build(:all) - |> FilterParams.type("blah") - |> FilterParams.conditions([ConditionParams.build(:text)])]) - |> Filtrex.validate_structure == - {:error, "Invalid filter type 'blah'"} + |> FilterParams.conditions([ConditionParams.build(:text)]) + |> FilterParams.sub_filters([ + FilterParams.build(:all) + |> FilterParams.type("blah") + |> FilterParams.conditions([ConditionParams.build(:text)]) + ]) + |> Filtrex.validate_structure() == + {:error, "Invalid filter type 'blah'"} end test "trickling up errors from conditions" do - params = FilterParams.build(:all) |> FilterParams.conditions([ - ConditionParams.build(:text, column: "wrong_column"), - ConditionParams.build(:text, comparator: "invalid") - ]) + params = + FilterParams.build(:all) + |> FilterParams.conditions([ + ConditionParams.build(:text, column: "wrong_column"), + ConditionParams.build(:text, comparator: "invalid") + ]) assert Filtrex.parse(@config, params) == - {:error, "Unknown column 'wrong_column', Invalid text comparator 'invalid'"} + {:error, "Unknown column 'wrong_column', Invalid text comparator 'invalid'"} end test "creating an 'all' ecto filter query" do - params = FilterParams.build(:all) |> FilterParams.conditions([ - ConditionParams.build(:text, comparator: "contains", value: "earth"), - ConditionParams.build(:text, comparator: "does not equal", value: "The earth was without form and void;") - ]) + params = + FilterParams.build(:all) + |> FilterParams.conditions([ + ConditionParams.build(:text, comparator: "contains", value: "earth"), + ConditionParams.build(:text, + comparator: "does not equal", + value: "The earth was without form and void;" + ) + ]) - {:ok, filter} = Filtrex.parse(@config, params) - assert_count filter, 1 + {:ok, filter} = Filtrex.parse(@config, params) + assert_count(filter, 1) end test "creating an 'any' ecto filter query" do - params = FilterParams.build(:any) |> FilterParams.conditions([ - ConditionParams.build(:date, comparator: "on or before", value: "2016-03-20"), - ConditionParams.build(:date, comparator: "on or after", value: "2016-05-04") - ]) + params = + FilterParams.build(:any) + |> FilterParams.conditions([ + ConditionParams.build(:date, comparator: "on or before", value: "2016-03-20"), + ConditionParams.build(:date, comparator: "on or after", value: "2016-05-04") + ]) - {:ok, filter} = Filtrex.parse(@config, params) - assert_count filter, 6 + {:ok, filter} = Filtrex.parse(@config, params) + assert_count(filter, 6) end test "creating a 'none' ecto filter query" do - params = FilterParams.build(:none) |> FilterParams.conditions([ - ConditionParams.build(:text, comparator: "contains", value: "earth"), - ConditionParams.build(:text, value: "Chris McCord") - ]) + params = + FilterParams.build(:none) + |> FilterParams.conditions([ + ConditionParams.build(:text, comparator: "contains", value: "earth"), + ConditionParams.build(:text, value: "Chris McCord") + ]) - {:ok, filter} = Filtrex.parse(@config, params) - assert_count filter, 4 + {:ok, filter} = Filtrex.parse(@config, params) + assert_count(filter, 4) end test "creating subfilters" do - params = FilterParams.build(:any) |> FilterParams.conditions([ - ConditionParams.build(:text, comparator: "contains", value: "earth"), - ConditionParams.build(:text, value: "Chris McCord") - ]) |> FilterParams.sub_filters([ - FilterParams.build(:all) |> FilterParams.conditions([ - ConditionParams.build(:date, comparator: "after", value: "2016-03-26"), - ConditionParams.build(:date, comparator: "before", value: "2016-06-01") + params = + FilterParams.build(:any) + |> FilterParams.conditions([ + ConditionParams.build(:text, comparator: "contains", value: "earth"), + ConditionParams.build(:text, value: "Chris McCord") + ]) + |> FilterParams.sub_filters([ + FilterParams.build(:all) + |> FilterParams.conditions([ + ConditionParams.build(:date, comparator: "after", value: "2016-03-26"), + ConditionParams.build(:date, comparator: "before", value: "2016-06-01") + ]) ]) - ]) - {:ok, filter} = Filtrex.parse(@config, params) - assert_count filter, 5 + {:ok, filter} = Filtrex.parse(@config, params) + assert_count(filter, 5) end test "creating filter with numbers in the conditions" do - params = FilterParams.build(:all) |> FilterParams.conditions([ - ConditionParams.build(:number_rating, comparator: "greater than or", value: 50), - ConditionParams.build(:number_rating, comparator: "less than", value: 99.9), - ConditionParams.build(:number_upvotes, comparator: "greater than", value: 100) - ]) + params = + FilterParams.build(:all) + |> FilterParams.conditions([ + ConditionParams.build(:number_rating, comparator: "greater than or", value: 50), + ConditionParams.build(:number_rating, comparator: "less than", value: 99.9), + ConditionParams.build(:number_upvotes, comparator: "greater than", value: 100) + ]) - {:ok, filter} = Filtrex.parse(@config, params) - assert_count filter, 1 + {:ok, filter} = Filtrex.parse(@config, params) + assert_count(filter, 1) end test "creating a filter with a datetime expression" do - params = FilterParams.build(:all) |> FilterParams.conditions([ - ConditionParams.build(:datetime, comparator: "on or after", value: "2016-03-20T12:34:58.000Z"), - ConditionParams.build(:datetime, comparator: "on or before", value: "2016-04-02T13:00:00.000Z") - ]) + params = + FilterParams.build(:all) + |> FilterParams.conditions([ + ConditionParams.build(:datetime, + comparator: "on or after", + value: "2016-03-20T12:34:58.000Z" + ), + ConditionParams.build(:datetime, + comparator: "on or before", + value: "2016-04-02T13:00:00.000Z" + ) + ]) - {:ok, filter} = Filtrex.parse(@config, params) - assert_count filter, 1 + {:ok, filter} = Filtrex.parse(@config, params) + assert_count(filter, 1) end test "parsing parameters" do - query_string = "title_contains=earth&date_column_between[start]=2016-01-10&date_column_between[end]=2016-12-10&flag=false&filter_union=any" + query_string = + "title_contains=earth&date_column_between[start]=2016-01-10&date_column_between[end]=2016-12-10&flag=false&filter_union=any" + params = Plug.Conn.Query.decode(query_string) {:ok, filter} = Filtrex.parse_params(@config, params) + assert filter == %Filtrex{ - type: "any", - conditions: [ - %Filtrex.Condition.Date{ - type: :date, column: "date_column", comparator: "between", inverse: false, - value: %{start: Timex.to_date({2016, 1, 10}), end: Timex.to_date({2016, 12, 10})}}, - %Filtrex.Condition.Boolean{ - type: :boolean, column: "flag", - comparator: "equals", value: false, inverse: false}, - %Filtrex.Condition.Text{ - type: :text, column: "title", - comparator: "contains", value: "earth", inverse: false}]} + type: "any", + conditions: [ + %Filtrex.Condition.Date{ + type: :date, + column: "date_column", + comparator: "between", + inverse: false, + value: %{start: Timex.to_date({2016, 1, 10}), end: Timex.to_date({2016, 12, 10})} + }, + %Filtrex.Condition.Boolean{ + type: :boolean, + column: "flag", + comparator: "equals", + value: false, + inverse: false + }, + %Filtrex.Condition.Text{ + type: :text, + column: "title", + comparator: "contains", + value: "earth", + inverse: false + } + ] + } end test "parsing empty parameters" do {:ok, filter} = Filtrex.parse_params(@config, %{}) - assert_count filter, 7 + assert_count(filter, 7) end test "parsing string keys" do - {:ok, filter} = Filtrex.parse(@config, %{ - "filter" => %{"type" => "all", "conditions" => [ - %{"type" => "text", "column" => "title", - "comparator" => "contains", "value" => "earth"}, - %{"type" => "text", "column" => "title", - "comparator" => "does not equal", - "value" => "The earth was without form and void;"} - ]} - }) - assert_count filter, 1 + {:ok, filter} = + Filtrex.parse(@config, %{ + "filter" => %{ + "type" => "all", + "conditions" => [ + %{ + "type" => "text", + "column" => "title", + "comparator" => "contains", + "value" => "earth" + }, + %{ + "type" => "text", + "column" => "title", + "comparator" => "does not equal", + "value" => "The earth was without form and void;" + } + ] + } + }) + + assert_count(filter, 1) end test "parsing invalid string keys" do @@ -164,7 +219,7 @@ defmodule FiltrexTest do params = Plug.Conn.Query.decode(query_string) {:ok, filter} = Filtrex.parse_params(@config, params) existing_query = from(m in Filtrex.SampleModel, where: m.rating > 90.0) - assert_count existing_query, filter, 1 + assert_count(existing_query, filter, 1) end test ".query returns no results if allow_empty: false" do @@ -172,7 +227,7 @@ defmodule FiltrexTest do Filtrex.SampleModel |> where([m], m.rating > 90.0) |> Filtrex.query(%Filtrex{empty: true}, allow_empty: false) - |> Filtrex.Repo.all + |> Filtrex.Repo.all() assert length(results) == 0 end @@ -183,10 +238,15 @@ defmodule FiltrexTest do end test ".safe_parse returns %Filtrex{} when no error occurs" do - params = FilterParams.build(:all) |> FilterParams.conditions([ - ConditionParams.build(:text, comparator: "contains", value: "earth"), - ConditionParams.build(:text, comparator: "does not equal", value: "The earth was without form and void;") - ]) + params = + FilterParams.build(:all) + |> FilterParams.conditions([ + ConditionParams.build(:text, comparator: "contains", value: "earth"), + ConditionParams.build(:text, + comparator: "does not equal", + value: "The earth was without form and void;" + ) + ]) assert %Filtrex{} = Filtrex.safe_parse(@config, params) end @@ -198,15 +258,17 @@ defmodule FiltrexTest do end test ".safe_parse_params returns %Filtrex{} when no error occurs" do - query_string = "title_contains=earth&date_column_between[start]=2016-01-10&date_column_between[end]=2016-12-10&flag=false&filter_union=any" + query_string = + "title_contains=earth&date_column_between[start]=2016-01-10&date_column_between[end]=2016-12-10&flag=false&filter_union=any" + params = Plug.Conn.Query.decode(query_string) assert %Filtrex{} = Filtrex.safe_parse_params(@config, params) end defp assert_count(query \\ Filtrex.SampleModel, filter, count) do assert query - |> Filtrex.query(filter) - |> select([m], count(m.id)) - |> Filtrex.Repo.one! == count + |> Filtrex.query(filter) + |> select([m], count(m.id)) + |> Filtrex.Repo.one!() == count end end diff --git a/test/params_test.exs b/test/params_test.exs index 8af16e5..85e432f 100644 --- a/test/params_test.exs +++ b/test/params_test.exs @@ -1,45 +1,64 @@ defmodule ParamsTest do use ExUnit.Case - @config Filtrex.SampleModel.filtrex_config + @config Filtrex.SampleModel.filtrex_config() test "parsing valid text parameters" do params = %{"title_contains" => "blah"} + assert Filtrex.Params.parse_conditions(@config, params) == - {:ok, [%Filtrex.Condition.Text{type: :text, inverse: false, - column: "title", value: "blah", comparator: "contains" }]} + {:ok, + [ + %Filtrex.Condition.Text{ + type: :text, + inverse: false, + column: "title", + value: "blah", + comparator: "contains" + } + ]} end test "parsing valid date parameters" do params = %{"date_column_between" => %{"start" => "2016-03-10", "end" => "2016-03-20"}} + assert Filtrex.Params.parse_conditions(@config, params) == - {:ok, [%Filtrex.Condition.Date{ - type: :date, inverse: false, column: "date_column", comparator: "between", - value: %{start: Timex.to_date({2016, 3, 10}), end: Timex.to_date({2016, 3, 20})}}]} + {:ok, + [ + %Filtrex.Condition.Date{ + type: :date, + inverse: false, + column: "date_column", + comparator: "between", + value: %{start: Timex.to_date({2016, 3, 10}), end: Timex.to_date({2016, 3, 20})} + } + ]} end test "bubbling up errors from value parsing" do params = %{"date_column_between" => %{"start" => "2016-03-10"}} + assert Filtrex.Params.parse_conditions(@config, params) == - {:error, "Invalid date value format: Both a start and end key are required."} + {:error, "Invalid date value format: Both a start and end key are required."} end test "returning error if unknown keys" do params = %{"title_contains" => "blah", "extra_key" => "true"} + assert Filtrex.Params.parse_conditions(@config, params) == - {:error, "Unknown filter key 'extra_key'"} + {:error, "Unknown filter key 'extra_key'"} end test "sanitizing map keys recursively" do map = %{"key1" => %{"sub_key" => [%{:key => 1, "sub_sub_key" => nil}]}, "key2" => :value} assert Filtrex.Params.sanitize(map, [:key1, :sub_key, :sub_sub_key, :key2]) == - {:ok, %{key1: %{sub_key: [%{key: 1, sub_sub_key: nil}]}, key2: :value}} + {:ok, %{key1: %{sub_key: [%{key: 1, sub_sub_key: nil}]}, key2: :value}} assert Filtrex.Params.sanitize(map, [:key1, :sub_sub_key, :key2]) == - {:error, "Unknown key 'sub_key'"} + {:error, "Unknown key 'sub_key'"} assert Filtrex.Params.sanitize(%{1 => "value"}, [:key1, :sub_sub_key, :key2]) == - {:error, "Invalid key. Only string keys are supported."} + {:error, "Invalid key. Only string keys are supported."} end end diff --git a/test/support/factories/condition_params.ex b/test/support/factories/condition_params.ex index aed50f6..7eca54f 100644 --- a/test/support/factories/condition_params.ex +++ b/test/support/factories/condition_params.ex @@ -10,7 +10,12 @@ defmodule Factory.ConditionParams do end def factory(:datetime) do - %{type: "datetime", column: "datetime_column", comparator: "equals", value: "2016-04-02T13:00:00.000Z"} + %{ + type: "datetime", + column: "datetime_column", + comparator: "equals", + value: "2016-04-02T13:00:00.000Z" + } end def factory(:number_rating) do @@ -33,8 +38,8 @@ defmodule Factory.ConditionParams do Map.put(condition, :comparator, comparator) end - def equals(condition), do: comparator(condition, "equals") + def equals(condition), do: comparator(condition, "equals") def does_not_equal(condition), do: comparator(condition, "does not equal") - def on_or_after(condition), do: comparator(condition, "on or after") - def on_or_before(condition), do: comparator(condition, "on or before") + def on_or_after(condition), do: comparator(condition, "on or after") + def on_or_before(condition), do: comparator(condition, "on or before") end diff --git a/test/support/helpers.ex b/test/support/helpers.ex index eb8388d..53935f1 100644 --- a/test/support/helpers.ex +++ b/test/support/helpers.ex @@ -7,7 +7,7 @@ defmodule Filtrex.TestHelpers do Returns a quoted reference to a column in a query, such as `s.title`. """ def column_ref(column) do - quote [context: Filtrex.Utils.Encoder] do + quote context: Filtrex.Utils.Encoder do s.unquote(column) end end diff --git a/test/support/sample_model.ex b/test/support/sample_model.ex index 8e36852..b667966 100644 --- a/test/support/sample_model.ex +++ b/test/support/sample_model.ex @@ -2,23 +2,25 @@ defmodule Filtrex.SampleModel do use Ecto.Schema schema "sample_models" do - field :title - field :date_column, :date - field :datetime_column, :naive_datetime - field :upvotes, :integer - field :rating, :float - field :comments + field(:title) + field(:date_column, :date) + field(:datetime_column, :naive_datetime) + field(:upvotes, :integer) + field(:rating, :float) + field(:comments) timestamps() end def filtrex_config do - [%Filtrex.Type.Config{type: :number, keys: ~w(id), options: %{allowed_values: [1]}}, - %Filtrex.Type.Config{type: :text, keys: ~w(title)}, - %Filtrex.Type.Config{type: :date, keys: ~w(date_column)}, - %Filtrex.Type.Config{type: :number, keys: ~w(upvotes)}, - %Filtrex.Type.Config{type: :boolean, keys: ~w(flag)}, - %Filtrex.Type.Config{type: :datetime, keys: ~w(datetime_column)}, - %Filtrex.Type.Config{type: :number, keys: ~w(rating), options: %{allow_decimal: true}}] + [ + %Filtrex.Type.Config{type: :number, keys: ~w(id), options: %{allowed_values: [1]}}, + %Filtrex.Type.Config{type: :text, keys: ~w(title)}, + %Filtrex.Type.Config{type: :date, keys: ~w(date_column)}, + %Filtrex.Type.Config{type: :number, keys: ~w(upvotes)}, + %Filtrex.Type.Config{type: :boolean, keys: ~w(flag)}, + %Filtrex.Type.Config{type: :datetime, keys: ~w(datetime_column)}, + %Filtrex.Type.Config{type: :number, keys: ~w(rating), options: %{allow_decimal: true}} + ] end end diff --git a/test/test_helper.exs b/test/test_helper.exs index 77cd4c5..43906b4 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,35 +1,29 @@ data = [ - {"In the beginning, God created the heavens and the earth.", - {{2016, 1, 1}, {12, 34, 56}}, 99.9, 256}, - {"The earth was without form and void;", - {{2016, 2, 10}, {12, 34, 56}}, 84.2, 200}, - {"and the darkness was on the face of the deep.", - {{2016, 3, 20}, {12, 34, 56}}, 15.7, 132}, + {"In the beginning, God created the heavens and the earth.", {{2016, 1, 1}, {12, 34, 56}}, 99.9, + 256}, + {"The earth was without form and void;", {{2016, 2, 10}, {12, 34, 56}}, 84.2, 200}, + {"and the darkness was on the face of the deep.", {{2016, 3, 20}, {12, 34, 56}}, 15.7, 132}, {"Then God said, 'Let there be a firmament in the midst of the waters,'", - {{2016, 4, 2}, {12, 34, 56}}, 8.2, 5}, - {"José Valim", - {{2016, 5, 4}, {12, 34, 56}}, 45.3, 30}, - {"Eric Meadows-Jönsson", - {{2016, 6, 6}, {12, 34, 56}}, 10.4, 24}, - {"Chris McCord", - {{2016, 7, 8}, {12, 34, 56}}, 67.0, 10} + {{2016, 4, 2}, {12, 34, 56}}, 8.2, 5}, + {"José Valim", {{2016, 5, 4}, {12, 34, 56}}, 45.3, 30}, + {"Eric Meadows-Jönsson", {{2016, 6, 6}, {12, 34, 56}}, 10.4, 24}, + {"Chris McCord", {{2016, 7, 8}, {12, 34, 56}}, 67.0, 10} ] ExUnit.start() -Filtrex.Repo.start_link +Filtrex.Repo.start_link() Filtrex.Repo.delete_all(Filtrex.SampleModel) for {title, {d, t}, rating, upvotes} <- data do with {:ok, date} <- Date.from_erl(d), - {:ok, datetime} <- NaiveDateTime.from_erl({d, t}) - do - %Filtrex.SampleModel{ - title: title, - date_column: date, - datetime_column: datetime, - rating: rating, - upvotes: upvotes - } |> Filtrex.Repo.insert! - end + {:ok, datetime} <- NaiveDateTime.from_erl({d, t}) do + %Filtrex.SampleModel{ + title: title, + date_column: date, + datetime_column: datetime, + rating: rating, + upvotes: upvotes + } + |> Filtrex.Repo.insert!() + end end -