/
constraint.rb
115 lines (97 loc) · 5.29 KB
/
constraint.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
require 'date'
module Unleash
class Constraint
attr_accessor :context_name, :operator, :value, :inverted, :case_insensitive
OPERATORS = {
IN: ->(context_v, constraint_v){ constraint_v.include? context_v.to_s },
NOT_IN: ->(context_v, constraint_v){ !constraint_v.include? context_v.to_s },
STR_STARTS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.start_with? v } },
STR_ENDS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.end_with? v } },
STR_CONTAINS: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.include? v } },
NUM_EQ: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x - y).abs < Float::EPSILON } },
NUM_LT: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x > y) } },
NUM_LTE: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x >= y) } },
NUM_GT: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x < y) } },
NUM_GTE: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x <= y) } },
DATE_AFTER: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x < y) } },
DATE_BEFORE: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x > y) } },
SEMVER_EQ: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x == y) } },
SEMVER_GT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x < y) } },
SEMVER_LT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x > y) } },
FALLBACK_VALIDATOR: ->(_context_v, _constraint_v){ false }
}.freeze
LIST_OPERATORS = [:IN, :NOT_IN, :STR_STARTS_WITH, :STR_ENDS_WITH, :STR_CONTAINS].freeze
def initialize(context_name, operator, value = [], inverted: false, case_insensitive: false)
raise ArgumentError, "context_name is not a String" unless context_name.is_a?(String)
unless OPERATORS.include? operator.to_sym
Unleash.logger.warn "Operator #{operator} is not a supported operator, " \
"falling back to FALLBACK_VALIDATOR which skips this constraint."
operator = "FALLBACK_VALIDATOR"
end
self.log_inconsistent_constraint_configuration(operator.to_sym, value)
self.context_name = context_name
self.operator = operator.to_sym
self.value = value
self.inverted = !!inverted
self.case_insensitive = !!case_insensitive
end
def matches_context?(context)
Unleash.logger.debug "Unleash::Constraint matches_context? value: #{self.value} context.get_by_name(#{self.context_name})"
return false if context.nil?
match = matches_constraint?(context)
self.inverted ? !match : match
rescue KeyError
Unleash.logger.warn "Attemped to resolve a context key during constraint resolution: #{self.context_name} but it wasn't \
found on the context"
false
end
def self.on_valid_date(val1, val2)
val1 = DateTime.parse(val1)
val2 = DateTime.parse(val2)
yield(val1, val2)
rescue ArgumentError
Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
or constraint_value (#{val2}) into a date. Returning false!"
false
end
def self.on_valid_float(val1, val2)
val1 = Float(val1)
val2 = Float(val2)
yield(val1, val2)
rescue ArgumentError
Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
or constraint_value (#{val2}) into a number. Returning false!"
false
end
def self.on_valid_version(val1, val2)
val1 = Gem::Version.new(val1)
val2 = Gem::Version.new(val2)
yield(val1, val2)
rescue ArgumentError
Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
or constraint_value (#{val2}) into a version. Return false!"
false
end
# This should be a private method but for some reason this fails on Ruby 2.5
def log_inconsistent_constraint_configuration(operator, value)
Unleash.logger.warn "value is a String, operator is expecting an Array" if LIST_OPERATORS.include?(operator) && value.is_a?(String)
Unleash.logger.warn "value is an Array, operator is expecting a String" if !LIST_OPERATORS.include?(operator) && value.is_a?(Array)
end
private
def matches_constraint?(context)
Unleash.logger.debug "Unleash::Constraint matches_constraint? value: #{self.value} operator: #{self.operator} " \
" context.get_by_name(#{self.context_name})"
unless OPERATORS.include?(self.operator)
Unleash.logger.warn "Invalid constraint operator: #{self.operator}, this should be unreachable. Always returning false."
false
end
# when the operator is NOT_IN and there is no data, return true. In all other cases the operator doesn't match.
return self.operator == :NOT_IN unless context.include?(self.context_name)
v = self.value.dup
context_value = context.get_by_name(self.context_name)
v.map!(&:upcase) if self.case_insensitive
context_value.upcase! if self.case_insensitive
OPERATORS[self.operator].call(context_value, v)
end
end
end