Skip to content

Commit 0878cfc

Browse files
committed
Adding UUID condition
1 parent 392a87a commit 0878cfc

34 files changed

+878
-465
lines changed

.formatter.exs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[
2+
inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"]
3+
]

config/config.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,6 @@ use Mix.Config
3030

3131
config :filtrex, ecto_repos: [Filtrex.Repo]
3232

33-
if Mix.env == :test do
33+
if Mix.env() == :test do
3434
import_config "test.exs"
3535
end

lib/filtrex.ex

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,18 @@ defmodule Filtrex do
1919
defstruct type: nil, conditions: [], sub_filters: [], empty: false
2020

2121
@whitelist [
22-
:filter, :type, :conditions, :sub_filters,
23-
:column, :comparator, :value, :start, :end
22+
:filter,
23+
:type,
24+
:conditions,
25+
:sub_filters,
26+
:column,
27+
:comparator,
28+
:value,
29+
:start,
30+
:end
2431
]
2532

26-
@type t :: Filtrex.t
33+
@type t :: Filtrex.t()
2734

2835
@doc """
2936
Parses a filter expression and returns an error or the parsed filter with
@@ -35,18 +42,18 @@ defmodule Filtrex do
3542
[%Filtrex.Type.Config{type: :text, keys: ~w(title comments)}]
3643
```
3744
"""
38-
@spec parse([Filtrex.Type.Config.t], Map.t) :: {:error, String.t} | {:ok, Filtrex.t}
45+
@spec parse([Filtrex.Type.Config.t()], map) :: {:error, String.t()} | {:ok, Filtrex.t()}
3946
def parse(configs, map) do
4047
with {:ok, sanitized} <- Filtrex.Params.sanitize(map, @whitelist),
4148
{:ok, valid_structured_map} <- validate_structure(sanitized),
42-
do: parse_validated_structure(configs, valid_structured_map)
49+
do: parse_validated_structure(configs, valid_structured_map)
4350
end
4451

