Module: Ruby::Rego::Evaluator::OperatorEvaluator

Defined in:
lib/ruby/rego/evaluator/operator_evaluator.rb

Overview

Shared operator application helpers. rubocop:disable Metrics/ModuleLength

Constant Summary collapse

EQUALITY_OPERATORS =
{
  eq: lambda do |lhs, rhs|
    return UndefinedValue.new if lhs.is_a?(UndefinedValue) || rhs.is_a?(UndefinedValue)

    BooleanValue.new(lhs == rhs)
  end,
  neq: lambda do |lhs, rhs|
    return UndefinedValue.new if lhs.is_a?(UndefinedValue) || rhs.is_a?(UndefinedValue)

    BooleanValue.new(lhs != rhs)
  end
}.freeze
LOGICAL_OPERATORS =
{
  and: ->(lhs, rhs) { BooleanValue.new(lhs.truthy? && rhs.truthy?) },
  or: ->(lhs, rhs) { BooleanValue.new(lhs.truthy? || rhs.truthy?) }
}.freeze
COMPARISON_OPERATORS =
{
  lt: ->(lhs, rhs) { lhs < rhs },
  lte: ->(lhs, rhs) { lhs <= rhs },
  gt: ->(lhs, rhs) { lhs > rhs },
  gte: ->(lhs, rhs) { lhs >= rhs }
}.freeze
ARITHMETIC_OPERATORS =

div is always big.Float in OPA (5 / 2 -> 2.5), and mod is integer-only and otherwise undefined (5.5 % 2 -> undefined) with Go’s truncated remainder; Number centralizes both so they match OPA regardless of operand type. plus/minus/mult use the operands’ own numeric arithmetic: a Number routes through the big.Float engine, but two Ruby Integers stay EXACT. OPA instead rounds every operation (even integer+integer) through a 64-bit big.Float, so 2^64 + 1 is 2^64 in OPA but exact here — a deferred divergence for integer arithmetic past 2^64 (unchanged from before the Number model; routing all integer arithmetic through the engine is the builtin sweep).

{
  plus: ->(lhs, rhs) { lhs + rhs },
  minus: ->(lhs, rhs) { lhs - rhs },
  mult: ->(lhs, rhs) { lhs * rhs },
  div: ->(lhs, rhs) { Number.div(lhs, rhs) },
  mod: ->(lhs, rhs) { Number.modulo(lhs, rhs) }
}.freeze
MEMBERSHIP_OPERATORS =
{
  in: ->(lhs, rhs) { membership_value(lhs, rhs) }
}.freeze
UNARY_OPERATORS =
{
  not: ->(operand) { BooleanValue.new(!operand.truthy?) },
  minus: lambda do |operand|
    number = numeric_value(operand)
    number ? Value.from_ruby(-number) : UndefinedValue.new
  end
}.freeze

Class Method Summary collapse

Class Method Details

.apply(operator, left, right) ⇒ Value

rubocop:disable Metrics/MethodLength

Parameters:

Returns:



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/ruby/rego/evaluator/operator_evaluator.rb', line 61

def self.apply(operator, left, right)
  handlers = [
    -> { apply_logical(operator, left, right) },
    -> { EQUALITY_OPERATORS[operator]&.call(left, right) },
    -> { MEMBERSHIP_OPERATORS[operator]&.call(left, right) },
    -> { apply_comparison(operator, left, right) },
    -> { apply_arithmetic(operator, left, right) }
  ]

  handlers.each do |handler|
    value = handler.call
    return value if value
  end

  UndefinedValue.new
end

.apply_arithmetic(operator, left, right) ⇒ Object



101
102
103
104
105
106
# File 'lib/ruby/rego/evaluator/operator_evaluator.rb', line 101

def self.apply_arithmetic(operator, left, right)
  arithmetic = ARITHMETIC_OPERATORS[operator]
  return unless arithmetic

  arithmetic_values(operator, left, right, &arithmetic)
end

.apply_comparison(operator, left, right) ⇒ Object



94
95
96
97
98
99
# File 'lib/ruby/rego/evaluator/operator_evaluator.rb', line 94

def self.apply_comparison(operator, left, right)
  comparison = COMPARISON_OPERATORS[operator]
  return unless comparison

  compare_values(left, right, &comparison)
