lib/rule_engine/operation/arithmetic_op.ex

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

defmodule Noizu.RuleEngine.Op.ArithmeticOp do
  @type t :: %__MODULE__{
               name: String.t | nil,
               description: String.t | nil,
               identifier: String.t | list | tuple, # Materialized Path.
               arguments: list,
               operation: :"+" | :"-" | :"*" | :"/" | :"^",
               arithmetic_strategy: any,
               settings: Keyword.t,
             }

  defstruct [
    name: nil,
    description: nil,
    identifier: nil,
    arguments: [],
    operation: :"+",
    arithmetic_strategy: :default,
    settings: [short_circuit?: :auto, async?: :auto, throw_on_timeout?: :auto]
  ]
end

defimpl Noizu.RuleEngine.ScriptProtocol, for: Noizu.RuleEngine.Op.ArithmeticOp 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

  def perform_arithmetic(left_arg, right_arg, this, state, context, options) do
    cs = this.arithmetic_strategy || :default
    cond do
      cs == :default ->
        c = case this.operation do
          :"+" -> left_arg + right_arg
          :"-" -> left_arg - right_arg
          :"*" -> left_arg * right_arg
          :"/" -> left_arg / right_arg
          :"^" -> :math.pow(left_arg, right_arg)
        end
        {c, nil}
      match?({_,_,3}, cs) ->
        {m,f,3} = cs
        {:erlang.apply(m, f, [this.operation, left_arg, right_arg]), nil}
      match?({_,_,6}, cs) ->
        {m,f,6} = cs
        :erlang.apply(m, f, [this.operation, left_arg, right_arg, state, context, options])
      is_function(cs, 3) -> {cs.(this.operation, left_arg, right_arg), nil}
      is_function(cs, 6) -> cs.(this.operation, left_arg, right_arg, state, context, options)
      true -> throw Noizu.RuleEngine.Error.Basic.new("[ScriptError] - #{identifier(this, state, context, options)} Invalid comparison strategy #{inspect cs}", 404)
    end
  end

  #-----------------
  # execute!/5
  #-----------------
  def execute!(:sync, this, state, context, options) do
    [h|t] = this.arguments
    {first_arg, state} = Noizu.RuleEngine.ScriptProtocol.execute!(h, state, context, options)
    #cs = this.arithmetic_strategy || :default
    {product, state} = Enum.reduce(t, {first_arg, state},
      fn(child, {left_arg, state}) ->
          {right_arg, state} = Noizu.RuleEngine.ScriptProtocol.execute!(child, state, context, options)
          {product, s} = perform_arithmetic(left_arg, right_arg, this, state, context, options)
          {product, s || state}
      end
    )
    {product, state}
  end

  def execute!(:async, this, state, context, options) do
    yield_wait = this.settings[:timeout] || options[:timeout] || 15_000
    [first_arg|remaining_args] = this.arguments
               |> Enum.map(fn(child) -> Task.async(fn -> (Noizu.RuleEngine.ScriptProtocol.execute!(child, state, context, options)) end) end)
               |> Task.yield_many(yield_wait)
               |> Enum.map(
                    fn({task, res}) ->
                      case res do
                        {:ok, {o, _s}} -> o
                        _ ->
                          Task.shutdown(task, yield_wait)
                          {:error, {Noizu.RuleEngine.ScriptProtocol, {:timeout, task}}}
                      end
                    end)

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

    {product, _} = Enum.reduce(remaining_args || [], first_arg,
      fn(right_arg, left_arg) ->
        cond do
          match?({:error, {Noizu.RuleEngine.ScriptProtocol, {:timeout, _}}}, right_arg) ->
            {:error, {Noizu.RuleEngine.ScriptProtocol, {:timeout, task}}} = right_arg
            throw Noizu.RuleEngine.Error.Basic.new("[ScriptError] - #{identifier(this, state, context, options)} Execute Child Task Failed to Complete #{inspect task}", 404)
          true -> elem(perform_arithmetic(left_arg, right_arg, this, state, context, options), 0)
        end
      end)
    {product, state}
  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: Helper.render_arg_list("CMP #{this.comparison}", identifier(this, state, context), this.arguments || [], state, context, %{})

  #---------------------
  # render/4
  #---------------------
  def render(this, state, context, options), do: Helper.render_arg_list("CMP #{this.comparison}", identifier(this, state, context, options), this.arguments || [], state, context, options)
end