4552
@doc """
4653
Parses a filter expression, like `parse/2`. If any exception is raised when
4754
parsing the map, a `%Filtrex{empty: true}` struct will be returned.
4855
"""
49-
@spec safe_parse([Filtrex.Type.Config.t], Map.t) :: Filtrex.t
56+
@spec safe_parse([Filtrex.Type.Config.t()], map) :: Filtrex.t()
5057
def safe_parse(configs, map) do
5158
try do
5259
{:ok, filter} = parse(configs, map)
@@ -65,6 +72,7 @@ defmodule Filtrex do
6572
```
6673
"""
6774
def parse_params(_configs, params) when params == %{}, do: {:ok, %Filtrex{empty: true}}
75+
6876
def parse_params(configs, params) do
6977
with {:ok, {type, params}} <- parse_params_filter_union(params),
7078
{:ok, conditions} <- Filtrex.Params.parse_conditions(configs, params),
@@ -77,6 +85,7 @@ defmodule Filtrex do
7785
will be returned.
7886
"""
7987
def safe_parse_params(_configs, params) when params == %{}, do: %Filtrex{empty: true}
88+
8089
def safe_parse_params(configs, params) do
8190
try do
8291
{:ok, filter} = parse_params(configs, params)
@@ -98,8 +107,9 @@ defmodule Filtrex do
98107
Filtrex.query(query, filter, allow_empty: true)
99108
```
100109
"""
101-
@spec query(Ecto.Queryable.t, Filtrex.t, Keyword.t) :: Ecto.Query.t
110+
@spec query(Ecto.Queryable.t(), Filtrex.t(), Keyword.t()) :: Ecto.Query.t()
102111
def query(queryable, filter, opts \\ [allow_empty: true])
112+
103113
def query(queryable, %Filtrex{empty: true}, opts) do
104114
if opts[:allow_empty] do
105115
queryable
@@ -113,6 +123,7 @@ defmodule Filtrex do
113123
queryable
114124
|> Filtrex.AST.build_query(filter)
115125
|> Code.eval_quoted([], __ENV__)
126+
116127
result
117128
end
118129

@@ -123,42 +134,53 @@ defmodule Filtrex do
123134
case map do
124135
%{filter: %{type: type}} when type not in ~w(all any none) ->
125136
{:error, "Invalid filter type '#{type}'"}
137+
126138
%{filter: %{conditions: conditions}} when conditions == [] or not is_list(conditions) ->
127139
{:error, "One or more conditions required to filter"}
140+
128141
%{filter: %{sub_filters: sub_filters}} when not is_list(sub_filters) ->
129142
{:error, "Sub-filters must be a valid list of filters"}
143+
130144
validated = %{filter: params} ->
131145
sub_filters = Map.get(params, :sub_filters, [])
132-
result = Enum.reduce_while(sub_filters, {:ok, []}, fn (sub_map, {:ok, acc}) ->
133-
case validate_structure(sub_map) do
134-
{:ok, sub_validated} -> {:cont, {:ok, acc ++ [sub_validated]}}
135-
{:error, error} -> {:halt, {:error, error}}
136-
end
137-
end)
146+
147+
result =
148+
Enum.reduce_while(sub_filters, {:ok, []}, fn sub_map, {:ok, acc} ->
149+
case validate_structure(sub_map) do
150+
{:ok, sub_validated} -> {:cont, {:ok, acc ++ [sub_validated]}}
151+
{:error, error} -> {:halt, {:error, error}}
152+
end
153+
end)
154+
138155
with {:ok, validated_sub_filters} <- result,
139-
do: {:ok, put_in(validated.filter[:sub_filters], validated_sub_filters)}
156+
do: {:ok, put_in(validated.filter[:sub_filters], validated_sub_filters)}
157+
140158
_ ->
141159
{:error, "Invalid filter structure"}
142160
end
143161
end
144162

145163
defp parse_validated_structure(configs, %{filter: params}) do
146-
parsed_filters = Enum.reduce_while(params[:sub_filters], {:ok, []}, fn (to_parse, {:ok, acc}) ->
147-
case parse(configs, to_parse) do
148-
{:ok, filter} -> {:cont, {:ok, acc ++ [filter]}}
149-
{:error, error} -> {:halt, {:error, error}}
150-
end
151-
end)
164+
parsed_filters =
165+
Enum.reduce_while(params[:sub_filters], {:ok, []}, fn to_parse, {:ok, acc} ->
166+
case parse(configs, to_parse) do
167+
{:ok, filter} -> {:cont, {:ok, acc ++ [filter]}}
168+
{:error, error} -> {:halt, {:error, error}}
169+
end
170+
end)
171+
152172
with {:ok, filters} <- parsed_filters,
153-
do: parse_conditions(configs, params[:type], params[:conditions])
154-
|> parse_condition_results(params[:type], filters)
173+
do:
174+
parse_conditions(configs, params[:type], params[:conditions])
175+
|> parse_condition_results(params[:type], filters)
155176
end
156177

157178
defp parse_conditions(configs, type, conditions) do
158-
Enum.reduce(conditions, %{errors: [], conditions: []}, fn (map, acc) ->
179+
Enum.reduce(conditions, %{errors: [], conditions: []}, fn map, acc ->
159180
case Filtrex.Condition.parse(configs, Map.put(map, :inverse, inverse_for(type))) do
160181
{:error, error} ->
161182
update_list_in_map(acc, :errors, error)
183+
162184
{:ok, condition} ->
163185
update_list_in_map(acc, :conditions, condition)
164186
end
@@ -168,6 +190,7 @@ defmodule Filtrex do
168190
defp parse_condition_results(%{errors: [], conditions: conditions}, type, parsed_filters) do
169191
{:ok, %Filtrex{type: type, conditions: conditions, sub_filters: parsed_filters}}
170192
end
193+
171194
defp parse_condition_results(%{errors: errors}, _, _) do
172195
{:error, Enum.join(errors, ", ")}
173196
end
@@ -176,15 +199,17 @@ defmodule Filtrex do
176199
case Map.fetch(params, "filter_union") do
177200
{:ok, type} when type in ~w(all any none) ->
178201
{:ok, {type, Map.delete(params, "filter_union")}}
202+
179203
:error ->
180204
{:ok, {"all", params}}
205+
181206
_ ->
182207
{:error, "Invalid filter union"}
183208
end
184209
end
185210

186211
defp inverse_for("none"), do: true
187-
defp inverse_for(_), do: false
212+
defp inverse_for(_), do: false
188213

189214
defp update_list_in_map(map, key, value) do
190215
values = Map.get(map, key)

lib/filtrex/ast.ex

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,16 @@ defmodule Filtrex.AST do
1313

1414
defp build_fragments(filter) do
1515
join = logical_join(filter.type)
16+
1617
Enum.map(filter.conditions, &Filtrex.Encoder.encode/1)
17-
|> fragments(join)
18-
|> build_sub_fragments(join, filter.sub_filters)
18+
|> fragments(join)
19+
|> build_sub_fragments(join, filter.sub_filters)
1920
end
2021

2122
defp build_sub_fragments(fragments, _, []), do: fragments
23+
2224
defp build_sub_fragments(fragments, join, sub_filters) do
23-
Enum.reduce(sub_filters, fragments, fn (sub_filter, [expression | values]) ->
25+
Enum.reduce(sub_filters, fragments, fn sub_filter, [expression | values] ->
2426
[sub_expression | sub_values] = build_fragments(sub_filter)
2527
[join(expression, sub_expression, join) | values ++ sub_values]
2628
end)
@@ -32,14 +34,15 @@ defmodule Filtrex.AST do
3234

3335
defp fragments(fragments, join) do
3436
Enum.reduce(fragments, ["" | []], fn
35-
(%{expression: new_expression, values: new_values}, ["" | values]) ->
37+
%{expression: new_expression, values: new_values}, ["" | values] ->
3638
["(#{new_expression})" | values ++ new_values]
37-
(%{expression: new_expression, values: new_values}, [expression | values]) ->
39+
40+
%{expression: new_expression, values: new_values}, [expression | values] ->
3841
combined = "#{expression} #{join} (#{new_expression})"
3942
[combined | values ++ new_values]
4043
end)
4144
end
4245

4346
defp logical_join("any"), do: "OR"
44-
defp logical_join(_), do: "AND"
47+
defp logical_join(_), do: "AND"
4548
end

lib/filtrex/condition.ex

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,20 @@ defmodule Filtrex.Condition do
1212
Filtrex.Condition.Date,
1313
Filtrex.Condition.DateTime,
1414
Filtrex.Condition.Boolean,
15-
Filtrex.Condition.Number
15+
Filtrex.Condition.Number,
16+
Filtrex.Condition.UUID
1617
]
1718

18-
@callback parse(Filtrex.Type.Config.t, %{inverse: boolean, column: String.t, value: any, comparator: String.t}) :: {:ok, any} | {:error, any}
19-
@callback type :: Atom.t
20-
@callback comparators :: [String.t]
19+
@type t :: %__MODULE__{}
20+
21+
@callback parse(Filtrex.Type.Config.t(), %{
22+
inverse: boolean,
23+
column: String.t(),
24+
value: any,
25+
comparator: String.t()
26+
}) :: {:ok, any} | {:error, any}
27+
@callback type :: atom()
28+
@callback comparators :: [String.t()]
2129

2230
defstruct column: nil, comparator: nil, value: nil
2331

@@ -54,10 +62,14 @@ defmodule Filtrex.Condition do
5462
case condition_module(type) do
5563
nil ->
5664
{:error, "Unknown filter condition '#{type}'"}
65+
5766
module ->
5867
type_atom = String.to_existing_atom(type)
59-
config = Filtrex.Type.Config.configs_for_type(configs, type_atom)
68+
69+
config =
70+
Filtrex.Type.Config.configs_for_type(configs, type_atom)
6071
|> Filtrex.Type.Config.config(options[:column])
72+
6173
if config do
6274
module.parse(config, Map.delete(options, :type))
6375
else
@@ -68,21 +80,24 @@ defmodule Filtrex.Condition do
6880

6981
@doc "Parses a params key into the condition type, column, and comparator"
7082
def param_key_type(configs, key_with_comparator) do
71-
result = Enum.find_value(condition_modules(), fn (module) ->
72-
Enum.find_value(module.comparators, fn (comparator) ->
73-
normalized = "_" <> String.replace(comparator, " ", "_")
74-
key = String.replace_trailing(key_with_comparator, normalized, "")
75-
config = Filtrex.Type.Config.config(configs, key)
76-
if !is_nil(config) and key in config.keys and config.type == module.type do
77-
{:ok, module, config, key, comparator}
78-
end
83+
result =
84+
Enum.find_value(condition_modules(), fn module ->
85+
Enum.find_value(module.comparators, fn comparator ->
86+
normalized = "_" <> String.replace(comparator, " ", "_")
87+
key = String.replace_trailing(key_with_comparator, normalized, "")
88+
config = Filtrex.Type.Config.config(configs, key)
89+
90+
if !is_nil(config) and key in config.keys and config.type == module.type do
91+
{:ok, module, config, key, comparator}
92+
end
93+
end)
7994
end)
80-
end)
95+
8196
if result, do: result, else: {:error, "Unknown filter key '#{key_with_comparator}'"}
8297
end
8398

8499
@doc "Helper method to validate that a comparator is in list"
85-
@spec validate_comparator(atom, binary, List.t) :: {:ok, binary} | {:error, binary}
100+
@spec validate_comparator(atom, binary, list) :: {:ok, binary} | {:error, binary}
86101
def validate_comparator(type, comparator, comparators) do
87102
if comparator in comparators do
88103
{:ok, comparator}
@@ -92,9 +107,10 @@ defmodule Filtrex.Condition do
92107
end
93108

94109
@doc "Helper method to validate whether a value is in a list"
95-
@spec validate_in(any, List.t) :: nil | any
110+
@spec validate_in(any, list) :: nil | any
96111
def validate_in(nil, _), do: nil
97112
def validate_in(_, nil), do: nil
113+
98114
def validate_in(value, list) do
99115
cond do
100116
value in list -> value
@@ -103,32 +119,35 @@ defmodule Filtrex.Condition do
103119
end
104120

105121
@doc "Helper method to validate whether a value is a binary"
106-
@spec validate_is_binary(any) :: nil | String.t
122+
@spec validate_is_binary(any) :: nil | String.t()
107123
def validate_is_binary(value) when is_binary(value), do: value
108124
def validate_is_binary(_), do: nil
109125

110126
@doc "Generates an error description for a generic parse error"
111-
@spec parse_error(any, Atom.t, Atom.t) :: String.t
127+
@spec parse_error(any, atom, atom) :: String.t()
112128
def parse_error(value, type, filter_type) do
113129
"Invalid #{to_string(filter_type)} #{to_string(type)} '#{value}'"
114130
end
115131

116132
@doc "Generates an error description for a parse error resulting from an invalid value type"
117-
@spec parse_value_type_error(any, Atom.t) :: String.t
133+
@spec parse_value_type_error(any, atom) :: String.t()
118134
def parse_value_type_error(column, filter_type) when is_binary(column) do
119135
"Invalid #{to_string(filter_type)} value for #{column}"
120136
end
137+
121138
def parse_value_type_error(column, filter_type) do
122-
opts = struct(Inspect.Opts, [])
123-
iodata = Inspect.Algebra.to_doc(column, opts)
139+
opts = struct(Inspect.Opts, [])
140+
141+
iodata =
142+
Inspect.Algebra.to_doc(column, opts)
124143
|> Inspect.Algebra.format(opts.width)
125-
|> Enum.join
144+
|> Enum.join()
126145

127146
if String.length(iodata) <= 15 do
128147
parse_value_type_error("'#{iodata}'", filter_type)
129148
else
130149
"'#{String.slice(iodata, 0..12)}...#{String.slice(iodata, -3..-1)}'"
131-
|> parse_value_type_error(filter_type)
150+
|> parse_value_type_error(filter_type)
132151
end
133152
end
134153

@@ -138,7 +157,7 @@ defmodule Filtrex.Condition do
138157
end
139158

140159
defp condition_module(type) do
141-
Enum.find(condition_modules(), fn (module) ->
160+
Enum.find(condition_modules(), fn module ->
142161
type == to_string(module.type)
143162
end)
144163
end

lib/filtrex/conditions/boolean.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ defmodule Filtrex.Condition.Boolean do
2424
case condition do
2525
%Condition.Boolean{comparator: nil} ->
2626
{:error, parse_error(comparator, :comparator, :date)}
27+
2728
%Condition.Boolean{value: nil} ->
2829
{:error, parse_value_type_error(value, :boolean)}
30+
2931
_ ->
3032
{:ok, condition}
3133
end
@@ -38,7 +40,7 @@ defmodule Filtrex.Condition.Boolean do
3840
defp validate_value(_), do: nil
3941

4042
defimpl Filtrex.Encoder do
41-
encoder "equals", "does not equal", "column = ?"
42-
encoder "does not equal", "equals", "column != ?"
43+
encoder("equals", "does not equal", "column = ?")
44+
encoder("does not equal", "equals", "column != ?")
4345
end
4446
end

0 commit comments

Comments
 (0)