#------------------------------------------------------------------------------- # Author: Keith Brings # Copyright (C) 2018 Noizu Labs, Inc. All rights reserved. #------------------------------------------------------------------------------- defmodule Noizu.RuleEngine.Op.ComparisonOp do @type t :: %__MODULE__{ name: String.t | nil, description: String.t | nil, identifier: String.t | list | tuple, # Materialized Path. arguments: list, comparison: :"==" | :"<>" | :"<=" | :">=" | :"<" | :">", comparison_strategy: any, settings: Keyword.t, } defstruct [ name: nil, description: nil, identifier: nil, arguments: [], comparison: :"==", comparison_strategy: :default, settings: [short_circuit?: :auto, async?: :auto, throw_on_timeout?: :auto] ] end defimpl Noizu.RuleEngine.ScriptProtocol, for: Noizu.RuleEngine.Op.ComparisonOp 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 this.settings[:short_circuit?] == :required -> execute!(:short_circuit, this, state, context, options) # Ignore Async settings when short_circuit is mandatory 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) Enum.member?([true, :auto, nil], this.settings[:short_circuit?]) -> execute!(:short_circuit, this, state, context, options) true -> execute!(:all, this, state, context, options) end end def perform_comparison(left_arg, right_arg, this, state, context, options) do cs = this.comparison_strategy || :default cond do cs == :default -> c = case this.comparison do :"==" -> left_arg == right_arg :"<>" -> left_arg != right_arg :"<" -> left_arg < right_arg :">" -> left_arg > right_arg :"<=" -> left_arg <= right_arg :">=" -> left_arg >= right_arg end {c, nil} match?({_,_,3}, cs) -> {m, f, 3} = cs {:erlang.apply(m, f, [this.comparison, left_arg, right_arg]), nil} match?({_,_,6}, cs) -> {m, f, 6} = cs :erlang.apply(m, f, [this.comparison, left_arg, right_arg, state, context, options]) is_function(cs, 3) -> {cs.(this.comparison, left_arg, right_arg), nil} is_function(cs, 6) -> cs.(this.comparison, 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!(:short_circuit, this, state, context, options) do cond do length(this.arguments || []) < 2 -> throw Noizu.RuleEngine.Error.Basic.new("[ScriptError] - #{identifier(this, state, context, options)} ComparisonOp requires at least 2 arguments", 311) true -> [h|t] = this.arguments p = Noizu.RuleEngine.ScriptProtocol.execute!(h, state, context, options) # cs = this.comparison_strategy || :default {sentinel, {_o, updated_state}} = Enum.reduce(t, {true, p}, fn(child, {sentinel, {o, s}}) -> if sentinel do {c_o, c_s} = Noizu.RuleEngine.ScriptProtocol.execute!(child, s, context, options) {c, s} = perform_comparison(o, c_o, this, c_s, context, options) {c, {c_o, s || c_s}} else {sentinel, {o, s}} end end ) {sentinel, updated_state} end end def execute!(:all, this, state, context, options) do cond do length(this.arguments || []) < 2 -> throw Noizu.RuleEngine.Error.Basic.new("[ScriptError] - #{identifier(this, state, context, options)} ComparisonOp requires at least 2 arguments", 311) true -> [h|t] = this.arguments p = Noizu.RuleEngine.ScriptProtocol.execute!(h, state, context, options) #cs = this.comparison_strategy || :default {sentinel, {_o, updated_state}} = Enum.reduce(t, {true, p}, fn(child, {sentinel, {o, s}}) -> if sentinel do {c_o, c_s} = Noizu.RuleEngine.ScriptProtocol.execute!(child, s, context, options) {c, s} = perform_comparison(o, c_o, this, c_s, context, options) {c, {c_o, s || c_s}} else {_, c_s} = Noizu.RuleEngine.ScriptProtocol.execute!(child, s, context, options) {sentinel, {o, c_s}} end end ) {sentinel, updated_state} end end def execute!(:async, this, state, context, options) do cond do length(this.arguments || []) < 2 -> throw Noizu.RuleEngine.Error.Basic.new("[ScriptError] - #{identifier(this, state, context, options)} ComparisonOp requires at least 2 arguments", 311) true -> 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 || [], {true, first_arg}, fn(right_arg, {product, 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) product -> {elem(perform_comparison(left_arg, right_arg, this, state, context, options), 0), right_arg} true -> {product, nil} end end) {product, 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: 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