lib/rule_engine/operation/between_op.ex

#-------------------------------------------------------------------------------
# Author: Keith Brings
# Copyright (C) 2018 Noizu Labs, Inc. All rights reserved.
#-------------------------------------------------------------------------------

defmodule Noizu.RuleEngine.Op.BetweenOp do
  @type t :: %__MODULE__{
    name: String.t | nil,
    description: String.t | nil,
    identifier: String.t | list | tuple, # Materialized Path.
    settings: Keyword.t,
    strict: boolean,
    argument: any,
    lower_bound: any,
    upper_bound: any,
  }

  defstruct [
    name: nil,
    description: nil,
    identifier: nil,
    comparison_strategy: :default,
    settings: [async?: :auto, throw_on_timeout?: :auto],
    strict: false,
    argument: nil,
    lower_bound: nil,
    upper_bound: nil
  ]
end

defimpl Noizu.RuleEngine.ScriptProtocol, for: Noizu.RuleEngine.Op.BetweenOp do
  alias Noizu.RuleEngine.Helper
  #-----------------
  # execute!/3
  #-----------------
  def execute!(this, state, context), do: execute!(this, state, context, %{})

  #-----------------
  # execute!/4
  #-----------------
  def execute!(this, state, context, options) do
    cond do
      Enum.member?([true, :auto, :required], this.settings[:async?]) && (options[:settings] && options.settings.supports_async? == true) -> execute!(:async, this, state, context, options)
      this.settings[:async?] == :required -> throw Noizu.RuleEngine.Error.Basic.new("[ScriptError] Unable to perform required async execute on #{this.__struct__} - #{identifier(this, state, context)}", 310)
      true -> execute!(:sync, this, state, context, options)
    end
  end

  #-----------------
  # execute!/5
  #-----------------
  def execute!(:sync, this, state, context, options) do
    {arg, state} = Noizu.RuleEngine.ScriptProtocol.execute!(this.argument, state, context, options)
    {lb, state} = Noizu.RuleEngine.ScriptProtocol.execute!(this.lower_bound, state, context, options)
    {ub, state} = Noizu.RuleEngine.ScriptProtocol.execute!(this.upper_bound, state, context, options)
    perform_check!(arg, lb, ub, this, state, context, options)
  end

  def execute!(:async, this, state, context, options) do
    yield_wait = this.settings[:timeout] || options[:timeout] || 15_000
    # Async execute arguments
    tasks = [
              Task.async(fn -> Noizu.RuleEngine.ScriptProtocol.execute!(this.argument, state, context, options) |> elem(0) end),
              Task.async(fn -> Noizu.RuleEngine.ScriptProtocol.execute!(this.lower_bound, state, context, options)  |> elem(0) end),
              Task.async(fn -> Noizu.RuleEngine.ScriptProtocol.execute!(this.upper_bound, state, context, options)  |> elem(0) end),
            ]
    [arg, lb, ub] = tasks
                    |> Task.yield_many(yield_wait)
                    |> Enum.map(
                         fn({task, res}) ->
                           case res do
                             {:ok, v} -> v
                             _ ->
                               Task.shutdown(task, yield_wait)
                               {:error, {Noizu.RuleEngine.ScriptProtocol, {:timeout, task}}}
                           end
                         end)
    perform_check!(arg, lb, ub, this, state, context, options)
  end

  #-----------------
  # perform_check!/7
  #-----------------
  defp perform_check!(arg, lb, ub, this, state, context, options) do
    # Comparison Strategy
    cs = this.comparison_strategy || :default
    {lb_c, ub_c} = this.strict && {:">", :"<"} || {:">=", :"<="}

    # Perform Comparison / Error Check
    outcome = cond do
      match?({:error, {Noizu.RuleEngine.ScriptProtocol, {:timeout, _}}}, arg) -> arg
      match?({:error, {Noizu.RuleEngine.ScriptProtocol, {:timeout, _}}}, lb) -> lb
      match?({:error, {Noizu.RuleEngine.ScriptProtocol, {:timeout, _}}}, ub) -> ub
      cs == :default && this.strict -> (arg > lb && arg < ub)
      cs == :default -> (arg >= lb && arg <= ub)
      match?({_, _, 3}, cs) -> :erlang.apply(elem(cs, 0), elem(cs, 1), [lb_c, arg, lb]) && :erlang.apply(elem(cs, 0), elem(cs, 1), [ub_c, arg, ub])
      match?({_, _, 6}, cs) -> :erlang.apply(elem(cs, 0), elem(cs, 1), [lb_c, arg, lb, state, context, options]) && :erlang.apply(elem(cs, 0), elem(cs, 1), [ub_c, arg, ub, state, context, options])
      is_function(cs, 3) -> cs.(lb_c, arg, lb) && cs.(ub_c, arg, ub)
      is_function(cs, 6) -> elem(cs.(lb_c, arg, lb, state, context, options), 0) && elem(cs.(ub_c, arg, ub, state, context, options), 0)
      true -> throw Noizu.RuleEngine.Error.Basic.new("[ScriptError] - #{identifier(this, state, context, options)} Invalid comparison strategy #{inspect cs}", 404)
    end

    case outcome do
      {:error, {Noizu.RuleEngine.ScriptProtocol, {:timeout, task}}} ->
        throw Noizu.RuleEngine.Error.Basic.new("[ScriptError] - #{identifier(this, state, context, options)} Execute Child Task Failed to Complete #{inspect task}", 404)
      _ -> {outcome, state}
    end
  end

  #---------------------
  # identifier/3
  #---------------------
  def identifier(this, _state, _context), do: Helper.identifier(this)

  #---------------------
  # identifier/4
  #---------------------
  def identifier(this, _state, _context, _options), do: Helper.identifier(this)

  #---------------------
  # render/3
  #---------------------
  def render(this, state, context), do: render(this, state, context, %{})

  #---------------------
  # render/4
  #---------------------
  def render(this, state, context, options) do
    Helper.render_arg_list("BETWEEN", identifier(this, state, context, options), [this.argument, this.lower_bound, this.upper_bound], state, context, options)
  end
end