Module: Feat::Operators
- Defined in:
- lib/feat/operators.rb
Overview
Per-operator predicates. Defensive: type-mismatch / parse-failure returns false rather than raising — matches the JS engine’s posture against malformed contexts at the edge.
segment_match / segment_not_match are dispatched by the rule evaluator (they recurse into the datafile’s segments map), not here.
Constant Summary collapse
- REDOS_SHAPES =
ReDoS guard for matches_regex. Caps pattern length and rejects the most common catastrophic-backtracking shapes. False positives just turn the rule into a non-match, which is the safe default.
/\([^)]*[+*][^)]*\)\s*[+*]|\([^)]*\|[^)]*\)\s*[+*]/- SEMVER_RE =
/\A(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?\z/
Class Method Summary collapse
- .any_eq?(lhs, values) ⇒ Boolean
- .any_string_value?(values) ⇒ Boolean
- .compare_semver(a, b) ⇒ Object
- .date_cmp(lhs, values) ⇒ Object
-
.deep_eq(a, b) ⇒ Object
JS-engine-compatible equality with string/number coercion.
- .empty?(lhs) ⇒ Boolean
- .float_eq(num, str) ⇒ Object
- .match(operator, lhs, values) ⇒ Object
- .numeric_cmp(lhs, values) ⇒ Object
- .parse_semver(x) ⇒ Object
- .safe_regex?(pattern) ⇒ Boolean
- .semver_cmp(lhs, values) ⇒ Object
- .string_any?(lhs, values) ⇒ Boolean
- .to_number(x) ⇒ Object
- .to_time(x) ⇒ Object
Class Method Details
.any_eq?(lhs, values) ⇒ Boolean
81 82 83 |
# File 'lib/feat/operators.rb', line 81 def any_eq?(lhs, values) values.any? { |v| deep_eq(lhs, v) } end |
.any_string_value?(values) ⇒ Boolean
91 92 93 |
# File 'lib/feat/operators.rb', line 91 def any_string_value?(values) values.any? { |v| v.is_a?(String) && yield(v) } end |
.compare_semver(a, b) ⇒ Object
157 158 159 160 161 162 163 164 165 |
# File 'lib/feat/operators.rb', line 157 def compare_semver(a, b) 3.times { |i| return a[i] - b[i] if a[i] != b[i] } ap, bp = a[3], b[3] return 0 if ap == bp return 1 if ap.nil? return -1 if bp.nil? ap <=> bp end |
.date_cmp(lhs, values) ⇒ Object
115 116 117 118 119 120 121 122 123 |
# File 'lib/feat/operators.rb', line 115 def date_cmp(lhs, values) a = to_time(lhs) return false if a.nil? values.any? do |v| b = to_time(v) b && yield(a, b) end end |
.deep_eq(a, b) ⇒ Object
JS-engine-compatible equality with string/number coercion.
62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/feat/operators.rb', line 62 def deep_eq(a, b) return true if a == b if a.is_a?(Numeric) && b.is_a?(String) return a.to_s == b || float_eq(a, b) end if a.is_a?(String) && b.is_a?(Numeric) return a == b.to_s || float_eq(b, a) end false end |
.empty?(lhs) ⇒ Boolean
57 58 59 |
# File 'lib/feat/operators.rb', line 57 def empty?(lhs) lhs.nil? || lhs == "" end |
.float_eq(num, str) ⇒ Object
75 76 77 78 79 |
# File 'lib/feat/operators.rb', line 75 def float_eq(num, str) Float(str) == num.to_f rescue ArgumentError, TypeError false end |
.match(operator, lhs, values) ⇒ Object
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/feat/operators.rb', line 14 def match(operator, lhs, values) case operator when "is_one_of" then any_eq?(lhs, values) when "is_not_one_of" then !any_eq?(lhs, values) when "is_empty" then empty?(lhs) when "is_not_empty" then !empty?(lhs) when "contains" then string_any?(lhs, values) { |s, v| s.include?(v) } when "does_not_contain" return true unless lhs.is_a?(String) !string_any?(lhs, values) { |s, v| s.include?(v) } when "starts_with" then string_any?(lhs, values) { |s, v| s.start_with?(v) } when "ends_with" then string_any?(lhs, values) { |s, v| s.end_with?(v) } when "matches_regex" return false unless lhs.is_a?(String) any_string_value?(values) { |v| next false unless safe_regex?(v) begin !!(lhs =~ Regexp.new(v)) rescue RegexpError false end } when "gt" then numeric_cmp(lhs, values) { |a, b| a > b } when "gte" then numeric_cmp(lhs, values) { |a, b| a >= b } when "lt" then numeric_cmp(lhs, values) { |a, b| a < b } when "lte" then numeric_cmp(lhs, values) { |a, b| a <= b } when "before" then date_cmp(lhs, values) { |a, b| a < b } when "after" then date_cmp(lhs, values) { |a, b| a > b } when "semver_eq" then semver_cmp(lhs, values) { |c| c.zero? } when "semver_gt" then semver_cmp(lhs, values) { |c| c.positive? } when "semver_gte" then semver_cmp(lhs, values) { |c| c >= 0 } when "semver_lt" then semver_cmp(lhs, values) { |c| c.negative? } when "semver_lte" then semver_cmp(lhs, values) { |c| c <= 0 } when "segment_match", "segment_not_match" false else false end end |
.numeric_cmp(lhs, values) ⇒ Object
95 96 97 98 99 100 101 102 103 |
# File 'lib/feat/operators.rb', line 95 def numeric_cmp(lhs, values) a = to_number(lhs) return false if a.nil? values.any? do |v| b = to_number(v) b && yield(a, b) end end |
.parse_semver(x) ⇒ Object
148 149 150 151 152 153 154 155 |
# File 'lib/feat/operators.rb', line 148 def parse_semver(x) return nil unless x.is_a?(String) m = x.strip.match(SEMVER_RE) return nil if m.nil? [m[1].to_i, m[2].to_i, m[3].to_i, m[4]] end |
.safe_regex?(pattern) ⇒ Boolean
139 140 141 142 143 144 |
# File 'lib/feat/operators.rb', line 139 def safe_regex?(pattern) return false if pattern.length > 512 return false if REDOS_SHAPES.match?(pattern) true end |
.semver_cmp(lhs, values) ⇒ Object
167 168 169 170 171 172 173 174 175 |
# File 'lib/feat/operators.rb', line 167 def semver_cmp(lhs, values) a = parse_semver(lhs) return false if a.nil? values.any? do |v| b = parse_semver(v) b && yield(compare_semver(a, b)) end end |
.string_any?(lhs, values) ⇒ Boolean
85 86 87 88 89 |
# File 'lib/feat/operators.rb', line 85 def string_any?(lhs, values) return false unless lhs.is_a?(String) values.any? { |v| v.is_a?(String) && yield(lhs, v) } end |
.to_number(x) ⇒ Object
105 106 107 108 109 110 111 112 113 |
# File 'lib/feat/operators.rb', line 105 def to_number(x) case x when Numeric then x.to_f when String Float(x) end rescue ArgumentError, TypeError nil end |
.to_time(x) ⇒ Object
125 126 127 128 129 130 131 132 |
# File 'lib/feat/operators.rb', line 125 def to_time(x) case x when String then Time.iso8601(x) when Numeric then Time.at(x.to_f / 1000.0) end rescue ArgumentError nil end |