end

.apply_logical(operator, left, right) ⇒ Object



89
90
91
92
# File 'lib/ruby/rego/evaluator/operator_evaluator.rb', line 89

def self.apply_logical(operator, left, right)
  handler = LOGICAL_OPERATORS[operator]
  handler&.call(left, right)
end

.apply_unary(operator, operand) ⇒ Value

Parameters:

  • operator (Symbol)
  • operand (Value)

Returns:



82
83
84
85
86
87
# File 'lib/ruby/rego/evaluator/operator_evaluator.rb', line 82

def self.apply_unary(operator, operand)
  handler = UNARY_OPERATORS[operator]
  return UndefinedValue.new unless handler

  handler.call(operand)
end

.arithmetic_values(operator, left, right) ⇒ Object



123
124
125
126
127
128
129
130
131
132
133
# File 'lib/ruby/rego/evaluator/operator_evaluator.rb', line 123

def self.arithmetic_values(operator, left, right)
  left_value = numeric_value(left)
  right_value = numeric_value(right)
  return UndefinedValue.new unless left_value && right_value
  return UndefinedValue.new if division_by_zero?(operator, right_value)

  result = yield(left_value, right_value)
  return UndefinedValue.new if result.nil? # e.g. mod on a non-integer operand

  Value.from_ruby(result)
end

.collection_values(value) ⇒ Object

:reek:DuplicateMethodCall



160
161
162
163
164
165
166
167
168
# File 'lib/ruby/rego/evaluator/operator_evaluator.rb', line 160

def self.collection_values(value)
  return UndefinedValue.new unless value.is_a?(ArrayValue) || value.is_a?(SetValue) || value.is_a?(ObjectValue)

  collection = value.value
  return collection if value.is_a?(ArrayValue)
  return collection.to_a if value.is_a?(SetValue)

  collection.keys.map { |key| Value.from_ruby(key) }
end

.comparable?(left_value, right_value) ⇒ Boolean

Returns:

  • (Boolean)


118
119
120
121
# File 'lib/ruby/rego/evaluator/operator_evaluator.rb', line 118

def self.comparable?(left_value, right_value)
  (left_value.is_a?(Numeric) && right_value.is_a?(Numeric)) ||
    (left_value.is_a?(String) && right_value.is_a?(String))
end

.compare_values(left, right) ⇒ Object



108
109
110
111
112
113
114
115
116
# File 'lib/ruby/rego/evaluator/operator_evaluator.rb', line 108

def self.compare_values(left, right)
  left_value = left.to_ruby
  right_value = right.to_ruby
  return UndefinedValue.new unless comparable?(left_value, right_value)

  BooleanValue.new(yield(left_value, right_value))
rescue ArgumentError
  UndefinedValue.new
end

.division_by_zero?(operator, right_value) ⇒ Boolean

Returns:

  • (Boolean)


135
136
137
# File 'lib/ruby/rego/evaluator/operator_evaluator.rb', line 135

def self.division_by_zero?(operator, right_value)
  %i[div mod].include?(operator) && right_value.zero?
end

.membership_value(lhs, rhs) ⇒ Object



146
147
148
149
150
151
152
153
# File 'lib/ruby/rego/evaluator/operator_evaluator.rb', line 146

def self.membership_value(lhs, rhs)
  return UndefinedValue.new if undefined_operand?(lhs, rhs)

  values = collection_values(rhs)
  return values if values.is_a?(UndefinedValue)

  BooleanValue.new(values.any? { |element| element == lhs })
end

.numeric_value(value) ⇒ Object



139
140
141
142
143
144
# File 'lib/ruby/rego/evaluator/operator_evaluator.rb', line 139

def self.numeric_value(value)
  ruby = value.to_ruby
  return ruby if ruby.is_a?(Numeric)

  nil
end

.undefined_operand?(lhs, rhs) ⇒ Boolean

Returns:

  • (Boolean)


155
156
157
# File 'lib/ruby/rego/evaluator/operator_evaluator.rb', line 155

def self.undefined_operand?(lhs, rhs)
  lhs.is_a?(UndefinedValue) || rhs.is_a?(UndefinedValue)
end