Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config
import Config

# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion config/test.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use Mix.Config
import Config

config :filtrex, Filtrex.Repo,
adapter: Ecto.Adapters.Postgres,
Expand Down
64 changes: 40 additions & 24 deletions lib/filtrex/condition.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,14 @@ defmodule Filtrex.Condition do
Filtrex.Condition.Number
]

@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]
@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()]

defstruct column: nil, comparator: nil, value: nil

Expand Down Expand Up @@ -54,10 +59,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
Expand All @@ -68,21 +77,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.t()) :: {:ok, binary} | {:error, binary}
def validate_comparator(type, comparator, comparators) do
if comparator in comparators do
{:ok, comparator}
Expand All @@ -92,9 +104,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.t()) :: 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
Expand All @@ -103,32 +116,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.t(), Atom.t()) :: 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.t()) :: 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

Expand All @@ -138,8 +154,8 @@ defmodule Filtrex.Condition do
end

defp condition_module(type) do
Enum.find(condition_modules(), fn (module) ->
type == to_string(module.type)
Enum.find(condition_modules(), fn module ->
type == to_string(module.type())
end)
end
end
55 changes: 37 additions & 18 deletions lib/filtrex/conditions/number.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,37 @@ 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
Expand All @@ -46,43 +59,49 @@ 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

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
end

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
Expand All @@ -91,11 +110,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
33 changes: 22 additions & 11 deletions lib/filtrex/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -61,21 +61,29 @@ defmodule Filtrex.Type.Config do
end
end

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)
for module <- Filtrex.Condition.condition_modules() do
@doc "Generate a config struct for `#{to_string(module) |> String.slice(7..-1//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)
defmacro unquote(module.type())(key, opts) do
type = unquote(module.type())

quote do
unquote(type)([to_string(unquote(key))], unquote(opts))
end
Expand All @@ -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
14 changes: 7 additions & 7 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ defmodule Filtrex.Mixfile do
defp deps do
[
{:postgrex, ">= 0.0.0", only: :test},
{:ecto, "~> 3.0"},
{:ecto_sql, "~> 3.0"},
{:timex, "~> 3.1"},
{:earmark, "~> 0.1", only: :dev},
{:ex_doc, "~> 0.11", only: :dev},
{:ecto, "~> 3.12.5"},
{:ecto_sql, "~> 3.12.1"},
{:timex, "~> 3.7.11"},
{:earmark, "~> 1.4.17", only: :dev},
{:ex_doc, "~> 0.35", only: :dev},
{:inch_ex, ">= 0.0.0", only: [:dev, :docs]},
{:plug, "~> 1.1.2", only: :test},
{:ex_machina, "~> 0.6.1", only: :test},
{:plug, "~> 1.16.1", only: :test},
{:ex_machina, "~> 2.8.0", only: :test},
{:mix_test_watch, "~> 0.3", only: :dev, runtime: false}
]
end
Expand Down
Loading