Skip to content

Commit

Permalink
Merge 06f3775 into c745ed4
Browse files Browse the repository at this point in the history
  • Loading branch information
sighphyre committed Mar 7, 2022
2 parents c745ed4 + 06f3775 commit e50c1e1
Show file tree
Hide file tree
Showing 6 changed files with 460 additions and 24 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pull_request.yml
Expand Up @@ -33,7 +33,7 @@ jobs:
- name: Install dependencies
run: bundle install
- name: Download test cases
run: git clone --depth 5 --branch v4.0.0 https://github.com/Unleash/client-specification.git client-specification
run: git clone --depth 5 --branch v4.1.0 https://github.com/Unleash/client-specification.git client-specification
- name: rubocop
uses: reviewdog/action-rubocop@v2
with:
Expand Down
94 changes: 84 additions & 10 deletions lib/unleash/constraint.rb
@@ -1,26 +1,100 @@
require 'date'

module Unleash
class Constraint
attr_accessor :context_name, :operator, :values
attr_accessor :context_name, :operator, :value, :inverted, :case_insensitive

OPERATORS = {
IN: ->(context_value, constraint_value){ constraint_value.include? context_value },
NOT_IN: ->(context_value, constraint_value){ !constraint_value.include? context_value },
STR_STARTS_WITH: ->(context_value, constraint_value){ constraint_value.any?{ |v| context_value.start_with? v } },
STR_ENDS_WITH: ->(context_value, constraint_value){ constraint_value.any?{ |v| context_value.end_with? v } },
STR_CONTAINS: ->(context_value, constraint_value){ constraint_value.any?{ |v| context_value.include? v } },
NUM_EQ: ->(context_value, constraint_value){ on_valid_float(constraint_value, context_value){ |x, y| (x - y).abs < Float::EPSILON } },
NUM_LT: ->(context_value, constraint_value){ on_valid_float(constraint_value, context_value){ |x, y| (x > y) } },
NUM_LTE: ->(context_value, constraint_value){ on_valid_float(constraint_value, context_value){ |x, y| (x >= y) } },
NUM_GT: ->(context_value, constraint_value){ on_valid_float(constraint_value, context_value){ |x, y| (x < y) } },
NUM_GTE: ->(context_value, constraint_value){ on_valid_float(constraint_value, context_value){ |x, y| (x <= y) } },
DATE_AFTER: ->(context_value, constraint_value){ on_valid_date(constraint_value, context_value){ |x, y| (x < y) } },
DATE_BEFORE: ->(context_value, constraint_value){ on_valid_date(constraint_value, context_value){ |x, y| (x > y) } },
SEMVER_EQ: ->(context_value, constraint_value){ on_valid_version(constraint_value, context_value){ |x, y| (x == y) } },
SEMVER_GT: ->(context_value, constraint_value){ on_valid_version(constraint_value, context_value){ |x, y| (x < y) } },
SEMVER_LT: ->(context_value, constraint_value){ on_valid_version(constraint_value, context_value){ |x, y| (x > y) } }
}.freeze

VALID_OPERATORS = ['IN', 'NOT_IN'].freeze
VALID_LIST_TYPES = ["IN", "NOT_IN", "STR_STARTS_WITH", "STR_ENDS_WITH", "STR_CONTAINS"].map(&:to_sym).freeze

def initialize(context_name, operator, values = [])
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)
raise ArgumentError, "operator does not hold a valid value:" + VALID_OPERATORS unless VALID_OPERATORS.include? operator
raise ArgumentError, "values does not hold an Array" unless values.is_a?(Array)
raise ArgumentError, "operator does not hold a valid value:" + OPERATORS.keys unless OPERATORS.include? operator.to_sym

self.validate_constraint_value_type(operator.to_sym, value)

self.context_name = context_name
self.operator = operator
self.values = values
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? values: #{self.values} context.get_by_name(#{self.context_name})" \
Unleash.logger.debug "Unleash::Constraint matches_context? value: #{self.value} context.get_by_name(#{self.context_name})" \
" #{context.get_by_name(self.context_name)} "
match = matches_constraint?(context)
self.inverted ? !match : match
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. This will always return 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. This will always return 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. This will always return false."
false
end

# This should be a private method but for some reason this fails on Ruby 2.5
def validate_constraint_value_type(operator, value)
raise ArgumentError, "context_name is not an Array" if VALID_LIST_TYPES.include?(operator) && value.is_a?(String)
raise ArgumentError, "context_name is not a String" if !VALID_LIST_TYPES.include?(operator) && value.is_a?(Array)
end

private

def matches_constraint?(context)
unless OPERATORS.include?(self.operator)
Unleash.logger.warn "Invalid constraint operator: #{self.operator}, this should be unreachable. Always returning false."
false
end

v = self.value.dup
context_value = context.get_by_name(self.context_name)

is_included = self.values.include? context.get_by_name(self.context_name)
v.map!(&:upcase) if self.case_insensitive
context_value.upcase! if self.case_insensitive

operator == 'IN' ? is_included : !is_included
OPERATORS[self.operator].call(context_value, v)
end
end
end
3 changes: 2 additions & 1 deletion lib/unleash/context.rb
@@ -1,6 +1,6 @@
module Unleash
class Context
ATTRS = [:app_name, :environment, :user_id, :session_id, :remote_address].freeze
ATTRS = [:app_name, :environment, :user_id, :session_id, :remote_address, :current_time].freeze

attr_accessor(*[ATTRS, :properties].flatten)

Expand All @@ -12,6 +12,7 @@ def initialize(params = {})
self.user_id = value_for('userId', params)
self.session_id = value_for('sessionId', params)
self.remote_address = value_for('remoteAddress', params)
self.current_time = value_for('currentTime', params, Time.now.utc.iso8601.to_s)

properties = value_for('properties', params)
self.properties = properties.is_a?(Hash) ? properties.transform_keys(&:to_sym) : {}
Expand Down
4 changes: 3 additions & 1 deletion lib/unleash/feature_toggle.rb
Expand Up @@ -139,7 +139,9 @@ def initialize_strategies(params)
Constraint.new(
c.fetch('contextName'),
c.fetch('operator'),
c.fetch('values')
c.fetch('values', nil) || c.fetch('value', nil),
inverted: c.fetch('inverted', false),
case_insensitive: c.fetch('caseInsensitive', false)
)
end
)
Expand Down

0 comments on commit e50c1e1

Please sign in to comment.