From 54a6d4580b9f83d35814a5c8cb49f0f1d285bc01 Mon Sep 17 00:00:00 2001 From: Varun Pai Date: Wed, 6 Mar 2024 13:49:41 -0800 Subject: [PATCH] add logic to support semver comparisons --- Gemfile | 1 + Gemfile.lock | 20 ++++++++++++-------- lib/eppo_client/rules.rb | 35 +++++++++++++++++++++++++++++++++-- lib/eppo_client/version.rb | 2 +- spec/rules_spec.rb | 19 +++++++++++++++++++ 5 files changed, 66 insertions(+), 11 deletions(-) diff --git a/Gemfile b/Gemfile index f7200f1..b6119ee 100644 --- a/Gemfile +++ b/Gemfile @@ -3,3 +3,4 @@ source 'http://rubygems.org' gemspec +gem 'semver2', '~> 3.4', '>= 3.4.2' diff --git a/Gemfile.lock b/Gemfile.lock index 6c8be57..19a5888 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - eppo-server-sdk (0.2.3) + eppo-server-sdk (0.2.4) concurrent-ruby (~> 1.1, >= 1.1.9) faraday (~> 2.7, >= 2.7.1) faraday-retry (~> 2.0, >= 2.0.0) @@ -12,18 +12,20 @@ GEM addressable (2.8.1) public_suffix (>= 2.0.2, < 6.0) ast (2.4.2) - concurrent-ruby (1.1.10) + concurrent-ruby (1.2.3) crack (0.4.5) rexml diff-lcs (1.5.0) - faraday (2.7.2) - faraday-net_http (>= 2.0, < 3.1) - ruby2_keywords (>= 0.0.4) - faraday-net_http (3.0.2) - faraday-retry (2.0.0) + faraday (2.9.0) + faraday-net_http (>= 2.0, < 3.2) + faraday-net_http (3.1.0) + net-http + faraday-retry (2.2.0) faraday (~> 2.0) hashdiff (1.0.1) jaro_winkler (1.5.6) + net-http (0.4.1) + uri parallel (1.22.1) parser (3.2.0.0) ast (~> 2.4.1) @@ -53,8 +55,9 @@ GEM ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 2.0) ruby-progressbar (1.11.0) - ruby2_keywords (0.0.5) + semver2 (3.4.2) unicode-display_width (1.8.0) + uri (0.13.0) webmock (3.18.1) addressable (>= 2.8.0) crack (>= 0.3.2) @@ -69,6 +72,7 @@ DEPENDENCIES rake (~> 13.0, >= 13.0.6) rspec (~> 3.12, >= 3.12.0) rubocop (~> 0.82.0) + semver2 (~> 3.4, >= 3.4.2) webmock (~> 3.18, >= 3.18.1) BUNDLED WITH diff --git a/lib/eppo_client/rules.rb b/lib/eppo_client/rules.rb index 15588b4..1e71e52 100644 --- a/lib/eppo_client/rules.rb +++ b/lib/eppo_client/rules.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +require 'semver' # The helper module for rules module EppoClient @@ -60,7 +61,12 @@ def evaluate_condition(subject_attributes, condition) when OperatorType::NOT_ONE_OF !condition.value.map(&:downcase).include?(subject_value.to_s.downcase) else - subject_value.is_a?(Numeric) && evaluate_numeric_condition(subject_value, condition) + # Numeric operator: value could be numeric or semver. + if subject_value.is_a?(Numeric) + evaluate_numeric_condition(subject_value, condition) + elsif valid_semver?(subject_value) + compare_semver(subject_value, condition.value, condition.operator) + end end end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength @@ -82,6 +88,31 @@ def evaluate_numeric_condition(subject_value, condition) end # rubocop:enable Metrics/MethodLength + # rubocop:disable Metrics/MethodLength + def compare_semver(attribute_value, condition_value, operator) + unless valid_semver?(attribute_value) && valid_semver?(condition_value) + return false + end + + case operator + when OperatorType::GT + SemVer.parse(attribute_value) > SemVer.parse(condition_value) + when OperatorType::GTE + SemVer.parse(attribute_value) >= SemVer.parse(condition_value) + when OperatorType::LT + SemVer.parse(attribute_value) < SemVer.parse(condition_value) + when OperatorType::LTE + SemVer.parse(attribute_value) <= SemVer.parse(condition_value) + else + false + end + end + # rubocop:enable Metrics/MethodLength + + def valid_semver?(string) + !SemVer.parse(string).nil? + end + module_function :find_matching_rule, :matches_rule, :evaluate_condition, - :evaluate_numeric_condition + :evaluate_numeric_condition, :valid_semver?, :compare_semver end diff --git a/lib/eppo_client/version.rb b/lib/eppo_client/version.rb index 43178f2..eafb08b 100644 --- a/lib/eppo_client/version.rb +++ b/lib/eppo_client/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module EppoClient - VERSION = '0.2.3' + VERSION = '0.2.4' end diff --git a/spec/rules_spec.rb b/spec/rules_spec.rb index 12a1c0f..a2bb2e0 100644 --- a/spec/rules_spec.rb +++ b/spec/rules_spec.rb @@ -16,6 +16,7 @@ rule_with_empty_conditions = EppoClient::Rule.new(allocation_key: 'allocation', conditions: []) # rubocop:disable Metrics/BlockLength +# rubocop:disable Layout/LineLength describe EppoClient::Rule do it 'tests find_matching_rule_when_no_rules_match' do subject_attributes = { 'age' => 20, 'country' => 'US' } @@ -44,6 +45,23 @@ expect(EppoClient.find_matching_rule({ 'age' => '99' }, [numeric_rule])).to be_nil end + it 'tests find matching rule for semver string' do + semver_greater_than_condition = EppoClient::Condition.new( + operator: EppoClient::OperatorType::GTE, value: '1.0.0', attribute: 'version' + ) + semver_less_than_condition = EppoClient::Condition.new( + operator: EppoClient::OperatorType::LTE, value: '2.0.0', attribute: 'version' + ) + semver_rule = EppoClient::Rule.new( + allocation_key: 'allocation', + conditions: [semver_less_than_condition, semver_greater_than_condition] + ) + + expect(EppoClient.find_matching_rule({ 'version' => '1.1.0' }, [semver_rule])).to be(semver_rule) + expect(EppoClient.find_matching_rule({ 'version' => '2.0.0' }, [semver_rule])).to be(semver_rule) + expect(EppoClient.find_matching_rule({ 'version' => '2.1.0' }, [semver_rule])).to be_nil + end + it 'tests find matching rule with numeric value and regex' do condition = EppoClient::Condition.new( operator: EppoClient::OperatorType::MATCHES, value: '[0-9]+', attribute: 'age' @@ -138,3 +156,4 @@ end end # rubocop:enable Metrics/BlockLength +# rubocop:enable Layout/LineLength