Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement ANY (OR) evaluation mode #5

Merged
merged 10 commits into from
Sep 23, 2024
Merged
17 changes: 16 additions & 1 deletion lib/geoffrey/rule.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule Geoffrey.Rule do
defstruct [
:name,
:desc,
{:eval_mode, :all},
{:priority, 0},
{:conditions, []},
{:actions, []},
Expand All @@ -13,9 +14,11 @@ defmodule Geoffrey.Rule do
{:errors, []}
]

@type eval_mode :: :all | :any
@type t :: %__MODULE__{
name: String.t(),
desc: String.t(),
eval_mode: eval_mode(),
priority: integer(),
conditions: [],
actions: []
Expand Down Expand Up @@ -47,6 +50,14 @@ defmodule Geoffrey.Rule do
%{rule | priority: priority}
end

@doc """
Actualiza la prioridad de una regla. El valor debe ser un numero entero
"""
@spec set_eval_mode(__MODULE__.t(), eval_mode()) :: __MODULE__.t()
def set_eval_mode(rule, eval_mode) when eval_mode in [:all, :any] do
%{rule | eval_mode: eval_mode}
end

@doc """
Agrega una condicion a la regla
"""
Expand Down Expand Up @@ -121,10 +132,14 @@ defmodule Geoffrey.Rule do

# Evalua las condiciones de una regla
@spec eval_conditions(__MODULE__.t(), map()) :: boolean()
defp eval_conditions(%__MODULE__{conditions: conditions}, input) do
defp eval_conditions(%__MODULE__{eval_mode: :all, conditions: conditions}, input) do
Enum.all?(conditions, &Condition.eval(&1, input))
end

defp eval_conditions(%__MODULE__{eval_mode: :any, conditions: conditions}, input) do
Enum.any?(conditions, &Condition.eval(&1, input))
end

# Ejecuta las acciones asignadas si la regla es valida
@spec run_actions(__MODULE__.t()) :: __MODULE__.t()
defp run_actions(%__MODULE__{actions: actions, valid?: true} = rule) do
Expand Down
26 changes: 12 additions & 14 deletions lib/geoffrey/rule_group.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,30 @@ defmodule Geoffrey.RuleGroup do

defstruct rules: [],
valid?: false,
type: :all,
eval_mode: :all,
result: nil

@type t :: %__MODULE__{
rules: [Rule.t()],
valid?: boolean(),
type: atom(),
eval_mode: atom(),
result: [any()]
}

@valid_types ~w(all any)a

@doc """
Crea un nuevo grupo de reglas
"""
@spec new :: __MODULE__.t()
def new do
%__MODULE__{type: :any}
%__MODULE__{eval_mode: :any}
end

@doc """
Actualiza el tipo de grupo de reglas
"""
@spec set_type(__MODULE__.t(), atom()) :: __MODULE__.t()
def set_type(%__MODULE__{} = rule_group, type) when type in @valid_types do
%{rule_group | type: type}
@spec set_eval_mode(__MODULE__.t(), Rule.eval_mode()) :: __MODULE__.t()
def set_eval_mode(%__MODULE__{} = rule_group, eval_mode) when eval_mode in [:all, :any] do
%{rule_group | eval_mode: eval_mode}
end

@doc """
Expand Down Expand Up @@ -73,7 +71,7 @@ defmodule Geoffrey.RuleGroup do
# que el grupo sea valido.
# Si el grupo es de tipo `any` con que alguna regla evalue el grupo sera valido
@spec eval_rules(__MODULE__.t(), map()) :: __MODULE__.t()
defp eval_rules(%__MODULE__{type: :all, rules: rules} = rule_group, input) do
defp eval_rules(%__MODULE__{eval_mode: :all, rules: rules} = rule_group, input) do
rules_evaluations = Enum.map(rules, &Rule.eval(&1, input))

case Enum.all?(rules_evaluations, & &1.valid?) do
Expand All @@ -82,11 +80,11 @@ defmodule Geoffrey.RuleGroup do
%{rule_group | valid?: true, result: result}

_ ->
rule_group
%{rule_group | valid?: false, result: nil}
end
end

defp eval_rules(%__MODULE__{type: :any, rules: rules} = rule_group, input) do
defp eval_rules(%__MODULE__{eval_mode: :any, rules: rules} = rule_group, input) do
valid_rule =
Enum.find(rules, fn rule ->
%Rule{valid?: valid?} = Rule.eval(rule, input)
Expand All @@ -95,10 +93,10 @@ defmodule Geoffrey.RuleGroup do

case valid_rule do
nil ->
false
%{rule_group | valid?: false, result: nil}

%{result: result} = _rule ->
%{rule_group | result: result}
%Rule{result: result} ->
%{rule_group | valid?: true, result: result}
end
end

Expand Down
44 changes: 41 additions & 3 deletions lib/geoffrey/types/condition.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ if Code.ensure_loaded?(Ecto.Type) do

use Ecto.Type

alias Geoffrey.Rules.Condition
alias Geoffrey.Parsers.Text

require Logger
Expand Down Expand Up @@ -39,10 +40,16 @@ if Code.ensure_loaded?(Ecto.Type) do
{:ok, conditions}
end

def dump(%{type: type, field: field, comparator: comparator, compare_to: compare_to}) do
type = get_type_code(type)
def dump(%Condition{} = condition) do
condition_str = condition_to_string(condition)

{:ok, "#{comparator}|#{field}|#{type}#{compare_to}"}
{:ok, condition_str}
end

def dump(%{type: _, field: _, comparator: _, compare_to: _} = condition) do
condition_str = condition_to_string(condition)

{:ok, condition_str}
end

def dump(condition) when is_binary(condition) do
Expand All @@ -54,6 +61,19 @@ if Code.ensure_loaded?(Ecto.Type) do
end

@spec condition_to_string(map) :: String.t()
defp condition_to_string(%Condition{
comparator: comparator,
field: field,
compare_to: compare_to
}) do
type =
compare_to
|> get_value_type()
|> get_type_code()

build_db_string(comparator, field, type, compare_to)
end

defp condition_to_string(%{
type: type,
field: field,
Expand All @@ -62,13 +82,31 @@ if Code.ensure_loaded?(Ecto.Type) do
}) do
type = get_type_code(type)

build_db_string(comparator, field, type, compare_to)
end

defp build_db_string(comparator, field, type, compare_to) do
field = flatten_field(field)
"#{comparator}|#{field}|#{type}#{compare_to}"
end

@spec get_value_type(integer | float | binary) :: String.t()
defp get_value_type(value) when is_integer(value), do: "integer"
defp get_value_type(value) when is_float(value), do: "float"
defp get_value_type(value) when is_binary(value), do: "string"

# Obtiene el codigo que se agrega antes del value dependiendo el tipo
@spec get_type_code(any()) :: String.t()
defp get_type_code("integer"), do: "i#"
defp get_type_code("float"), do: "f#"
defp get_type_code(_), do: ""

defp flatten_field([_ | _] = field) do
Enum.join(field, ".")
end

defp flatten_field(field) when is_binary(field) do
field
end
end
end
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule Geoffrey.MixProject do
def project do
[
app: :geoffrey,
version: "0.2.2",
version: "0.2.3",
erickgnavar marked this conversation as resolved.
Show resolved Hide resolved
elixir: "~> 1.11",
start_permanent: Mix.env() == :prod,
aliases: aliases(),
Expand Down
18 changes: 14 additions & 4 deletions test/geoffrey_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ defmodule GeoffreyTest do
"regla_prueba"
|> Rule.new("Una prueba")
|> Rule.set_priority(1)
|> Rule.set_eval_mode(:any)
|> Rule.add_condition("|gt|age|i#30")
|> Rule.add_condition(condition)
|> Rule.add_action(:example)
Expand All @@ -52,29 +53,38 @@ defmodule GeoffreyTest do
end

test "Rule eval" do
invalid_input = %{
"age" => 30,
input = %{
"age" => 35,
"height" => 1.85
}

input = %{
"age" => 35,
input_2 = %{
"age" => 30,
"height" => 1.85
}

invalid_input = %{
"age" => 30,
"height" => 1.84
}

condition = Condition.new("eq", ["height"], 1.85)

rule =
"regla_prueba"
|> Rule.new("Una prueba")
|> Rule.set_priority(1)
|> Rule.set_eval_mode(:any)
|> Rule.add_condition("|gt|age|i#30")
|> Rule.add_condition(condition)
|> Rule.add_action(fn x -> Map.put(x, "type", :example) end)
|> Rule.add_action(fn x -> Map.put(x, "valid?", true) end)

%Rule{result: result} = Geoffrey.Rule.eval(rule, input)
assert %{"type" => :example, "valid?" => true} = result

assert %Rule{valid?: true} = Geoffrey.Rule.eval(rule, input_2)

assert %Rule{valid?: false} = Geoffrey.Rule.eval(rule, invalid_input)

input = %{
Expand Down
Loading