Class: Nodepile::RuleRecordEvaluator

Inherits:
Object
  • Object
show all
Defined in:
lib/nodepile/rule_eval.rb

Overview

Represents a single rule record as represernted by a KeyedArrayAccessor object. Note that this class does not rely on metadata of the KeyedArrayAccessor and doesn’t even verify that the object it represents actually contains any formulas.

Generally speaking, any field whose first character is a question mark is considered to have a calculation/formula in it. Formulas (not counting the question mark) are simply ruby expressions. The formulas in the id fields (‘_id’, ‘_links_to’, and ‘_links_from’) are given special treatment as described below.

Dynamic calculations must start with the question mark character ‘?’. They will use the Ruby language itself with a tightly constrained binding that defines one primary object simply named “v” (standing for values). That object only supports a handful of operations including:

v['fielaname']  to evaluate a field
v.include?('fieldname')  to determine whether the field exists
v[:this] to evaluate this field without having to explicitly name it

of fields. Note that blank “fields” will often return nil and also non-existent fields will be nil.

Note, that some exceptional calculation rules may be triggered if this calculation is for an id field. See the #uses_id_calcs?() method.

Defined Under Namespace

Classes: EvalFrame, HashMask

Constant Summary collapse

EDGE_ID_FIELDS =
['_links_from','_links_to'].freeze
NODE_ID_FIELDS =
['_id'].freeze
ID_FIELD_NAMES =
(NODE_ID_FIELDS + EDGE_ID_FIELDS).freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(rule_record_kaa) ⇒ RuleRecordEvaluator

Returns a new instance of RuleRecordEvaluator.

Parameters:

  • rule_record_kaa (KeyedArrayAccessor)

    This ia a rule record that contains one or more formulas in its fields as indicated by a leading question mark in either _id, _links_from, or _links_to



33
34
35
36
37
# File 'lib/nodepile/rule_eval.rb', line 33

def initialize(rule_record_kaa)
    @kaa = rule_record_kaa
    @match_fields = @kaa['_id'].nil? ? EDGE_ID_FIELDS : NODE_ID_FIELDS  # assuming it's well formed
    @match_type = nil
end

Class Method Details

.eval_calc(rule_field_defn, this_field_name, eval_against_key_value_map) ⇒ Object

Evaluate a calculation using standard logic. Formulas may use syntax to reference the values of the eval_against_key_value_map object. For example “?v[‘column X’].to_f > 17.2 ? ‘red’ : ‘black’” calcs based on ‘column X’ contents



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/nodepile/rule_eval.rb', line 93

def self.eval_calc(rule_field_defn,this_field_name,eval_against_key_value_map)
    return rule_field_defn unless rule_field_defn.start_with?('?')
    begin
        ruby_code = rule_field_defn.dup.tap{|s| s[0] = ' '} # get rid of leading question mark
        val = EvalFrame.evaluate(ruby_code,this_field_name,eval_against_key_value_map)
    rescue StandardError => e
        #TODO: Probably will need to remove this and replace with more fault
        # tolerant strategy that fails gracefully (perhaps by nil-valuing the field)
        raise "Error attempting to evaluate this formula { #{rule_field_defn} } : #{e.message}"
    end
    case val
        when true,false,nil
            #no-op
        when Regexp
            if /^?\s*\//.match?(rule_field_defn) && ID_FIELD_NAMES.include?(this_field_name)
                val = val.match?(eval_against_key_value_map[this_field_name])
            else
                raise "Rule should evaluate to a 'boolean' except when triggering Regex/glob matching on an id field."
            end
        when String
            if /^?\s*['"]/.match?(rule_field_defn) && ID_FIELD_NAMES.include?(this_field_name)
                val = File.fnmatch?(val,eval_against_key_value_map[this_field_name])
            else
                #no-op... returning a string is a good behavior for calculations
            end
        else
            raise "For field [#{this_field_name}] the rule expression must evaluate to true, false, or nil except when triggering regex/glob matching on an id field."
    end #case
    return val
end

Instance Method Details

#calculate_rule(otr) ⇒ KeyedArrayAccessor

Calculates the “value” of the rule when applied to a specific record NOTE: this does not test for #match_record?() which you probably want to do first. Note that the three id fields are ALWAYS left as nil after this method. Calculated values will be coerced to string via #to_s so if the default behavior isn’t the right one for you, you should convert to string yourself This also doesn’t test whether the records conform.

Parameters:

Returns:



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/nodepile/rule_eval.rb', line 74

def calculate_rule(otr)
    kaa = @kaa.dup
    kaa.kv_map!{|k,v|   
        if v.nil?
            #no-op
        elsif ID_FIELD_NAMES.include?(k)
            nil  # we never overlay key fields
        elsif v.start_with?('?')
            self.class.eval_calc(v,k,otr)&.to_s 
        else
            v # leave field unaltered by calculation logic
        end
       }
    return kaa
end

#match_record?(otr) ⇒ Boolean

Confirm that this particular rule applies to the given node or edge

Parameters:

Returns:

  • (Boolean)

    true if the identifying fields match. For node entities the ‘_id’ field is the key. For edges, the ‘_links_from’ and ‘_links_to’ fields are the identifying fields.



44
45
46
47
48
49
50
51
52
53
# File 'lib/nodepile/rule_eval.rb', line 44

def match_record?(otr)
    return @match_fields.all?{|key|
            myval = @kaa[key]
            if myval.start_with?('?')
                self.class.eval_calc(myval,key,otr) 
            else
                myval == otr[key]
            end
           }
end

#uses_dynamic_match?Boolean

Returns true if the calculation used by #match_record?() uses complex calculation logic

Returns:

  • (Boolean)


57
58
59
60
61
62
# File 'lib/nodepile/rule_eval.rb', line 57

def uses_dynamic_match?
    @match_type ||= @match_fields.any?{|k|
                                       @kaa[k].then{|v| v.start_with?('?') && v.include?('v[')}
                                      } ? :dynamic : :static
    return @match_type == :dynamic
end