Class: LcpRuby::ConditionEvaluator

Inherits:
Object
  • Object
show all
Defined in:
lib/lcp_ruby/condition_evaluator.rb

Constant Summary collapse

MAX_NESTING_DEPTH =
20

Class Method Summary collapse

Class Method Details

.client_evaluable?(condition) ⇒ Boolean

Returns true if the condition can be evaluated client-side (flat field-value with literal value, no dot-path, no dynamic refs)

Returns:

  • (Boolean)


31
32
33
34
35
36
37
38
39
40
# File 'lib/lcp_ruby/condition_evaluator.rb', line 31

def client_evaluable?(condition)
  return false unless condition.is_a?(Hash)

  normalized = condition.transform_keys(&:to_s)
  return false unless normalized.key?("field")
  return false if normalized["value"].is_a?(Hash)
  return false if normalized["field"].to_s.include?(".")

  true
end

.condition_type(condition) ⇒ Object

Returns the condition type: :field_value, :service, :compound, :collection, or nil



14
15
16
17
18
19
20
21
22
23
24
25
26
27
# File 'lib/lcp_ruby/condition_evaluator.rb', line 14

def condition_type(condition)
  return nil unless condition.is_a?(Hash)

  normalized = condition.transform_keys(&:to_s)
  if normalized.key?("all") || normalized.key?("any") || normalized.key?("not")
    :compound
  elsif normalized.key?("collection")
    :collection
  elsif normalized.key?("field")
    :field_value
  elsif normalized.key?("service")
    :service
  end
end

.evaluate_any(record, condition, context: {}, depth: 0) ⇒ Object

Unified recursive entry point: routes to the appropriate evaluator

Raises:

  • (ArgumentError)


43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/lcp_ruby/condition_evaluator.rb', line 43

def evaluate_any(record, condition, context: {}, depth: 0)
  raise ArgumentError, "condition must be a Hash, got #{condition.class}" unless condition.is_a?(Hash)

  if depth > MAX_NESTING_DEPTH
    raise ConditionError, "condition nesting depth exceeded maximum of #{MAX_NESTING_DEPTH}"
  end

  normalized = condition.transform_keys(&:to_s)

  if normalized.key?("all")
    children = normalized["all"]
    if children.empty?
      if defined?(Rails) && Rails.respond_to?(:logger) && !Rails.env.production?
        Rails.logger.warn("[LcpRuby] Empty 'all' condition list evaluates to true (vacuous truth)")
      end
      return true
    end
    children.all? { |child| evaluate_any(record, child, context: context, depth: depth + 1) }
  elsif normalized.key?("any")
    children = normalized["any"]
    return false if children.empty?
    children.any? { |child| evaluate_any(record, child, context: context, depth: depth + 1) }
  elsif normalized.key?("not")
    child = normalized["not"]
    !evaluate_any(record, child, context: context, depth: depth + 1)
  elsif normalized.key?("collection")
    evaluate_collection(record, normalized, context: context, depth: depth)
  elsif normalized.key?("field")
    evaluate_field(record, normalized, context: context)
  elsif normalized.key?("service")
    evaluate_service(record, normalized, context: context)
  else
    raise ConditionError, "condition must contain 'field', 'service', 'all', 'any', 'not', or 'collection' key"
  end
end

.safe_regexp(pattern) ⇒ Object

Builds a Regexp with a timeout to prevent ReDoS



7
8
9
10
11
# File 'lib/lcp_ruby/condition_evaluator.rb', line 7

def safe_regexp(pattern)
  Regexp.new(pattern, timeout: 1)
rescue RegexpError
  /\A(?!)\z/
end