From 606444d7fefcdf472fa98ca530a5dfd29f6ba26b Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Tue, 31 Aug 2021 15:13:39 -0400 Subject: [PATCH 001/176] Make single rules work for memory adapter --- Guardfile | 1 + lib/flipper.rb | 1 + lib/flipper/actor.rb | 9 +- lib/flipper/adapter.rb | 1 + lib/flipper/adapters/memory.rb | 2 + lib/flipper/feature.rb | 1 + lib/flipper/gate.rb | 1 + lib/flipper/gate_values.rb | 4 + lib/flipper/gates/rule.rb | 38 +++++ lib/flipper/rule.rb | 80 ++++++++++ spec/flipper/rule_spec.rb | 246 +++++++++++++++++++++++++++++++ spec/flipper_integration_spec.rb | 22 +++ 12 files changed, 403 insertions(+), 3 deletions(-) create mode 100644 lib/flipper/gates/rule.rb create mode 100644 lib/flipper/rule.rb create mode 100644 spec/flipper/rule_spec.rb diff --git a/Guardfile b/Guardfile index 2f99e2e45..ad6c9b856 100644 --- a/Guardfile +++ b/Guardfile @@ -16,6 +16,7 @@ rspec_options = { guard 'rspec', rspec_options do watch(%r{^spec/.+_spec\.rb$}) watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } + watch('lib/flipper/rule.rb') { 'spec/flipper_integration_spec.rb' } watch('lib/flipper/ui/middleware.rb') { 'spec/flipper/ui_spec.rb' } watch('lib/flipper/api/middleware.rb') { 'spec/flipper/api_spec.rb' } watch(/shared_adapter_specs\.rb$/) { 'spec' } diff --git a/lib/flipper.rb b/lib/flipper.rb index f7b256424..71e0bf0fc 100644 --- a/lib/flipper.rb +++ b/lib/flipper.rb @@ -156,6 +156,7 @@ def groups_registry=(registry) require 'flipper/middleware/memoizer' require 'flipper/middleware/setup_env' require 'flipper/registry' +require 'flipper/rule' require 'flipper/type' require 'flipper/types/actor' require 'flipper/types/boolean' diff --git a/lib/flipper/actor.rb b/lib/flipper/actor.rb index e434ecbed..f2f24a5e0 100644 --- a/lib/flipper/actor.rb +++ b/lib/flipper/actor.rb @@ -2,14 +2,17 @@ # to Flipper::Feature#enabled?. module Flipper class Actor - attr_reader :flipper_id + attr_reader :flipper_id, :flipper_properties - def initialize(flipper_id) + def initialize(flipper_id, flipper_properties = {}) @flipper_id = flipper_id + @flipper_properties = flipper_properties.merge("flipper_id" => flipper_id) end def eql?(other) - self.class.eql?(other.class) && @flipper_id == other.flipper_id + self.class.eql?(other.class) && + @flipper_id == other.flipper_id && + @flipper_properties == other.flipper_properties end alias_method :==, :eql? end diff --git a/lib/flipper/adapter.rb b/lib/flipper/adapter.rb index 3d4737b63..39f1563d4 100644 --- a/lib/flipper/adapter.rb +++ b/lib/flipper/adapter.rb @@ -16,6 +16,7 @@ def default_config boolean: nil, groups: Set.new, actors: Set.new, + rules: Set.new, percentage_of_actors: nil, percentage_of_time: nil, } diff --git a/lib/flipper/adapters/memory.rb b/lib/flipper/adapters/memory.rb index 0c53207c3..24fe7f5b7 100644 --- a/lib/flipper/adapters/memory.rb +++ b/lib/flipper/adapters/memory.rb @@ -71,6 +71,8 @@ def enable(feature, gate, thing) @source[feature.key][gate.key] = thing.value.to_s when :set @source[feature.key][gate.key] << thing.value.to_s + when :json + @source[feature.key][gate.key] << thing.value else raise "#{gate} is not supported by this adapter yet" end diff --git a/lib/flipper/feature.rb b/lib/flipper/feature.rb index a43e212d1..c71ba3d13 100644 --- a/lib/flipper/feature.rb +++ b/lib/flipper/feature.rb @@ -342,6 +342,7 @@ def gates @gates ||= [ Gates::Boolean.new, Gates::Actor.new, + Gates::Rule.new, Gates::PercentageOfActors.new, Gates::PercentageOfTime.new, Gates::Group.new, diff --git a/lib/flipper/gate.rb b/lib/flipper/gate.rb index da7af2e55..6997f13fa 100644 --- a/lib/flipper/gate.rb +++ b/lib/flipper/gate.rb @@ -59,3 +59,4 @@ def inspect require 'flipper/gates/group' require 'flipper/gates/percentage_of_actors' require 'flipper/gates/percentage_of_time' +require 'flipper/gates/rule' diff --git a/lib/flipper/gate_values.rb b/lib/flipper/gate_values.rb index 71967244c..3ed7b0680 100644 --- a/lib/flipper/gate_values.rb +++ b/lib/flipper/gate_values.rb @@ -9,6 +9,7 @@ class GateValues 'boolean' => '@boolean', 'actors' => '@actors', 'groups' => '@groups', + 'rules' => '@rules', 'percentage_of_time' => '@percentage_of_time', 'percentage_of_actors' => '@percentage_of_actors', }.freeze @@ -16,6 +17,7 @@ class GateValues attr_reader :boolean attr_reader :actors attr_reader :groups + attr_reader :rules attr_reader :percentage_of_actors attr_reader :percentage_of_time @@ -23,6 +25,7 @@ def initialize(adapter_values) @boolean = Typecast.to_boolean(adapter_values[:boolean]) @actors = Typecast.to_set(adapter_values[:actors]) @groups = Typecast.to_set(adapter_values[:groups]) + @rules = Typecast.to_set(adapter_values[:rules]) @percentage_of_actors = Typecast.to_percentage(adapter_values[:percentage_of_actors]) @percentage_of_time = Typecast.to_percentage(adapter_values[:percentage_of_time]) end @@ -38,6 +41,7 @@ def eql?(other) boolean == other.boolean && actors == other.actors && groups == other.groups && + rules == other.rules && percentage_of_actors == other.percentage_of_actors && percentage_of_time == other.percentage_of_time end diff --git a/lib/flipper/gates/rule.rb b/lib/flipper/gates/rule.rb new file mode 100644 index 000000000..670c438f1 --- /dev/null +++ b/lib/flipper/gates/rule.rb @@ -0,0 +1,38 @@ +module Flipper + module Gates + class Rule < Gate + # Internal: The name of the gate. Used for instrumentation, etc. + def name + :rule + end + + # Internal: Name converted to value safe for adapter. + def key + :rules + end + + def data_type + :json + end + + def enabled?(value) + !value.empty? + end + + # Internal: Checks if the gate is open for a thing. + # + # Returns true if gate open for thing, false if not. + def open?(context) + rules = context.values[key] + rules.any? { |hash| + rule = Flipper::Rule.from_hash(hash) + rule.open?(context.feature_name, context.thing) + } + end + + def protects?(thing) + thing.is_a?(Flipper::Rule) + end + end + end +end diff --git a/lib/flipper/rule.rb b/lib/flipper/rule.rb new file mode 100644 index 000000000..0d3da49ee --- /dev/null +++ b/lib/flipper/rule.rb @@ -0,0 +1,80 @@ +module Flipper + class Rule + + def self.from_hash(hash) + value = hash.fetch("value") + new(value.fetch("left"), value.fetch("operator"), value.fetch("right")) + end + + def initialize(left, operator, right) + @left = left + @operator = operator + @right = right + end + + def value + { + "type" => "Rule", + "value" => { + "left" => @left, + "operator" => @operator, + "right" => @right, + } + } + end + + def open?(feature_name, actor) + attributes = actor.flipper_properties + left_value = evaluate(@left, attributes) + right_value = evaluate(@right, attributes) + operator_name = @operator.fetch("value") + + !! case operator_name + when "eq" + left_value == right_value + when "neq" + left_value != right_value + when "gt" + left_value && right_value && left_value > right_value + when "gte" + left_value && right_value && left_value >= right_value + when "lt" + left_value && right_value && left_value < right_value + when "lte" + left_value && right_value && left_value <= right_value + when "in" + left_value && right_value && right_value.include?(left_value) + when "nin" + left_value && right_value && !right_value.include?(left_value) + when "percentage" + # this is to support up to 3 decimal places in percentages + scaling_factor = 1_000 + id = "#{feature_name}#{left_value}" + left_value && right_value && (Zlib.crc32(id) % (100 * scaling_factor) < right_value * scaling_factor) + else + raise "operator not implemented: #{operator_name}" + end + end + + private + + def evaluate(hash, attributes) + type = hash.fetch("type") + + case type + when "property" + attributes[hash.fetch("value")] + when "array" + hash.fetch("value") + when "string" + hash.fetch("value") + when "random" + rand hash.fetch("value") + when "integer" + hash.fetch("value") + else + raise "type not found: #{type.inspect}" + end + end + end +end diff --git a/spec/flipper/rule_spec.rb b/spec/flipper/rule_spec.rb new file mode 100644 index 000000000..1aa4a2b25 --- /dev/null +++ b/spec/flipper/rule_spec.rb @@ -0,0 +1,246 @@ +require 'helper' + +RSpec.describe Flipper::Rule do + let(:feature_name) { "search" } + + describe "#open?" do + context "eq" do + let(:rule) { + Flipper::Rule.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "basic"} + ) + } + + it "returns true when property matches" do + actor = Flipper::Actor.new("User;1", { + "plan" => "basic", + }) + expect(rule.open?(feature_name, actor)).to be(true) + end + + it "returns false when property does not match" do + actor = Flipper::Actor.new("User;1", { + "plan" => "premium", + }) + expect(rule.open?(feature_name, actor)).to be(false) + end + end + + context "neq" do + let(:rule) { + Flipper::Rule.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "neq"}, + {"type" => "string", "value" => "basic"} + ) + } + + it "returns true when property does NOT match" do + actor = Flipper::Actor.new("User;1", { + "plan" => "premium", + }) + expect(rule.open?(feature_name, actor)).to be(true) + end + + it "returns false when property does match" do + actor = Flipper::Actor.new("User;1", { + "plan" => "basic", + }) + expect(rule.open?(feature_name, actor)).to be(false) + end + end + + context "gt" do + let(:rule) { + Flipper::Rule.new( + {"type" => "property", "value" => "age"}, + {"type" => "operator", "value" => "gt"}, + {"type" => "integer", "value" => 20} + ) + } + + it "returns true when property matches" do + actor = Flipper::Actor.new("User;1", { + "age" => 21, + }) + expect(rule.open?(feature_name, actor)).to be(true) + end + + it "returns false when property does NOT match" do + actor = Flipper::Actor.new("User;1", { + "age" => 20, + }) + expect(rule.open?(feature_name, actor)).to be(false) + end + end + + context "gte" do + let(:rule) { + Flipper::Rule.new( + {"type" => "property", "value" => "age"}, + {"type" => "operator", "value" => "gte"}, + {"type" => "integer", "value" => 20} + ) + } + + it "returns true when property matches" do + actor = Flipper::Actor.new("User;1", { + "age" => 20, + }) + expect(rule.open?(feature_name, actor)).to be(true) + end + + it "returns false when property does NOT match" do + actor = Flipper::Actor.new("User;1", { + "age" => 19, + }) + expect(rule.open?(feature_name, actor)).to be(false) + end + end + + context "lt" do + let(:rule) { + Flipper::Rule.new( + {"type" => "property", "value" => "age"}, + {"type" => "operator", "value" => "lt"}, + {"type" => "integer", "value" => 21} + ) + } + + it "returns true when property matches" do + actor = Flipper::Actor.new("User;1", { + "age" => 20, + }) + expect(rule.open?(feature_name, actor)).to be(true) + end + + it "returns false when property does NOT match" do + actor = Flipper::Actor.new("User;1", { + "age" => 21, + }) + expect(rule.open?(feature_name, actor)).to be(false) + end + end + + context "lt with rand type" do + let(:rule) { + Flipper::Rule.new( + {"type" => "random", "value" => 100}, + {"type" => "operator", "value" => "lt"}, + {"type" => "integer", "value" => 25} + ) + } + + it "returns true when property matches" do + results = [] + (1..1000).to_a.each do |n| + actor = Flipper::Actor.new("User;#{n}") + results << rule.open?(feature_name, actor) + end + + enabled, disabled = results.partition { |r| r } + expect(enabled.size).to be_within(10).of(250) + end + end + + context "lte" do + let(:rule) { + Flipper::Rule.new( + {"type" => "property", "value" => "age"}, + {"type" => "operator", "value" => "lte"}, + {"type" => "integer", "value" => 21} + ) + } + + it "returns true when property matches" do + actor = Flipper::Actor.new("User;1", { + "age" => 21, + }) + expect(rule.open?(feature_name, actor)).to be(true) + end + + it "returns false when property does NOT match" do + actor = Flipper::Actor.new("User;1", { + "age" => 22, + }) + expect(rule.open?(feature_name, actor)).to be(false) + end + end + + context "in" do + let(:rule) { + Flipper::Rule.new( + {"type" => "property", "value" => "age"}, + {"type" => "operator", "value" => "in"}, + {"type" => "array", "value" => [20, 21, 22]} + ) + } + + it "returns true when property matches" do + actor = Flipper::Actor.new("User;1", { + "age" => 21, + }) + expect(rule.open?(feature_name, actor)).to be(true) + end + + it "returns false when property does NOT match" do + actor = Flipper::Actor.new("User;1", { + "age" => 10, + }) + expect(rule.open?(feature_name, actor)).to be(false) + end + end + + context "nin" do + let(:rule) { + Flipper::Rule.new( + {"type" => "property", "value" => "age"}, + {"type" => "operator", "value" => "nin"}, + {"type" => "array", "value" => [20, 21, 22]} + ) + } + + it "returns true when property matches" do + actor = Flipper::Actor.new("User;1", { + "age" => 10, + }) + expect(rule.open?(feature_name, actor)).to be(true) + end + + it "returns false when property does NOT match" do + actor = Flipper::Actor.new("User;1", { + "age" => 20, + }) + expect(rule.open?(feature_name, actor)).to be(false) + end + end + + context "percentage" do + let(:rule) { + Flipper::Rule.new( + {"type" => "property", "value" => "flipper_id"}, + {"type" => "operator", "value" => "percentage"}, + {"type" => "integer", "value" => 25} + ) + } + + it "returns true when property matches" do + results = [] + (1..1000).to_a.each do |n| + actor = Flipper::Actor.new("User;#{n}") + results << rule.open?(feature_name, actor) + end + + enabled, disabled = results.partition { |r| r } + expect(enabled.size).to be_within(10).of(250) + end + + it "returns false when property does NOT match" do + actor = Flipper::Actor.new("User;1") + expect(rule.open?(feature_name, actor)).to be(false) + end + end + end +end diff --git a/spec/flipper_integration_spec.rb b/spec/flipper_integration_spec.rb index 45939e012..b598935c3 100644 --- a/spec/flipper_integration_spec.rb +++ b/spec/flipper_integration_spec.rb @@ -22,6 +22,17 @@ double 'Non Flipper Thing', flipper_id: 1, admin?: nil, dev?: false end + let(:basic_plan_thing) do + double 'Non Flipper Thing', flipper_id: 1, flipper_properties: { + "plan" => "basic", + } + end + let(:premium_plan_thing) do + double 'Non Flipper Thing', flipper_id: 10, flipper_properties: { + "plan" => "premium", + } + end + let(:pitt) { Flipper::Actor.new(1) } let(:clooney) { Flipper::Actor.new(10) } @@ -538,4 +549,15 @@ expect(feature.enabled?(dev_thing)).to eq(false) end end + + it "works" do + rule = Flipper::Rule.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "basic"} + ) + feature.enable rule + + expect(feature.enabled?(basic_plan_thing)).to be(true) + end end From ec27cb73018ef05b4b543dadd09e455de775939c Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Tue, 31 Aug 2021 15:50:59 -0400 Subject: [PATCH 002/176] Make any and all work :party: --- lib/flipper.rb | 2 + lib/flipper/all.rb | 18 +++++ lib/flipper/any.rb | 18 +++++ lib/flipper/gates/rule.rb | 4 +- lib/flipper/rule.rb | 15 ++++- spec/flipper_integration_spec.rb | 110 ++++++++++++++++++++++++++++--- 6 files changed, 155 insertions(+), 12 deletions(-) create mode 100644 lib/flipper/all.rb create mode 100644 lib/flipper/any.rb diff --git a/lib/flipper.rb b/lib/flipper.rb index 71e0bf0fc..66d2bbe30 100644 --- a/lib/flipper.rb +++ b/lib/flipper.rb @@ -145,6 +145,8 @@ def groups_registry=(registry) require 'flipper/adapters/memoizable' require 'flipper/adapters/memory' require 'flipper/adapters/instrumented' +require 'flipper/all' +require 'flipper/any' require 'flipper/configuration' require 'flipper/dsl' require 'flipper/errors' diff --git a/lib/flipper/all.rb b/lib/flipper/all.rb new file mode 100644 index 000000000..285ef84df --- /dev/null +++ b/lib/flipper/all.rb @@ -0,0 +1,18 @@ +module Flipper + class All + def initialize(*rules) + @rules = rules + end + + def value + { + "type" => "All", + "value" => @rules.map(&:value), + } + end + + def open?(feature_name, actor) + @rules.all? { |rule| rule.open?(feature_name, actor) } + end + end +end diff --git a/lib/flipper/any.rb b/lib/flipper/any.rb new file mode 100644 index 000000000..7a538ec09 --- /dev/null +++ b/lib/flipper/any.rb @@ -0,0 +1,18 @@ +module Flipper + class Any + def initialize(*rules) + @rules = rules + end + + def value + { + "type" => "Any", + "value" => @rules.map(&:value), + } + end + + def open?(feature_name, actor) + @rules.any? { |rule| rule.open?(feature_name, actor) } + end + end +end diff --git a/lib/flipper/gates/rule.rb b/lib/flipper/gates/rule.rb index 670c438f1..5c2e59e7c 100644 --- a/lib/flipper/gates/rule.rb +++ b/lib/flipper/gates/rule.rb @@ -31,7 +31,9 @@ def open?(context) end def protects?(thing) - thing.is_a?(Flipper::Rule) + thing.is_a?(Flipper::Rule) || + thing.is_a?(Flipper::Any) || + thing.is_a?(Flipper::All) end end end diff --git a/lib/flipper/rule.rb b/lib/flipper/rule.rb index 0d3da49ee..57fae02aa 100644 --- a/lib/flipper/rule.rb +++ b/lib/flipper/rule.rb @@ -2,8 +2,19 @@ module Flipper class Rule def self.from_hash(hash) - value = hash.fetch("value") - new(value.fetch("left"), value.fetch("operator"), value.fetch("right")) + type = hash.fetch("type") + + case type + when "Any" + value = hash.fetch("value") + Any.new(*value.map { |v| from_hash(v) }) + when "All" + value = hash.fetch("value") + All.new(*value.map { |v| from_hash(v) }) + when "Rule" + value = hash.fetch("value") + new(value.fetch("left"), value.fetch("operator"), value.fetch("right")) + end end def initialize(left, operator, right) diff --git a/spec/flipper_integration_spec.rb b/spec/flipper_integration_spec.rb index b598935c3..59911a6a1 100644 --- a/spec/flipper_integration_spec.rb +++ b/spec/flipper_integration_spec.rb @@ -550,14 +550,106 @@ end end - it "works" do - rule = Flipper::Rule.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "basic"} - ) - feature.enable rule - - expect(feature.enabled?(basic_plan_thing)).to be(true) + + context "for rule" do + it "works" do + rule = Flipper::Rule.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "basic"} + ) + feature.enable rule + + expect(feature.enabled?(basic_plan_thing)).to be(true) + expect(feature.enabled?(premium_plan_thing)).to be(false) + end + end + + context "for Any" do + it "works" do + rule = Flipper::Any.new( + Flipper::Rule.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "basic"} + ), + Flipper::Rule.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "plus"} + ) + ) + feature.enable rule + + expect(feature.enabled?(basic_plan_thing)).to be(true) + expect(feature.enabled?(premium_plan_thing)).to be(false) + end + end + + context "for All" do + it "works" do + true_actor = Flipper::Actor.new("User;1", { + "plan" => "basic", + "age" => 21, + }) + false_actor = Flipper::Actor.new("User;1", { + "plan" => "basic", + "age" => 20, + }) + rule = Flipper::All.new( + Flipper::Rule.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "basic"} + ), + Flipper::Rule.new( + {"type" => "property", "value" => "age"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "integer", "value" => 21} + ) + ) + feature.enable rule + + expect(feature.enabled?(true_actor)).to be(true) + expect(feature.enabled?(false_actor)).to be(false) + end + + it "works when nested" do + admin_actor = Flipper::Actor.new("User;1", { + "admin" => true, + }) + true_actor = Flipper::Actor.new("User;1", { + "plan" => "basic", + "age" => 21, + }) + false_actor = Flipper::Actor.new("User;1", { + "plan" => "basic", + "age" => 20, + }) + rule = Flipper::Any.new( + Flipper::Rule.new( + {"type" => "property", "value" => "admin"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => true} + ), + Flipper::All.new( + Flipper::Rule.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "basic"} + ), + Flipper::Rule.new( + {"type" => "property", "value" => "age"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "integer", "value" => 21} + ) + ) + ) + feature.enable rule + + expect(feature.enabled?(admin_actor)).to be(true) + expect(feature.enabled?(true_actor)).to be(true) + expect(feature.enabled?(false_actor)).to be(false) + end end end From 1e68ed5ed784dbfdd719a2f7c759a335d63589ea Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 1 Sep 2021 08:10:01 -0400 Subject: [PATCH 003/176] Get pstore_spec's passing I don't think this is done but they are green for now . --- lib/flipper/adapters/pstore.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/flipper/adapters/pstore.rb b/lib/flipper/adapters/pstore.rb index 55301a57b..22e996cdc 100644 --- a/lib/flipper/adapters/pstore.rb +++ b/lib/flipper/adapters/pstore.rb @@ -1,3 +1,4 @@ +require 'json' require 'pstore' require 'set' require 'flipper' @@ -91,6 +92,8 @@ def enable(feature, gate, thing) write key(feature, gate), thing.value.to_s when :set set_add key(feature, gate), thing.value.to_s + when :json + set_add key(feature, gate), JSON.dump(thing.value) else raise "#{gate} is not supported by this adapter yet" end @@ -112,6 +115,10 @@ def disable(feature, gate, thing) @store.transaction do set_delete key(feature, gate), thing.value.to_s end + when :json + @store.transaction do + set_delete key(feature, gate), JSON.dump(thing.value) + end else raise "#{gate} is not supported by this adapter yet" end @@ -159,6 +166,8 @@ def result_for_feature(feature) read key(feature, gate) when :set set_members key(feature, gate) + when :json + set_members(key(feature, gate)) else raise "#{gate} is not supported by this adapter yet" end From a8d7f636eb0ee36d71dc745177f804ccdcca5af6 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 1 Sep 2021 08:10:07 -0400 Subject: [PATCH 004/176] Get rollout specs passing --- lib/flipper/adapters/rollout.rb | 1 + spec/flipper/adapters/rollout_spec.rb | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/flipper/adapters/rollout.rb b/lib/flipper/adapters/rollout.rb index 4e268f4b6..e0161adb1 100644 --- a/lib/flipper/adapters/rollout.rb +++ b/lib/flipper/adapters/rollout.rb @@ -53,6 +53,7 @@ def get(feature) actors: actors, percentage_of_actors: percentage_of_actors, percentage_of_time: nil, + rules: Set.new, } end diff --git a/spec/flipper/adapters/rollout_spec.rb b/spec/flipper/adapters/rollout_spec.rb index b9e91f563..5cecc64b7 100644 --- a/spec/flipper/adapters/rollout_spec.rb +++ b/spec/flipper/adapters/rollout_spec.rb @@ -4,7 +4,7 @@ require 'flipper/adapters/rollout' require 'flipper/spec/shared_adapter_specs' -RSpec.describe Flipper::Adapters::Rollout do +RSpec.describe Flipper::Adapters::Rollout, focus: true do let(:redis) { Redis.new } let(:rollout) { Rollout.new(redis) } let(:source_adapter) { described_class.new(rollout) } @@ -37,6 +37,7 @@ boolean: nil, groups: Set.new([:admins]), actors: Set.new(["1"]), + rules: Set.new, percentage_of_actors: 20.0, percentage_of_time: nil, } @@ -50,6 +51,7 @@ boolean: true, groups: Set.new, actors: Set.new, + rules: Set.new, percentage_of_actors: nil, percentage_of_time: nil, } @@ -65,6 +67,7 @@ boolean: true, groups: Set.new, actors: Set.new, + rules: Set.new, percentage_of_actors: nil, percentage_of_time: nil, } From d2e1b0fe74b023ad076c003a74974a9799580bb2 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 1 Sep 2021 08:10:33 -0400 Subject: [PATCH 005/176] Remove errant focus --- spec/flipper/adapters/rollout_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/flipper/adapters/rollout_spec.rb b/spec/flipper/adapters/rollout_spec.rb index 5cecc64b7..3734f7520 100644 --- a/spec/flipper/adapters/rollout_spec.rb +++ b/spec/flipper/adapters/rollout_spec.rb @@ -4,7 +4,7 @@ require 'flipper/adapters/rollout' require 'flipper/spec/shared_adapter_specs' -RSpec.describe Flipper::Adapters::Rollout, focus: true do +RSpec.describe Flipper::Adapters::Rollout do let(:redis) { Redis.new } let(:rollout) { Rollout.new(redis) } let(:source_adapter) { described_class.new(rollout) } From 8a2918102a4f16563d77ba3a1f9e5e1bd3aeac45 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 1 Sep 2021 08:24:36 -0400 Subject: [PATCH 006/176] Minor whitespace --- spec/flipper_integration_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/flipper_integration_spec.rb b/spec/flipper_integration_spec.rb index 59911a6a1..3b5eee1c7 100644 --- a/spec/flipper_integration_spec.rb +++ b/spec/flipper_integration_spec.rb @@ -554,9 +554,9 @@ context "for rule" do it "works" do rule = Flipper::Rule.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "basic"} + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "basic"} ) feature.enable rule From 0c2fb586cf0377f45baad9de177f58ced9d2f273 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 1 Sep 2021 09:00:21 -0400 Subject: [PATCH 007/176] Get pstore and redis adapters really working with new json stuff --- lib/flipper/adapters/pstore.rb | 2 +- lib/flipper/adapters/redis.rb | 14 +++++++++++ lib/flipper/spec/shared_adapter_specs.rb | 30 ++++++++++++++++++++++-- spec/flipper/adapters/redis_spec.rb | 2 +- 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/lib/flipper/adapters/pstore.rb b/lib/flipper/adapters/pstore.rb index 22e996cdc..f53ec6dcb 100644 --- a/lib/flipper/adapters/pstore.rb +++ b/lib/flipper/adapters/pstore.rb @@ -167,7 +167,7 @@ def result_for_feature(feature) when :set set_members key(feature, gate) when :json - set_members(key(feature, gate)) + set_members(key(feature, gate)).map { |member| JSON.parse(member) }.to_set else raise "#{gate} is not supported by this adapter yet" end diff --git a/lib/flipper/adapters/redis.rb b/lib/flipper/adapters/redis.rb index 493741392..aaa889678 100644 --- a/lib/flipper/adapters/redis.rb +++ b/lib/flipper/adapters/redis.rb @@ -78,6 +78,8 @@ def enable(feature, gate, thing) @client.hset feature.key, gate.key, thing.value.to_s when :set @client.hset feature.key, to_field(gate, thing), 1 + when :json + @client.hset feature.key, to_json_field(gate, thing), 1 else unsupported_data_type gate.data_type end @@ -100,6 +102,8 @@ def disable(feature, gate, thing) @client.hset feature.key, gate.key, thing.value.to_s when :set @client.hdel feature.key, to_field(gate, thing) + when :json + @client.hdel feature.key, to_json_field(gate, thing) else unsupported_data_type gate.data_type end @@ -148,6 +152,8 @@ def result_for_feature(feature, doc) doc[gate.key.to_s] when :set fields_to_gate_value fields, gate + when :json + json_fields_to_gate_value fields, gate else unsupported_data_type gate.data_type end @@ -161,6 +167,10 @@ def to_field(gate, thing) "#{gate.key}/#{thing.value}" end + def to_json_field(gate, thing) + "#{gate.key}/#{JSON.dump(thing.value)}" + end + # Private: Returns a set of values given an array of fields and a gate. # # Returns a Set of the values enabled for the gate. @@ -171,6 +181,10 @@ def fields_to_gate_value(fields, gate) values.to_set end + def json_fields_to_gate_value(fields, gate) + fields_to_gate_value(fields, gate).map! { |member| JSON.parse(member) } + end + # Private def unsupported_data_type(data_type) raise "#{data_type} is not supported by this adapter" diff --git a/lib/flipper/spec/shared_adapter_specs.rb b/lib/flipper/spec/shared_adapter_specs.rb index 6e83bf111..a89aaa2bf 100644 --- a/lib/flipper/spec/shared_adapter_specs.rb +++ b/lib/flipper/spec/shared_adapter_specs.rb @@ -5,10 +5,11 @@ let(:feature) { flipper[:stats] } let(:boolean_gate) { feature.gate(:boolean) } + let(:rule_gate) { feature.gate(:rule) } let(:group_gate) { feature.gate(:group) } let(:actor_gate) { feature.gate(:actor) } let(:actors_gate) { feature.gate(:percentage_of_actors) } - let(:time_gate) { feature.gate(:percentage_of_time) } + let(:time_gate) { feature.gate(:percentage_of_time) } before do Flipper.register(:admins) do |actor| @@ -58,10 +59,35 @@ expect(subject.enable(feature, time_gate, flipper.time(45))).to eq(true) expect(subject.disable(feature, boolean_gate, flipper.boolean(false))).to eq(true) - expect(subject.get(feature)).to eq(subject.default_config) end + it 'can enable, disable and get value for rule gate' do + basic_rule = Flipper::Rule.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "basic"} + ) + age_rule = Flipper::Rule.new( + {"type" => "property", "value" => "age"}, + {"type" => "operator", "value" => "gte"}, + {"type" => "integer", "value" => 21} + ) + expect(subject.enable(feature, rule_gate, basic_rule)).to eq(true) + expect(subject.enable(feature, rule_gate, age_rule)).to eq(true) + result = subject.get(feature) + expect(result[:rules]).to include(basic_rule.value) + expect(result[:rules]).to include(age_rule.value) + + expect(subject.disable(feature, rule_gate, basic_rule)).to eq(true) + result = subject.get(feature) + expect(result[:rules]).to include(age_rule.value) + + expect(subject.disable(feature, rule_gate, age_rule)).to eq(true) + result = subject.get(feature) + expect(result[:rules]).to be_empty + end + it 'can enable, disable and get value for group gate' do expect(subject.enable(feature, group_gate, flipper.group(:admins))).to eq(true) expect(subject.enable(feature, group_gate, flipper.group(:early_access))).to eq(true) diff --git a/spec/flipper/adapters/redis_spec.rb b/spec/flipper/adapters/redis_spec.rb index 0f0fe1f48..1309e1e2f 100644 --- a/spec/flipper/adapters/redis_spec.rb +++ b/spec/flipper/adapters/redis_spec.rb @@ -2,7 +2,7 @@ require 'flipper/adapters/redis' require 'flipper/spec/shared_adapter_specs' -RSpec.describe Flipper::Adapters::Redis do +RSpec.describe Flipper::Adapters::Redis, focus: true do let(:client) do options = {} From 2a94b20f277eeee259cae72c1548bb581ecea299 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 1 Sep 2021 09:04:27 -0400 Subject: [PATCH 008/176] Get memory adapter working for json --- lib/flipper/adapters/memory.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/flipper/adapters/memory.rb b/lib/flipper/adapters/memory.rb index 24fe7f5b7..d5f0c42da 100644 --- a/lib/flipper/adapters/memory.rb +++ b/lib/flipper/adapters/memory.rb @@ -91,6 +91,8 @@ def disable(feature, gate, thing) @source[feature.key][gate.key] = thing.value.to_s when :set @source[feature.key][gate.key].delete thing.value.to_s + when :json + @source[feature.key][gate.key].delete thing.value else raise "#{gate} is not supported by this adapter yet" end From bd03b5231a7ba33cd8af26304893075dffbb32ef Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 1 Sep 2021 09:08:27 -0400 Subject: [PATCH 009/176] Get read only working with rules --- spec/flipper/adapters/read_only_spec.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/spec/flipper/adapters/read_only_spec.rb b/spec/flipper/adapters/read_only_spec.rb index 12caa81bb..3d0536c1b 100644 --- a/spec/flipper/adapters/read_only_spec.rb +++ b/spec/flipper/adapters/read_only_spec.rb @@ -9,6 +9,7 @@ let(:boolean_gate) { feature.gate(:boolean) } let(:group_gate) { feature.gate(:group) } let(:actor_gate) { feature.gate(:actor) } + let(:rule_gate) { feature.gate(:rule) } let(:actors_gate) { feature.gate(:percentage_of_actors) } let(:time_gate) { feature.gate(:percentage_of_time) } @@ -42,16 +43,32 @@ end it 'can get feature' do + rule = Flipper::Rule.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "basic"} + ) actor22 = Flipper::Actor.new('22') adapter.enable(feature, boolean_gate, flipper.boolean) adapter.enable(feature, group_gate, flipper.group(:admins)) adapter.enable(feature, actor_gate, flipper.actor(actor22)) adapter.enable(feature, actors_gate, flipper.actors(25)) adapter.enable(feature, time_gate, flipper.time(45)) + adapter.enable(feature, rule_gate, rule) expect(subject.get(feature)).to eq(boolean: 'true', groups: Set['admins'], actors: Set['22'], + rules: Set[ + { + "type" => "Rule", + "value" => { + "left" => {"type" => "property", "value" => "plan"}, + "operator" => {"type" => "operator", "value" => "eq"}, + "right" => {"type" => "string", "value" => "basic"}, + } + } + ], percentage_of_actors: '25', percentage_of_time: '45') end From b4dc0da3d963aecabb8c01675d8254c0dfbc41a3 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 1 Sep 2021 09:11:59 -0400 Subject: [PATCH 010/176] Get mongo working for rules --- lib/flipper/adapters/mongo.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/flipper/adapters/mongo.rb b/lib/flipper/adapters/mongo.rb index eae2c9da8..088edd48e 100644 --- a/lib/flipper/adapters/mongo.rb +++ b/lib/flipper/adapters/mongo.rb @@ -84,6 +84,10 @@ def enable(feature, gate, thing) update feature.key, '$addToSet' => { gate.key.to_s => thing.value.to_s, } + when :json + update feature.key, '$addToSet' => { + gate.key.to_s => JSON.dump(thing.value), + } else unsupported_data_type gate.data_type end @@ -106,6 +110,8 @@ def disable(feature, gate, thing) update feature.key, '$set' => { gate.key.to_s => thing.value.to_s } when :set update feature.key, '$pull' => { gate.key.to_s => thing.value.to_s } + when :json + update feature.key, '$pull' => { gate.key.to_s => JSON.dump(thing.value) } else unsupported_data_type gate.data_type end @@ -167,6 +173,8 @@ def result_for_feature(feature, doc) doc[gate.key.to_s] when :set doc.fetch(gate.key.to_s) { Set.new }.to_set + when :json + doc.fetch(gate.key.to_s) { Set.new }.map! { |member| JSON.parse(member) }.to_set else unsupported_data_type gate.data_type end From 6d9ab6e8823997e0d02051e24f104987315f190a Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 1 Sep 2021 09:14:46 -0400 Subject: [PATCH 011/176] Get moneta working with rules --- lib/flipper/adapters/moneta.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/flipper/adapters/moneta.rb b/lib/flipper/adapters/moneta.rb index 7eac40167..1c67ad2c3 100644 --- a/lib/flipper/adapters/moneta.rb +++ b/lib/flipper/adapters/moneta.rb @@ -70,6 +70,10 @@ def enable(feature, gate, thing) result = get(feature) result[gate.key] << thing.value.to_s moneta[key(feature.key)] = result + when :json + result = get(feature) + result[gate.key] << thing.value + moneta[key(feature.key)] = result end true end @@ -93,6 +97,10 @@ def disable(feature, gate, thing) result = get(feature) result[gate.key] = result[gate.key].delete(thing.value.to_s) moneta[key(feature.key)] = result + when :json + result = get(feature) + result[gate.key] = result[gate.key].delete(thing.value) + moneta[key(feature.key)] = result end true end From e2934620d850c098e81ed748b90ea7b31f16e8d0 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Wed, 1 Sep 2021 11:08:45 -0500 Subject: [PATCH 012/176] Get active record working with rules --- lib/flipper/adapters/active_record.rb | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/flipper/adapters/active_record.rb b/lib/flipper/adapters/active_record.rb index a433cbef8..cedfb23ac 100644 --- a/lib/flipper/adapters/active_record.rb +++ b/lib/flipper/adapters/active_record.rb @@ -120,7 +120,7 @@ def get_all # Public: Enables a gate for a given thing. # # feature - The Flipper::Feature for the gate. - # gate - The Flipper::Gate to disable. + # gate - The Flipper::Gate to enable. # thing - The Flipper::Type being enabled for the gate. # # Returns true. @@ -132,6 +132,8 @@ def enable(feature, gate, thing) set(feature, gate, thing) when :set enable_multi(feature, gate, thing) + when :json + set_json(feature, gate, thing) else unsupported_data_type gate.data_type end @@ -154,6 +156,8 @@ def disable(feature, gate, thing) set(feature, gate, thing) when :set @gate_class.where(feature_key: feature.key, key: gate.key, value: thing.value).destroy_all + when :json + @gate_class.where(feature_key: feature.key, key: gate.key, value: JSON.dump(thing.value)).destroy_all else unsupported_data_type gate.data_type end @@ -200,6 +204,14 @@ def enable_multi(feature, gate, thing) # already added so no need move on with life end + def set_json(feature, gate, thing) + @gate_class.create! do |g| + g.feature_key = feature.key + g.key = gate.key + g.value = JSON.dump(thing.value) + end + end + def result_for_feature(feature, db_gates) db_gates ||= [] result = {} @@ -216,6 +228,8 @@ def result_for_feature(feature, db_gates) end when :set db_gates.select { |db_gate| db_gate.key == gate.key.to_s }.map(&:value).to_set + when :json + db_gates.select { |db_gate| db_gate.key == gate.key.to_s }.map { |gate| JSON.load(gate.value) }.to_set else unsupported_data_type gate.data_type end From c816331fcc0947e91bb0431f8c94a56671598e27 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Wed, 1 Sep 2021 11:55:21 -0500 Subject: [PATCH 013/176] Get sequel working with rules --- lib/flipper/adapters/sequel.rb | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/flipper/adapters/sequel.rb b/lib/flipper/adapters/sequel.rb index 4f102ffbf..31ad3ebcf 100644 --- a/lib/flipper/adapters/sequel.rb +++ b/lib/flipper/adapters/sequel.rb @@ -128,9 +128,9 @@ def enable(feature, gate, thing) set(feature, gate, thing, clear: true) when :integer set(feature, gate, thing) - when :set + when :set, :json begin - @gate_class.create(gate_attrs(feature, gate, thing)) + @gate_class.create(gate_attrs(feature, gate, thing, json: gate.data_type == :json)) rescue ::Sequel::UniqueConstraintViolation end else @@ -153,9 +153,8 @@ def disable(feature, gate, thing) clear(feature) when :integer set(feature, gate, thing) - when :set - @gate_class.where(gate_attrs(feature, gate, thing)) - .delete + when :set, :json + @gate_class.where(gate_attrs(feature, gate, thing, json: gate.data_type == :json)).delete else unsupported_data_type gate.data_type end @@ -187,11 +186,11 @@ def set(feature, gate, thing, options = {}) end end - def gate_attrs(feature, gate, thing) + def gate_attrs(feature, gate, thing, json: false) { feature_key: feature.key.to_s, key: gate.key.to_s, - value: thing.value.to_s, + value: json ? JSON.dump(thing.value) : thing.value.to_s, } end @@ -210,6 +209,8 @@ def result_for_feature(feature, db_gates) end when :set db_gates.select { |db_gate| db_gate.key == gate.key.to_s }.map(&:value).to_set + when :json + db_gates.select { |db_gate| db_gate.key == gate.key.to_s }.map {|gate| JSON.load(gate.value) }.to_set else unsupported_data_type gate.data_type end From ae07e5064b4e771c7f2a1e298bdf786c751e695d Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Wed, 1 Sep 2021 14:20:34 -0500 Subject: [PATCH 014/176] Refactor AR to use serialize and query methods --- lib/flipper/adapters/active_record.rb | 40 +++++++-------------- spec/flipper/adapters/active_record_spec.rb | 2 +- 2 files changed, 14 insertions(+), 28 deletions(-) diff --git a/lib/flipper/adapters/active_record.rb b/lib/flipper/adapters/active_record.rb index cedfb23ac..7b7174f7d 100644 --- a/lib/flipper/adapters/active_record.rb +++ b/lib/flipper/adapters/active_record.rb @@ -14,6 +14,8 @@ class Feature < ::ActiveRecord::Base "flipper_features", ::ActiveRecord::Base.table_name_suffix, ].join + + has_many :gates, foreign_key: "feature_key", primary_key: "key" end # Private: Do not use outside of this adapter. @@ -23,6 +25,8 @@ class Gate < ::ActiveRecord::Base "flipper_gates", ::ActiveRecord::Base.table_name_suffix, ].join + + serialize :value, JSON end # Public: The name of the adapter. @@ -101,19 +105,13 @@ def get_multi(features) end def get_all - features = ::Arel::Table.new(@feature_class.table_name.to_sym) - gates = ::Arel::Table.new(@gate_class.table_name.to_sym) - rows_query = features.join(gates, Arel::Nodes::OuterJoin) - .on(features[:key].eq(gates[:feature_key])) - .project(features[:key].as('feature_key'), gates[:key], gates[:value]) - rows = ::ActiveRecord::Base.connection.select_all rows_query - db_gates = rows.map { |row| Gate.new(row) } - grouped_db_gates = db_gates.group_by(&:feature_key) result = Hash.new { |hash, key| hash[key] = default_config } - features = grouped_db_gates.keys.map { |key| Flipper::Feature.new(key, self) } - features.each do |feature| - result[feature.key] = result_for_feature(feature, grouped_db_gates[feature.key]) + + @feature_class.includes(:gates).all.each do |f| + feature = Flipper::Feature.new(f.key, self) + result[feature.key] = result_for_feature(feature, f.gates) end + result end @@ -130,10 +128,8 @@ def enable(feature, gate, thing) set(feature, gate, thing, clear: true) when :integer set(feature, gate, thing) - when :set + when :set, :json enable_multi(feature, gate, thing) - when :json - set_json(feature, gate, thing) else unsupported_data_type gate.data_type end @@ -157,7 +153,7 @@ def disable(feature, gate, thing) when :set @gate_class.where(feature_key: feature.key, key: gate.key, value: thing.value).destroy_all when :json - @gate_class.where(feature_key: feature.key, key: gate.key, value: JSON.dump(thing.value)).destroy_all + @gate_class.where(feature_key: feature.key, key: gate.key, value: thing.value).destroy_all else unsupported_data_type gate.data_type end @@ -196,7 +192,7 @@ def enable_multi(feature, gate, thing) @gate_class.create! do |g| g.feature_key = feature.key g.key = gate.key - g.value = thing.value.to_s + g.value = thing.value end nil @@ -204,14 +200,6 @@ def enable_multi(feature, gate, thing) # already added so no need move on with life end - def set_json(feature, gate, thing) - @gate_class.create! do |g| - g.feature_key = feature.key - g.key = gate.key - g.value = JSON.dump(thing.value) - end - end - def result_for_feature(feature, db_gates) db_gates ||= [] result = {} @@ -226,10 +214,8 @@ def result_for_feature(feature, db_gates) if detected_db_gate = db_gates.detect { |db_gate| db_gate.key == gate.key.to_s } detected_db_gate.value end - when :set + when :set, :json db_gates.select { |db_gate| db_gate.key == gate.key.to_s }.map(&:value).to_set - when :json - db_gates.select { |db_gate| db_gate.key == gate.key.to_s }.map { |gate| JSON.load(gate.value) }.to_set else unsupported_data_type gate.data_type end diff --git a/spec/flipper/adapters/active_record_spec.rb b/spec/flipper/adapters/active_record_spec.rb index 66c0c4306..82765db96 100644 --- a/spec/flipper/adapters/active_record_spec.rb +++ b/spec/flipper/adapters/active_record_spec.rb @@ -27,7 +27,7 @@ CREATE TABLE flipper_gates ( id integer PRIMARY KEY, feature_key text NOT NULL, - key text NOT NULL, + key string NOT NULL, value text DEFAULT NULL, created_at datetime NOT NULL, updated_at datetime NOT NULL From 8abc2288d4993b0a7529ab22f12990de8624a1c6 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Wed, 1 Sep 2021 14:21:00 -0500 Subject: [PATCH 015/176] Update Sequel migration --- lib/generators/flipper/templates/sequel_migration.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/generators/flipper/templates/sequel_migration.rb b/lib/generators/flipper/templates/sequel_migration.rb index 0a782a358..83e772219 100644 --- a/lib/generators/flipper/templates/sequel_migration.rb +++ b/lib/generators/flipper/templates/sequel_migration.rb @@ -9,7 +9,7 @@ def up create_table :flipper_gates do |_t| String :feature_key, null: false String :key, null: false - String :value + Text :value DateTime :created_at, null: false DateTime :updated_at, null: false primary_key [:feature_key, :key, :value] From bd23dddc3695476d923331584a48d8e9606ae4a5 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Wed, 1 Sep 2021 14:50:26 -0500 Subject: [PATCH 016/176] Move rules into a namespace --- lib/flipper.rb | 4 +- lib/flipper/all.rb | 18 ---- lib/flipper/any.rb | 18 ---- lib/flipper/gates/rule.rb | 8 +- lib/flipper/rule.rb | 91 ------------------- lib/flipper/rules.rb | 12 +++ lib/flipper/rules/all.rb | 11 +++ lib/flipper/rules/any.rb | 24 +++++ lib/flipper/rules/condition.rb | 80 ++++++++++++++++ lib/flipper/spec/shared_adapter_specs.rb | 4 +- .../{rule_spec.rb => rules/condition_spec.rb} | 22 ++--- spec/flipper_integration_spec.rb | 16 ++-- 12 files changed, 153 insertions(+), 155 deletions(-) delete mode 100644 lib/flipper/all.rb delete mode 100644 lib/flipper/any.rb delete mode 100644 lib/flipper/rule.rb create mode 100644 lib/flipper/rules.rb create mode 100644 lib/flipper/rules/all.rb create mode 100644 lib/flipper/rules/any.rb create mode 100644 lib/flipper/rules/condition.rb rename spec/flipper/{rule_spec.rb => rules/condition_spec.rb} (93%) diff --git a/lib/flipper.rb b/lib/flipper.rb index 66d2bbe30..d6a691276 100644 --- a/lib/flipper.rb +++ b/lib/flipper.rb @@ -145,8 +145,6 @@ def groups_registry=(registry) require 'flipper/adapters/memoizable' require 'flipper/adapters/memory' require 'flipper/adapters/instrumented' -require 'flipper/all' -require 'flipper/any' require 'flipper/configuration' require 'flipper/dsl' require 'flipper/errors' @@ -158,7 +156,7 @@ def groups_registry=(registry) require 'flipper/middleware/memoizer' require 'flipper/middleware/setup_env' require 'flipper/registry' -require 'flipper/rule' +require 'flipper/rules' require 'flipper/type' require 'flipper/types/actor' require 'flipper/types/boolean' diff --git a/lib/flipper/all.rb b/lib/flipper/all.rb deleted file mode 100644 index 285ef84df..000000000 --- a/lib/flipper/all.rb +++ /dev/null @@ -1,18 +0,0 @@ -module Flipper - class All - def initialize(*rules) - @rules = rules - end - - def value - { - "type" => "All", - "value" => @rules.map(&:value), - } - end - - def open?(feature_name, actor) - @rules.all? { |rule| rule.open?(feature_name, actor) } - end - end -end diff --git a/lib/flipper/any.rb b/lib/flipper/any.rb deleted file mode 100644 index 7a538ec09..000000000 --- a/lib/flipper/any.rb +++ /dev/null @@ -1,18 +0,0 @@ -module Flipper - class Any - def initialize(*rules) - @rules = rules - end - - def value - { - "type" => "Any", - "value" => @rules.map(&:value), - } - end - - def open?(feature_name, actor) - @rules.any? { |rule| rule.open?(feature_name, actor) } - end - end -end diff --git a/lib/flipper/gates/rule.rb b/lib/flipper/gates/rule.rb index 5c2e59e7c..784efe107 100644 --- a/lib/flipper/gates/rule.rb +++ b/lib/flipper/gates/rule.rb @@ -25,15 +25,15 @@ def enabled?(value) def open?(context) rules = context.values[key] rules.any? { |hash| - rule = Flipper::Rule.from_hash(hash) + rule = Flipper::Rules.build(hash) rule.open?(context.feature_name, context.thing) } end def protects?(thing) - thing.is_a?(Flipper::Rule) || - thing.is_a?(Flipper::Any) || - thing.is_a?(Flipper::All) + thing.is_a?(Flipper::Rules::Condition) || + thing.is_a?(Flipper::Rules::Any) || + thing.is_a?(Flipper::Rules::All) end end end diff --git a/lib/flipper/rule.rb b/lib/flipper/rule.rb deleted file mode 100644 index 57fae02aa..000000000 --- a/lib/flipper/rule.rb +++ /dev/null @@ -1,91 +0,0 @@ -module Flipper - class Rule - - def self.from_hash(hash) - type = hash.fetch("type") - - case type - when "Any" - value = hash.fetch("value") - Any.new(*value.map { |v| from_hash(v) }) - when "All" - value = hash.fetch("value") - All.new(*value.map { |v| from_hash(v) }) - when "Rule" - value = hash.fetch("value") - new(value.fetch("left"), value.fetch("operator"), value.fetch("right")) - end - end - - def initialize(left, operator, right) - @left = left - @operator = operator - @right = right - end - - def value - { - "type" => "Rule", - "value" => { - "left" => @left, - "operator" => @operator, - "right" => @right, - } - } - end - - def open?(feature_name, actor) - attributes = actor.flipper_properties - left_value = evaluate(@left, attributes) - right_value = evaluate(@right, attributes) - operator_name = @operator.fetch("value") - - !! case operator_name - when "eq" - left_value == right_value - when "neq" - left_value != right_value - when "gt" - left_value && right_value && left_value > right_value - when "gte" - left_value && right_value && left_value >= right_value - when "lt" - left_value && right_value && left_value < right_value - when "lte" - left_value && right_value && left_value <= right_value - when "in" - left_value && right_value && right_value.include?(left_value) - when "nin" - left_value && right_value && !right_value.include?(left_value) - when "percentage" - # this is to support up to 3 decimal places in percentages - scaling_factor = 1_000 - id = "#{feature_name}#{left_value}" - left_value && right_value && (Zlib.crc32(id) % (100 * scaling_factor) < right_value * scaling_factor) - else - raise "operator not implemented: #{operator_name}" - end - end - - private - - def evaluate(hash, attributes) - type = hash.fetch("type") - - case type - when "property" - attributes[hash.fetch("value")] - when "array" - hash.fetch("value") - when "string" - hash.fetch("value") - when "random" - rand hash.fetch("value") - when "integer" - hash.fetch("value") - else - raise "type not found: #{type.inspect}" - end - end - end -end diff --git a/lib/flipper/rules.rb b/lib/flipper/rules.rb new file mode 100644 index 000000000..c4998fa03 --- /dev/null +++ b/lib/flipper/rules.rb @@ -0,0 +1,12 @@ +require 'flipper/rules/condition' +require 'flipper/rules/any' +require 'flipper/rules/all' + +module Flipper + module Rules + def self.build(hash) + type = const_get(hash.fetch("type")) + type.build(hash.fetch["value"]) + end + end +end diff --git a/lib/flipper/rules/all.rb b/lib/flipper/rules/all.rb new file mode 100644 index 000000000..8ec70e864 --- /dev/null +++ b/lib/flipper/rules/all.rb @@ -0,0 +1,11 @@ +require 'flipper/rules/all' + +module Flipper + module Rules + class All < Any + def open?(feature_name, actor) + @rules.all? { |rule| rule.open?(feature_name, actor) } + end + end + end +end diff --git a/lib/flipper/rules/any.rb b/lib/flipper/rules/any.rb new file mode 100644 index 000000000..616f344d3 --- /dev/null +++ b/lib/flipper/rules/any.rb @@ -0,0 +1,24 @@ +module Flipper + module Rules + class Any + def self.build(rules) + new(rules.map { |rule| Flipper::Rules.build(rule) }) + end + + def initialize(rules) + @rules = rules + end + + def value + { + "type" => self.class.name.split('::').last, + "value" => @rules.map(&:value), + } + end + + def open?(feature_name, actor) + @rules.any? { |rule| rule.open?(feature_name, actor) } + end + end + end +end diff --git a/lib/flipper/rules/condition.rb b/lib/flipper/rules/condition.rb new file mode 100644 index 000000000..486c5643a --- /dev/null +++ b/lib/flipper/rules/condition.rb @@ -0,0 +1,80 @@ +module Flipper + module Rules + class Condition + def self.build(hash) + new(hash.fetch("left"), hash.fetch("operator"), hash.fetch("right")) + end + + def initialize(left, operator, right) + @left = left + @operator = operator + @right = right + end + + def value + { + "type" => "Rule", + "value" => { + "left" => @left, + "operator" => @operator, + "right" => @right, + } + } + end + + def open?(feature_name, actor) + attributes = actor.flipper_properties + left_value = evaluate(@left, attributes) + right_value = evaluate(@right, attributes) + operator_name = @operator.fetch("value") + + !! case operator_name + when "eq" + left_value == right_value + when "neq" + left_value != right_value + when "gt" + left_value && right_value && left_value > right_value + when "gte" + left_value && right_value && left_value >= right_value + when "lt" + left_value && right_value && left_value < right_value + when "lte" + left_value && right_value && left_value <= right_value + when "in" + left_value && right_value && right_value.include?(left_value) + when "nin" + left_value && right_value && !right_value.include?(left_value) + when "percentage" + # this is to support up to 3 decimal places in percentages + scaling_factor = 1_000 + id = "#{feature_name}#{left_value}" + left_value && right_value && (Zlib.crc32(id) % (100 * scaling_factor) < right_value * scaling_factor) + else + raise "operator not implemented: #{operator_name}" + end + end + + private + + def evaluate(hash, attributes) + type = hash.fetch("type") + + case type + when "property" + attributes[hash.fetch("value")] + when "array" + hash.fetch("value") + when "string" + hash.fetch("value") + when "random" + rand hash.fetch("value") + when "integer" + hash.fetch("value") + else + raise "type not found: #{type.inspect}" + end + end + end + end +end diff --git a/lib/flipper/spec/shared_adapter_specs.rb b/lib/flipper/spec/shared_adapter_specs.rb index a89aaa2bf..f7ad2943a 100644 --- a/lib/flipper/spec/shared_adapter_specs.rb +++ b/lib/flipper/spec/shared_adapter_specs.rb @@ -63,12 +63,12 @@ end it 'can enable, disable and get value for rule gate' do - basic_rule = Flipper::Rule.new( + basic_rule = Flipper::Rules::Condition.new( {"type" => "property", "value" => "plan"}, {"type" => "operator", "value" => "eq"}, {"type" => "string", "value" => "basic"} ) - age_rule = Flipper::Rule.new( + age_rule = Flipper::Rules::Condition.new( {"type" => "property", "value" => "age"}, {"type" => "operator", "value" => "gte"}, {"type" => "integer", "value" => 21} diff --git a/spec/flipper/rule_spec.rb b/spec/flipper/rules/condition_spec.rb similarity index 93% rename from spec/flipper/rule_spec.rb rename to spec/flipper/rules/condition_spec.rb index 1aa4a2b25..35520dc1d 100644 --- a/spec/flipper/rule_spec.rb +++ b/spec/flipper/rules/condition_spec.rb @@ -1,12 +1,12 @@ require 'helper' -RSpec.describe Flipper::Rule do +RSpec.describe Flipper::Rules::Condition do let(:feature_name) { "search" } describe "#open?" do context "eq" do let(:rule) { - Flipper::Rule.new( + Flipper::Rules::Condition.new( {"type" => "property", "value" => "plan"}, {"type" => "operator", "value" => "eq"}, {"type" => "string", "value" => "basic"} @@ -30,7 +30,7 @@ context "neq" do let(:rule) { - Flipper::Rule.new( + Flipper::Rules::Condition.new( {"type" => "property", "value" => "plan"}, {"type" => "operator", "value" => "neq"}, {"type" => "string", "value" => "basic"} @@ -54,7 +54,7 @@ context "gt" do let(:rule) { - Flipper::Rule.new( + Flipper::Rules::Condition.new( {"type" => "property", "value" => "age"}, {"type" => "operator", "value" => "gt"}, {"type" => "integer", "value" => 20} @@ -78,7 +78,7 @@ context "gte" do let(:rule) { - Flipper::Rule.new( + Flipper::Rules::Condition.new( {"type" => "property", "value" => "age"}, {"type" => "operator", "value" => "gte"}, {"type" => "integer", "value" => 20} @@ -102,7 +102,7 @@ context "lt" do let(:rule) { - Flipper::Rule.new( + Flipper::Rules::Condition.new( {"type" => "property", "value" => "age"}, {"type" => "operator", "value" => "lt"}, {"type" => "integer", "value" => 21} @@ -126,7 +126,7 @@ context "lt with rand type" do let(:rule) { - Flipper::Rule.new( + Flipper::Rules::Condition.new( {"type" => "random", "value" => 100}, {"type" => "operator", "value" => "lt"}, {"type" => "integer", "value" => 25} @@ -147,7 +147,7 @@ context "lte" do let(:rule) { - Flipper::Rule.new( + Flipper::Rules::Condition.new( {"type" => "property", "value" => "age"}, {"type" => "operator", "value" => "lte"}, {"type" => "integer", "value" => 21} @@ -171,7 +171,7 @@ context "in" do let(:rule) { - Flipper::Rule.new( + Flipper::Rules::Condition.new( {"type" => "property", "value" => "age"}, {"type" => "operator", "value" => "in"}, {"type" => "array", "value" => [20, 21, 22]} @@ -195,7 +195,7 @@ context "nin" do let(:rule) { - Flipper::Rule.new( + Flipper::Rules::Condition.new( {"type" => "property", "value" => "age"}, {"type" => "operator", "value" => "nin"}, {"type" => "array", "value" => [20, 21, 22]} @@ -219,7 +219,7 @@ context "percentage" do let(:rule) { - Flipper::Rule.new( + Flipper::Rules::Condition.new( {"type" => "property", "value" => "flipper_id"}, {"type" => "operator", "value" => "percentage"}, {"type" => "integer", "value" => 25} diff --git a/spec/flipper_integration_spec.rb b/spec/flipper_integration_spec.rb index 3b5eee1c7..2c00c453f 100644 --- a/spec/flipper_integration_spec.rb +++ b/spec/flipper_integration_spec.rb @@ -553,7 +553,7 @@ context "for rule" do it "works" do - rule = Flipper::Rule.new( + rule = Flipper::Rules::Condition.new( {"type" => "property", "value" => "plan"}, {"type" => "operator", "value" => "eq"}, {"type" => "string", "value" => "basic"} @@ -568,12 +568,12 @@ context "for Any" do it "works" do rule = Flipper::Any.new( - Flipper::Rule.new( + Flipper::Rules::Condition.new( {"type" => "property", "value" => "plan"}, {"type" => "operator", "value" => "eq"}, {"type" => "string", "value" => "basic"} ), - Flipper::Rule.new( + Flipper::Rules::Condition.new( {"type" => "property", "value" => "plan"}, {"type" => "operator", "value" => "eq"}, {"type" => "string", "value" => "plus"} @@ -597,12 +597,12 @@ "age" => 20, }) rule = Flipper::All.new( - Flipper::Rule.new( + Flipper::Rules::Condition.new( {"type" => "property", "value" => "plan"}, {"type" => "operator", "value" => "eq"}, {"type" => "string", "value" => "basic"} ), - Flipper::Rule.new( + Flipper::Rules::Condition.new( {"type" => "property", "value" => "age"}, {"type" => "operator", "value" => "eq"}, {"type" => "integer", "value" => 21} @@ -627,18 +627,18 @@ "age" => 20, }) rule = Flipper::Any.new( - Flipper::Rule.new( + Flipper::Rules::Condition.new( {"type" => "property", "value" => "admin"}, {"type" => "operator", "value" => "eq"}, {"type" => "string", "value" => true} ), Flipper::All.new( - Flipper::Rule.new( + Flipper::Rules::Condition.new( {"type" => "property", "value" => "plan"}, {"type" => "operator", "value" => "eq"}, {"type" => "string", "value" => "basic"} ), - Flipper::Rule.new( + Flipper::Rules::Condition.new( {"type" => "property", "value" => "age"}, {"type" => "operator", "value" => "eq"}, {"type" => "integer", "value" => 21} From 783538c29ff318fcf6ba6f4cc68a2592044af314 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Wed, 1 Sep 2021 14:59:29 -0500 Subject: [PATCH 017/176] Rename #open? to #matches? for rules --- lib/flipper/gates/rule.rb | 2 +- lib/flipper/rules.rb | 2 +- lib/flipper/rules/all.rb | 4 +-- lib/flipper/rules/any.rb | 10 +++---- lib/flipper/rules/condition.rb | 2 +- spec/flipper/rules/condition_spec.rb | 40 ++++++++++++++-------------- spec/flipper_integration_spec.rb | 8 +++--- 7 files changed, 34 insertions(+), 34 deletions(-) diff --git a/lib/flipper/gates/rule.rb b/lib/flipper/gates/rule.rb index 784efe107..6e8e26b4b 100644 --- a/lib/flipper/gates/rule.rb +++ b/lib/flipper/gates/rule.rb @@ -26,7 +26,7 @@ def open?(context) rules = context.values[key] rules.any? { |hash| rule = Flipper::Rules.build(hash) - rule.open?(context.feature_name, context.thing) + rule.matches?(context.feature_name, context.thing) } end diff --git a/lib/flipper/rules.rb b/lib/flipper/rules.rb index c4998fa03..30e18193e 100644 --- a/lib/flipper/rules.rb +++ b/lib/flipper/rules.rb @@ -6,7 +6,7 @@ module Flipper module Rules def self.build(hash) type = const_get(hash.fetch("type")) - type.build(hash.fetch["value"]) + type.build(hash.fetch("value")) end end end diff --git a/lib/flipper/rules/all.rb b/lib/flipper/rules/all.rb index 8ec70e864..a0d2e6bf1 100644 --- a/lib/flipper/rules/all.rb +++ b/lib/flipper/rules/all.rb @@ -3,8 +3,8 @@ module Flipper module Rules class All < Any - def open?(feature_name, actor) - @rules.all? { |rule| rule.open?(feature_name, actor) } + def matches?(feature_name, actor) + @rules.all? { |rule| rule.matches?(feature_name, actor) } end end end diff --git a/lib/flipper/rules/any.rb b/lib/flipper/rules/any.rb index 616f344d3..f1b8d9875 100644 --- a/lib/flipper/rules/any.rb +++ b/lib/flipper/rules/any.rb @@ -2,11 +2,11 @@ module Flipper module Rules class Any def self.build(rules) - new(rules.map { |rule| Flipper::Rules.build(rule) }) + new(*rules.map { |rule| Flipper::Rules.build(rule) }) end - def initialize(rules) - @rules = rules + def initialize(*rules) + @rules = rules.flatten end def value @@ -16,8 +16,8 @@ def value } end - def open?(feature_name, actor) - @rules.any? { |rule| rule.open?(feature_name, actor) } + def matches?(feature_name, actor) + @rules.any? { |rule| rule.matches?(feature_name, actor) } end end end diff --git a/lib/flipper/rules/condition.rb b/lib/flipper/rules/condition.rb index 486c5643a..08eea5fc0 100644 --- a/lib/flipper/rules/condition.rb +++ b/lib/flipper/rules/condition.rb @@ -22,7 +22,7 @@ def value } end - def open?(feature_name, actor) + def matches?(feature_name, actor) attributes = actor.flipper_properties left_value = evaluate(@left, attributes) right_value = evaluate(@right, attributes) diff --git a/spec/flipper/rules/condition_spec.rb b/spec/flipper/rules/condition_spec.rb index 35520dc1d..8f4c446f5 100644 --- a/spec/flipper/rules/condition_spec.rb +++ b/spec/flipper/rules/condition_spec.rb @@ -3,7 +3,7 @@ RSpec.describe Flipper::Rules::Condition do let(:feature_name) { "search" } - describe "#open?" do + describe "#matches?" do context "eq" do let(:rule) { Flipper::Rules::Condition.new( @@ -17,14 +17,14 @@ actor = Flipper::Actor.new("User;1", { "plan" => "basic", }) - expect(rule.open?(feature_name, actor)).to be(true) + expect(rule.matches?(feature_name, actor)).to be(true) end it "returns false when property does not match" do actor = Flipper::Actor.new("User;1", { "plan" => "premium", }) - expect(rule.open?(feature_name, actor)).to be(false) + expect(rule.matches?(feature_name, actor)).to be(false) end end @@ -41,14 +41,14 @@ actor = Flipper::Actor.new("User;1", { "plan" => "premium", }) - expect(rule.open?(feature_name, actor)).to be(true) + expect(rule.matches?(feature_name, actor)).to be(true) end it "returns false when property does match" do actor = Flipper::Actor.new("User;1", { "plan" => "basic", }) - expect(rule.open?(feature_name, actor)).to be(false) + expect(rule.matches?(feature_name, actor)).to be(false) end end @@ -65,14 +65,14 @@ actor = Flipper::Actor.new("User;1", { "age" => 21, }) - expect(rule.open?(feature_name, actor)).to be(true) + expect(rule.matches?(feature_name, actor)).to be(true) end it "returns false when property does NOT match" do actor = Flipper::Actor.new("User;1", { "age" => 20, }) - expect(rule.open?(feature_name, actor)).to be(false) + expect(rule.matches?(feature_name, actor)).to be(false) end end @@ -89,14 +89,14 @@ actor = Flipper::Actor.new("User;1", { "age" => 20, }) - expect(rule.open?(feature_name, actor)).to be(true) + expect(rule.matches?(feature_name, actor)).to be(true) end it "returns false when property does NOT match" do actor = Flipper::Actor.new("User;1", { "age" => 19, }) - expect(rule.open?(feature_name, actor)).to be(false) + expect(rule.matches?(feature_name, actor)).to be(false) end end @@ -113,14 +113,14 @@ actor = Flipper::Actor.new("User;1", { "age" => 20, }) - expect(rule.open?(feature_name, actor)).to be(true) + expect(rule.matches?(feature_name, actor)).to be(true) end it "returns false when property does NOT match" do actor = Flipper::Actor.new("User;1", { "age" => 21, }) - expect(rule.open?(feature_name, actor)).to be(false) + expect(rule.matches?(feature_name, actor)).to be(false) end end @@ -137,7 +137,7 @@ results = [] (1..1000).to_a.each do |n| actor = Flipper::Actor.new("User;#{n}") - results << rule.open?(feature_name, actor) + results << rule.matches?(feature_name, actor) end enabled, disabled = results.partition { |r| r } @@ -158,14 +158,14 @@ actor = Flipper::Actor.new("User;1", { "age" => 21, }) - expect(rule.open?(feature_name, actor)).to be(true) + expect(rule.matches?(feature_name, actor)).to be(true) end it "returns false when property does NOT match" do actor = Flipper::Actor.new("User;1", { "age" => 22, }) - expect(rule.open?(feature_name, actor)).to be(false) + expect(rule.matches?(feature_name, actor)).to be(false) end end @@ -182,14 +182,14 @@ actor = Flipper::Actor.new("User;1", { "age" => 21, }) - expect(rule.open?(feature_name, actor)).to be(true) + expect(rule.matches?(feature_name, actor)).to be(true) end it "returns false when property does NOT match" do actor = Flipper::Actor.new("User;1", { "age" => 10, }) - expect(rule.open?(feature_name, actor)).to be(false) + expect(rule.matches?(feature_name, actor)).to be(false) end end @@ -206,14 +206,14 @@ actor = Flipper::Actor.new("User;1", { "age" => 10, }) - expect(rule.open?(feature_name, actor)).to be(true) + expect(rule.matches?(feature_name, actor)).to be(true) end it "returns false when property does NOT match" do actor = Flipper::Actor.new("User;1", { "age" => 20, }) - expect(rule.open?(feature_name, actor)).to be(false) + expect(rule.matches?(feature_name, actor)).to be(false) end end @@ -230,7 +230,7 @@ results = [] (1..1000).to_a.each do |n| actor = Flipper::Actor.new("User;#{n}") - results << rule.open?(feature_name, actor) + results << rule.matches?(feature_name, actor) end enabled, disabled = results.partition { |r| r } @@ -239,7 +239,7 @@ it "returns false when property does NOT match" do actor = Flipper::Actor.new("User;1") - expect(rule.open?(feature_name, actor)).to be(false) + expect(rule.matches?(feature_name, actor)).to be(false) end end end diff --git a/spec/flipper_integration_spec.rb b/spec/flipper_integration_spec.rb index 2c00c453f..0cfb66b6b 100644 --- a/spec/flipper_integration_spec.rb +++ b/spec/flipper_integration_spec.rb @@ -567,7 +567,7 @@ context "for Any" do it "works" do - rule = Flipper::Any.new( + rule = Flipper::Rules::Any.new( Flipper::Rules::Condition.new( {"type" => "property", "value" => "plan"}, {"type" => "operator", "value" => "eq"}, @@ -596,7 +596,7 @@ "plan" => "basic", "age" => 20, }) - rule = Flipper::All.new( + rule = Flipper::Rules::All.new( Flipper::Rules::Condition.new( {"type" => "property", "value" => "plan"}, {"type" => "operator", "value" => "eq"}, @@ -626,13 +626,13 @@ "plan" => "basic", "age" => 20, }) - rule = Flipper::Any.new( + rule = Flipper::Rules::Any.new( Flipper::Rules::Condition.new( {"type" => "property", "value" => "admin"}, {"type" => "operator", "value" => "eq"}, {"type" => "string", "value" => true} ), - Flipper::All.new( + Flipper::Rules::All.new( Flipper::Rules::Condition.new( {"type" => "property", "value" => "plan"}, {"type" => "operator", "value" => "eq"}, From 371854aa55bef33ccb67dc8c848faa3a81d4ebb7 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Wed, 1 Sep 2021 15:35:48 -0500 Subject: [PATCH 018/176] Replace case statement with lookup of operations --- lib/flipper/rules/condition.rb | 46 ++++++++++++++++------------------ 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/lib/flipper/rules/condition.rb b/lib/flipper/rules/condition.rb index 08eea5fc0..a5252b07f 100644 --- a/lib/flipper/rules/condition.rb +++ b/lib/flipper/rules/condition.rb @@ -1,6 +1,23 @@ module Flipper module Rules class Condition + OPERATIONS = { + "eq" => -> (left:, right:, **) { left == right }, + "neq" => -> (left:, right:, **) { left != right }, + "gt" => -> (left:, right:, **) { left && right && left > right }, + "gte" => -> (left:, right:, **) { left && right && left >= right }, + "lt" => -> (left:, right:, **) { left && right && left < right }, + "lte" => -> (left:, right:, **) { left && right && left <= right }, + "in" => -> (left:, right:, **) { left && right && right.include?(left) }, + "nin" => -> (left:, right:, **) { left && right && !right.include?(left) }, + "percentage" => -> (left:, right:, feature_name:) do + # this is to support up to 3 decimal places in percentages + scaling_factor = 1_000 + id = "#{feature_name}#{left}" + left && right && (Zlib.crc32(id) % (100 * scaling_factor) < right * scaling_factor) + end + } + def self.build(hash) new(hash.fetch("left"), hash.fetch("operator"), hash.fetch("right")) end @@ -13,7 +30,7 @@ def initialize(left, operator, right) def value { - "type" => "Rule", + "type" => "Condition", "value" => { "left" => @left, "operator" => @operator, @@ -27,32 +44,11 @@ def matches?(feature_name, actor) left_value = evaluate(@left, attributes) right_value = evaluate(@right, attributes) operator_name = @operator.fetch("value") - - !! case operator_name - when "eq" - left_value == right_value - when "neq" - left_value != right_value - when "gt" - left_value && right_value && left_value > right_value - when "gte" - left_value && right_value && left_value >= right_value - when "lt" - left_value && right_value && left_value < right_value - when "lte" - left_value && right_value && left_value <= right_value - when "in" - left_value && right_value && right_value.include?(left_value) - when "nin" - left_value && right_value && !right_value.include?(left_value) - when "percentage" - # this is to support up to 3 decimal places in percentages - scaling_factor = 1_000 - id = "#{feature_name}#{left_value}" - left_value && right_value && (Zlib.crc32(id) % (100 * scaling_factor) < right_value * scaling_factor) - else + operation = OPERATIONS.fetch(operator_name) do raise "operator not implemented: #{operator_name}" end + + !!operation.call(left: left_value, right: right_value, feature_name: feature_name) end private From 597efde3d65d7ecb6b5d1fd1521687f3efe97fce Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Wed, 1 Sep 2021 15:49:41 -0500 Subject: [PATCH 019/176] Remove duplication in case statement --- lib/flipper/rules/condition.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/flipper/rules/condition.rb b/lib/flipper/rules/condition.rb index a5252b07f..4e833ae4b 100644 --- a/lib/flipper/rules/condition.rb +++ b/lib/flipper/rules/condition.rb @@ -59,13 +59,9 @@ def evaluate(hash, attributes) case type when "property" attributes[hash.fetch("value")] - when "array" - hash.fetch("value") - when "string" - hash.fetch("value") when "random" rand hash.fetch("value") - when "integer" + when "array", "string", "integer" hash.fetch("value") else raise "type not found: #{type.inspect}" From 98ff6a39ad9378c5ed64c4d49ca682c02a6fdb24 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Wed, 1 Sep 2021 15:53:25 -0500 Subject: [PATCH 020/176] Remove redis focus --- spec/flipper/adapters/redis_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/flipper/adapters/redis_spec.rb b/spec/flipper/adapters/redis_spec.rb index 1309e1e2f..0f0fe1f48 100644 --- a/spec/flipper/adapters/redis_spec.rb +++ b/spec/flipper/adapters/redis_spec.rb @@ -2,7 +2,7 @@ require 'flipper/adapters/redis' require 'flipper/spec/shared_adapter_specs' -RSpec.describe Flipper::Adapters::Redis, focus: true do +RSpec.describe Flipper::Adapters::Redis do let(:client) do options = {} From e70d7c3ad74e32e6287654c0e0bc6f12b136b9f0 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Thu, 2 Sep 2021 13:04:39 -0400 Subject: [PATCH 021/176] Get http adapter passing for rules --- docs/api/README.md | 8 + lib/flipper.rb | 1 + lib/flipper/adapters/http.rb | 18 +- lib/flipper/api/error_response.rb | 2 + lib/flipper/api/json_params.rb | 1 + lib/flipper/api/middleware.rb | 1 + lib/flipper/api/v1/actions/rules_gate.rb | 53 ++++ lib/flipper/api/v1/decorators/gate.rb | 5 +- lib/flipper/dsl.rb | 29 +++ lib/flipper/feature.rb | 25 ++ .../flipper/api/v1/actions/rules_gate_spec.rb | 228 ++++++++++++++++++ spec/flipper/dsl_spec.rb | 15 ++ spec/flipper/feature_spec.rb | 4 +- spec/flipper_spec.rb | 23 +- spec/support/spec_helpers.rb | 22 +- 15 files changed, 422 insertions(+), 13 deletions(-) create mode 100644 lib/flipper/api/v1/actions/rules_gate.rb create mode 100644 spec/flipper/api/v1/actions/rules_gate_spec.rb diff --git a/docs/api/README.md b/docs/api/README.md index 128b2a99e..8bc3c5eee 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -882,3 +882,11 @@ The `flipper_id` parameter is invalid or missing. `flipper_id` cannot be empty. #### 5: Name Invalid The `name` parameter is missing. Make sure your request's body contains a `name` parameter. + +#### 6: Type Invalid + +The `type` parameter is missing. Make sure your request's body contains a `type` parameter. + +#### 7: Value Invalid + +The `value` parameter is missing. Make sure your request's body contains a `value` parameter. diff --git a/lib/flipper.rb b/lib/flipper.rb index 66d2bbe30..2370a8060 100644 --- a/lib/flipper.rb +++ b/lib/flipper.rb @@ -57,6 +57,7 @@ def instance=(flipper) # interface of Flipper::DSL. def_delegators :instance, :enabled?, :enable, :disable, :bool, :boolean, + :enable_rule, :disable_rule, :rule, :enable_actor, :disable_actor, :actor, :enable_group, :disable_group, :enable_percentage_of_actors, :disable_percentage_of_actors, diff --git a/lib/flipper/adapters/http.rb b/lib/flipper/adapters/http.rb index 5cbcea62d..14ba5ed4f 100644 --- a/lib/flipper/adapters/http.rb +++ b/lib/flipper/adapters/http.rb @@ -95,7 +95,7 @@ def remove(feature) end def enable(feature, gate, thing) - body = request_body_for_gate(gate, thing.value.to_s) + body = request_body_for_gate(gate, thing.value) query_string = gate.key == :groups ? "?allow_unregistered_groups=true" : "" response = @client.post("/features/#{feature.key}/#{gate.key}#{query_string}", body) raise Error, response unless response.is_a?(Net::HTTPOK) @@ -103,7 +103,7 @@ def enable(feature, gate, thing) end def disable(feature, gate, thing) - body = request_body_for_gate(gate, thing.value.to_s) + body = request_body_for_gate(gate, thing.value) query_string = gate.key == :groups ? "?allow_unregistered_groups=true" : "" response = case gate.key when :percentage_of_actors, :percentage_of_time @@ -128,11 +128,13 @@ def request_body_for_gate(gate, value) when :boolean {} when :groups - { name: value } + { name: value.to_s } when :actors - { flipper_id: value } + { flipper_id: value.to_s } when :percentage_of_actors, :percentage_of_time - { percentage: value } + { percentage: value.to_s } + when :rules + value else raise "#{gate.key} is not a valid flipper gate key" end @@ -156,13 +158,15 @@ def value_for_gate(gate, api_gate) case gate.data_type when :boolean, :integer value ? value.to_s : value - when :set + when :set, :json value ? value.to_set : Set.new else - unsupported_data_type(gate.data_type) + unsupported_data_type gate.data_type end end + private + def unsupported_data_type(data_type) raise "#{data_type} is not supported by this adapter" end diff --git a/lib/flipper/api/error_response.rb b/lib/flipper/api/error_response.rb index b2d10bba7..e4fc3e46c 100644 --- a/lib/flipper/api/error_response.rb +++ b/lib/flipper/api/error_response.rb @@ -28,6 +28,8 @@ def as_json Error.new(3, 'Percentage must be a positive number less than or equal to 100.', 422), flipper_id_invalid: Error.new(4, 'Required parameter flipper_id is missing.', 422), name_invalid: Error.new(5, 'Required parameter name is missing.', 422), + rule_type_invalid: Error.new(6, 'Required parameter rule type is missing.', 422), + rule_value_invalid: Error.new(7, 'Required parameter rule value is missing.', 422), }.freeze end end diff --git a/lib/flipper/api/json_params.rb b/lib/flipper/api/json_params.rb index 14d79a172..a8c4334d2 100644 --- a/lib/flipper/api/json_params.rb +++ b/lib/flipper/api/json_params.rb @@ -35,6 +35,7 @@ def call(env) def update_params(env, data) return if data.empty? parsed_request_body = JSON.parse(data) + env["parsed_request_body".freeze] = parsed_request_body parsed_query_string = parse_query(env[QUERY_STRING]) parsed_query_string.merge!(parsed_request_body) parameters = build_query(parsed_query_string) diff --git a/lib/flipper/api/middleware.rb b/lib/flipper/api/middleware.rb index 01e660672..eb741a8de 100644 --- a/lib/flipper/api/middleware.rb +++ b/lib/flipper/api/middleware.rb @@ -14,6 +14,7 @@ def initialize(app, options = {}) @env_key = options.fetch(:env_key, 'flipper') @action_collection = ActionCollection.new + @action_collection.add Api::V1::Actions::RulesGate @action_collection.add Api::V1::Actions::PercentageOfTimeGate @action_collection.add Api::V1::Actions::PercentageOfActorsGate @action_collection.add Api::V1::Actions::ActorsGate diff --git a/lib/flipper/api/v1/actions/rules_gate.rb b/lib/flipper/api/v1/actions/rules_gate.rb new file mode 100644 index 000000000..8f8c5f880 --- /dev/null +++ b/lib/flipper/api/v1/actions/rules_gate.rb @@ -0,0 +1,53 @@ +require 'flipper/api/action' +require 'flipper/api/v1/decorators/feature' + +module Flipper + module Api + module V1 + module Actions + class RulesGate < Api::Action + include FeatureNameFromRoute + + route %r{\A/features/(?.*)/rules/?\Z} + + def post + ensure_valid_params + feature = flipper[feature_name] + feature.enable Flipper::Rule.from_hash(rule_hash) + + decorated_feature = Decorators::Feature.new(feature) + json_response(decorated_feature.as_json, 200) + end + + def delete + ensure_valid_params + feature = flipper[feature_name] + feature.disable Flipper::Rule.from_hash(rule_hash) + + decorated_feature = Decorators::Feature.new(feature) + json_response(decorated_feature.as_json, 200) + end + + private + + def ensure_valid_params + json_error_response(:rule_type_invalid) if rule_type.nil? + json_error_response(:rule_value_invalid) if rule_value.nil? + end + + def rule_hash + @rule_hash ||= request.env["parsed_request_body".freeze] || {}.freeze + end + + def rule_type + @rule_type ||= rule_hash["type".freeze] + end + + def rule_value + @rule_value ||= rule_hash["value".freeze] + end + end + end + end + end +end diff --git a/lib/flipper/api/v1/decorators/gate.rb b/lib/flipper/api/v1/decorators/gate.rb index f971e28fa..eb9e5f238 100644 --- a/lib/flipper/api/v1/decorators/gate.rb +++ b/lib/flipper/api/v1/decorators/gate.rb @@ -24,9 +24,12 @@ def as_json private + # Set of types that should be represented as Array in JSON. + JSON_ARRAY_TYPES = Set[:set, :json].freeze + # json doesn't like sets def value_as_json - data_type == :set ? value.to_a : value + JSON_ARRAY_TYPES.include?(data_type) ? value.to_a : value end end end diff --git a/lib/flipper/dsl.rb b/lib/flipper/dsl.rb index 683edd1b3..0ec373c58 100644 --- a/lib/flipper/dsl.rb +++ b/lib/flipper/dsl.rb @@ -44,6 +44,16 @@ def enable(name, *args) feature(name).enable(*args) end + # Public: Enable a feature for a rule. + # + # name - The String or Symbol name of the feature. + # rule - a Flipper::Rule instance. + # + # Returns result of Feature#enable. + def enable_rule(name, rule) + feature(name).enable_rule(rule) + end + # Public: Enable a feature for an actor. # # name - The String or Symbol name of the feature. @@ -98,6 +108,16 @@ def disable(name, *args) feature(name).disable(*args) end + # Public: Disable a feature for a rule. + # + # name - The String or Symbol name of the feature. + # rule - a Flipper::Rule instance. + # + # Returns result of Feature#disable. + def disable_rule(name, rule) + feature(name).disable_rule(rule) + end + # Public: Disable a feature for an actor. # # name - The String or Symbol name of the feature. @@ -243,6 +263,15 @@ def actor(thing) Types::Actor.new(thing) end + # Public: Wraps an object as a flipper rule. + # + # hash - The Hash that you would like to turn into a rule. + # + # Returns an instance of Flipper::Rule. + def rule(hash) + Rule.from_hash(hash) + end + # Public: Shortcut for getting a percentage of time instance. # # number - The percentage of time that should be enabled. diff --git a/lib/flipper/feature.rb b/lib/flipper/feature.rb index c71ba3d13..dd9566808 100644 --- a/lib/flipper/feature.rb +++ b/lib/flipper/feature.rb @@ -119,6 +119,15 @@ def enabled?(thing = nil) end end + # Public: Enables a rule for a feature. + # + # rule - a Flipper::Rule instance. + # + # Returns result of enable. + def enable_rule(rule) + enable rule + end + # Public: Enables a feature for an actor. # # actor - a Flipper::Types::Actor instance or an object that responds @@ -159,6 +168,15 @@ def enable_percentage_of_actors(percentage) enable Types::PercentageOfActors.wrap(percentage) end + # Public: Disables a rule for a feature. + # + # rule - a Flipper::Rule instance. + # + # Returns result of disable. + def disable_rule(rule) + disable rule + end + # Public: Disables a feature for an actor. # # actor - a Flipper::Types::Actor instance or an object that responds @@ -257,6 +275,13 @@ def groups_value gate_values.groups end + # Public: Get the adapter value for the rules gate. + # + # Returns Set of rules. + def rules_value + gate_values.rules + end + # Public: Get the adapter value for the actors gate. # # Returns Set of String flipper_id's. diff --git a/spec/flipper/api/v1/actions/rules_gate_spec.rb b/spec/flipper/api/v1/actions/rules_gate_spec.rb new file mode 100644 index 000000000..5f37a1b95 --- /dev/null +++ b/spec/flipper/api/v1/actions/rules_gate_spec.rb @@ -0,0 +1,228 @@ +require 'helper' + +RSpec.describe Flipper::Api::V1::Actions::RulesGate do + let(:app) { build_api(flipper) } + let(:actor) { + Flipper::Actor.new('1', { + "plan" => "basic", + "age" => 21, + }) + } + let(:rule) { + Flipper::Rule.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "basic"} + ) + } + + describe 'enable' do + before do + flipper[:my_feature].disable_rule(rule) + post '/features/my_feature/rules', JSON.dump(rule.value), "CONTENT_TYPE" => "application/json" + end + + it 'enables feature for rule' do + expect(last_response.status).to eq(200) + expect(flipper[:my_feature].enabled?(actor)).to be_truthy + expect(flipper[:my_feature].enabled_gate_names).to eq([:rule]) + end + + it 'returns decorated feature with rule enabled' do + gate = json_response['gates'].find { |gate| gate['key'] == 'rules' } + expect(gate['value']).to eq([rule.value]) + end + end + + describe 'disable' do + before do + flipper[:my_feature].enable_rule(rule) + delete '/features/my_feature/rules', JSON.dump(rule.value), "CONTENT_TYPE" => "application/json" + end + + it 'disables rule for feature' do + expect(last_response.status).to eq(200) + expect(flipper[:my_feature].enabled?(actor)).to be_falsy + expect(flipper[:my_feature].enabled_gate_names).to be_empty + end + + it 'returns decorated feature with rule gate disabled' do + gate = json_response['gates'].find { |gate| gate['key'] == 'rules' } + expect(gate['value']).to be_empty + end + end + + describe 'enable feature with slash in name' do + before do + flipper["my/feature"].disable_rule(rule) + post '/features/my/feature/rules', JSON.dump(rule.value), "CONTENT_TYPE" => "application/json" + end + + it 'enables feature for rule' do + expect(last_response.status).to eq(200) + expect(flipper["my/feature"].enabled?(actor)).to be_truthy + expect(flipper["my/feature"].enabled_gate_names).to eq([:rule]) + end + + it 'returns decorated feature with rule enabled' do + gate = json_response['gates'].find { |gate| gate['key'] == 'rules' } + expect(gate['value']).to eq([rule.value]) + end + end + + describe 'enable feature with space in name' do + before do + flipper["sp ace"].disable_rule(rule) + post '/features/sp%20ace/rules', JSON.dump(rule.value), "CONTENT_TYPE" => "application/json" + end + + it 'enables feature for rule' do + expect(last_response.status).to eq(200) + expect(flipper["sp ace"].enabled?(actor)).to be_truthy + expect(flipper["sp ace"].enabled_gate_names).to eq([:rule]) + end + + it 'returns decorated feature with rule enabled' do + gate = json_response['gates'].find { |gate| gate['key'] == 'rules' } + expect(gate['value']).to eq([rule.value]) + end + end + + describe 'enable missing type parameter' do + before do + data = rule.value + data.delete("type") + post '/features/my_feature/rules', JSON.dump(data), "CONTENT_TYPE" => "application/json" + end + + it 'returns correct error response' do + expect(last_response.status).to eq(422) + expect(json_response).to eq(api_rule_type_invalid_response) + end + end + + describe 'disable missing type parameter' do + before do + data = rule.value + data.delete("type") + delete '/features/my_feature/rules' + end + + it 'returns correct error response' do + expect(last_response.status).to eq(422) + expect(json_response).to eq(api_rule_type_invalid_response) + end + end + + describe 'enable missing value parameter' do + before do + data = rule.value + data.delete("value") + post '/features/my_feature/rules', JSON.dump(data), "CONTENT_TYPE" => "application/json" + end + + it 'returns correct error response' do + expect(last_response.status).to eq(422) + expect(json_response).to eq(api_rule_value_invalid_response) + end + end + + describe 'disable missing value parameter' do + before do + data = rule.value + data.delete("value") + delete '/features/my_feature/rules', JSON.dump(data), "CONTENT_TYPE" => "application/json" + end + + it 'returns correct error response' do + expect(last_response.status).to eq(422) + expect(json_response).to eq(api_rule_value_invalid_response) + end + end + + describe 'enable nil type parameter' do + before do + data = rule.value + data["type"] = nil + post '/features/my_feature/rules', JSON.dump(data), "CONTENT_TYPE" => "application/json" + end + + it 'returns correct error response' do + expect(last_response.status).to eq(422) + expect(json_response).to eq(api_rule_type_invalid_response) + end + end + + describe 'disable nil type parameter' do + before do + data = rule.value + data["type"] = nil + delete '/features/my_feature/rules', JSON.dump(data), "CONTENT_TYPE" => "application/json" + end + + it 'returns correct error response' do + expect(last_response.status).to eq(422) + expect(json_response).to eq(api_rule_type_invalid_response) + end + end + + describe 'enable nil value parameter' do + before do + data = rule.value + data["value"] = nil + post '/features/my_feature/rules', JSON.dump(data), "CONTENT_TYPE" => "application/json" + end + + it 'returns correct error response' do + expect(last_response.status).to eq(422) + expect(json_response).to eq(api_rule_value_invalid_response) + end + end + + describe 'disable nil value parameter' do + before do + data = rule.value + data["value"] = nil + delete '/features/my_feature/rules', JSON.dump(data), "CONTENT_TYPE" => "application/json" + end + + it 'returns correct error response' do + expect(last_response.status).to eq(422) + expect(json_response).to eq(api_rule_value_invalid_response) + end + end + + describe 'enable missing feature' do + before do + post '/features/my_feature/rules', JSON.dump(rule.value), "CONTENT_TYPE" => "application/json" + end + + it 'enables rule for feature' do + expect(last_response.status).to eq(200) + expect(flipper[:my_feature].enabled?(actor)).to be_truthy + expect(flipper[:my_feature].enabled_gate_names).to eq([:rule]) + end + + it 'returns decorated feature with rule enabled' do + gate = json_response['gates'].find { |gate| gate['key'] == 'rules' } + expect(gate['value']).to eq([rule.value]) + end + end + + describe 'disable missing feature' do + before do + delete '/features/my_feature/rules', JSON.dump(rule.value), "CONTENT_TYPE" => "application/json" + end + + it 'disables rule for feature' do + expect(last_response.status).to eq(200) + expect(flipper[:my_feature].enabled?(actor)).to be_falsy + expect(flipper[:my_feature].enabled_gate_names).to be_empty + end + + it 'returns decorated feature with rule gate disabled' do + gate = json_response['gates'].find { |gate| gate['key'] == 'rules' } + expect(gate['value']).to be_empty + end + end +end diff --git a/spec/flipper/dsl_spec.rb b/spec/flipper/dsl_spec.rb index 4d98d7e70..4906f8b3c 100644 --- a/spec/flipper/dsl_spec.rb +++ b/spec/flipper/dsl_spec.rb @@ -139,6 +139,21 @@ end end + describe '#rule' do + context 'for Hash' do + it 'returns rule instance' do + rule = Flipper::Rule.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "basic"} + ) + result = subject.rule(rule.value) + expect(result).to be_instance_of(Flipper::Rule) + expect(result.value).to eq(rule.value) + end + end + end + describe '#actor' do context 'for a thing' do it 'returns actor instance' do diff --git a/spec/flipper/feature_spec.rb b/spec/flipper/feature_spec.rb index 51256e2ec..48c91c6d6 100644 --- a/spec/flipper/feature_spec.rb +++ b/spec/flipper/feature_spec.rb @@ -64,7 +64,7 @@ instance.gates.each do |gate| expect(gate).to be_a(Flipper::Gate) end - expect(instance.gates.size).to be(5) + expect(instance.gates.size).to be(6) end end @@ -802,12 +802,14 @@ :actor, :boolean, :group, + :rule, ]) expect(subject.disabled_gate_names.to_set).to eq(Set[ :actor, :boolean, :group, + :rule, ]) end end diff --git a/spec/flipper_spec.rb b/spec/flipper_spec.rb index 76bc8bbd0..a2b4c999e 100644 --- a/spec/flipper_spec.rb +++ b/spec/flipper_spec.rb @@ -65,7 +65,18 @@ describe "delegation to instance" do let(:group) { Flipper::Types::Group.new(:admins) } - let(:actor) { Flipper::Actor.new("1") } + let(:actor) { + Flipper::Actor.new("1", { + "plan" => "basic", + }) + } + let(:rule) { + Flipper::Rule.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "basic"} + ) + } before do described_class.configure do |config| @@ -97,6 +108,16 @@ expect(described_class.boolean).to eq(described_class.instance.boolean) end + it 'delegates enable_rule to instance' do + described_class.enable_rule(:search, rule) + expect(described_class.instance.enabled?(:search, actor)).to be(true) + end + + it 'delegates disable_rule to instance' do + described_class.disable_rule(:search, rule) + expect(described_class.instance.enabled?(:search, actor)).to be(false) + end + it 'delegates enable_actor to instance' do described_class.enable_actor(:search, actor) expect(described_class.instance.enabled?(:search, actor)).to be(true) diff --git a/spec/support/spec_helpers.rb b/spec/support/spec_helpers.rb index 743646ec4..050169918 100644 --- a/spec/support/spec_helpers.rb +++ b/spec/support/spec_helpers.rb @@ -42,6 +42,14 @@ def api_not_found_response } end + def api_positive_percentage_error_response + { + 'code' => 3, + 'message' => 'Percentage must be a positive number less than or equal to 100.', + 'more_info' => api_error_code_reference_url, + } + end + def api_flipper_id_is_missing_response { 'code' => 4, @@ -50,10 +58,18 @@ def api_flipper_id_is_missing_response } end - def api_positive_percentage_error_response + def api_rule_type_invalid_response { - 'code' => 3, - 'message' => 'Percentage must be a positive number less than or equal to 100.', + 'code' => 6, + 'message' => 'Required parameter rule type is missing.', + 'more_info' => api_error_code_reference_url, + } + end + + def api_rule_value_invalid_response + { + 'code' => 7, + 'message' => 'Required parameter rule value is missing.', 'more_info' => api_error_code_reference_url, } end From 80620a444667a1378bb71146091f7abf0ebcee45 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Thu, 2 Sep 2021 13:19:23 -0400 Subject: [PATCH 022/176] Updates to get things passing again after merging learn the rules namespace --- lib/flipper/api/v1/actions/rules_gate.rb | 4 ++-- lib/flipper/dsl.rb | 8 ++++---- lib/flipper/feature.rb | 4 ++-- lib/flipper/rules/condition.rb | 4 ++-- spec/flipper/adapter_spec.rb | 1 + spec/flipper/adapters/read_only_spec.rb | 4 ++-- spec/flipper/api/v1/actions/feature_spec.rb | 20 +++++++++++++++++++ spec/flipper/api/v1/actions/features_spec.rb | 10 ++++++++++ .../flipper/api/v1/actions/rules_gate_spec.rb | 2 +- spec/flipper/dsl_spec.rb | 4 ++-- spec/flipper_spec.rb | 2 +- 11 files changed, 47 insertions(+), 16 deletions(-) diff --git a/lib/flipper/api/v1/actions/rules_gate.rb b/lib/flipper/api/v1/actions/rules_gate.rb index 8f8c5f880..778702c11 100644 --- a/lib/flipper/api/v1/actions/rules_gate.rb +++ b/lib/flipper/api/v1/actions/rules_gate.rb @@ -13,7 +13,7 @@ class RulesGate < Api::Action def post ensure_valid_params feature = flipper[feature_name] - feature.enable Flipper::Rule.from_hash(rule_hash) + feature.enable Flipper::Rules.build(rule_hash) decorated_feature = Decorators::Feature.new(feature) json_response(decorated_feature.as_json, 200) @@ -22,7 +22,7 @@ def post def delete ensure_valid_params feature = flipper[feature_name] - feature.disable Flipper::Rule.from_hash(rule_hash) + feature.disable Flipper::Rules.build(rule_hash) decorated_feature = Decorators::Feature.new(feature) json_response(decorated_feature.as_json, 200) diff --git a/lib/flipper/dsl.rb b/lib/flipper/dsl.rb index 0ec373c58..1ab7066dd 100644 --- a/lib/flipper/dsl.rb +++ b/lib/flipper/dsl.rb @@ -47,7 +47,7 @@ def enable(name, *args) # Public: Enable a feature for a rule. # # name - The String or Symbol name of the feature. - # rule - a Flipper::Rule instance. + # rule - a flipper rule instance. # # Returns result of Feature#enable. def enable_rule(name, rule) @@ -111,7 +111,7 @@ def disable(name, *args) # Public: Disable a feature for a rule. # # name - The String or Symbol name of the feature. - # rule - a Flipper::Rule instance. + # rule - a flipper rule instance. # # Returns result of Feature#disable. def disable_rule(name, rule) @@ -267,9 +267,9 @@ def actor(thing) # # hash - The Hash that you would like to turn into a rule. # - # Returns an instance of Flipper::Rule. + # Returns an instance of flipper rule def rule(hash) - Rule.from_hash(hash) + Rules.build(hash) end # Public: Shortcut for getting a percentage of time instance. diff --git a/lib/flipper/feature.rb b/lib/flipper/feature.rb index dd9566808..4721b1860 100644 --- a/lib/flipper/feature.rb +++ b/lib/flipper/feature.rb @@ -121,7 +121,7 @@ def enabled?(thing = nil) # Public: Enables a rule for a feature. # - # rule - a Flipper::Rule instance. + # rule - a flipper rule instance. # # Returns result of enable. def enable_rule(rule) @@ -170,7 +170,7 @@ def enable_percentage_of_actors(percentage) # Public: Disables a rule for a feature. # - # rule - a Flipper::Rule instance. + # rule - a flipper rule instance. # # Returns result of disable. def disable_rule(rule) diff --git a/lib/flipper/rules/condition.rb b/lib/flipper/rules/condition.rb index 4e833ae4b..2a690c036 100644 --- a/lib/flipper/rules/condition.rb +++ b/lib/flipper/rules/condition.rb @@ -15,8 +15,8 @@ class Condition scaling_factor = 1_000 id = "#{feature_name}#{left}" left && right && (Zlib.crc32(id) % (100 * scaling_factor) < right * scaling_factor) - end - } + end + }.freeze def self.build(hash) new(hash.fetch("left"), hash.fetch("operator"), hash.fetch("right")) diff --git a/spec/flipper/adapter_spec.rb b/spec/flipper/adapter_spec.rb index d74f59bbe..40ab4cf48 100644 --- a/spec/flipper/adapter_spec.rb +++ b/spec/flipper/adapter_spec.rb @@ -8,6 +8,7 @@ boolean: nil, groups: Set.new, actors: Set.new, + rules: Set.new, percentage_of_actors: nil, percentage_of_time: nil, } diff --git a/spec/flipper/adapters/read_only_spec.rb b/spec/flipper/adapters/read_only_spec.rb index 3d0536c1b..4f5a58632 100644 --- a/spec/flipper/adapters/read_only_spec.rb +++ b/spec/flipper/adapters/read_only_spec.rb @@ -43,7 +43,7 @@ end it 'can get feature' do - rule = Flipper::Rule.new( + rule = Flipper::Rules::Condition.new( {"type" => "property", "value" => "plan"}, {"type" => "operator", "value" => "eq"}, {"type" => "string", "value" => "basic"} @@ -61,7 +61,7 @@ actors: Set['22'], rules: Set[ { - "type" => "Rule", + "type" => "Condition", "value" => { "left" => {"type" => "property", "value" => "plan"}, "operator" => {"type" => "operator", "value" => "eq"}, diff --git a/spec/flipper/api/v1/actions/feature_spec.rb b/spec/flipper/api/v1/actions/feature_spec.rb index f738ba667..f5a3f544b 100644 --- a/spec/flipper/api/v1/actions/feature_spec.rb +++ b/spec/flipper/api/v1/actions/feature_spec.rb @@ -27,6 +27,11 @@ 'name' => 'actor', 'value' => [], }, + { + 'key' => 'rules', + 'name' => 'rule', + 'value' => [], + }, { 'key' => 'percentage_of_actors', 'name' => 'percentage_of_actors', @@ -71,6 +76,11 @@ 'name' => 'actor', 'value' => [], }, + { + 'key' => 'rules', + 'name' => 'rule', + 'value' => [], + }, { 'key' => 'percentage_of_actors', 'name' => 'percentage_of_actors', @@ -131,6 +141,11 @@ 'name' => 'actor', 'value' => [], }, + { + 'key' => 'rules', + 'name' => 'rule', + 'value' => [], + }, { 'key' => 'percentage_of_actors', 'name' => 'percentage_of_actors', @@ -175,6 +190,11 @@ 'name' => 'actor', 'value' => [], }, + { + 'key' => 'rules', + 'name' => 'rule', + 'value' => [], + }, { 'key' => 'percentage_of_actors', 'name' => 'percentage_of_actors', diff --git a/spec/flipper/api/v1/actions/features_spec.rb b/spec/flipper/api/v1/actions/features_spec.rb index 8915cac6a..5243287d7 100644 --- a/spec/flipper/api/v1/actions/features_spec.rb +++ b/spec/flipper/api/v1/actions/features_spec.rb @@ -30,6 +30,11 @@ 'name' => 'actor', 'value' => ['10'], }, + { + 'key' => 'rules', + 'name' => 'rule', + 'value' => [], + }, { 'key' => 'percentage_of_actors', 'name' => 'percentage_of_actors', @@ -125,6 +130,11 @@ 'name' => 'actor', 'value' => [], }, + { + 'key' => 'rules', + 'name' => 'rule', + 'value' => [], + }, { 'key' => 'percentage_of_actors', 'name' => 'percentage_of_actors', diff --git a/spec/flipper/api/v1/actions/rules_gate_spec.rb b/spec/flipper/api/v1/actions/rules_gate_spec.rb index 5f37a1b95..eae0a1d45 100644 --- a/spec/flipper/api/v1/actions/rules_gate_spec.rb +++ b/spec/flipper/api/v1/actions/rules_gate_spec.rb @@ -9,7 +9,7 @@ }) } let(:rule) { - Flipper::Rule.new( + Flipper::Rules::Condition.new( {"type" => "property", "value" => "plan"}, {"type" => "operator", "value" => "eq"}, {"type" => "string", "value" => "basic"} diff --git a/spec/flipper/dsl_spec.rb b/spec/flipper/dsl_spec.rb index 4906f8b3c..61d3093d3 100644 --- a/spec/flipper/dsl_spec.rb +++ b/spec/flipper/dsl_spec.rb @@ -142,13 +142,13 @@ describe '#rule' do context 'for Hash' do it 'returns rule instance' do - rule = Flipper::Rule.new( + rule = Flipper::Rules::Condition.new( {"type" => "property", "value" => "plan"}, {"type" => "operator", "value" => "eq"}, {"type" => "string", "value" => "basic"} ) result = subject.rule(rule.value) - expect(result).to be_instance_of(Flipper::Rule) + expect(result).to be_instance_of(Flipper::Rules::Condition) expect(result.value).to eq(rule.value) end end diff --git a/spec/flipper_spec.rb b/spec/flipper_spec.rb index a2b4c999e..932aeb68b 100644 --- a/spec/flipper_spec.rb +++ b/spec/flipper_spec.rb @@ -71,7 +71,7 @@ }) } let(:rule) { - Flipper::Rule.new( + Flipper::Rules::Condition.new( {"type" => "property", "value" => "plan"}, {"type" => "operator", "value" => "eq"}, {"type" => "string", "value" => "basic"} From 2eea0d618a5f22f99fda5d437e732917fd0a4ed5 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Thu, 2 Sep 2021 13:21:51 -0400 Subject: [PATCH 023/176] Get cloud specs passing again --- spec/flipper/cloud_spec.rb | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/spec/flipper/cloud_spec.rb b/spec/flipper/cloud_spec.rb index ec0d3e0c6..9af07ff2c 100644 --- a/spec/flipper/cloud_spec.rb +++ b/spec/flipper/cloud_spec.rb @@ -132,10 +132,10 @@ cloud_flipper = Flipper::Cloud.new(token: "asdf") get_all = { - "logging" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: "5"}, - "search" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, - "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, - "test" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, + "logging" => {actors: Set.new, boolean: nil, groups: Set.new, rules: Set.new, percentage_of_actors: nil, percentage_of_time: "5"}, + "search" => {actors: Set.new, boolean: "true", groups: Set.new, rules: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, + "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, rules: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, + "test" => {actors: Set.new, boolean: "true", groups: Set.new, rules: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, } expect(flipper.adapter.get_all).to eq(get_all) @@ -161,10 +161,10 @@ cloud_flipper = Flipper::Cloud.new(token: "asdf") get_all = { - "logging" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: "5"}, - "search" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, - "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, - "test" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, + "logging" => {actors: Set.new, boolean: nil, groups: Set.new, rules: Set.new, percentage_of_actors: nil, percentage_of_time: "5"}, + "search" => {actors: Set.new, boolean: "true", groups: Set.new, rules: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, + "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, rules: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, + "test" => {actors: Set.new, boolean: "true", groups: Set.new, rules: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, } expect(flipper.adapter.get_all).to eq(get_all) @@ -189,10 +189,10 @@ cloud_flipper = Flipper::Cloud.new(token: "asdf") get_all = { - "logging" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: "5"}, - "search" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, - "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, - "test" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, + "logging" => {actors: Set.new, boolean: nil, groups: Set.new, rules: Set.new, percentage_of_actors: nil, percentage_of_time: "5"}, + "search" => {actors: Set.new, boolean: "true", groups: Set.new, rules: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, + "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, rules: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, + "test" => {actors: Set.new, boolean: "true", groups: Set.new, rules: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, } expect(flipper.adapter.get_all).to eq(get_all) From 6b90259bb9975e78422466f0d5012435e81f5e38 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Thu, 2 Sep 2021 13:21:59 -0400 Subject: [PATCH 024/176] Add more leniance to the random test --- spec/flipper/rules/condition_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/flipper/rules/condition_spec.rb b/spec/flipper/rules/condition_spec.rb index 8f4c446f5..6875192b9 100644 --- a/spec/flipper/rules/condition_spec.rb +++ b/spec/flipper/rules/condition_spec.rb @@ -141,7 +141,7 @@ end enabled, disabled = results.partition { |r| r } - expect(enabled.size).to be_within(10).of(250) + expect(enabled.size).to be_within(30).of(250) end end From b09ea7d4b82e703f2d38edb125dfebfe2a49b7b1 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Thu, 2 Sep 2021 13:33:11 -0400 Subject: [PATCH 025/176] Add a handy superclass --- lib/flipper/gates/rule.rb | 4 +--- lib/flipper/rules/any.rb | 4 +++- lib/flipper/rules/condition.rb | 4 +++- lib/flipper/rules/rule.rb | 8 ++++++++ 4 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 lib/flipper/rules/rule.rb diff --git a/lib/flipper/gates/rule.rb b/lib/flipper/gates/rule.rb index 6e8e26b4b..c5dee99cc 100644 --- a/lib/flipper/gates/rule.rb +++ b/lib/flipper/gates/rule.rb @@ -31,9 +31,7 @@ def open?(context) end def protects?(thing) - thing.is_a?(Flipper::Rules::Condition) || - thing.is_a?(Flipper::Rules::Any) || - thing.is_a?(Flipper::Rules::All) + thing.is_a?(Flipper::Rules::Rule) end end end diff --git a/lib/flipper/rules/any.rb b/lib/flipper/rules/any.rb index f1b8d9875..7d72903ea 100644 --- a/lib/flipper/rules/any.rb +++ b/lib/flipper/rules/any.rb @@ -1,6 +1,8 @@ +require 'flipper/rules/rule' + module Flipper module Rules - class Any + class Any < Rule def self.build(rules) new(*rules.map { |rule| Flipper::Rules.build(rule) }) end diff --git a/lib/flipper/rules/condition.rb b/lib/flipper/rules/condition.rb index 2a690c036..f203c49ca 100644 --- a/lib/flipper/rules/condition.rb +++ b/lib/flipper/rules/condition.rb @@ -1,6 +1,8 @@ +require 'flipper/rules/rule' + module Flipper module Rules - class Condition + class Condition < Rule OPERATIONS = { "eq" => -> (left:, right:, **) { left == right }, "neq" => -> (left:, right:, **) { left != right }, diff --git a/lib/flipper/rules/rule.rb b/lib/flipper/rules/rule.rb new file mode 100644 index 000000000..9d7ce2ea6 --- /dev/null +++ b/lib/flipper/rules/rule.rb @@ -0,0 +1,8 @@ +module Flipper + module Rules + # Base class for the various rules. Just makes it easier to detect if a + # rule is being used or not. + class Rule + end + end +end From cc6724cc5eee6e082a882047d284b9a1a9f22efa Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Thu, 2 Sep 2021 13:33:19 -0400 Subject: [PATCH 026/176] Fix incorrect require --- lib/flipper/rules/all.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/flipper/rules/all.rb b/lib/flipper/rules/all.rb index a0d2e6bf1..90cf4f6f8 100644 --- a/lib/flipper/rules/all.rb +++ b/lib/flipper/rules/all.rb @@ -1,4 +1,4 @@ -require 'flipper/rules/all' +require 'flipper/rules/any' module Flipper module Rules From 818944eb459dd7c94a866855444438dd8deab22d Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Thu, 2 Sep 2021 13:39:49 -0400 Subject: [PATCH 027/176] Allow passing rule or hash to enable_rule/disable_rule --- lib/flipper/feature.rb | 8 ++++---- lib/flipper/rules.rb | 8 ++++++++ spec/flipper/feature_spec.rb | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/lib/flipper/feature.rb b/lib/flipper/feature.rb index 4721b1860..ca0e435c8 100644 --- a/lib/flipper/feature.rb +++ b/lib/flipper/feature.rb @@ -121,11 +121,11 @@ def enabled?(thing = nil) # Public: Enables a rule for a feature. # - # rule - a flipper rule instance. + # rule - a rule or Hash that can be converted to a rule. # # Returns result of enable. def enable_rule(rule) - enable rule + enable Rules.wrap(rule) end # Public: Enables a feature for an actor. @@ -170,11 +170,11 @@ def enable_percentage_of_actors(percentage) # Public: Disables a rule for a feature. # - # rule - a flipper rule instance. + # rule - a rule or Hash that can be converted to a rule. # # Returns result of disable. def disable_rule(rule) - disable rule + disable Rules.wrap(rule) end # Public: Disables a feature for an actor. diff --git a/lib/flipper/rules.rb b/lib/flipper/rules.rb index 30e18193e..00d7c3c41 100644 --- a/lib/flipper/rules.rb +++ b/lib/flipper/rules.rb @@ -4,6 +4,14 @@ module Flipper module Rules + def self.wrap(thing) + if thing.is_a?(Flipper::Rules::Rule) + thing + else + build(thing) + end + end + def self.build(hash) type = const_get(hash.fetch("type")) type.build(hash.fetch("value")) diff --git a/spec/flipper/feature_spec.rb b/spec/flipper/feature_spec.rb index 48c91c6d6..b3f3f9744 100644 --- a/spec/flipper/feature_spec.rb +++ b/spec/flipper/feature_spec.rb @@ -650,6 +650,38 @@ end end + describe '#enable_rule/disable_rule' do + context "with rule instance" do + it "updates gate values to include rule" do + rule = Flipper::Rules::Condition.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "basic"} + ) + expect(subject.gate_values.rules).to be_empty + subject.enable_rule(rule) + expect(subject.gate_values.rules).to eq(Set[rule.value]) + subject.disable_rule(rule) + expect(subject.gate_values.rules).to be_empty + end + end + + context "with Hash" do + it "updates gate values to include rule" do + rule = Flipper::Rules::Condition.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "basic"} + ) + expect(subject.gate_values.rules).to be_empty + subject.enable_rule(rule.value) + expect(subject.gate_values.rules).to eq(Set[rule.value]) + subject.disable_rule(rule.value) + expect(subject.gate_values.rules).to be_empty + end + end + end + describe '#enable_actor/disable_actor' do context 'with object that responds to flipper_id' do it 'updates the gate values to include the actor' do From cdf6cadc86e6f32fc3dd64705f6fe781f1bfd0f9 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Thu, 2 Sep 2021 13:41:47 -0400 Subject: [PATCH 028/176] Update dsl to work with instance or hash --- lib/flipper/dsl.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/flipper/dsl.rb b/lib/flipper/dsl.rb index 1ab7066dd..60133a073 100644 --- a/lib/flipper/dsl.rb +++ b/lib/flipper/dsl.rb @@ -47,7 +47,7 @@ def enable(name, *args) # Public: Enable a feature for a rule. # # name - The String or Symbol name of the feature. - # rule - a flipper rule instance. + # rule - a Flipper::Rules::Rule instance or a Hash. # # Returns result of Feature#enable. def enable_rule(name, rule) @@ -111,7 +111,7 @@ def disable(name, *args) # Public: Disable a feature for a rule. # # name - The String or Symbol name of the feature. - # rule - a flipper rule instance. + # rule - a Flipper::Rules::Rule instance or a Hash. # # Returns result of Feature#disable. def disable_rule(name, rule) @@ -263,13 +263,13 @@ def actor(thing) Types::Actor.new(thing) end - # Public: Wraps an object as a flipper rule. + # Public: Wraps an object as a Flipper::Rules::Rule. # - # hash - The Hash that you would like to turn into a rule. + # thing - The rule or Hash that you would like to wrap. # - # Returns an instance of flipper rule - def rule(hash) - Rules.build(hash) + # Returns an instance of Flipper::Rules::Rule. + def rule(thing) + Rules.wrap(thing) end # Public: Shortcut for getting a percentage of time instance. From 7606965092ea0cc55bd3d803ce350f822021d054 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Fri, 3 Sep 2021 08:46:58 -0400 Subject: [PATCH 029/176] Minor whitespace --- spec/flipper_integration_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/flipper_integration_spec.rb b/spec/flipper_integration_spec.rb index 0cfb66b6b..a36bf8328 100644 --- a/spec/flipper_integration_spec.rb +++ b/spec/flipper_integration_spec.rb @@ -550,7 +550,6 @@ end end - context "for rule" do it "works" do rule = Flipper::Rules::Condition.new( From 1c5e0d5b91acca2d830f205f496ec812eb785870 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Fri, 3 Sep 2021 08:47:18 -0400 Subject: [PATCH 030/176] Add equality and specs for any/all/condition --- lib/flipper/rules/any.rb | 11 +- lib/flipper/rules/condition.rb | 10 ++ spec/flipper/rules/all_spec.rb | 159 +++++++++++++++++++++++++++ spec/flipper/rules/any_spec.rb | 156 ++++++++++++++++++++++++++ spec/flipper/rules/condition_spec.rb | 35 ++++++ 5 files changed, 369 insertions(+), 2 deletions(-) create mode 100644 spec/flipper/rules/all_spec.rb create mode 100644 spec/flipper/rules/any_spec.rb diff --git a/lib/flipper/rules/any.rb b/lib/flipper/rules/any.rb index 7d72903ea..8d2a85d7f 100644 --- a/lib/flipper/rules/any.rb +++ b/lib/flipper/rules/any.rb @@ -3,10 +3,12 @@ module Flipper module Rules class Any < Rule - def self.build(rules) - new(*rules.map { |rule| Flipper::Rules.build(rule) }) + def self.build(*rules) + new(rules.flatten.map { |rule| Flipper::Rules.build(rule) }) end + attr_reader :rules + def initialize(*rules) @rules = rules.flatten end @@ -18,6 +20,11 @@ def value } end + def eql?(other) + self.class.eql?(other.class) && @rules == other.rules + end + alias_method :==, :eql? + def matches?(feature_name, actor) @rules.any? { |rule| rule.matches?(feature_name, actor) } end diff --git a/lib/flipper/rules/condition.rb b/lib/flipper/rules/condition.rb index f203c49ca..80503adcc 100644 --- a/lib/flipper/rules/condition.rb +++ b/lib/flipper/rules/condition.rb @@ -24,6 +24,8 @@ def self.build(hash) new(hash.fetch("left"), hash.fetch("operator"), hash.fetch("right")) end + attr_reader :left, :operator, :right + def initialize(left, operator, right) @left = left @operator = operator @@ -41,6 +43,14 @@ def value } end + def eql?(other) + self.class.eql?(other.class) && + @left == other.left && + @operator == other.operator && + @right == other.right + end + alias_method :==, :eql? + def matches?(feature_name, actor) attributes = actor.flipper_properties left_value = evaluate(@left, attributes) diff --git a/spec/flipper/rules/all_spec.rb b/spec/flipper/rules/all_spec.rb new file mode 100644 index 000000000..bfc208462 --- /dev/null +++ b/spec/flipper/rules/all_spec.rb @@ -0,0 +1,159 @@ +require 'helper' + +RSpec.describe Flipper::Rules::All do + let(:feature_name) { "search" } + let(:plan_condition) { + Flipper::Rules::Condition.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "basic"} + ) + } + let(:age_condition) { + Flipper::Rules::Condition.new( + {"type" => "property", "value" => "age"}, + {"type" => "operator", "value" => "gte"}, + {"type" => "integer", "value" => 21} + ) + } + let(:any_rule) { + Flipper::Rules::Any.new( + plan_condition, + age_condition + ) + } + let(:rule) { + Flipper::Rules::All.new( + plan_condition, + age_condition + ) + } + + describe "#initialize" do + it "flattens rules" do + instance = Flipper::Rules::Any.new([[plan_condition, age_condition]]) + expect(instance.rules).to eq([ + plan_condition, + age_condition, + ]) + end + end + + describe ".build" do + context "for Array of Hashes" do + it "builds instance" do + instance = Flipper::Rules::All.build([plan_condition.value, age_condition.value]) + expect(instance).to be_instance_of(Flipper::Rules::All) + expect(instance.rules).to eq([ + plan_condition, + age_condition, + ]) + end + end + + context "for nested Array of Hashes" do + it "builds instance" do + instance = Flipper::Rules::All.build([[plan_condition.value, age_condition.value]]) + expect(instance).to be_instance_of(Flipper::Rules::All) + expect(instance.rules).to eq([ + plan_condition, + age_condition, + ]) + end + end + + context "for Array with All rule" do + it "builds instance" do + instance = Flipper::Rules::All.build(any_rule.value) + expect(instance).to be_instance_of(Flipper::Rules::All) + expect(instance.rules).to eq([any_rule]) + end + end + end + + describe "#value" do + it "returns type and value" do + expect(rule.value).to eq({ + "type" => "All", + "value" => [ + plan_condition.value, + age_condition.value, + ], + }) + end + end + + describe "#eql?" do + it "returns true if equal" do + other_rule = Flipper::Rules::All.new( + Flipper::Rules::Condition.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "basic"} + ), + Flipper::Rules::Condition.new( + {"type" => "property", "value" => "age"}, + {"type" => "operator", "value" => "gte"}, + {"type" => "integer", "value" => 21} + ) + ) + expect(rule).to eql(other_rule) + expect(rule == other_rule).to be(true) + end + + it "returns false if not equal" do + other_rule = Flipper::Rules::All.new( + Flipper::Rules::Condition.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "premium"} + ), + Flipper::Rules::Condition.new( + {"type" => "property", "value" => "age"}, + {"type" => "operator", "value" => "gte"}, + {"type" => "integer", "value" => 21} + ) + ) + expect(rule).not_to eql(other_rule) + expect(rule == other_rule).to be(false) + end + + it "returns false if not rule" do + expect(rule).not_to eql(Object.new) + expect(rule == Object.new).to be(false) + end + end + + describe "#matches?" do + let(:rule) { + Flipper::Rules::All.new( + Flipper::Rules::Condition.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "basic"} + ), + Flipper::Rules::Condition.new( + {"type" => "property", "value" => "age"}, + {"type" => "operator", "value" => "gte"}, + {"type" => "integer", "value" => 21} + ) + ) + } + + it "returns true when all conditions match" do + actor = Flipper::Actor.new("User;1", "plan" => "basic", "age" => 21) + expect(rule.matches?(feature_name, actor)).to be(true) + end + + it "returns false when any condition does NOT match" do + actor = Flipper::Actor.new("User;1", "plan" => "premium", "age" => 18) + expect(rule.matches?(feature_name, actor)).to be(false) + + actor = Flipper::Actor.new("User;1", "plan" => "basic", "age" => 20) + expect(rule.matches?(feature_name, actor)).to be(false) + + actor = Flipper::Actor.new("User;1", "plan" => "premium", "age" => 21) + expect(rule.matches?(feature_name, actor)).to be(false) + end + end +end diff --git a/spec/flipper/rules/any_spec.rb b/spec/flipper/rules/any_spec.rb new file mode 100644 index 000000000..b7e8c36c0 --- /dev/null +++ b/spec/flipper/rules/any_spec.rb @@ -0,0 +1,156 @@ +require 'helper' + +RSpec.describe Flipper::Rules::Any do + let(:feature_name) { "search" } + let(:plan_condition) { + Flipper::Rules::Condition.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "basic"} + ) + } + let(:age_condition) { + Flipper::Rules::Condition.new( + {"type" => "property", "value" => "age"}, + {"type" => "operator", "value" => "gte"}, + {"type" => "integer", "value" => 21} + ) + } + let(:all_rule) { + Flipper::Rules::All.new( + plan_condition, + age_condition + ) + } + let(:rule) { + Flipper::Rules::Any.new( + plan_condition, + age_condition + ) + } + + describe "#initialize" do + it "flattens rules" do + instance = Flipper::Rules::Any.new([[plan_condition, age_condition]]) + expect(instance.rules).to eq([ + plan_condition, + age_condition, + ]) + end + end + + describe ".build" do + context "for Array of Hashes" do + it "builds instance" do + instance = Flipper::Rules::Any.build([plan_condition.value, age_condition.value]) + expect(instance).to be_instance_of(Flipper::Rules::Any) + expect(instance.rules).to eq([ + plan_condition, + age_condition, + ]) + end + end + + context "for nested Array of Hashes" do + it "builds instance" do + instance = Flipper::Rules::Any.build([[plan_condition.value, age_condition.value]]) + expect(instance).to be_instance_of(Flipper::Rules::Any) + expect(instance.rules).to eq([ + plan_condition, + age_condition, + ]) + end + end + + context "for Array with All rule" do + it "builds instance" do + instance = Flipper::Rules::Any.build(all_rule.value) + expect(instance).to be_instance_of(Flipper::Rules::Any) + expect(instance.rules).to eq([all_rule]) + end + end + end + + describe "#value" do + it "returns type and value" do + expect(rule.value).to eq({ + "type" => "Any", + "value" => [ + plan_condition.value, + age_condition.value, + ], + }) + end + end + + describe "#eql?" do + it "returns true if equal" do + other_rule = Flipper::Rules::Any.new( + Flipper::Rules::Condition.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "basic"} + ), + Flipper::Rules::Condition.new( + {"type" => "property", "value" => "age"}, + {"type" => "operator", "value" => "gte"}, + {"type" => "integer", "value" => 21} + ) + ) + expect(rule).to eql(other_rule) + expect(rule == other_rule).to be(true) + end + + it "returns false if not equal" do + other_rule = Flipper::Rules::Any.new( + Flipper::Rules::Condition.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "premium"} + ), + Flipper::Rules::Condition.new( + {"type" => "property", "value" => "age"}, + {"type" => "operator", "value" => "gte"}, + {"type" => "integer", "value" => 21} + ) + ) + expect(rule).not_to eql(other_rule) + expect(rule == other_rule).to be(false) + end + + it "returns false if not rule" do + expect(rule).not_to eql(Object.new) + expect(rule == Object.new).to be(false) + end + end + + describe "#matches?" do + let(:rule) { + Flipper::Rules::Any.new( + Flipper::Rules::Condition.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "basic"} + ), + Flipper::Rules::Condition.new( + {"type" => "property", "value" => "age"}, + {"type" => "operator", "value" => "gte"}, + {"type" => "integer", "value" => 21} + ) + ) + } + + it "returns true when any condition matches" do + plan_actor = Flipper::Actor.new("User;1", "plan" => "basic") + expect(rule.matches?(feature_name, plan_actor)).to be(true) + + age_actor = Flipper::Actor.new("User;1", "age" => 21) + expect(rule.matches?(feature_name, age_actor)).to be(true) + end + + it "returns false when no condition matches" do + actor = Flipper::Actor.new("User;1", "plan" => "premium", "age" => 18) + expect(rule.matches?(feature_name, actor)).to be(false) + end + end +end diff --git a/spec/flipper/rules/condition_spec.rb b/spec/flipper/rules/condition_spec.rb index 6875192b9..793dc2542 100644 --- a/spec/flipper/rules/condition_spec.rb +++ b/spec/flipper/rules/condition_spec.rb @@ -3,6 +3,41 @@ RSpec.describe Flipper::Rules::Condition do let(:feature_name) { "search" } + describe "#eql?" do + let(:rule) { + Flipper::Rules::Condition.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "basic"} + ) + } + + it "returns true if equal" do + other_rule = Flipper::Rules::Condition.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "basic"} + ) + expect(rule).to eql(other_rule) + expect(rule == other_rule).to be(true) + end + + it "returns false if not equal" do + other_rule = Flipper::Rules::Condition.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "premium"} + ) + expect(rule).not_to eql(other_rule) + expect(rule == other_rule).to be(false) + end + + it "returns false if not rule" do + expect(rule).not_to eql(Object.new) + expect(rule == Object.new).to be(false) + end + end + describe "#matches?" do context "eq" do let(:rule) { From 861cdd76c226522a5024aef1c33388ceb8631b95 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sat, 4 Sep 2021 20:02:33 -0400 Subject: [PATCH 031/176] Add super basic rules example --- examples/rules.rb | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 examples/rules.rb diff --git a/examples/rules.rb b/examples/rules.rb new file mode 100644 index 000000000..67bbbbc36 --- /dev/null +++ b/examples/rules.rb @@ -0,0 +1,31 @@ +require 'bundler/setup' +require 'flipper' + +class User < Struct.new(:id, :flipper_properties) + include Flipper::Identifier +end + +user = User.new(1, { + "plan" => "basic", + "age" => 39, +}) + +p Flipper.enabled?(:something, user) +any_rule = Flipper::Rules::Any.new( + Flipper::Rules::Condition.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "basic"} + ), + Flipper::Rules::Condition.new( + {"type" => "property", "value" => "age"}, + {"type" => "operator", "value" => "gte"}, + {"type" => "integer", "value" => 21} + ) +) + +Flipper.enable_rule :something, any_rule +p Flipper.enabled?(:something, user) + +Flipper.disable_rule :something, any_rule +p Flipper.enabled?(:something, user) From f539c3aa66803f2a171ca2cc6fb63b015247334c Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sun, 5 Sep 2021 13:22:19 -0400 Subject: [PATCH 032/176] Added some shortcuts to see how it felt --- examples/rules.rb | 20 +++++++ lib/flipper.rb | 13 +++++ lib/flipper/rules.rb | 1 + lib/flipper/rules/property.rb | 102 ++++++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+) create mode 100644 lib/flipper/rules/property.rb diff --git a/examples/rules.rb b/examples/rules.rb index 67bbbbc36..22665d74f 100644 --- a/examples/rules.rb +++ b/examples/rules.rb @@ -10,6 +10,7 @@ class User < Struct.new(:id, :flipper_properties) "age" => 39, }) +puts 'Verbose' p Flipper.enabled?(:something, user) any_rule = Flipper::Rules::Any.new( Flipper::Rules::Condition.new( @@ -29,3 +30,22 @@ class User < Struct.new(:id, :flipper_properties) Flipper.disable_rule :something, any_rule p Flipper.enabled?(:something, user) + +puts +puts 'Fancy' +p Flipper.enabled?(:something, user) +any_rule = Flipper.any( + Flipper.property("plan").eq("basic"), + Flipper.property("age").gte(21) +) + +Flipper.enable_rule :something, any_rule +p Flipper.enabled?(:something, user) + +Flipper.disable_rule :something, any_rule +p Flipper.enabled?(:something, user) + +Flipper.enable_rule :something, Flipper.any( + Flipper.property("plan").eq("basic"), + Flipper.property("age").gte(21) +) diff --git a/lib/flipper.rb b/lib/flipper.rb index 2e4611a55..b18ab7309 100644 --- a/lib/flipper.rb +++ b/lib/flipper.rb @@ -69,6 +69,19 @@ def instance=(flipper) :memoize=, :memoizing?, :sync, :sync_secret # For Flipper::Cloud. Will error for OSS Flipper. + + def property(*args) + Flipper::Rules::Property.new(*args) + end + + def any(*args) + Flipper::Rules::Any.new(*args) + end + + def all(*args) + Flipper::Rules::All.new(*args) + end + # Public: Use this to register a group by name. # # name - The Symbol name of the group. diff --git a/lib/flipper/rules.rb b/lib/flipper/rules.rb index 00d7c3c41..c484c81d8 100644 --- a/lib/flipper/rules.rb +++ b/lib/flipper/rules.rb @@ -1,6 +1,7 @@ require 'flipper/rules/condition' require 'flipper/rules/any' require 'flipper/rules/all' +require 'flipper/rules/property' module Flipper module Rules diff --git a/lib/flipper/rules/property.rb b/lib/flipper/rules/property.rb new file mode 100644 index 000000000..5c1877340 --- /dev/null +++ b/lib/flipper/rules/property.rb @@ -0,0 +1,102 @@ +module Flipper + module Rules + class Property + def initialize(name) + @name = name + end + + def value + { + "type" => "property", + "value" => @name, + } + end + + def eq(object) + Flipper::Rules::Condition.new( + value, + {"type" => "operator", "value" => "eq"}, + {"type" => typeof(object), "value" => object} + ) + end + + def neq(object) + Flipper::Rules::Condition.new( + value, + {"type" => "operator", "value" => "neq"}, + {"type" => typeof(object), "value" => object} + ) + end + + def gt(object) + Flipper::Rules::Condition.new( + value, + {"type" => "operator", "value" => "gt"}, + {"type" => "integer", "value" => object} + ) + end + + def gte(object) + Flipper::Rules::Condition.new( + value, + {"type" => "operator", "value" => "gte"}, + {"type" => "integer", "value" => object} + ) + end + + def lt(object) + Flipper::Rules::Condition.new( + value, + {"type" => "operator", "value" => "lt"}, + {"type" => "integer", "value" => object} + ) + end + + def lte(object) + Flipper::Rules::Condition.new( + value, + {"type" => "operator", "value" => "lte"}, + {"type" => "integer", "value" => object} + ) + end + + def in(object) + Flipper::Rules::Condition.new( + value, + {"type" => "operator", "value" => "in"}, + {"type" => "array", "value" => object} + ) + end + + def nin(object) + Flipper::Rules::Condition.new( + value, + {"type" => "operator", "value" => "nin"}, + {"type" => "array", "value" => object} + ) + end + + def percentage(object) + Flipper::Rules::Condition.new( + value, + {"type" => "operator", "value" => "percentage"}, + {"type" => "integer", "value" => object} + ) + end + + private + + def typeof(object) + if object.is_a?(String) + "string" + elsif object.is_a?(Integer) + "integer" + elsif object.respond_to?(:to_a) + "array" + else + raise "unsupported type inference for #{object.inspect}" + end + end + end + end +end From d711954bddb0ac30cf9286a71ac74c0e0c5e96ed Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sun, 5 Sep 2021 13:39:20 -0400 Subject: [PATCH 033/176] Add more complex examples of rules --- examples/rules.rb | 91 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 76 insertions(+), 15 deletions(-) diff --git a/examples/rules.rb b/examples/rules.rb index 67bbbbc36..c6937e938 100644 --- a/examples/rules.rb +++ b/examples/rules.rb @@ -8,24 +8,85 @@ class User < Struct.new(:id, :flipper_properties) user = User.new(1, { "plan" => "basic", "age" => 39, + "roles" => ["team_user"] }) -p Flipper.enabled?(:something, user) -any_rule = Flipper::Rules::Any.new( - Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "basic"} - ), - Flipper::Rules::Condition.new( - {"type" => "property", "value" => "age"}, - {"type" => "operator", "value" => "gte"}, - {"type" => "integer", "value" => 21} - ) +admin_user = User.new(2, { + "roles" => ["admin", "team_user"], +}) + +other_user = User.new(3, { + "plan" => "plus", + "age" => 18, + "roles" => ["org_admin"] +}) + +age_rule = Flipper::Rules::Condition.new( + {"type" => "property", "value" => "age"}, + {"type" => "operator", "value" => "gte"}, + {"type" => "integer", "value" => 21} +) +plan_rule = Flipper::Rules::Condition.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "basic"} +) +admin_rule = Flipper::Rules::Condition.new( + {"type" => "string", "value" => "admin"}, + {"type" => "operator", "value" => "in"}, + {"type" => "property", "value" => "roles"} ) -Flipper.enable_rule :something, any_rule -p Flipper.enabled?(:something, user) +puts "Single Rule" +p should_be_false: Flipper.enabled?(:something, user) +puts "Enabling single rule" +Flipper.enable_rule :something, plan_rule +p should_be_true: Flipper.enabled?(:something, user) +p should_be_false: Flipper.enabled?(:something, admin_user) +p should_be_false: Flipper.enabled?(:something, other_user) +puts "Disabling single rule" +Flipper.disable_rule :something, plan_rule +p should_be_false: Flipper.enabled?(:something, user) + +puts "\n\nAny Rule" +p should_be_false: Flipper.enabled?(:something, user) +any_rule = Flipper::Rules::Any.new(plan_rule, age_rule) +puts "Enabling any rule" +Flipper.enable_rule :something, any_rule +p should_be_true: Flipper.enabled?(:something, user) +p should_be_false: Flipper.enabled?(:something, admin_user) +p should_be_false: Flipper.enabled?(:something, other_user) +puts "Disabling any rule" Flipper.disable_rule :something, any_rule -p Flipper.enabled?(:something, user) +p should_be_false: Flipper.enabled?(:something, user) + + +puts "\n\nAll Rule" +p should_be_false: Flipper.enabled?(:something, user) +all_rule = Flipper::Rules::All.new(plan_rule, age_rule) +puts "Enabling all rule" +Flipper.enable_rule :something, all_rule +p should_be_true: Flipper.enabled?(:something, user) +p should_be_false: Flipper.enabled?(:something, admin_user) +p should_be_false: Flipper.enabled?(:something, other_user) +puts "Disabling all rule" +Flipper.disable_rule :something, all_rule +p should_be_false: Flipper.enabled?(:something, user) + + +puts "\n\nNested Rule" +nested_rule = Flipper::Rules::Any.new(admin_rule, all_rule) +p should_be_false: Flipper.enabled?(:something, user) +p should_be_false: Flipper.enabled?(:something, admin_user) +p should_be_false: Flipper.enabled?(:something, other_user) +puts "Enabling nested rule" +Flipper.enable_rule :something, nested_rule +p should_be_true: Flipper.enabled?(:something, user) +p should_be_true: Flipper.enabled?(:something, admin_user) +p should_be_false: Flipper.enabled?(:something, other_user) +puts "Disabling nested rule" +Flipper.disable_rule :something, nested_rule +p should_be_false: Flipper.enabled?(:something, user) +p should_be_false: Flipper.enabled?(:something, admin_user) +p should_be_false: Flipper.enabled?(:something, other_user) From 23af908b5e1484e8ec5c54a5bbef75b1622676bb Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sun, 5 Sep 2021 14:14:51 -0400 Subject: [PATCH 034/176] Add boolean and set of actors examples Also added boolean, and null types and renamed attributes to properties. --- examples/rules.rb | 47 ++++++++++++++++++++++++++-- lib/flipper/actor.rb | 2 +- lib/flipper/rules/condition.rb | 16 +++++----- spec/flipper/rules/condition_spec.rb | 39 +++++++++++++++++++++++ 4 files changed, 94 insertions(+), 10 deletions(-) diff --git a/examples/rules.rb b/examples/rules.rb index c6937e938..669af4a14 100644 --- a/examples/rules.rb +++ b/examples/rules.rb @@ -6,16 +6,25 @@ class User < Struct.new(:id, :flipper_properties) end user = User.new(1, { + "type" => "User", + "id" => 1, + "flipper_id" => "User;1", "plan" => "basic", "age" => 39, "roles" => ["team_user"] }) admin_user = User.new(2, { + "type" => "User", + "id" => 2, + "flipper_id" => "User;2", "roles" => ["admin", "team_user"], }) other_user = User.new(3, { + "type" => "User", + "id" => 3, + "flipper_id" => "User;3", "plan" => "plus", "age" => 18, "roles" => ["org_admin"] @@ -38,6 +47,7 @@ class User < Struct.new(:id, :flipper_properties) ) puts "Single Rule" +########################################################### p should_be_false: Flipper.enabled?(:something, user) puts "Enabling single rule" Flipper.enable_rule :something, plan_rule @@ -50,8 +60,10 @@ class User < Struct.new(:id, :flipper_properties) puts "\n\nAny Rule" -p should_be_false: Flipper.enabled?(:something, user) +########################################################### any_rule = Flipper::Rules::Any.new(plan_rule, age_rule) +########################################################### +p should_be_false: Flipper.enabled?(:something, user) puts "Enabling any rule" Flipper.enable_rule :something, any_rule p should_be_true: Flipper.enabled?(:something, user) @@ -63,8 +75,10 @@ class User < Struct.new(:id, :flipper_properties) puts "\n\nAll Rule" -p should_be_false: Flipper.enabled?(:something, user) +########################################################### all_rule = Flipper::Rules::All.new(plan_rule, age_rule) +########################################################### +p should_be_false: Flipper.enabled?(:something, user) puts "Enabling all rule" Flipper.enable_rule :something, all_rule p should_be_true: Flipper.enabled?(:something, user) @@ -76,7 +90,9 @@ class User < Struct.new(:id, :flipper_properties) puts "\n\nNested Rule" +########################################################### nested_rule = Flipper::Rules::Any.new(admin_rule, all_rule) +########################################################### p should_be_false: Flipper.enabled?(:something, user) p should_be_false: Flipper.enabled?(:something, admin_user) p should_be_false: Flipper.enabled?(:something, other_user) @@ -90,3 +106,30 @@ class User < Struct.new(:id, :flipper_properties) p should_be_false: Flipper.enabled?(:something, user) p should_be_false: Flipper.enabled?(:something, admin_user) p should_be_false: Flipper.enabled?(:something, other_user) + +puts "\n\nBoolean Rule" +########################################################### +boolean_rule = Flipper::Rules::Condition.new( + {"type" => "boolean", "value" => true}, + {"type" => "operator", "value" => "eq"}, + {"type" => "boolean", "value" => true}, +) +########################################################### +Flipper.enable_rule :something, boolean_rule +p should_be_true: Flipper.enabled?(:something) +p should_be_true: Flipper.enabled?(:something, user) +Flipper.disable_rule :something, boolean_rule + +puts "\n\nSet of Actors Rule" +########################################################### +set_of_actors_rule = Flipper::Rules::Condition.new( + {"type" => "property", "value" => "flipper_id"}, + {"type" => "operator", "value" => "in"}, + {"type" => "array", "value" => ["User;1", "User;3"]}, +) +########################################################### +Flipper.enable_rule :something, set_of_actors_rule +p should_be_true: Flipper.enabled?(:something, user) +p should_be_true: Flipper.enabled?(:something, other_user) +p should_be_false: Flipper.enabled?(:something, admin_user) +Flipper.disable_rule :something, set_of_actors_rule diff --git a/lib/flipper/actor.rb b/lib/flipper/actor.rb index f2f24a5e0..4a5852799 100644 --- a/lib/flipper/actor.rb +++ b/lib/flipper/actor.rb @@ -6,7 +6,7 @@ class Actor def initialize(flipper_id, flipper_properties = {}) @flipper_id = flipper_id - @flipper_properties = flipper_properties.merge("flipper_id" => flipper_id) + @flipper_properties = flipper_properties end def eql?(other) diff --git a/lib/flipper/rules/condition.rb b/lib/flipper/rules/condition.rb index 80503adcc..5e6b3c36e 100644 --- a/lib/flipper/rules/condition.rb +++ b/lib/flipper/rules/condition.rb @@ -51,10 +51,10 @@ def eql?(other) end alias_method :==, :eql? - def matches?(feature_name, actor) - attributes = actor.flipper_properties - left_value = evaluate(@left, attributes) - right_value = evaluate(@right, attributes) + def matches?(feature_name, actor = nil) + properties = actor ? actor.flipper_properties.merge("flipper_id" => actor.flipper_id) : {}.freeze + left_value = evaluate(@left, properties) + right_value = evaluate(@right, properties) operator_name = @operator.fetch("value") operation = OPERATIONS.fetch(operator_name) do raise "operator not implemented: #{operator_name}" @@ -65,16 +65,18 @@ def matches?(feature_name, actor) private - def evaluate(hash, attributes) + def evaluate(hash, properties) type = hash.fetch("type") case type when "property" - attributes[hash.fetch("value")] + properties[hash.fetch("value")] when "random" rand hash.fetch("value") - when "array", "string", "integer" + when "array", "string", "integer", "boolean" hash.fetch("value") + when "null" + nil else raise "type not found: #{type.inspect}" end diff --git a/spec/flipper/rules/condition_spec.rb b/spec/flipper/rules/condition_spec.rb index 793dc2542..6562a3eeb 100644 --- a/spec/flipper/rules/condition_spec.rb +++ b/spec/flipper/rules/condition_spec.rb @@ -39,6 +39,45 @@ end describe "#matches?" do + context "with no actor" do + it "does not error for condition that returns true" do + rule = Flipper::Rules::Condition.new( + {"type" => "boolean", "value" => true}, + {"type" => "operator", "value" => "eq"}, + {"type" => "boolean", "value" => true}, + ) + expect(rule.matches?(feature_name, nil)).to be(true) + end + + it "does not error for condition that returns false" do + rule = Flipper::Rules::Condition.new( + {"type" => "boolean", "value" => true}, + {"type" => "operator", "value" => "eq"}, + {"type" => "boolean", "value" => false}, + ) + expect(rule.matches?(feature_name, nil)).to be(false) + end + end + + context "with non-Flipper::Actor object that quacks like a duck" do + it "works" do + user_class = Class.new(Struct.new(:id, :flipper_properties)) do + def flipper_id + "User;#{id}" + end + end + user = user_class.new(1, {}) + + rule = Flipper::Rules::Condition.new( + {"type" => "property", "value" => "flipper_id"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "User;1"} + ) + expect(rule.matches?(feature_name, user)).to be(true) + expect(rule.matches?(feature_name, user_class.new(2, {}))).to be(false) + end + end + context "eq" do let(:rule) { Flipper::Rules::Condition.new( From 5ed009fccf4a491f7401b0818e155b57faeea043 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sun, 5 Sep 2021 14:22:35 -0400 Subject: [PATCH 035/176] Add % of actors example --- examples/rules.rb | 52 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/examples/rules.rb b/examples/rules.rb index 669af4a14..762c05902 100644 --- a/examples/rules.rb +++ b/examples/rules.rb @@ -5,6 +5,15 @@ class User < Struct.new(:id, :flipper_properties) include Flipper::Identifier end +class Org < Struct.new(:id, :flipper_properties) + include Flipper::Identifier +end + +org = Org.new(1, { + "type" => "Org", + "id" => 1, +}) + user = User.new(1, { "type" => "User", "id" => 1, @@ -58,7 +67,6 @@ class User < Struct.new(:id, :flipper_properties) Flipper.disable_rule :something, plan_rule p should_be_false: Flipper.enabled?(:something, user) - puts "\n\nAny Rule" ########################################################### any_rule = Flipper::Rules::Any.new(plan_rule, age_rule) @@ -73,7 +81,6 @@ class User < Struct.new(:id, :flipper_properties) Flipper.disable_rule :something, any_rule p should_be_false: Flipper.enabled?(:something, user) - puts "\n\nAll Rule" ########################################################### all_rule = Flipper::Rules::All.new(plan_rule, age_rule) @@ -88,7 +95,6 @@ class User < Struct.new(:id, :flipper_properties) Flipper.disable_rule :something, all_rule p should_be_false: Flipper.enabled?(:something, user) - puts "\n\nNested Rule" ########################################################### nested_rule = Flipper::Rules::Any.new(admin_rule, all_rule) @@ -112,7 +118,7 @@ class User < Struct.new(:id, :flipper_properties) boolean_rule = Flipper::Rules::Condition.new( {"type" => "boolean", "value" => true}, {"type" => "operator", "value" => "eq"}, - {"type" => "boolean", "value" => true}, + {"type" => "boolean", "value" => true} ) ########################################################### Flipper.enable_rule :something, boolean_rule @@ -125,7 +131,7 @@ class User < Struct.new(:id, :flipper_properties) set_of_actors_rule = Flipper::Rules::Condition.new( {"type" => "property", "value" => "flipper_id"}, {"type" => "operator", "value" => "in"}, - {"type" => "array", "value" => ["User;1", "User;3"]}, + {"type" => "array", "value" => ["User;1", "User;3"]} ) ########################################################### Flipper.enable_rule :something, set_of_actors_rule @@ -133,3 +139,39 @@ class User < Struct.new(:id, :flipper_properties) p should_be_true: Flipper.enabled?(:something, other_user) p should_be_false: Flipper.enabled?(:something, admin_user) Flipper.disable_rule :something, set_of_actors_rule + +puts "\n\n% of Actors Rule" +########################################################### +percentage_of_actors = Flipper::Rules::Condition.new( + {"type" => "property", "value" => "flipper_id"}, + {"type" => "operator", "value" => "percentage"}, + {"type" => "integer", "value" => 30} +) +########################################################### +Flipper.enable_rule :something, percentage_of_actors +p should_be_false: Flipper.enabled?(:something, user) +p should_be_false: Flipper.enabled?(:something, other_user) +p should_be_true: Flipper.enabled?(:something, admin_user) +Flipper.disable_rule :something, percentage_of_actors + +puts "\n\n% of Actors Per Type Rule" +########################################################### +percentage_of_actors_per_type = Flipper::Rules::All.new( + Flipper::Rules::Condition.new( + {"type" => "property", "value" => "type"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "User"} + ), + Flipper::Rules::Condition.new( + {"type" => "property", "value" => "flipper_id"}, + {"type" => "operator", "value" => "percentage"}, + {"type" => "integer", "value" => 40} + ) +) +########################################################### +Flipper.enable_rule :something, percentage_of_actors_per_type +p should_be_false: Flipper.enabled?(:something, user) # not in the 40% enabled for Users +p should_be_true: Flipper.enabled?(:something, other_user) +p should_be_true: Flipper.enabled?(:something, admin_user) +p should_be_false: Flipper.enabled?(:something, org) # not a User +Flipper.disable_rule :something, percentage_of_actors_per_type From a95bfb42925e302abfb6ef92be7ff2fa005e243d Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sun, 5 Sep 2021 14:31:30 -0400 Subject: [PATCH 036/176] Show enabling per org in same rule as per user --- examples/rules.rb | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/examples/rules.rb b/examples/rules.rb index 762c05902..075dad908 100644 --- a/examples/rules.rb +++ b/examples/rules.rb @@ -156,22 +156,37 @@ class Org < Struct.new(:id, :flipper_properties) puts "\n\n% of Actors Per Type Rule" ########################################################### -percentage_of_actors_per_type = Flipper::Rules::All.new( - Flipper::Rules::Condition.new( - {"type" => "property", "value" => "type"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "User"} +percentage_of_actors_per_type = Flipper::Rules::Any.new( + Flipper::Rules::All.new( + Flipper::Rules::Condition.new( + {"type" => "property", "value" => "type"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "User"} + ), + Flipper::Rules::Condition.new( + {"type" => "property", "value" => "flipper_id"}, + {"type" => "operator", "value" => "percentage"}, + {"type" => "integer", "value" => 40} + ) ), - Flipper::Rules::Condition.new( - {"type" => "property", "value" => "flipper_id"}, - {"type" => "operator", "value" => "percentage"}, - {"type" => "integer", "value" => 40} + Flipper::Rules::All.new( + Flipper::Rules::Condition.new( + {"type" => "property", "value" => "type"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "Org"} + ), + Flipper::Rules::Condition.new( + {"type" => "property", "value" => "flipper_id"}, + {"type" => "operator", "value" => "percentage"}, + {"type" => "integer", "value" => 10} + ) ) ) + ########################################################### Flipper.enable_rule :something, percentage_of_actors_per_type p should_be_false: Flipper.enabled?(:something, user) # not in the 40% enabled for Users p should_be_true: Flipper.enabled?(:something, other_user) p should_be_true: Flipper.enabled?(:something, admin_user) -p should_be_false: Flipper.enabled?(:something, org) # not a User +p should_be_false: Flipper.enabled?(:something, org) # not in the 10% of enabled for Orgs Flipper.disable_rule :something, percentage_of_actors_per_type From a54e54715707d3e3e518a6043f9c7bc0ce9a699a Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sun, 5 Sep 2021 20:04:40 -0400 Subject: [PATCH 037/176] Add percentage of time example using rules --- examples/rules.rb | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/examples/rules.rb b/examples/rules.rb index 075dad908..f54861d0a 100644 --- a/examples/rules.rb +++ b/examples/rules.rb @@ -182,7 +182,6 @@ class Org < Struct.new(:id, :flipper_properties) ) ) ) - ########################################################### Flipper.enable_rule :something, percentage_of_actors_per_type p should_be_false: Flipper.enabled?(:something, user) # not in the 40% enabled for Users @@ -190,3 +189,17 @@ class Org < Struct.new(:id, :flipper_properties) p should_be_true: Flipper.enabled?(:something, admin_user) p should_be_false: Flipper.enabled?(:something, org) # not in the 10% of enabled for Orgs Flipper.disable_rule :something, percentage_of_actors_per_type + +puts "\n\nPercentage of Time Rule" +percentage_of_time_rule = Flipper::Rules::Condition.new( + {"type" => "random", "value" => 100}, + {"type" => "operator", "value" => "lt"}, + {"type" => "integer", "value" => 50} +) +########################################################### +Flipper.enable_rule :something, percentage_of_time_rule +results = (1..10000).map { |n| Flipper.enabled?(:something, user) } +enabled, disabled = results.partition { |r| r } +p should_be_close_to_5000: enabled.size +p should_be_close_to_5000: disabled.size +Flipper.disable_rule :something, percentage_of_time_rule From 20b3e6f092c0166c2d592e778893ba18c7974265 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Mon, 6 Sep 2021 14:48:21 -0400 Subject: [PATCH 038/176] Add property specs and some type stuff --- lib/flipper/rules.rb | 35 +++ lib/flipper/rules/property.rb | 38 +-- spec/flipper/rules/property_spec.rb | 380 ++++++++++++++++++++++++++++ spec/flipper/rules_spec.rb | 91 +++++++ 4 files changed, 520 insertions(+), 24 deletions(-) create mode 100644 spec/flipper/rules/property_spec.rb create mode 100644 spec/flipper/rules_spec.rb diff --git a/lib/flipper/rules.rb b/lib/flipper/rules.rb index c484c81d8..46f1183e9 100644 --- a/lib/flipper/rules.rb +++ b/lib/flipper/rules.rb @@ -17,5 +17,40 @@ def self.build(hash) type = const_get(hash.fetch("type")) type.build(hash.fetch("value")) end + + SUPPORTED_VALUE_TYPES_MAP = { + String => "string", + Integer => "integer", + NilClass => "null", + TrueClass => "boolean", + FalseClass => "boolean", + Array => "array", + }.freeze + + SUPPORTED_VALUE_TYPES = SUPPORTED_VALUE_TYPES_MAP.keys.freeze + + def self.type_of(object) + klass, type = SUPPORTED_VALUE_TYPES_MAP.detect { |klass, type| object.is_a?(klass) } + type + end + + def self.typed(object) + type = type_of(object) + if type.nil? + raise ArgumentError, "#{object.inspect} is an unsupported type. " + + "Object must be one of: #{SUPPORTED_VALUE_TYPES.join(", ")}." + end + [type, object] + end + + def self.require_integer(object) + raise ArgumentError, "object must be integer" unless object.is_a?(Integer) + object + end + + def self.require_array(object) + raise ArgumentError, "object must be array" unless object.is_a?(Array) + object + end end end diff --git a/lib/flipper/rules/property.rb b/lib/flipper/rules/property.rb index 5c1877340..03d8aab1a 100644 --- a/lib/flipper/rules/property.rb +++ b/lib/flipper/rules/property.rb @@ -1,8 +1,10 @@ module Flipper module Rules class Property + attr_reader :name + def initialize(name) - @name = name + @name = name.to_s end def value @@ -13,18 +15,20 @@ def value end def eq(object) + type, object = Rules.typed(object) Flipper::Rules::Condition.new( value, {"type" => "operator", "value" => "eq"}, - {"type" => typeof(object), "value" => object} + {"type" => type, "value" => object} ) end def neq(object) + type, object = Rules.typed(object) Flipper::Rules::Condition.new( value, {"type" => "operator", "value" => "neq"}, - {"type" => typeof(object), "value" => object} + {"type" => type, "value" => object} ) end @@ -32,7 +36,7 @@ def gt(object) Flipper::Rules::Condition.new( value, {"type" => "operator", "value" => "gt"}, - {"type" => "integer", "value" => object} + {"type" => "integer", "value" => Rules.require_integer(object)} ) end @@ -40,7 +44,7 @@ def gte(object) Flipper::Rules::Condition.new( value, {"type" => "operator", "value" => "gte"}, - {"type" => "integer", "value" => object} + {"type" => "integer", "value" => Rules.require_integer(object)} ) end @@ -48,7 +52,7 @@ def lt(object) Flipper::Rules::Condition.new( value, {"type" => "operator", "value" => "lt"}, - {"type" => "integer", "value" => object} + {"type" => "integer", "value" => Rules.require_integer(object)} ) end @@ -56,7 +60,7 @@ def lte(object) Flipper::Rules::Condition.new( value, {"type" => "operator", "value" => "lte"}, - {"type" => "integer", "value" => object} + {"type" => "integer", "value" => Rules.require_integer(object)} ) end @@ -64,7 +68,7 @@ def in(object) Flipper::Rules::Condition.new( value, {"type" => "operator", "value" => "in"}, - {"type" => "array", "value" => object} + {"type" => "array", "value" => Rules.require_array(object)} ) end @@ -72,7 +76,7 @@ def nin(object) Flipper::Rules::Condition.new( value, {"type" => "operator", "value" => "nin"}, - {"type" => "array", "value" => object} + {"type" => "array", "value" => Rules.require_array(object)} ) end @@ -80,23 +84,9 @@ def percentage(object) Flipper::Rules::Condition.new( value, {"type" => "operator", "value" => "percentage"}, - {"type" => "integer", "value" => object} + {"type" => "integer", "value" => Rules.require_integer(object)} ) end - - private - - def typeof(object) - if object.is_a?(String) - "string" - elsif object.is_a?(Integer) - "integer" - elsif object.respond_to?(:to_a) - "array" - else - raise "unsupported type inference for #{object.inspect}" - end - end end end end diff --git a/spec/flipper/rules/property_spec.rb b/spec/flipper/rules/property_spec.rb new file mode 100644 index 000000000..72e2f1900 --- /dev/null +++ b/spec/flipper/rules/property_spec.rb @@ -0,0 +1,380 @@ +require 'helper' + +RSpec.describe Flipper::Rules::Property do + describe "#initialize" do + it "works with string name" do + property = described_class.new("plan") + expect(property.name).to eq("plan") + end + + it "works with symbol name" do + property = described_class.new(:plan) + expect(property.name).to eq("plan") + end + end + + describe "#value" do + it "returns Hash with type and value" do + expect(described_class.new("plan").value).to eq({ + "type" => "property", + "value" => "plan", + }) + end + end + + describe "#eq" do + context "with string" do + it "returns equal condition" do + expect(described_class.new(:plan).eq("basic")).to eq(Flipper::Rules::Condition.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "basic"} + )) + end + end + + context "with boolean" do + it "returns equal condition" do + expect(described_class.new(:admin).eq(true)).to eq(Flipper::Rules::Condition.new( + {"type" => "property", "value" => "admin"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "boolean", "value" => true} + )) + end + end + + context "with integer" do + it "returns equal condition" do + expect(described_class.new(:age).eq(21)).to eq(Flipper::Rules::Condition.new( + {"type" => "property", "value" => "age"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "integer", "value" => 21} + )) + end + end + + context "with array" do + it "returns equal condition" do + expect(described_class.new(:roles).eq(["admin"])).to eq(Flipper::Rules::Condition.new( + {"type" => "property", "value" => "roles"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "array", "value" => ["admin"]} + )) + end + end + + context "with nil" do + it "returns equal condition" do + expect(described_class.new(:admin).eq(nil)).to eq(Flipper::Rules::Condition.new( + {"type" => "property", "value" => "admin"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "null", "value" => nil} + )) + end + end + end + + describe "#neq" do + context "with string" do + it "returns not equal condition" do + expect(described_class.new(:plan).neq("basic")).to eq(Flipper::Rules::Condition.new( + {"type" => "property", "value" => "plan"}, + {"type" => "operator", "value" => "neq"}, + {"type" => "string", "value" => "basic"} + )) + end + end + + context "with boolean" do + it "returns not equal condition" do + expect(described_class.new(:admin).neq(true)).to eq(Flipper::Rules::Condition.new( + {"type" => "property", "value" => "admin"}, + {"type" => "operator", "value" => "neq"}, + {"type" => "boolean", "value" => true} + )) + end + end + + context "with integer" do + it "returns not equal condition" do + expect(described_class.new(:age).neq(21)).to eq(Flipper::Rules::Condition.new( + {"type" => "property", "value" => "age"}, + {"type" => "operator", "value" => "neq"}, + {"type" => "integer", "value" => 21} + )) + end + end + + context "with array" do + it "returns not equal condition" do + expect(described_class.new(:roles).neq(["admin"])).to eq(Flipper::Rules::Condition.new( + {"type" => "property", "value" => "roles"}, + {"type" => "operator", "value" => "neq"}, + {"type" => "array", "value" => ["admin"]} + )) + end + end + + context "with nil" do + it "returns not equal condition" do + expect(described_class.new(:admin).neq(nil)).to eq(Flipper::Rules::Condition.new( + {"type" => "property", "value" => "admin"}, + {"type" => "operator", "value" => "neq"}, + {"type" => "null", "value" => nil} + )) + end + end + end + + describe "#gt" do + context "with integer" do + it "returns condition" do + expect(described_class.new(:age).gt(21)).to eq(Flipper::Rules::Condition.new( + {"type" => "property", "value" => "age"}, + {"type" => "operator", "value" => "gt"}, + {"type" => "integer", "value" => 21} + )) + end + end + + context "with string" do + it "raises error" do + expect { described_class.new(:age).gt("231") }.to raise_error(ArgumentError) + end + end + + context "with boolean" do + it "raises error" do + expect { described_class.new(:age).gt(true) }.to raise_error(ArgumentError) + end + end + + context "with array" do + it "raises error" do + expect { described_class.new(:age).gt(["admin"]) }.to raise_error(ArgumentError) + end + end + + context "with nil" do + it "raises error" do + expect { described_class.new(:age).gt(nil) }.to raise_error(ArgumentError) + end + end + end + + describe "#gte" do + context "with integer" do + it "returns condition" do + expect(described_class.new(:age).gte(21)).to eq(Flipper::Rules::Condition.new( + {"type" => "property", "value" => "age"}, + {"type" => "operator", "value" => "gte"}, + {"type" => "integer", "value" => 21} + )) + end + end + + context "with string" do + it "raises error" do + expect { described_class.new(:age).gte("231") }.to raise_error(ArgumentError) + end + end + + context "with boolean" do + it "raises error" do + expect { described_class.new(:age).gte(true) }.to raise_error(ArgumentError) + end + end + + context "with array" do + it "raises error" do + expect { described_class.new(:age).gte(["admin"]) }.to raise_error(ArgumentError) + end + end + + context "with nil" do + it "raises error" do + expect { described_class.new(:age).gte(nil) }.to raise_error(ArgumentError) + end + end + end + + describe "#lt" do + context "with integer" do + it "returns condition" do + expect(described_class.new(:age).lt(21)).to eq(Flipper::Rules::Condition.new( + {"type" => "property", "value" => "age"}, + {"type" => "operator", "value" => "lt"}, + {"type" => "integer", "value" => 21} + )) + end + end + + context "with string" do + it "raises error" do + expect { described_class.new(:age).lt("231") }.to raise_error(ArgumentError) + end + end + + context "with boolean" do + it "raises error" do + expect { described_class.new(:age).lt(true) }.to raise_error(ArgumentError) + end + end + + context "with array" do + it "raises error" do + expect { described_class.new(:age).lt(["admin"]) }.to raise_error(ArgumentError) + end + end + + context "with nil" do + it "raises error" do + expect { described_class.new(:age).lt(nil) }.to raise_error(ArgumentError) + end + end + end + + describe "#lte" do + context "with integer" do + it "returns condition" do + expect(described_class.new(:age).lte(21)).to eq(Flipper::Rules::Condition.new( + {"type" => "property", "value" => "age"}, + {"type" => "operator", "value" => "lte"}, + {"type" => "integer", "value" => 21} + )) + end + end + + context "with string" do + it "raises error" do + expect { described_class.new(:age).lte("231") }.to raise_error(ArgumentError) + end + end + + context "with boolean" do + it "raises error" do + expect { described_class.new(:age).lte(true) }.to raise_error(ArgumentError) + end + end + + context "with array" do + it "raises error" do + expect { described_class.new(:age).lte(["admin"]) }.to raise_error(ArgumentError) + end + end + + context "with nil" do + it "raises error" do + expect { described_class.new(:age).lte(nil) }.to raise_error(ArgumentError) + end + end + end + + describe "#in" do + context "with array" do + it "returns condition" do + expect(described_class.new(:role).in(["admin"])).to eq(Flipper::Rules::Condition.new( + {"type" => "property", "value" => "role"}, + {"type" => "operator", "value" => "in"}, + {"type" => "array", "value" => ["admin"]} + )) + end + end + + context "with string" do + it "raises error" do + expect { described_class.new(:role).in("231") }.to raise_error(ArgumentError) + end + end + + context "with boolean" do + it "raises error" do + expect { described_class.new(:role).in(true) }.to raise_error(ArgumentError) + end + end + + context "with integer" do + it "raises error" do + expect { described_class.new(:role).in(21) }.to raise_error(ArgumentError) + end + end + + context "with nil" do + it "raises error" do + expect { described_class.new(:role).in(nil) }.to raise_error(ArgumentError) + end + end + end + + describe "#nin" do + context "with array" do + it "returns condition" do + expect(described_class.new(:role).nin(["admin"])).to eq(Flipper::Rules::Condition.new( + {"type" => "property", "value" => "role"}, + {"type" => "operator", "value" => "nin"}, + {"type" => "array", "value" => ["admin"]} + )) + end + end + + context "with string" do + it "raises error" do + expect { described_class.new(:role).nin("231") }.to raise_error(ArgumentError) + end + end + + context "with boolean" do + it "raises error" do + expect { described_class.new(:role).nin(true) }.to raise_error(ArgumentError) + end + end + + context "with integer" do + it "raises error" do + expect { described_class.new(:role).nin(21) }.to raise_error(ArgumentError) + end + end + + context "with nil" do + it "raises error" do + expect { described_class.new(:role).nin(nil) }.to raise_error(ArgumentError) + end + end + end + + describe "#percentage" do + context "with integer" do + it "returns condition" do + expect(described_class.new(:flipper_id).percentage(25)).to eq(Flipper::Rules::Condition.new( + {"type" => "property", "value" => "flipper_id"}, + {"type" => "operator", "value" => "percentage"}, + {"type" => "integer", "value" => 25} + )) + end + end + + context "with string" do + it "raises error" do + expect { described_class.new(:flipper_id).percentage("231") }.to raise_error(ArgumentError) + end + end + + context "with boolean" do + it "raises error" do + expect { described_class.new(:flipper_id).percentage(true) }.to raise_error(ArgumentError) + end + end + + context "with array" do + it "raises error" do + expect { described_class.new(:flipper_id).percentage(["admin"]) }.to raise_error(ArgumentError) + end + end + + context "with nil" do + it "raises error" do + expect { described_class.new(:flipper_id).percentage(nil) }.to raise_error(ArgumentError) + end + end + end +end diff --git a/spec/flipper/rules_spec.rb b/spec/flipper/rules_spec.rb new file mode 100644 index 000000000..da58e3de7 --- /dev/null +++ b/spec/flipper/rules_spec.rb @@ -0,0 +1,91 @@ +require 'helper' + +RSpec.describe Flipper::Rules do + describe ".type_of" do + context "for string" do + it "returns string" do + expect(described_class.type_of("test")).to eq("string") + end + end + + context "for integer" do + it "returns integer" do + expect(described_class.type_of(21)).to eq("integer") + end + end + + context "for nil" do + it "returns nil" do + expect(described_class.type_of(nil)).to eq("null") + end + end + + context "for true" do + it "returns boolean" do + expect(described_class.type_of(true)).to eq("boolean") + end + end + + context "for false" do + it "returns boolean" do + expect(described_class.type_of(false)).to eq("boolean") + end + end + + context "for array" do + it "returns array" do + expect(described_class.type_of([1, 2, 3])).to eq("array") + end + end + + context "for unsupported type" do + it "returns nil" do + expect(described_class.type_of(Object.new)).to be(nil) + end + end + end + + describe ".typed" do + context "with string" do + it "returns array of type and value" do + expect(described_class.typed("test")).to eq(["string", "test"]) + end + end + + context "with integer" do + it "returns array of type and value" do + expect(described_class.typed(21)).to eq(["integer", 21]) + end + end + + context "with nil" do + it "returns array of type and value" do + expect(described_class.typed(nil)).to eq(["null", nil]) + end + end + + context "with true" do + it "returns array of type and value" do + expect(described_class.typed(true)).to eq(["boolean", true]) + end + end + + context "with false" do + it "returns array of type and value" do + expect(described_class.typed(false)).to eq(["boolean", false]) + end + end + + context "with array" do + it "returns array of type and value" do + expect(described_class.typed(["test"])).to eq(["array", ["test"]]) + end + end + + context "with unsupported type" do + it "returns array of type and value" do + expect { described_class.typed({}) }.to raise_error(ArgumentError, /{} is an unsupported type\. Object must be one of: String, Integer, NilClass, TrueClass, FalseClass, Array\./) + end + end + end +end From 27079d662a5b52bf00f1afccb5a615763eed8742 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Mon, 6 Sep 2021 14:58:50 -0400 Subject: [PATCH 039/176] Add equality for property --- lib/flipper/rules/property.rb | 6 ++++++ spec/flipper/rules/property_spec.rb | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/lib/flipper/rules/property.rb b/lib/flipper/rules/property.rb index 03d8aab1a..c3bf0abfe 100644 --- a/lib/flipper/rules/property.rb +++ b/lib/flipper/rules/property.rb @@ -14,6 +14,12 @@ def value } end + def eql?(other) + self.class.eql?(other.class) && + @name == other.name + end + alias_method :==, :eql? + def eq(object) type, object = Rules.typed(object) Flipper::Rules::Condition.new( diff --git a/spec/flipper/rules/property_spec.rb b/spec/flipper/rules/property_spec.rb index 72e2f1900..1d284674a 100644 --- a/spec/flipper/rules/property_spec.rb +++ b/spec/flipper/rules/property_spec.rb @@ -22,6 +22,20 @@ end end + describe "equality" do + it "returns true if equal" do + expect(described_class.new("name").eql?(described_class.new("name"))).to be(true) + end + + it "returns false if name does not match" do + expect(described_class.new("name").eql?(described_class.new("age"))).to be(false) + end + + it "returns false for different class" do + expect(described_class.new("name").eql?(Object.new)).to be(false) + end + end + describe "#eq" do context "with string" do it "returns equal condition" do From 5564e0d89cbe0db4d5de3900cf2b089d1857ef96 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Mon, 6 Sep 2021 14:59:06 -0400 Subject: [PATCH 040/176] Match signatures to classes for the shortcuts --- lib/flipper.rb | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/flipper.rb b/lib/flipper.rb index b18ab7309..d1457bb4b 100644 --- a/lib/flipper.rb +++ b/lib/flipper.rb @@ -69,17 +69,16 @@ def instance=(flipper) :memoize=, :memoizing?, :sync, :sync_secret # For Flipper::Cloud. Will error for OSS Flipper. - - def property(*args) - Flipper::Rules::Property.new(*args) + def property(name) + Flipper::Rules::Property.new(name) end - def any(*args) - Flipper::Rules::Any.new(*args) + def any(*rules) + Flipper::Rules::Any.new(*rules) end - def all(*args) - Flipper::Rules::All.new(*args) + def all(*rules) + Flipper::Rules::All.new(*rules) end # Public: Use this to register a group by name. From cc20d804f4bde24848595cf98714b145c3fec979 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Mon, 6 Sep 2021 15:01:36 -0400 Subject: [PATCH 041/176] Add specs for top level flipper methods --- spec/flipper_spec.rb | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/spec/flipper_spec.rb b/spec/flipper_spec.rb index 932aeb68b..685ddb8e5 100644 --- a/spec/flipper_spec.rb +++ b/spec/flipper_spec.rb @@ -372,4 +372,28 @@ expect(described_class.instance_variable_get('@groups_registry')).to eq(registry) end end + + describe ".property" do + it "returns Flipper::Rules::Property instance" do + expect(Flipper.property("name")).to eq(Flipper::Rules::Property.new("name")) + end + end + + describe ".any" do + let(:age_rule) { Flipper.property(:age).gte(21) } + let(:plan_rule) { Flipper.property(:plan).eq("basic") } + + it "returns Flipper::Rules::Any instance" do + expect(Flipper.any(age_rule, plan_rule)).to eq(Flipper::Rules::Any.new(age_rule, plan_rule)) + end + end + + describe ".all" do + let(:age_rule) { Flipper.property(:age).gte(21) } + let(:plan_rule) { Flipper.property(:plan).eq("basic") } + + it "returns Flipper::Rules::All instance" do + expect(Flipper.all(age_rule, plan_rule)).to eq(Flipper::Rules::All.new(age_rule, plan_rule)) + end + end end From 55a54bb9b6d46973d47dfb124db485ee952e52ce Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Mon, 6 Sep 2021 15:11:02 -0400 Subject: [PATCH 042/176] Use shortcuts in examples --- examples/rules.rb | 60 +++++++++++------------------------------------ 1 file changed, 14 insertions(+), 46 deletions(-) diff --git a/examples/rules.rb b/examples/rules.rb index f54861d0a..82c22d498 100644 --- a/examples/rules.rb +++ b/examples/rules.rb @@ -39,16 +39,8 @@ class Org < Struct.new(:id, :flipper_properties) "roles" => ["org_admin"] }) -age_rule = Flipper::Rules::Condition.new( - {"type" => "property", "value" => "age"}, - {"type" => "operator", "value" => "gte"}, - {"type" => "integer", "value" => 21} -) -plan_rule = Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "basic"} -) +age_rule = Flipper.property(:age).gte(21) +plan_rule = Flipper.property(:plan).eq("basic") admin_rule = Flipper::Rules::Condition.new( {"type" => "string", "value" => "admin"}, {"type" => "operator", "value" => "in"}, @@ -69,7 +61,7 @@ class Org < Struct.new(:id, :flipper_properties) puts "\n\nAny Rule" ########################################################### -any_rule = Flipper::Rules::Any.new(plan_rule, age_rule) +any_rule = Flipper.any(plan_rule, age_rule) ########################################################### p should_be_false: Flipper.enabled?(:something, user) puts "Enabling any rule" @@ -83,7 +75,7 @@ class Org < Struct.new(:id, :flipper_properties) puts "\n\nAll Rule" ########################################################### -all_rule = Flipper::Rules::All.new(plan_rule, age_rule) +all_rule = Flipper.all(plan_rule, age_rule) ########################################################### p should_be_false: Flipper.enabled?(:something, user) puts "Enabling all rule" @@ -97,7 +89,7 @@ class Org < Struct.new(:id, :flipper_properties) puts "\n\nNested Rule" ########################################################### -nested_rule = Flipper::Rules::Any.new(admin_rule, all_rule) +nested_rule = Flipper.any(admin_rule, all_rule) ########################################################### p should_be_false: Flipper.enabled?(:something, user) p should_be_false: Flipper.enabled?(:something, admin_user) @@ -128,11 +120,7 @@ class Org < Struct.new(:id, :flipper_properties) puts "\n\nSet of Actors Rule" ########################################################### -set_of_actors_rule = Flipper::Rules::Condition.new( - {"type" => "property", "value" => "flipper_id"}, - {"type" => "operator", "value" => "in"}, - {"type" => "array", "value" => ["User;1", "User;3"]} -) +set_of_actors_rule = Flipper.property(:flipper_id).in(["User;1", "User;3"]) ########################################################### Flipper.enable_rule :something, set_of_actors_rule p should_be_true: Flipper.enabled?(:something, user) @@ -142,11 +130,7 @@ class Org < Struct.new(:id, :flipper_properties) puts "\n\n% of Actors Rule" ########################################################### -percentage_of_actors = Flipper::Rules::Condition.new( - {"type" => "property", "value" => "flipper_id"}, - {"type" => "operator", "value" => "percentage"}, - {"type" => "integer", "value" => 30} -) +percentage_of_actors = Flipper.property(:flipper_id).percentage(30) ########################################################### Flipper.enable_rule :something, percentage_of_actors p should_be_false: Flipper.enabled?(:something, user) @@ -156,30 +140,14 @@ class Org < Struct.new(:id, :flipper_properties) puts "\n\n% of Actors Per Type Rule" ########################################################### -percentage_of_actors_per_type = Flipper::Rules::Any.new( - Flipper::Rules::All.new( - Flipper::Rules::Condition.new( - {"type" => "property", "value" => "type"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "User"} - ), - Flipper::Rules::Condition.new( - {"type" => "property", "value" => "flipper_id"}, - {"type" => "operator", "value" => "percentage"}, - {"type" => "integer", "value" => 40} - ) +percentage_of_actors_per_type = Flipper.any( + Flipper.all( + Flipper.property(:type).eq("User"), + Flipper.property(:flipper_id).percentage(40), ), - Flipper::Rules::All.new( - Flipper::Rules::Condition.new( - {"type" => "property", "value" => "type"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "Org"} - ), - Flipper::Rules::Condition.new( - {"type" => "property", "value" => "flipper_id"}, - {"type" => "operator", "value" => "percentage"}, - {"type" => "integer", "value" => 10} - ) + Flipper.all( + Flipper.property(:type).eq("Org"), + Flipper.property(:flipper_id).percentage(10), ) ) ########################################################### From 993c421e16be9b98d57cb291738b1772c6fff15b Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Mon, 6 Sep 2021 15:13:30 -0400 Subject: [PATCH 043/176] Remove some of the comment lines and stuff --- examples/rules.rb | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/examples/rules.rb b/examples/rules.rb index 82c22d498..24df0ec4e 100644 --- a/examples/rules.rb +++ b/examples/rules.rb @@ -48,57 +48,58 @@ class Org < Struct.new(:id, :flipper_properties) ) puts "Single Rule" -########################################################### p should_be_false: Flipper.enabled?(:something, user) + puts "Enabling single rule" Flipper.enable_rule :something, plan_rule p should_be_true: Flipper.enabled?(:something, user) p should_be_false: Flipper.enabled?(:something, admin_user) p should_be_false: Flipper.enabled?(:something, other_user) + puts "Disabling single rule" Flipper.disable_rule :something, plan_rule p should_be_false: Flipper.enabled?(:something, user) puts "\n\nAny Rule" -########################################################### any_rule = Flipper.any(plan_rule, age_rule) -########################################################### p should_be_false: Flipper.enabled?(:something, user) + puts "Enabling any rule" Flipper.enable_rule :something, any_rule p should_be_true: Flipper.enabled?(:something, user) p should_be_false: Flipper.enabled?(:something, admin_user) p should_be_false: Flipper.enabled?(:something, other_user) + puts "Disabling any rule" Flipper.disable_rule :something, any_rule p should_be_false: Flipper.enabled?(:something, user) puts "\n\nAll Rule" -########################################################### all_rule = Flipper.all(plan_rule, age_rule) -########################################################### p should_be_false: Flipper.enabled?(:something, user) + puts "Enabling all rule" Flipper.enable_rule :something, all_rule p should_be_true: Flipper.enabled?(:something, user) p should_be_false: Flipper.enabled?(:something, admin_user) p should_be_false: Flipper.enabled?(:something, other_user) + puts "Disabling all rule" Flipper.disable_rule :something, all_rule p should_be_false: Flipper.enabled?(:something, user) puts "\n\nNested Rule" -########################################################### nested_rule = Flipper.any(admin_rule, all_rule) -########################################################### p should_be_false: Flipper.enabled?(:something, user) p should_be_false: Flipper.enabled?(:something, admin_user) p should_be_false: Flipper.enabled?(:something, other_user) + puts "Enabling nested rule" Flipper.enable_rule :something, nested_rule p should_be_true: Flipper.enabled?(:something, user) p should_be_true: Flipper.enabled?(:something, admin_user) p should_be_false: Flipper.enabled?(:something, other_user) + puts "Disabling nested rule" Flipper.disable_rule :something, nested_rule p should_be_false: Flipper.enabled?(:something, user) @@ -106,22 +107,18 @@ class Org < Struct.new(:id, :flipper_properties) p should_be_false: Flipper.enabled?(:something, other_user) puts "\n\nBoolean Rule" -########################################################### boolean_rule = Flipper::Rules::Condition.new( {"type" => "boolean", "value" => true}, {"type" => "operator", "value" => "eq"}, {"type" => "boolean", "value" => true} ) -########################################################### Flipper.enable_rule :something, boolean_rule p should_be_true: Flipper.enabled?(:something) p should_be_true: Flipper.enabled?(:something, user) Flipper.disable_rule :something, boolean_rule puts "\n\nSet of Actors Rule" -########################################################### set_of_actors_rule = Flipper.property(:flipper_id).in(["User;1", "User;3"]) -########################################################### Flipper.enable_rule :something, set_of_actors_rule p should_be_true: Flipper.enabled?(:something, user) p should_be_true: Flipper.enabled?(:something, other_user) @@ -129,9 +126,7 @@ class Org < Struct.new(:id, :flipper_properties) Flipper.disable_rule :something, set_of_actors_rule puts "\n\n% of Actors Rule" -########################################################### percentage_of_actors = Flipper.property(:flipper_id).percentage(30) -########################################################### Flipper.enable_rule :something, percentage_of_actors p should_be_false: Flipper.enabled?(:something, user) p should_be_false: Flipper.enabled?(:something, other_user) @@ -139,7 +134,6 @@ class Org < Struct.new(:id, :flipper_properties) Flipper.disable_rule :something, percentage_of_actors puts "\n\n% of Actors Per Type Rule" -########################################################### percentage_of_actors_per_type = Flipper.any( Flipper.all( Flipper.property(:type).eq("User"), @@ -150,7 +144,6 @@ class Org < Struct.new(:id, :flipper_properties) Flipper.property(:flipper_id).percentage(10), ) ) -########################################################### Flipper.enable_rule :something, percentage_of_actors_per_type p should_be_false: Flipper.enabled?(:something, user) # not in the 40% enabled for Users p should_be_true: Flipper.enabled?(:something, other_user) @@ -164,7 +157,6 @@ class Org < Struct.new(:id, :flipper_properties) {"type" => "operator", "value" => "lt"}, {"type" => "integer", "value" => 50} ) -########################################################### Flipper.enable_rule :something, percentage_of_time_rule results = (1..10000).map { |n| Flipper.enabled?(:something, user) } enabled, disabled = results.partition { |r| r } From 8fb6c6e61737e8acd4006f42fca1cf1b65e975d2 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Mon, 6 Sep 2021 20:57:36 -0400 Subject: [PATCH 044/176] Add Flipper::Rules::Object to start organizing the objects used in rules --- lib/flipper/rules.rb | 36 +-- lib/flipper/rules/object.rb | 133 +++++++++ lib/flipper/rules/property.rb | 98 +------ spec/flipper/rules/object_spec.rb | 440 ++++++++++++++++++++++++++++ spec/flipper/rules/property_spec.rb | 4 +- spec/flipper/rules_spec.rb | 87 ------ 6 files changed, 584 insertions(+), 214 deletions(-) create mode 100644 lib/flipper/rules/object.rb create mode 100644 spec/flipper/rules/object_spec.rb diff --git a/lib/flipper/rules.rb b/lib/flipper/rules.rb index 46f1183e9..50e162f6a 100644 --- a/lib/flipper/rules.rb +++ b/lib/flipper/rules.rb @@ -1,6 +1,7 @@ require 'flipper/rules/condition' require 'flipper/rules/any' require 'flipper/rules/all' +require 'flipper/rules/object' require 'flipper/rules/property' module Flipper @@ -17,40 +18,5 @@ def self.build(hash) type = const_get(hash.fetch("type")) type.build(hash.fetch("value")) end - - SUPPORTED_VALUE_TYPES_MAP = { - String => "string", - Integer => "integer", - NilClass => "null", - TrueClass => "boolean", - FalseClass => "boolean", - Array => "array", - }.freeze - - SUPPORTED_VALUE_TYPES = SUPPORTED_VALUE_TYPES_MAP.keys.freeze - - def self.type_of(object) - klass, type = SUPPORTED_VALUE_TYPES_MAP.detect { |klass, type| object.is_a?(klass) } - type - end - - def self.typed(object) - type = type_of(object) - if type.nil? - raise ArgumentError, "#{object.inspect} is an unsupported type. " + - "Object must be one of: #{SUPPORTED_VALUE_TYPES.join(", ")}." - end - [type, object] - end - - def self.require_integer(object) - raise ArgumentError, "object must be integer" unless object.is_a?(Integer) - object - end - - def self.require_array(object) - raise ArgumentError, "object must be array" unless object.is_a?(Array) - object - end end end diff --git a/lib/flipper/rules/object.rb b/lib/flipper/rules/object.rb new file mode 100644 index 000000000..07310ff0f --- /dev/null +++ b/lib/flipper/rules/object.rb @@ -0,0 +1,133 @@ +module Flipper + module Rules + class Object + SUPPORTED_VALUE_TYPES_MAP = { + String => "string", + Integer => "integer", + NilClass => "null", + TrueClass => "boolean", + FalseClass => "boolean", + Array => "array", + }.freeze + + SUPPORTED_VALUE_TYPES = SUPPORTED_VALUE_TYPES_MAP.keys.freeze + + attr_reader :type, :value + + def initialize(value) + @type = type_of(value) + @value = value + end + + def to_h + { + "type" => @type, + "value" => @value, + } + end + + def eql?(other) + self.class.eql?(other.class) && + @type == other.type && + @value == other.value + end + alias_method :==, :eql? + + def eq(object) + Flipper::Rules::Condition.new( + to_h, + {"type" => "operator", "value" => "eq"}, + Rules::Object.new(object).to_h + ) + end + + def neq(object) + Flipper::Rules::Condition.new( + to_h, + {"type" => "operator", "value" => "neq"}, + Rules::Object.new(object).to_h + ) + end + + def gt(object) + Flipper::Rules::Condition.new( + to_h, + {"type" => "operator", "value" => "gt"}, + {"type" => "integer", "value" => require_integer(object)} + ) + end + + def gte(object) + Flipper::Rules::Condition.new( + to_h, + {"type" => "operator", "value" => "gte"}, + {"type" => "integer", "value" => require_integer(object)} + ) + end + + def lt(object) + Flipper::Rules::Condition.new( + to_h, + {"type" => "operator", "value" => "lt"}, + {"type" => "integer", "value" => require_integer(object)} + ) + end + + def lte(object) + Flipper::Rules::Condition.new( + to_h, + {"type" => "operator", "value" => "lte"}, + {"type" => "integer", "value" => require_integer(object)} + ) + end + + def in(object) + Flipper::Rules::Condition.new( + to_h, + {"type" => "operator", "value" => "in"}, + {"type" => "array", "value" => require_array(object)} + ) + end + + def nin(object) + Flipper::Rules::Condition.new( + to_h, + {"type" => "operator", "value" => "nin"}, + {"type" => "array", "value" => require_array(object)} + ) + end + + def percentage(object) + Flipper::Rules::Condition.new( + to_h, + {"type" => "operator", "value" => "percentage"}, + {"type" => "integer", "value" => require_integer(object)} + ) + end + + private + + def type_of(object) + type_class = SUPPORTED_VALUE_TYPES.detect { |klass, type| object.is_a?(klass) } + + if type_class.nil? + raise ArgumentError, + "#{object.inspect} is not a supported primitive." + + " Object must be one of: #{SUPPORTED_VALUE_TYPES.join(", ")}." + end + + SUPPORTED_VALUE_TYPES_MAP[type_class] + end + + def require_integer(object) + raise ArgumentError, "object must be integer" unless object.is_a?(Integer) + object + end + + def require_array(object) + raise ArgumentError, "object must be array" unless object.is_a?(Array) + object + end + end + end +end diff --git a/lib/flipper/rules/property.rb b/lib/flipper/rules/property.rb index c3bf0abfe..c1d317ed2 100644 --- a/lib/flipper/rules/property.rb +++ b/lib/flipper/rules/property.rb @@ -1,97 +1,15 @@ +require 'flipper/rules/object' + module Flipper module Rules - class Property - attr_reader :name - - def initialize(name) - @name = name.to_s - end - - def value - { - "type" => "property", - "value" => @name, - } - end - - def eql?(other) - self.class.eql?(other.class) && - @name == other.name - end - alias_method :==, :eql? - - def eq(object) - type, object = Rules.typed(object) - Flipper::Rules::Condition.new( - value, - {"type" => "operator", "value" => "eq"}, - {"type" => type, "value" => object} - ) - end - - def neq(object) - type, object = Rules.typed(object) - Flipper::Rules::Condition.new( - value, - {"type" => "operator", "value" => "neq"}, - {"type" => type, "value" => object} - ) - end - - def gt(object) - Flipper::Rules::Condition.new( - value, - {"type" => "operator", "value" => "gt"}, - {"type" => "integer", "value" => Rules.require_integer(object)} - ) - end - - def gte(object) - Flipper::Rules::Condition.new( - value, - {"type" => "operator", "value" => "gte"}, - {"type" => "integer", "value" => Rules.require_integer(object)} - ) - end - - def lt(object) - Flipper::Rules::Condition.new( - value, - {"type" => "operator", "value" => "lt"}, - {"type" => "integer", "value" => Rules.require_integer(object)} - ) - end - - def lte(object) - Flipper::Rules::Condition.new( - value, - {"type" => "operator", "value" => "lte"}, - {"type" => "integer", "value" => Rules.require_integer(object)} - ) - end - - def in(object) - Flipper::Rules::Condition.new( - value, - {"type" => "operator", "value" => "in"}, - {"type" => "array", "value" => Rules.require_array(object)} - ) - end - - def nin(object) - Flipper::Rules::Condition.new( - value, - {"type" => "operator", "value" => "nin"}, - {"type" => "array", "value" => Rules.require_array(object)} - ) + class Property < Object + def initialize(value) + @type = "property".freeze + @value = value.to_s end - def percentage(object) - Flipper::Rules::Condition.new( - value, - {"type" => "operator", "value" => "percentage"}, - {"type" => "integer", "value" => Rules.require_integer(object)} - ) + def name + @value end end end diff --git a/spec/flipper/rules/object_spec.rb b/spec/flipper/rules/object_spec.rb new file mode 100644 index 000000000..8d1bb1987 --- /dev/null +++ b/spec/flipper/rules/object_spec.rb @@ -0,0 +1,440 @@ +require 'helper' + +RSpec.describe Flipper::Rules::Object do + describe "#initialize" do + context "with string" do + it "returns array of type and value" do + instance = described_class.new("test") + expect(instance.type).to eq("string") + expect(instance.value).to eq("test") + end + end + + context "with integer" do + it "returns array of type and value" do + instance = described_class.new(21) + expect(instance.type).to eq("integer") + expect(instance.value).to eq(21) + end + end + + context "with nil" do + it "returns array of type and value" do + instance = described_class.new(nil) + expect(instance.type).to eq("null") + expect(instance.value).to be(nil) + end + end + + context "with true" do + it "returns array of type and value" do + instance = described_class.new(true) + expect(instance.type).to eq("boolean") + expect(instance.value).to be(true) + end + end + + context "with false" do + it "returns array of type and value" do + instance = described_class.new(false) + expect(instance.type).to eq("boolean") + expect(instance.value).to be(false) + end + end + + context "with array" do + it "returns array of type and value" do + instance = described_class.new(["test"]) + expect(instance.type).to eq("array") + expect(instance.value).to eq(["test"]) + end + end + + context "with unsupported type" do + it "returns array of type and value" do + expect { + described_class.new({}) + }.to raise_error(ArgumentError, /{} is not a supported primitive\. Object must be one of: String, Integer, NilClass, TrueClass, FalseClass, Array\./) + end + end + end + + describe "equality" do + it "returns true if equal" do + expect(described_class.new("test").eql?(described_class.new("test"))).to be(true) + end + + it "returns false if value does not match" do + expect(described_class.new("test").eql?(described_class.new("age"))).to be(false) + end + + it "returns false for different class" do + expect(described_class.new("test").eql?(Object.new)).to be(false) + end + end + + describe "#to_h" do + it "returns Hash with type and value" do + expect(described_class.new("test").to_h).to eq({ + "type" => "string", + "value" => "test", + }) + end + end + + describe "#eq" do + context "with string" do + it "returns equal condition" do + expect(described_class.new("plan").eq("basic")).to eq(Flipper::Rules::Condition.new( + {"type" => "string", "value" => "plan"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "basic"} + )) + end + end + + context "with boolean" do + it "returns equal condition" do + expect(described_class.new("admin").eq(true)).to eq(Flipper::Rules::Condition.new( + {"type" => "string", "value" => "admin"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "boolean", "value" => true} + )) + end + end + + context "with integer" do + it "returns equal condition" do + expect(described_class.new("age").eq(21)).to eq(Flipper::Rules::Condition.new( + {"type" => "string", "value" => "age"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "integer", "value" => 21} + )) + end + end + + context "with array" do + it "returns equal condition" do + expect(described_class.new("roles").eq(["admin"])).to eq(Flipper::Rules::Condition.new( + {"type" => "string", "value" => "roles"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "array", "value" => ["admin"]} + )) + end + end + + context "with nil" do + it "returns equal condition" do + expect(described_class.new("admin").eq(nil)).to eq(Flipper::Rules::Condition.new( + {"type" => "string", "value" => "admin"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "null", "value" => nil} + )) + end + end + end + + describe "#neq" do + context "with string" do + it "returns not equal condition" do + expect(described_class.new("plan").neq("basic")).to eq(Flipper::Rules::Condition.new( + {"type" => "string", "value" => "plan"}, + {"type" => "operator", "value" => "neq"}, + {"type" => "string", "value" => "basic"} + )) + end + end + + context "with boolean" do + it "returns not equal condition" do + expect(described_class.new("admin").neq(true)).to eq(Flipper::Rules::Condition.new( + {"type" => "string", "value" => "admin"}, + {"type" => "operator", "value" => "neq"}, + {"type" => "boolean", "value" => true} + )) + end + end + + context "with integer" do + it "returns not equal condition" do + expect(described_class.new("age").neq(21)).to eq(Flipper::Rules::Condition.new( + {"type" => "string", "value" => "age"}, + {"type" => "operator", "value" => "neq"}, + {"type" => "integer", "value" => 21} + )) + end + end + + context "with array" do + it "returns not equal condition" do + expect(described_class.new("roles").neq(["admin"])).to eq(Flipper::Rules::Condition.new( + {"type" => "string", "value" => "roles"}, + {"type" => "operator", "value" => "neq"}, + {"type" => "array", "value" => ["admin"]} + )) + end + end + + context "with nil" do + it "returns not equal condition" do + expect(described_class.new("admin").neq(nil)).to eq(Flipper::Rules::Condition.new( + {"type" => "string", "value" => "admin"}, + {"type" => "operator", "value" => "neq"}, + {"type" => "null", "value" => nil} + )) + end + end + end + + describe "#gt" do + context "with integer" do + it "returns condition" do + expect(described_class.new("age").gt(21)).to eq(Flipper::Rules::Condition.new( + {"type" => "string", "value" => "age"}, + {"type" => "operator", "value" => "gt"}, + {"type" => "integer", "value" => 21} + )) + end + end + + context "with string" do + it "raises error" do + expect { described_class.new("age").gt("231") }.to raise_error(ArgumentError) + end + end + + context "with boolean" do + it "raises error" do + expect { described_class.new("age").gt(true) }.to raise_error(ArgumentError) + end + end + + context "with array" do + it "raises error" do + expect { described_class.new("age").gt(["admin"]) }.to raise_error(ArgumentError) + end + end + + context "with nil" do + it "raises error" do + expect { described_class.new("age").gt(nil) }.to raise_error(ArgumentError) + end + end + end + + describe "#gte" do + context "with integer" do + it "returns condition" do + expect(described_class.new("age").gte(21)).to eq(Flipper::Rules::Condition.new( + {"type" => "string", "value" => "age"}, + {"type" => "operator", "value" => "gte"}, + {"type" => "integer", "value" => 21} + )) + end + end + + context "with string" do + it "raises error" do + expect { described_class.new("age").gte("231") }.to raise_error(ArgumentError) + end + end + + context "with boolean" do + it "raises error" do + expect { described_class.new("age").gte(true) }.to raise_error(ArgumentError) + end + end + + context "with array" do + it "raises error" do + expect { described_class.new("age").gte(["admin"]) }.to raise_error(ArgumentError) + end + end + + context "with nil" do + it "raises error" do + expect { described_class.new("age").gte(nil) }.to raise_error(ArgumentError) + end + end + end + + describe "#lt" do + context "with integer" do + it "returns condition" do + expect(described_class.new("age").lt(21)).to eq(Flipper::Rules::Condition.new( + {"type" => "string", "value" => "age"}, + {"type" => "operator", "value" => "lt"}, + {"type" => "integer", "value" => 21} + )) + end + end + + context "with string" do + it "raises error" do + expect { described_class.new("age").lt("231") }.to raise_error(ArgumentError) + end + end + + context "with boolean" do + it "raises error" do + expect { described_class.new("age").lt(true) }.to raise_error(ArgumentError) + end + end + + context "with array" do + it "raises error" do + expect { described_class.new("age").lt(["admin"]) }.to raise_error(ArgumentError) + end + end + + context "with nil" do + it "raises error" do + expect { described_class.new("age").lt(nil) }.to raise_error(ArgumentError) + end + end + end + + describe "#lte" do + context "with integer" do + it "returns condition" do + expect(described_class.new("age").lte(21)).to eq(Flipper::Rules::Condition.new( + {"type" => "string", "value" => "age"}, + {"type" => "operator", "value" => "lte"}, + {"type" => "integer", "value" => 21} + )) + end + end + + context "with string" do + it "raises error" do + expect { described_class.new("age").lte("231") }.to raise_error(ArgumentError) + end + end + + context "with boolean" do + it "raises error" do + expect { described_class.new("age").lte(true) }.to raise_error(ArgumentError) + end + end + + context "with array" do + it "raises error" do + expect { described_class.new("age").lte(["admin"]) }.to raise_error(ArgumentError) + end + end + + context "with nil" do + it "raises error" do + expect { described_class.new("age").lte(nil) }.to raise_error(ArgumentError) + end + end + end + + describe "#in" do + context "with array" do + it "returns condition" do + expect(described_class.new("role").in(["admin"])).to eq(Flipper::Rules::Condition.new( + {"type" => "string", "value" => "role"}, + {"type" => "operator", "value" => "in"}, + {"type" => "array", "value" => ["admin"]} + )) + end + end + + context "with string" do + it "raises error" do + expect { described_class.new("role").in("231") }.to raise_error(ArgumentError) + end + end + + context "with boolean" do + it "raises error" do + expect { described_class.new("role").in(true) }.to raise_error(ArgumentError) + end + end + + context "with integer" do + it "raises error" do + expect { described_class.new("role").in(21) }.to raise_error(ArgumentError) + end + end + + context "with nil" do + it "raises error" do + expect { described_class.new("role").in(nil) }.to raise_error(ArgumentError) + end + end + end + + describe "#nin" do + context "with array" do + it "returns condition" do + expect(described_class.new("role").nin(["admin"])).to eq(Flipper::Rules::Condition.new( + {"type" => "string", "value" => "role"}, + {"type" => "operator", "value" => "nin"}, + {"type" => "array", "value" => ["admin"]} + )) + end + end + + context "with string" do + it "raises error" do + expect { described_class.new("role").nin("231") }.to raise_error(ArgumentError) + end + end + + context "with boolean" do + it "raises error" do + expect { described_class.new("role").nin(true) }.to raise_error(ArgumentError) + end + end + + context "with integer" do + it "raises error" do + expect { described_class.new("role").nin(21) }.to raise_error(ArgumentError) + end + end + + context "with nil" do + it "raises error" do + expect { described_class.new("role").nin(nil) }.to raise_error(ArgumentError) + end + end + end + + describe "#percentage" do + context "with integer" do + it "returns condition" do + expect(described_class.new("flipper_id").percentage(25)).to eq(Flipper::Rules::Condition.new( + {"type" => "string", "value" => "flipper_id"}, + {"type" => "operator", "value" => "percentage"}, + {"type" => "integer", "value" => 25} + )) + end + end + + context "with string" do + it "raises error" do + expect { described_class.new("flipper_id").percentage("231") }.to raise_error(ArgumentError) + end + end + + context "with boolean" do + it "raises error" do + expect { described_class.new("flipper_id").percentage(true) }.to raise_error(ArgumentError) + end + end + + context "with array" do + it "raises error" do + expect { described_class.new("flipper_id").percentage(["admin"]) }.to raise_error(ArgumentError) + end + end + + context "with nil" do + it "raises error" do + expect { described_class.new("flipper_id").percentage(nil) }.to raise_error(ArgumentError) + end + end + end +end diff --git a/spec/flipper/rules/property_spec.rb b/spec/flipper/rules/property_spec.rb index 1d284674a..cc9d8ba8b 100644 --- a/spec/flipper/rules/property_spec.rb +++ b/spec/flipper/rules/property_spec.rb @@ -13,9 +13,9 @@ end end - describe "#value" do + describe "#to_h" do it "returns Hash with type and value" do - expect(described_class.new("plan").value).to eq({ + expect(described_class.new("plan").to_h).to eq({ "type" => "property", "value" => "plan", }) diff --git a/spec/flipper/rules_spec.rb b/spec/flipper/rules_spec.rb index da58e3de7..d3e57ade0 100644 --- a/spec/flipper/rules_spec.rb +++ b/spec/flipper/rules_spec.rb @@ -1,91 +1,4 @@ require 'helper' RSpec.describe Flipper::Rules do - describe ".type_of" do - context "for string" do - it "returns string" do - expect(described_class.type_of("test")).to eq("string") - end - end - - context "for integer" do - it "returns integer" do - expect(described_class.type_of(21)).to eq("integer") - end - end - - context "for nil" do - it "returns nil" do - expect(described_class.type_of(nil)).to eq("null") - end - end - - context "for true" do - it "returns boolean" do - expect(described_class.type_of(true)).to eq("boolean") - end - end - - context "for false" do - it "returns boolean" do - expect(described_class.type_of(false)).to eq("boolean") - end - end - - context "for array" do - it "returns array" do - expect(described_class.type_of([1, 2, 3])).to eq("array") - end - end - - context "for unsupported type" do - it "returns nil" do - expect(described_class.type_of(Object.new)).to be(nil) - end - end - end - - describe ".typed" do - context "with string" do - it "returns array of type and value" do - expect(described_class.typed("test")).to eq(["string", "test"]) - end - end - - context "with integer" do - it "returns array of type and value" do - expect(described_class.typed(21)).to eq(["integer", 21]) - end - end - - context "with nil" do - it "returns array of type and value" do - expect(described_class.typed(nil)).to eq(["null", nil]) - end - end - - context "with true" do - it "returns array of type and value" do - expect(described_class.typed(true)).to eq(["boolean", true]) - end - end - - context "with false" do - it "returns array of type and value" do - expect(described_class.typed(false)).to eq(["boolean", false]) - end - end - - context "with array" do - it "returns array of type and value" do - expect(described_class.typed(["test"])).to eq(["array", ["test"]]) - end - end - - context "with unsupported type" do - it "returns array of type and value" do - expect { described_class.typed({}) }.to raise_error(ArgumentError, /{} is an unsupported type\. Object must be one of: String, Integer, NilClass, TrueClass, FalseClass, Array\./) - end - end - end end From be97645424623b505586dd29dbcf2787bb05ed9c Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Mon, 6 Sep 2021 20:58:24 -0400 Subject: [PATCH 045/176] Minor formatting --- lib/flipper/rules/object.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/flipper/rules/object.rb b/lib/flipper/rules/object.rb index 07310ff0f..359be3569 100644 --- a/lib/flipper/rules/object.rb +++ b/lib/flipper/rules/object.rb @@ -2,12 +2,12 @@ module Flipper module Rules class Object SUPPORTED_VALUE_TYPES_MAP = { - String => "string", - Integer => "integer", - NilClass => "null", - TrueClass => "boolean", + String => "string", + Integer => "integer", + NilClass => "null", + TrueClass => "boolean", FalseClass => "boolean", - Array => "array", + Array => "array", }.freeze SUPPORTED_VALUE_TYPES = SUPPORTED_VALUE_TYPES_MAP.keys.freeze From 1aea677014766c8cba5064bcc7a3169a5aa37822 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Mon, 6 Sep 2021 21:10:38 -0400 Subject: [PATCH 046/176] Add new operator class and add operator/object to top level --- lib/flipper.rb | 8 ++++++ lib/flipper/rules.rb | 1 + lib/flipper/rules/object.rb | 23 +++++++++-------- lib/flipper/rules/operator.rb | 30 +++++++++++++++++++++++ spec/flipper/rules/operator_spec.rb | 38 +++++++++++++++++++++++++++++ 5 files changed, 90 insertions(+), 10 deletions(-) create mode 100644 lib/flipper/rules/operator.rb create mode 100644 spec/flipper/rules/operator_spec.rb diff --git a/lib/flipper.rb b/lib/flipper.rb index d1457bb4b..05e59ec33 100644 --- a/lib/flipper.rb +++ b/lib/flipper.rb @@ -73,6 +73,14 @@ def property(name) Flipper::Rules::Property.new(name) end + def object(object) + Flipper::Rules::Object.new(object) + end + + def operator(name) + Flipper::Rules::Object.new(name) + end + def any(*rules) Flipper::Rules::Any.new(*rules) end diff --git a/lib/flipper/rules.rb b/lib/flipper/rules.rb index 50e162f6a..d4e7e9b9b 100644 --- a/lib/flipper/rules.rb +++ b/lib/flipper/rules.rb @@ -1,6 +1,7 @@ require 'flipper/rules/condition' require 'flipper/rules/any' require 'flipper/rules/all' +require 'flipper/rules/operator' require 'flipper/rules/object' require 'flipper/rules/property' diff --git a/lib/flipper/rules/object.rb b/lib/flipper/rules/object.rb index 359be3569..83172f4d7 100644 --- a/lib/flipper/rules/object.rb +++ b/lib/flipper/rules/object.rb @@ -1,3 +1,6 @@ +require "flipper/rules/condition" +require "flipper/rules/operator" + module Flipper module Rules class Object @@ -36,15 +39,15 @@ def eql?(other) def eq(object) Flipper::Rules::Condition.new( to_h, - {"type" => "operator", "value" => "eq"}, - Rules::Object.new(object).to_h + Operator.new(:eq).to_h, + Object.new(object).to_h ) end def neq(object) Flipper::Rules::Condition.new( to_h, - {"type" => "operator", "value" => "neq"}, + Operator.new(:neq).to_h, Rules::Object.new(object).to_h ) end @@ -52,7 +55,7 @@ def neq(object) def gt(object) Flipper::Rules::Condition.new( to_h, - {"type" => "operator", "value" => "gt"}, + Operator.new(:gt).to_h, {"type" => "integer", "value" => require_integer(object)} ) end @@ -60,7 +63,7 @@ def gt(object) def gte(object) Flipper::Rules::Condition.new( to_h, - {"type" => "operator", "value" => "gte"}, + Operator.new(:gte).to_h, {"type" => "integer", "value" => require_integer(object)} ) end @@ -68,7 +71,7 @@ def gte(object) def lt(object) Flipper::Rules::Condition.new( to_h, - {"type" => "operator", "value" => "lt"}, + Operator.new(:lt).to_h, {"type" => "integer", "value" => require_integer(object)} ) end @@ -76,7 +79,7 @@ def lt(object) def lte(object) Flipper::Rules::Condition.new( to_h, - {"type" => "operator", "value" => "lte"}, + Operator.new(:lte).to_h, {"type" => "integer", "value" => require_integer(object)} ) end @@ -84,7 +87,7 @@ def lte(object) def in(object) Flipper::Rules::Condition.new( to_h, - {"type" => "operator", "value" => "in"}, + Operator.new(:in).to_h, {"type" => "array", "value" => require_array(object)} ) end @@ -92,7 +95,7 @@ def in(object) def nin(object) Flipper::Rules::Condition.new( to_h, - {"type" => "operator", "value" => "nin"}, + Operator.new(:nin).to_h, {"type" => "array", "value" => require_array(object)} ) end @@ -100,7 +103,7 @@ def nin(object) def percentage(object) Flipper::Rules::Condition.new( to_h, - {"type" => "operator", "value" => "percentage"}, + Operator.new(:percentage).to_h, {"type" => "integer", "value" => require_integer(object)} ) end diff --git a/lib/flipper/rules/operator.rb b/lib/flipper/rules/operator.rb new file mode 100644 index 000000000..9b6fa3bfe --- /dev/null +++ b/lib/flipper/rules/operator.rb @@ -0,0 +1,30 @@ +module Flipper + module Rules + class Operator + attr_reader :type, :value + + def initialize(value) + @type = "operator".freeze + @value = value.to_s + end + + def name + @value + end + + def to_h + { + "type" => @type, + "value" => @value, + } + end + + def eql?(other) + self.class.eql?(other.class) && + @type == other.type && + @value == other.value + end + alias_method :==, :eql? + end + end +end diff --git a/spec/flipper/rules/operator_spec.rb b/spec/flipper/rules/operator_spec.rb new file mode 100644 index 000000000..545f6a9a8 --- /dev/null +++ b/spec/flipper/rules/operator_spec.rb @@ -0,0 +1,38 @@ +require 'helper' + +RSpec.describe Flipper::Rules::Operator do + describe "#initialize" do + it "works with string name" do + property = described_class.new("eq") + expect(property.name).to eq("eq") + end + + it "works with symbol name" do + property = described_class.new(:eq) + expect(property.name).to eq("eq") + end + end + + describe "#to_h" do + it "returns Hash with type and value" do + expect(described_class.new("eq").to_h).to eq({ + "type" => "operator", + "value" => "eq", + }) + end + end + + describe "equality" do + it "returns true if equal" do + expect(described_class.new("eq").eql?(described_class.new("eq"))).to be(true) + end + + it "returns false if name does not match" do + expect(described_class.new("eq").eql?(described_class.new("neq"))).to be(false) + end + + it "returns false for different class" do + expect(described_class.new("eq").eql?(Object.new)).to be(false) + end + end +end From a745a41721b40d7c945a98016d294882a8efbef4 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Tue, 7 Sep 2021 08:27:32 -0400 Subject: [PATCH 047/176] Add support for properties as object in operator shortcuts --- examples/rules.rb | 9 +--- lib/flipper/rules/object.rb | 50 +++++++++++------ spec/flipper/rules/object_spec.rb | 90 +++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 23 deletions(-) diff --git a/examples/rules.rb b/examples/rules.rb index 24df0ec4e..cffeead05 100644 --- a/examples/rules.rb +++ b/examples/rules.rb @@ -17,7 +17,6 @@ class Org < Struct.new(:id, :flipper_properties) user = User.new(1, { "type" => "User", "id" => 1, - "flipper_id" => "User;1", "plan" => "basic", "age" => 39, "roles" => ["team_user"] @@ -26,14 +25,12 @@ class Org < Struct.new(:id, :flipper_properties) admin_user = User.new(2, { "type" => "User", "id" => 2, - "flipper_id" => "User;2", "roles" => ["admin", "team_user"], }) other_user = User.new(3, { "type" => "User", "id" => 3, - "flipper_id" => "User;3", "plan" => "plus", "age" => 18, "roles" => ["org_admin"] @@ -41,11 +38,7 @@ class Org < Struct.new(:id, :flipper_properties) age_rule = Flipper.property(:age).gte(21) plan_rule = Flipper.property(:plan).eq("basic") -admin_rule = Flipper::Rules::Condition.new( - {"type" => "string", "value" => "admin"}, - {"type" => "operator", "value" => "in"}, - {"type" => "property", "value" => "roles"} -) +admin_rule = Flipper.object("admin").in(Flipper.property(:roles)) puts "Single Rule" p should_be_false: Flipper.enabled?(:something, user) diff --git a/lib/flipper/rules/object.rb b/lib/flipper/rules/object.rb index 83172f4d7..8a376a713 100644 --- a/lib/flipper/rules/object.rb +++ b/lib/flipper/rules/object.rb @@ -40,7 +40,7 @@ def eq(object) Flipper::Rules::Condition.new( to_h, Operator.new(:eq).to_h, - Object.new(object).to_h + self.class.primitive_or_property(object).to_h ) end @@ -48,7 +48,7 @@ def neq(object) Flipper::Rules::Condition.new( to_h, Operator.new(:neq).to_h, - Rules::Object.new(object).to_h + self.class.primitive_or_property(object).to_h ) end @@ -56,7 +56,7 @@ def gt(object) Flipper::Rules::Condition.new( to_h, Operator.new(:gt).to_h, - {"type" => "integer", "value" => require_integer(object)} + self.class.integer_or_property(object) ) end @@ -64,7 +64,7 @@ def gte(object) Flipper::Rules::Condition.new( to_h, Operator.new(:gte).to_h, - {"type" => "integer", "value" => require_integer(object)} + self.class.integer_or_property(object) ) end @@ -72,7 +72,7 @@ def lt(object) Flipper::Rules::Condition.new( to_h, Operator.new(:lt).to_h, - {"type" => "integer", "value" => require_integer(object)} + self.class.integer_or_property(object) ) end @@ -80,7 +80,7 @@ def lte(object) Flipper::Rules::Condition.new( to_h, Operator.new(:lte).to_h, - {"type" => "integer", "value" => require_integer(object)} + self.class.integer_or_property(object) ) end @@ -88,7 +88,7 @@ def in(object) Flipper::Rules::Condition.new( to_h, Operator.new(:in).to_h, - {"type" => "array", "value" => require_array(object)} + self.class.array_or_property(object) ) end @@ -96,7 +96,7 @@ def nin(object) Flipper::Rules::Condition.new( to_h, Operator.new(:nin).to_h, - {"type" => "array", "value" => require_array(object)} + self.class.array_or_property(object) ) end @@ -104,7 +104,7 @@ def percentage(object) Flipper::Rules::Condition.new( to_h, Operator.new(:percentage).to_h, - {"type" => "integer", "value" => require_integer(object)} + self.class.integer_or_property(object) ) end @@ -122,14 +122,34 @@ def type_of(object) SUPPORTED_VALUE_TYPES_MAP[type_class] end - def require_integer(object) - raise ArgumentError, "object must be integer" unless object.is_a?(Integer) - object + def self.primitive_or_property(object) + if object.is_a?(Flipper::Rules::Property) + object + else + Object.new(object) + end end - def require_array(object) - raise ArgumentError, "object must be array" unless object.is_a?(Array) - object + def self.integer_or_property(object) + case object + when Integer + {"type" => "integer", "value" => object} + when Flipper::Rules::Property + object.to_h + else + raise ArgumentError, "object must be integer or property" unless object.is_a?(Integer) + end + end + + def self.array_or_property(object) + case object + when Array + {"type" => "array", "value" => object} + when Flipper::Rules::Property + object.to_h + else + raise ArgumentError, "object must be array or property" unless object.is_a?(Array) + end end end end diff --git a/spec/flipper/rules/object_spec.rb b/spec/flipper/rules/object_spec.rb index 8d1bb1987..f34f8b29b 100644 --- a/spec/flipper/rules/object_spec.rb +++ b/spec/flipper/rules/object_spec.rb @@ -132,6 +132,16 @@ )) end end + + context "with property" do + it "returns equal condition" do + expect(described_class.new("admin").eq(Flipper.property(:name))).to eq(Flipper::Rules::Condition.new( + {"type" => "string", "value" => "admin"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "property", "value" => "name"} + )) + end + end end describe "#neq" do @@ -184,6 +194,16 @@ )) end end + + context "with property" do + it "returns not equal condition" do + expect(described_class.new("plan").neq(Flipper.property(:name))).to eq(Flipper::Rules::Condition.new( + {"type" => "string", "value" => "plan"}, + {"type" => "operator", "value" => "neq"}, + {"type" => "property", "value" => "name"} + )) + end + end end describe "#gt" do @@ -197,6 +217,16 @@ end end + context "with property" do + it "returns condition" do + expect(described_class.new(21).gt(Flipper.property(:age))).to eq(Flipper::Rules::Condition.new( + {"type" => "integer", "value" => 21}, + {"type" => "operator", "value" => "gt"}, + {"type" => "property", "value" => "age"} + )) + end + end + context "with string" do it "raises error" do expect { described_class.new("age").gt("231") }.to raise_error(ArgumentError) @@ -233,6 +263,16 @@ end end + context "with property" do + it "returns condition" do + expect(described_class.new(21).gte(Flipper.property(:age))).to eq(Flipper::Rules::Condition.new( + {"type" => "integer", "value" => 21}, + {"type" => "operator", "value" => "gte"}, + {"type" => "property", "value" => "age"} + )) + end + end + context "with string" do it "raises error" do expect { described_class.new("age").gte("231") }.to raise_error(ArgumentError) @@ -269,6 +309,16 @@ end end + context "with property" do + it "returns condition" do + expect(described_class.new(21).lt(Flipper.property(:age))).to eq(Flipper::Rules::Condition.new( + {"type" => "integer", "value" => 21}, + {"type" => "operator", "value" => "lt"}, + {"type" => "property", "value" => "age"} + )) + end + end + context "with string" do it "raises error" do expect { described_class.new("age").lt("231") }.to raise_error(ArgumentError) @@ -305,6 +355,16 @@ end end + context "with property" do + it "returns condition" do + expect(described_class.new(21).lte(Flipper.property(:age))).to eq(Flipper::Rules::Condition.new( + {"type" => "integer", "value" => 21}, + {"type" => "operator", "value" => "lte"}, + {"type" => "property", "value" => "age"} + )) + end + end + context "with string" do it "raises error" do expect { described_class.new("age").lte("231") }.to raise_error(ArgumentError) @@ -341,6 +401,16 @@ end end + context "with property" do + it "returns condition" do + expect(described_class.new("admin").in(Flipper.property(:roles))).to eq(Flipper::Rules::Condition.new( + {"type" => "string", "value" => "admin"}, + {"type" => "operator", "value" => "in"}, + {"type" => "property", "value" => "roles"} + )) + end + end + context "with string" do it "raises error" do expect { described_class.new("role").in("231") }.to raise_error(ArgumentError) @@ -377,6 +447,16 @@ end end + context "with property" do + it "returns condition" do + expect(described_class.new("admin").nin(Flipper.property(:roles))).to eq(Flipper::Rules::Condition.new( + {"type" => "string", "value" => "admin"}, + {"type" => "operator", "value" => "nin"}, + {"type" => "property", "value" => "roles"} + )) + end + end + context "with string" do it "raises error" do expect { described_class.new("role").nin("231") }.to raise_error(ArgumentError) @@ -413,6 +493,16 @@ end end + context "with property" do + it "returns condition" do + expect(described_class.new("flipper_id").percentage(Flipper.property(:percentage))).to eq(Flipper::Rules::Condition.new( + {"type" => "string", "value" => "flipper_id"}, + {"type" => "operator", "value" => "percentage"}, + {"type" => "property", "value" => "percentage"} + )) + end + end + context "with string" do it "raises error" do expect { described_class.new("flipper_id").percentage("231") }.to raise_error(ArgumentError) From b5da92ecc795712a69ea1d8ce5b3c72903236917 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Tue, 7 Sep 2021 08:31:55 -0400 Subject: [PATCH 048/176] Cleanup rules example to make it easier to see if something doesn't return correctly --- examples/rules.rb | 92 +++++++++++++++++++++++++++-------------------- 1 file changed, 54 insertions(+), 38 deletions(-) diff --git a/examples/rules.rb b/examples/rules.rb index cffeead05..66d42dbdb 100644 --- a/examples/rules.rb +++ b/examples/rules.rb @@ -1,6 +1,22 @@ require 'bundler/setup' require 'flipper' +def assert(value) + p value + unless value + puts "#{value} expected to be true but was false. Please correct." + exit 1 + end +end + +def refute(value) + p value + if value + puts "#{value} expected to be false but was true. Please correct." + exit 1 + end +end + class User < Struct.new(:id, :flipper_properties) include Flipper::Identifier end @@ -41,63 +57,63 @@ class Org < Struct.new(:id, :flipper_properties) admin_rule = Flipper.object("admin").in(Flipper.property(:roles)) puts "Single Rule" -p should_be_false: Flipper.enabled?(:something, user) +refute Flipper.enabled?(:something, user) puts "Enabling single rule" Flipper.enable_rule :something, plan_rule -p should_be_true: Flipper.enabled?(:something, user) -p should_be_false: Flipper.enabled?(:something, admin_user) -p should_be_false: Flipper.enabled?(:something, other_user) +assert Flipper.enabled?(:something, user) +refute Flipper.enabled?(:something, admin_user) +refute Flipper.enabled?(:something, other_user) puts "Disabling single rule" Flipper.disable_rule :something, plan_rule -p should_be_false: Flipper.enabled?(:something, user) +refute Flipper.enabled?(:something, user) puts "\n\nAny Rule" any_rule = Flipper.any(plan_rule, age_rule) -p should_be_false: Flipper.enabled?(:something, user) +refute Flipper.enabled?(:something, user) puts "Enabling any rule" Flipper.enable_rule :something, any_rule -p should_be_true: Flipper.enabled?(:something, user) -p should_be_false: Flipper.enabled?(:something, admin_user) -p should_be_false: Flipper.enabled?(:something, other_user) +assert Flipper.enabled?(:something, user) +refute Flipper.enabled?(:something, admin_user) +refute Flipper.enabled?(:something, other_user) puts "Disabling any rule" Flipper.disable_rule :something, any_rule -p should_be_false: Flipper.enabled?(:something, user) +refute Flipper.enabled?(:something, user) puts "\n\nAll Rule" all_rule = Flipper.all(plan_rule, age_rule) -p should_be_false: Flipper.enabled?(:something, user) +refute Flipper.enabled?(:something, user) puts "Enabling all rule" Flipper.enable_rule :something, all_rule -p should_be_true: Flipper.enabled?(:something, user) -p should_be_false: Flipper.enabled?(:something, admin_user) -p should_be_false: Flipper.enabled?(:something, other_user) +assert Flipper.enabled?(:something, user) +refute Flipper.enabled?(:something, admin_user) +refute Flipper.enabled?(:something, other_user) puts "Disabling all rule" Flipper.disable_rule :something, all_rule -p should_be_false: Flipper.enabled?(:something, user) +refute Flipper.enabled?(:something, user) puts "\n\nNested Rule" nested_rule = Flipper.any(admin_rule, all_rule) -p should_be_false: Flipper.enabled?(:something, user) -p should_be_false: Flipper.enabled?(:something, admin_user) -p should_be_false: Flipper.enabled?(:something, other_user) +refute Flipper.enabled?(:something, user) +refute Flipper.enabled?(:something, admin_user) +refute Flipper.enabled?(:something, other_user) puts "Enabling nested rule" Flipper.enable_rule :something, nested_rule -p should_be_true: Flipper.enabled?(:something, user) -p should_be_true: Flipper.enabled?(:something, admin_user) -p should_be_false: Flipper.enabled?(:something, other_user) +assert Flipper.enabled?(:something, user) +assert Flipper.enabled?(:something, admin_user) +refute Flipper.enabled?(:something, other_user) puts "Disabling nested rule" Flipper.disable_rule :something, nested_rule -p should_be_false: Flipper.enabled?(:something, user) -p should_be_false: Flipper.enabled?(:something, admin_user) -p should_be_false: Flipper.enabled?(:something, other_user) +refute Flipper.enabled?(:something, user) +refute Flipper.enabled?(:something, admin_user) +refute Flipper.enabled?(:something, other_user) puts "\n\nBoolean Rule" boolean_rule = Flipper::Rules::Condition.new( @@ -106,24 +122,24 @@ class Org < Struct.new(:id, :flipper_properties) {"type" => "boolean", "value" => true} ) Flipper.enable_rule :something, boolean_rule -p should_be_true: Flipper.enabled?(:something) -p should_be_true: Flipper.enabled?(:something, user) +assert Flipper.enabled?(:something) +assert Flipper.enabled?(:something, user) Flipper.disable_rule :something, boolean_rule puts "\n\nSet of Actors Rule" set_of_actors_rule = Flipper.property(:flipper_id).in(["User;1", "User;3"]) Flipper.enable_rule :something, set_of_actors_rule -p should_be_true: Flipper.enabled?(:something, user) -p should_be_true: Flipper.enabled?(:something, other_user) -p should_be_false: Flipper.enabled?(:something, admin_user) +assert Flipper.enabled?(:something, user) +assert Flipper.enabled?(:something, other_user) +refute Flipper.enabled?(:something, admin_user) Flipper.disable_rule :something, set_of_actors_rule puts "\n\n% of Actors Rule" percentage_of_actors = Flipper.property(:flipper_id).percentage(30) Flipper.enable_rule :something, percentage_of_actors -p should_be_false: Flipper.enabled?(:something, user) -p should_be_false: Flipper.enabled?(:something, other_user) -p should_be_true: Flipper.enabled?(:something, admin_user) +refute Flipper.enabled?(:something, user) +refute Flipper.enabled?(:something, other_user) +assert Flipper.enabled?(:something, admin_user) Flipper.disable_rule :something, percentage_of_actors puts "\n\n% of Actors Per Type Rule" @@ -138,10 +154,10 @@ class Org < Struct.new(:id, :flipper_properties) ) ) Flipper.enable_rule :something, percentage_of_actors_per_type -p should_be_false: Flipper.enabled?(:something, user) # not in the 40% enabled for Users -p should_be_true: Flipper.enabled?(:something, other_user) -p should_be_true: Flipper.enabled?(:something, admin_user) -p should_be_false: Flipper.enabled?(:something, org) # not in the 10% of enabled for Orgs +refute Flipper.enabled?(:something, user) # not in the 40% enabled for Users +assert Flipper.enabled?(:something, other_user) +assert Flipper.enabled?(:something, admin_user) +refute Flipper.enabled?(:something, org) # not in the 10% of enabled for Orgs Flipper.disable_rule :something, percentage_of_actors_per_type puts "\n\nPercentage of Time Rule" @@ -153,6 +169,6 @@ class Org < Struct.new(:id, :flipper_properties) Flipper.enable_rule :something, percentage_of_time_rule results = (1..10000).map { |n| Flipper.enabled?(:something, user) } enabled, disabled = results.partition { |r| r } -p should_be_close_to_5000: enabled.size -p should_be_close_to_5000: disabled.size +assert (4500..5500).include?(enabled.size) +assert (4500..5500).include?(disabled.size) Flipper.disable_rule :something, percentage_of_time_rule From 46494eb2d175109ca1be469b71297871aba7fee3 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Tue, 7 Sep 2021 08:48:18 -0400 Subject: [PATCH 049/176] Add random type and allow any flipper object (not just properties) to be passed into operator shortcuts --- examples/rules.rb | 18 +++---- lib/flipper.rb | 4 ++ lib/flipper/rules.rb | 2 + lib/flipper/rules/object.rb | 30 +++++------ lib/flipper/rules/random.rb | 12 +++++ spec/flipper/rules/object_spec.rb | 90 +++++++++++++++++++++++++++++++ spec/flipper_spec.rb | 12 +++++ 7 files changed, 141 insertions(+), 27 deletions(-) create mode 100644 lib/flipper/rules/random.rb diff --git a/examples/rules.rb b/examples/rules.rb index 66d42dbdb..15f745f9e 100644 --- a/examples/rules.rb +++ b/examples/rules.rb @@ -116,11 +116,7 @@ class Org < Struct.new(:id, :flipper_properties) refute Flipper.enabled?(:something, other_user) puts "\n\nBoolean Rule" -boolean_rule = Flipper::Rules::Condition.new( - {"type" => "boolean", "value" => true}, - {"type" => "operator", "value" => "eq"}, - {"type" => "boolean", "value" => true} -) +boolean_rule = Flipper.object(true).eq(true) Flipper.enable_rule :something, boolean_rule assert Flipper.enabled?(:something) assert Flipper.enabled?(:something, user) @@ -161,14 +157,12 @@ class Org < Struct.new(:id, :flipper_properties) Flipper.disable_rule :something, percentage_of_actors_per_type puts "\n\nPercentage of Time Rule" -percentage_of_time_rule = Flipper::Rules::Condition.new( - {"type" => "random", "value" => 100}, - {"type" => "operator", "value" => "lt"}, - {"type" => "integer", "value" => 50} -) +percentage_of_time_rule = Flipper.random(100).lt(50) Flipper.enable_rule :something, percentage_of_time_rule results = (1..10000).map { |n| Flipper.enabled?(:something, user) } enabled, disabled = results.partition { |r| r } -assert (4500..5500).include?(enabled.size) -assert (4500..5500).include?(disabled.size) +p enabled: enabled.size +p disabled: disabled.size +assert (4_700..5_200).include?(enabled.size) +assert (4_700..5_200).include?(disabled.size) Flipper.disable_rule :something, percentage_of_time_rule diff --git a/lib/flipper.rb b/lib/flipper.rb index 05e59ec33..6c5d505c9 100644 --- a/lib/flipper.rb +++ b/lib/flipper.rb @@ -73,6 +73,10 @@ def property(name) Flipper::Rules::Property.new(name) end + def random(name) + Flipper::Rules::Random.new(name) + end + def object(object) Flipper::Rules::Object.new(object) end diff --git a/lib/flipper/rules.rb b/lib/flipper/rules.rb index d4e7e9b9b..c9a2252a5 100644 --- a/lib/flipper/rules.rb +++ b/lib/flipper/rules.rb @@ -1,9 +1,11 @@ require 'flipper/rules/condition' require 'flipper/rules/any' require 'flipper/rules/all' + require 'flipper/rules/operator' require 'flipper/rules/object' require 'flipper/rules/property' +require 'flipper/rules/random' module Flipper module Rules diff --git a/lib/flipper/rules/object.rb b/lib/flipper/rules/object.rb index 8a376a713..5396c0652 100644 --- a/lib/flipper/rules/object.rb +++ b/lib/flipper/rules/object.rb @@ -40,7 +40,7 @@ def eq(object) Flipper::Rules::Condition.new( to_h, Operator.new(:eq).to_h, - self.class.primitive_or_property(object).to_h + self.class.primitive_or_object(object).to_h ) end @@ -48,7 +48,7 @@ def neq(object) Flipper::Rules::Condition.new( to_h, Operator.new(:neq).to_h, - self.class.primitive_or_property(object).to_h + self.class.primitive_or_object(object).to_h ) end @@ -56,7 +56,7 @@ def gt(object) Flipper::Rules::Condition.new( to_h, Operator.new(:gt).to_h, - self.class.integer_or_property(object) + self.class.integer_or_object(object) ) end @@ -64,7 +64,7 @@ def gte(object) Flipper::Rules::Condition.new( to_h, Operator.new(:gte).to_h, - self.class.integer_or_property(object) + self.class.integer_or_object(object) ) end @@ -72,7 +72,7 @@ def lt(object) Flipper::Rules::Condition.new( to_h, Operator.new(:lt).to_h, - self.class.integer_or_property(object) + self.class.integer_or_object(object) ) end @@ -80,7 +80,7 @@ def lte(object) Flipper::Rules::Condition.new( to_h, Operator.new(:lte).to_h, - self.class.integer_or_property(object) + self.class.integer_or_object(object) ) end @@ -88,7 +88,7 @@ def in(object) Flipper::Rules::Condition.new( to_h, Operator.new(:in).to_h, - self.class.array_or_property(object) + self.class.array_or_object(object) ) end @@ -96,7 +96,7 @@ def nin(object) Flipper::Rules::Condition.new( to_h, Operator.new(:nin).to_h, - self.class.array_or_property(object) + self.class.array_or_object(object) ) end @@ -104,7 +104,7 @@ def percentage(object) Flipper::Rules::Condition.new( to_h, Operator.new(:percentage).to_h, - self.class.integer_or_property(object) + self.class.integer_or_object(object) ) end @@ -122,30 +122,30 @@ def type_of(object) SUPPORTED_VALUE_TYPES_MAP[type_class] end - def self.primitive_or_property(object) - if object.is_a?(Flipper::Rules::Property) + def self.primitive_or_object(object) + if object.is_a?(Flipper::Rules::Object) object else Object.new(object) end end - def self.integer_or_property(object) + def self.integer_or_object(object) case object when Integer {"type" => "integer", "value" => object} - when Flipper::Rules::Property + when Flipper::Rules::Object object.to_h else raise ArgumentError, "object must be integer or property" unless object.is_a?(Integer) end end - def self.array_or_property(object) + def self.array_or_object(object) case object when Array {"type" => "array", "value" => object} - when Flipper::Rules::Property + when Flipper::Rules::Object object.to_h else raise ArgumentError, "object must be array or property" unless object.is_a?(Array) diff --git a/lib/flipper/rules/random.rb b/lib/flipper/rules/random.rb new file mode 100644 index 000000000..a55bf964e --- /dev/null +++ b/lib/flipper/rules/random.rb @@ -0,0 +1,12 @@ +require 'flipper/rules/object' + +module Flipper + module Rules + class Random < Object + def initialize(value) + @type = "random".freeze + @value = value + end + end + end +end diff --git a/spec/flipper/rules/object_spec.rb b/spec/flipper/rules/object_spec.rb index f34f8b29b..70e6453c5 100644 --- a/spec/flipper/rules/object_spec.rb +++ b/spec/flipper/rules/object_spec.rb @@ -142,6 +142,16 @@ )) end end + + context "with object" do + it "returns equal condition" do + expect(described_class.new("admin").eq(Flipper.object("test"))).to eq(Flipper::Rules::Condition.new( + {"type" => "string", "value" => "admin"}, + {"type" => "operator", "value" => "eq"}, + {"type" => "string", "value" => "test"} + )) + end + end end describe "#neq" do @@ -204,6 +214,16 @@ )) end end + + context "with object" do + it "returns not equal condition" do + expect(described_class.new("plan").neq(Flipper.object("test"))).to eq(Flipper::Rules::Condition.new( + {"type" => "string", "value" => "plan"}, + {"type" => "operator", "value" => "neq"}, + {"type" => "string", "value" => "test"} + )) + end + end end describe "#gt" do @@ -227,6 +247,16 @@ end end + context "with object" do + it "returns condition" do + expect(described_class.new(21).gt(Flipper.object(22))).to eq(Flipper::Rules::Condition.new( + {"type" => "integer", "value" => 21}, + {"type" => "operator", "value" => "gt"}, + {"type" => "integer", "value" => 22} + )) + end + end + context "with string" do it "raises error" do expect { described_class.new("age").gt("231") }.to raise_error(ArgumentError) @@ -273,6 +303,16 @@ end end + context "with object" do + it "returns condition" do + expect(described_class.new(21).gte(Flipper.object(22))).to eq(Flipper::Rules::Condition.new( + {"type" => "integer", "value" => 21}, + {"type" => "operator", "value" => "gte"}, + {"type" => "integer", "value" => 22} + )) + end + end + context "with string" do it "raises error" do expect { described_class.new("age").gte("231") }.to raise_error(ArgumentError) @@ -319,6 +359,16 @@ end end + context "with object" do + it "returns condition" do + expect(described_class.new(21).lt(Flipper.object(22))).to eq(Flipper::Rules::Condition.new( + {"type" => "integer", "value" => 21}, + {"type" => "operator", "value" => "lt"}, + {"type" => "integer", "value" => 22} + )) + end + end + context "with string" do it "raises error" do expect { described_class.new("age").lt("231") }.to raise_error(ArgumentError) @@ -365,6 +415,16 @@ end end + context "with object" do + it "returns condition" do + expect(described_class.new(21).lte(Flipper.object(22))).to eq(Flipper::Rules::Condition.new( + {"type" => "integer", "value" => 21}, + {"type" => "operator", "value" => "lte"}, + {"type" => "integer", "value" => 22} + )) + end + end + context "with string" do it "raises error" do expect { described_class.new("age").lte("231") }.to raise_error(ArgumentError) @@ -411,6 +471,16 @@ end end + context "with object" do + it "returns condition" do + expect(described_class.new("admin").in(Flipper.object(["admin"]))).to eq(Flipper::Rules::Condition.new( + {"type" => "string", "value" => "admin"}, + {"type" => "operator", "value" => "in"}, + {"type" => "array", "value" => ["admin"]} + )) + end + end + context "with string" do it "raises error" do expect { described_class.new("role").in("231") }.to raise_error(ArgumentError) @@ -457,6 +527,16 @@ end end + context "with object" do + it "returns condition" do + expect(described_class.new("admin").nin(Flipper.object(["admin"]))).to eq(Flipper::Rules::Condition.new( + {"type" => "string", "value" => "admin"}, + {"type" => "operator", "value" => "nin"}, + {"type" => "array", "value" => ["admin"]} + )) + end + end + context "with string" do it "raises error" do expect { described_class.new("role").nin("231") }.to raise_error(ArgumentError) @@ -503,6 +583,16 @@ end end + context "with object" do + it "returns condition" do + expect(described_class.new("flipper_id").percentage(Flipper.object(21))).to eq(Flipper::Rules::Condition.new( + {"type" => "string", "value" => "flipper_id"}, + {"type" => "operator", "value" => "percentage"}, + {"type" => "integer", "value" => 21} + )) + end + end + context "with string" do it "raises error" do expect { described_class.new("flipper_id").percentage("231") }.to raise_error(ArgumentError) diff --git a/spec/flipper_spec.rb b/spec/flipper_spec.rb index 685ddb8e5..9aaa1d71a 100644 --- a/spec/flipper_spec.rb +++ b/spec/flipper_spec.rb @@ -379,6 +379,18 @@ end end + describe ".random" do + it "returns Flipper::Rules::Random instance" do + expect(Flipper.random(100)).to eq(Flipper::Rules::Random.new(100)) + end + end + + describe ".object" do + it "returns Flipper::Rules::Object instance" do + expect(Flipper.object("test")).to eq(Flipper::Rules::Object.new("test")) + end + end + describe ".any" do let(:age_rule) { Flipper.property(:age).gte(21) } let(:plan_rule) { Flipper.property(:plan).eq("basic") } From 3edcf0ca3e6cf9e5aa9f3d2f6c7dd28593cbef54 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Tue, 7 Sep 2021 09:01:00 -0400 Subject: [PATCH 050/176] Switch to capitalized type names everywhere We had a mix of capital and lowercase. Seems better to be consistent and capitalizing is more like a type in every other langauge so lets roll with that. --- lib/flipper/rules/condition.rb | 8 +- lib/flipper/rules/object.rb | 16 +- lib/flipper/rules/operator.rb | 2 +- lib/flipper/rules/property.rb | 2 +- lib/flipper/spec/shared_adapter_specs.rb | 12 +- spec/flipper/adapters/read_only_spec.rb | 12 +- .../flipper/api/v1/actions/rules_gate_spec.rb | 6 +- spec/flipper/dsl_spec.rb | 6 +- spec/flipper/feature_spec.rb | 12 +- spec/flipper/rules/all_spec.rb | 48 ++-- spec/flipper/rules/any_spec.rb | 48 ++-- spec/flipper/rules/condition_spec.rb | 96 ++++---- spec/flipper/rules/object_spec.rb | 224 +++++++++--------- spec/flipper/rules/operator_spec.rb | 2 +- spec/flipper/rules/property_spec.rb | 104 ++++---- spec/flipper_integration_spec.rb | 48 ++-- spec/flipper_spec.rb | 6 +- 17 files changed, 326 insertions(+), 326 deletions(-) diff --git a/lib/flipper/rules/condition.rb b/lib/flipper/rules/condition.rb index 5e6b3c36e..a85cb734d 100644 --- a/lib/flipper/rules/condition.rb +++ b/lib/flipper/rules/condition.rb @@ -69,13 +69,13 @@ def evaluate(hash, properties) type = hash.fetch("type") case type - when "property" + when "Property" properties[hash.fetch("value")] - when "random" + when "Random" rand hash.fetch("value") - when "array", "string", "integer", "boolean" + when "Array", "String", "Integer", "Boolean" hash.fetch("value") - when "null" + when "Null" nil else raise "type not found: #{type.inspect}" diff --git a/lib/flipper/rules/object.rb b/lib/flipper/rules/object.rb index 5396c0652..2602c0d26 100644 --- a/lib/flipper/rules/object.rb +++ b/lib/flipper/rules/object.rb @@ -5,12 +5,12 @@ module Flipper module Rules class Object SUPPORTED_VALUE_TYPES_MAP = { - String => "string", - Integer => "integer", - NilClass => "null", - TrueClass => "boolean", - FalseClass => "boolean", - Array => "array", + String => "String", + Integer => "Integer", + NilClass => "Null", + TrueClass => "Boolean", + FalseClass => "Boolean", + Array => "Array", }.freeze SUPPORTED_VALUE_TYPES = SUPPORTED_VALUE_TYPES_MAP.keys.freeze @@ -133,7 +133,7 @@ def self.primitive_or_object(object) def self.integer_or_object(object) case object when Integer - {"type" => "integer", "value" => object} + {"type" => "Integer", "value" => object} when Flipper::Rules::Object object.to_h else @@ -144,7 +144,7 @@ def self.integer_or_object(object) def self.array_or_object(object) case object when Array - {"type" => "array", "value" => object} + {"type" => "Array", "value" => object} when Flipper::Rules::Object object.to_h else diff --git a/lib/flipper/rules/operator.rb b/lib/flipper/rules/operator.rb index 9b6fa3bfe..6c4e091f8 100644 --- a/lib/flipper/rules/operator.rb +++ b/lib/flipper/rules/operator.rb @@ -4,7 +4,7 @@ class Operator attr_reader :type, :value def initialize(value) - @type = "operator".freeze + @type = "Operator".freeze @value = value.to_s end diff --git a/lib/flipper/rules/property.rb b/lib/flipper/rules/property.rb index c1d317ed2..fc844e877 100644 --- a/lib/flipper/rules/property.rb +++ b/lib/flipper/rules/property.rb @@ -4,7 +4,7 @@ module Flipper module Rules class Property < Object def initialize(value) - @type = "property".freeze + @type = "Property".freeze @value = value.to_s end diff --git a/lib/flipper/spec/shared_adapter_specs.rb b/lib/flipper/spec/shared_adapter_specs.rb index f7ad2943a..972d0d703 100644 --- a/lib/flipper/spec/shared_adapter_specs.rb +++ b/lib/flipper/spec/shared_adapter_specs.rb @@ -64,14 +64,14 @@ it 'can enable, disable and get value for rule gate' do basic_rule = Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "basic"} + {"type" => "Property", "value" => "plan"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "basic"} ) age_rule = Flipper::Rules::Condition.new( - {"type" => "property", "value" => "age"}, - {"type" => "operator", "value" => "gte"}, - {"type" => "integer", "value" => 21} + {"type" => "Property", "value" => "age"}, + {"type" => "Operator", "value" => "gte"}, + {"type" => "Integer", "value" => 21} ) expect(subject.enable(feature, rule_gate, basic_rule)).to eq(true) expect(subject.enable(feature, rule_gate, age_rule)).to eq(true) diff --git a/spec/flipper/adapters/read_only_spec.rb b/spec/flipper/adapters/read_only_spec.rb index 4f5a58632..c53c87e2f 100644 --- a/spec/flipper/adapters/read_only_spec.rb +++ b/spec/flipper/adapters/read_only_spec.rb @@ -44,9 +44,9 @@ it 'can get feature' do rule = Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "basic"} + {"type" => "Property", "value" => "plan"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "basic"} ) actor22 = Flipper::Actor.new('22') adapter.enable(feature, boolean_gate, flipper.boolean) @@ -63,9 +63,9 @@ { "type" => "Condition", "value" => { - "left" => {"type" => "property", "value" => "plan"}, - "operator" => {"type" => "operator", "value" => "eq"}, - "right" => {"type" => "string", "value" => "basic"}, + "left" => {"type" => "Property", "value" => "plan"}, + "operator" => {"type" => "Operator", "value" => "eq"}, + "right" => {"type" => "String", "value" => "basic"}, } } ], diff --git a/spec/flipper/api/v1/actions/rules_gate_spec.rb b/spec/flipper/api/v1/actions/rules_gate_spec.rb index eae0a1d45..0a13221b9 100644 --- a/spec/flipper/api/v1/actions/rules_gate_spec.rb +++ b/spec/flipper/api/v1/actions/rules_gate_spec.rb @@ -10,9 +10,9 @@ } let(:rule) { Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "basic"} + {"type" => "Property", "value" => "plan"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "basic"} ) } diff --git a/spec/flipper/dsl_spec.rb b/spec/flipper/dsl_spec.rb index 61d3093d3..beecd0273 100644 --- a/spec/flipper/dsl_spec.rb +++ b/spec/flipper/dsl_spec.rb @@ -143,9 +143,9 @@ context 'for Hash' do it 'returns rule instance' do rule = Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "basic"} + {"type" => "Property", "value" => "plan"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "basic"} ) result = subject.rule(rule.value) expect(result).to be_instance_of(Flipper::Rules::Condition) diff --git a/spec/flipper/feature_spec.rb b/spec/flipper/feature_spec.rb index b3f3f9744..45f764366 100644 --- a/spec/flipper/feature_spec.rb +++ b/spec/flipper/feature_spec.rb @@ -654,9 +654,9 @@ context "with rule instance" do it "updates gate values to include rule" do rule = Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "basic"} + {"type" => "Property", "value" => "plan"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "basic"} ) expect(subject.gate_values.rules).to be_empty subject.enable_rule(rule) @@ -669,9 +669,9 @@ context "with Hash" do it "updates gate values to include rule" do rule = Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "basic"} + {"type" => "Property", "value" => "plan"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "basic"} ) expect(subject.gate_values.rules).to be_empty subject.enable_rule(rule.value) diff --git a/spec/flipper/rules/all_spec.rb b/spec/flipper/rules/all_spec.rb index bfc208462..e9c63ddb0 100644 --- a/spec/flipper/rules/all_spec.rb +++ b/spec/flipper/rules/all_spec.rb @@ -4,16 +4,16 @@ let(:feature_name) { "search" } let(:plan_condition) { Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "basic"} + {"type" => "Property", "value" => "plan"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "basic"} ) } let(:age_condition) { Flipper::Rules::Condition.new( - {"type" => "property", "value" => "age"}, - {"type" => "operator", "value" => "gte"}, - {"type" => "integer", "value" => 21} + {"type" => "Property", "value" => "age"}, + {"type" => "Operator", "value" => "gte"}, + {"type" => "Integer", "value" => 21} ) } let(:any_rule) { @@ -87,14 +87,14 @@ it "returns true if equal" do other_rule = Flipper::Rules::All.new( Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "basic"} + {"type" => "Property", "value" => "plan"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "basic"} ), Flipper::Rules::Condition.new( - {"type" => "property", "value" => "age"}, - {"type" => "operator", "value" => "gte"}, - {"type" => "integer", "value" => 21} + {"type" => "Property", "value" => "age"}, + {"type" => "Operator", "value" => "gte"}, + {"type" => "Integer", "value" => 21} ) ) expect(rule).to eql(other_rule) @@ -104,14 +104,14 @@ it "returns false if not equal" do other_rule = Flipper::Rules::All.new( Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "premium"} + {"type" => "Property", "value" => "plan"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "premium"} ), Flipper::Rules::Condition.new( - {"type" => "property", "value" => "age"}, - {"type" => "operator", "value" => "gte"}, - {"type" => "integer", "value" => 21} + {"type" => "Property", "value" => "age"}, + {"type" => "Operator", "value" => "gte"}, + {"type" => "Integer", "value" => 21} ) ) expect(rule).not_to eql(other_rule) @@ -128,14 +128,14 @@ let(:rule) { Flipper::Rules::All.new( Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "basic"} + {"type" => "Property", "value" => "plan"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "basic"} ), Flipper::Rules::Condition.new( - {"type" => "property", "value" => "age"}, - {"type" => "operator", "value" => "gte"}, - {"type" => "integer", "value" => 21} + {"type" => "Property", "value" => "age"}, + {"type" => "Operator", "value" => "gte"}, + {"type" => "Integer", "value" => 21} ) ) } diff --git a/spec/flipper/rules/any_spec.rb b/spec/flipper/rules/any_spec.rb index b7e8c36c0..f5ea16b91 100644 --- a/spec/flipper/rules/any_spec.rb +++ b/spec/flipper/rules/any_spec.rb @@ -4,16 +4,16 @@ let(:feature_name) { "search" } let(:plan_condition) { Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "basic"} + {"type" => "Property", "value" => "plan"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "basic"} ) } let(:age_condition) { Flipper::Rules::Condition.new( - {"type" => "property", "value" => "age"}, - {"type" => "operator", "value" => "gte"}, - {"type" => "integer", "value" => 21} + {"type" => "Property", "value" => "age"}, + {"type" => "Operator", "value" => "gte"}, + {"type" => "Integer", "value" => 21} ) } let(:all_rule) { @@ -87,14 +87,14 @@ it "returns true if equal" do other_rule = Flipper::Rules::Any.new( Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "basic"} + {"type" => "Property", "value" => "plan"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "basic"} ), Flipper::Rules::Condition.new( - {"type" => "property", "value" => "age"}, - {"type" => "operator", "value" => "gte"}, - {"type" => "integer", "value" => 21} + {"type" => "Property", "value" => "age"}, + {"type" => "Operator", "value" => "gte"}, + {"type" => "Integer", "value" => 21} ) ) expect(rule).to eql(other_rule) @@ -104,14 +104,14 @@ it "returns false if not equal" do other_rule = Flipper::Rules::Any.new( Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "premium"} + {"type" => "Property", "value" => "plan"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "premium"} ), Flipper::Rules::Condition.new( - {"type" => "property", "value" => "age"}, - {"type" => "operator", "value" => "gte"}, - {"type" => "integer", "value" => 21} + {"type" => "Property", "value" => "age"}, + {"type" => "Operator", "value" => "gte"}, + {"type" => "Integer", "value" => 21} ) ) expect(rule).not_to eql(other_rule) @@ -128,14 +128,14 @@ let(:rule) { Flipper::Rules::Any.new( Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "basic"} + {"type" => "Property", "value" => "plan"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "basic"} ), Flipper::Rules::Condition.new( - {"type" => "property", "value" => "age"}, - {"type" => "operator", "value" => "gte"}, - {"type" => "integer", "value" => 21} + {"type" => "Property", "value" => "age"}, + {"type" => "Operator", "value" => "gte"}, + {"type" => "Integer", "value" => 21} ) ) } diff --git a/spec/flipper/rules/condition_spec.rb b/spec/flipper/rules/condition_spec.rb index 6562a3eeb..0fd23e472 100644 --- a/spec/flipper/rules/condition_spec.rb +++ b/spec/flipper/rules/condition_spec.rb @@ -6,17 +6,17 @@ describe "#eql?" do let(:rule) { Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "basic"} + {"type" => "Property", "value" => "plan"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "basic"} ) } it "returns true if equal" do other_rule = Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "basic"} + {"type" => "Property", "value" => "plan"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "basic"} ) expect(rule).to eql(other_rule) expect(rule == other_rule).to be(true) @@ -24,9 +24,9 @@ it "returns false if not equal" do other_rule = Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "premium"} + {"type" => "Property", "value" => "plan"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "premium"} ) expect(rule).not_to eql(other_rule) expect(rule == other_rule).to be(false) @@ -42,18 +42,18 @@ context "with no actor" do it "does not error for condition that returns true" do rule = Flipper::Rules::Condition.new( - {"type" => "boolean", "value" => true}, - {"type" => "operator", "value" => "eq"}, - {"type" => "boolean", "value" => true}, + {"type" => "Boolean", "value" => true}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "Boolean", "value" => true}, ) expect(rule.matches?(feature_name, nil)).to be(true) end it "does not error for condition that returns false" do rule = Flipper::Rules::Condition.new( - {"type" => "boolean", "value" => true}, - {"type" => "operator", "value" => "eq"}, - {"type" => "boolean", "value" => false}, + {"type" => "Boolean", "value" => true}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "Boolean", "value" => false}, ) expect(rule.matches?(feature_name, nil)).to be(false) end @@ -69,9 +69,9 @@ def flipper_id user = user_class.new(1, {}) rule = Flipper::Rules::Condition.new( - {"type" => "property", "value" => "flipper_id"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "User;1"} + {"type" => "Property", "value" => "flipper_id"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "User;1"} ) expect(rule.matches?(feature_name, user)).to be(true) expect(rule.matches?(feature_name, user_class.new(2, {}))).to be(false) @@ -81,9 +81,9 @@ def flipper_id context "eq" do let(:rule) { Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "basic"} + {"type" => "Property", "value" => "plan"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "basic"} ) } @@ -105,9 +105,9 @@ def flipper_id context "neq" do let(:rule) { Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "neq"}, - {"type" => "string", "value" => "basic"} + {"type" => "Property", "value" => "plan"}, + {"type" => "Operator", "value" => "neq"}, + {"type" => "String", "value" => "basic"} ) } @@ -129,9 +129,9 @@ def flipper_id context "gt" do let(:rule) { Flipper::Rules::Condition.new( - {"type" => "property", "value" => "age"}, - {"type" => "operator", "value" => "gt"}, - {"type" => "integer", "value" => 20} + {"type" => "Property", "value" => "age"}, + {"type" => "Operator", "value" => "gt"}, + {"type" => "Integer", "value" => 20} ) } @@ -153,9 +153,9 @@ def flipper_id context "gte" do let(:rule) { Flipper::Rules::Condition.new( - {"type" => "property", "value" => "age"}, - {"type" => "operator", "value" => "gte"}, - {"type" => "integer", "value" => 20} + {"type" => "Property", "value" => "age"}, + {"type" => "Operator", "value" => "gte"}, + {"type" => "Integer", "value" => 20} ) } @@ -177,9 +177,9 @@ def flipper_id context "lt" do let(:rule) { Flipper::Rules::Condition.new( - {"type" => "property", "value" => "age"}, - {"type" => "operator", "value" => "lt"}, - {"type" => "integer", "value" => 21} + {"type" => "Property", "value" => "age"}, + {"type" => "Operator", "value" => "lt"}, + {"type" => "Integer", "value" => 21} ) } @@ -201,9 +201,9 @@ def flipper_id context "lt with rand type" do let(:rule) { Flipper::Rules::Condition.new( - {"type" => "random", "value" => 100}, - {"type" => "operator", "value" => "lt"}, - {"type" => "integer", "value" => 25} + {"type" => "Random", "value" => 100}, + {"type" => "Operator", "value" => "lt"}, + {"type" => "Integer", "value" => 25} ) } @@ -222,9 +222,9 @@ def flipper_id context "lte" do let(:rule) { Flipper::Rules::Condition.new( - {"type" => "property", "value" => "age"}, - {"type" => "operator", "value" => "lte"}, - {"type" => "integer", "value" => 21} + {"type" => "Property", "value" => "age"}, + {"type" => "Operator", "value" => "lte"}, + {"type" => "Integer", "value" => 21} ) } @@ -246,9 +246,9 @@ def flipper_id context "in" do let(:rule) { Flipper::Rules::Condition.new( - {"type" => "property", "value" => "age"}, - {"type" => "operator", "value" => "in"}, - {"type" => "array", "value" => [20, 21, 22]} + {"type" => "Property", "value" => "age"}, + {"type" => "Operator", "value" => "in"}, + {"type" => "Array", "value" => [20, 21, 22]} ) } @@ -270,9 +270,9 @@ def flipper_id context "nin" do let(:rule) { Flipper::Rules::Condition.new( - {"type" => "property", "value" => "age"}, - {"type" => "operator", "value" => "nin"}, - {"type" => "array", "value" => [20, 21, 22]} + {"type" => "Property", "value" => "age"}, + {"type" => "Operator", "value" => "nin"}, + {"type" => "Array", "value" => [20, 21, 22]} ) } @@ -294,9 +294,9 @@ def flipper_id context "percentage" do let(:rule) { Flipper::Rules::Condition.new( - {"type" => "property", "value" => "flipper_id"}, - {"type" => "operator", "value" => "percentage"}, - {"type" => "integer", "value" => 25} + {"type" => "Property", "value" => "flipper_id"}, + {"type" => "Operator", "value" => "percentage"}, + {"type" => "Integer", "value" => 25} ) } diff --git a/spec/flipper/rules/object_spec.rb b/spec/flipper/rules/object_spec.rb index 70e6453c5..e53fbc297 100644 --- a/spec/flipper/rules/object_spec.rb +++ b/spec/flipper/rules/object_spec.rb @@ -5,7 +5,7 @@ context "with string" do it "returns array of type and value" do instance = described_class.new("test") - expect(instance.type).to eq("string") + expect(instance.type).to eq("String") expect(instance.value).to eq("test") end end @@ -13,7 +13,7 @@ context "with integer" do it "returns array of type and value" do instance = described_class.new(21) - expect(instance.type).to eq("integer") + expect(instance.type).to eq("Integer") expect(instance.value).to eq(21) end end @@ -21,7 +21,7 @@ context "with nil" do it "returns array of type and value" do instance = described_class.new(nil) - expect(instance.type).to eq("null") + expect(instance.type).to eq("Null") expect(instance.value).to be(nil) end end @@ -29,7 +29,7 @@ context "with true" do it "returns array of type and value" do instance = described_class.new(true) - expect(instance.type).to eq("boolean") + expect(instance.type).to eq("Boolean") expect(instance.value).to be(true) end end @@ -37,7 +37,7 @@ context "with false" do it "returns array of type and value" do instance = described_class.new(false) - expect(instance.type).to eq("boolean") + expect(instance.type).to eq("Boolean") expect(instance.value).to be(false) end end @@ -45,7 +45,7 @@ context "with array" do it "returns array of type and value" do instance = described_class.new(["test"]) - expect(instance.type).to eq("array") + expect(instance.type).to eq("Array") expect(instance.value).to eq(["test"]) end end @@ -76,7 +76,7 @@ describe "#to_h" do it "returns Hash with type and value" do expect(described_class.new("test").to_h).to eq({ - "type" => "string", + "type" => "String", "value" => "test", }) end @@ -86,9 +86,9 @@ context "with string" do it "returns equal condition" do expect(described_class.new("plan").eq("basic")).to eq(Flipper::Rules::Condition.new( - {"type" => "string", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "basic"} + {"type" => "String", "value" => "plan"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "basic"} )) end end @@ -96,9 +96,9 @@ context "with boolean" do it "returns equal condition" do expect(described_class.new("admin").eq(true)).to eq(Flipper::Rules::Condition.new( - {"type" => "string", "value" => "admin"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "boolean", "value" => true} + {"type" => "String", "value" => "admin"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "Boolean", "value" => true} )) end end @@ -106,9 +106,9 @@ context "with integer" do it "returns equal condition" do expect(described_class.new("age").eq(21)).to eq(Flipper::Rules::Condition.new( - {"type" => "string", "value" => "age"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "integer", "value" => 21} + {"type" => "String", "value" => "age"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "Integer", "value" => 21} )) end end @@ -116,9 +116,9 @@ context "with array" do it "returns equal condition" do expect(described_class.new("roles").eq(["admin"])).to eq(Flipper::Rules::Condition.new( - {"type" => "string", "value" => "roles"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "array", "value" => ["admin"]} + {"type" => "String", "value" => "roles"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "Array", "value" => ["admin"]} )) end end @@ -126,9 +126,9 @@ context "with nil" do it "returns equal condition" do expect(described_class.new("admin").eq(nil)).to eq(Flipper::Rules::Condition.new( - {"type" => "string", "value" => "admin"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "null", "value" => nil} + {"type" => "String", "value" => "admin"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "Null", "value" => nil} )) end end @@ -136,9 +136,9 @@ context "with property" do it "returns equal condition" do expect(described_class.new("admin").eq(Flipper.property(:name))).to eq(Flipper::Rules::Condition.new( - {"type" => "string", "value" => "admin"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "property", "value" => "name"} + {"type" => "String", "value" => "admin"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "Property", "value" => "name"} )) end end @@ -146,9 +146,9 @@ context "with object" do it "returns equal condition" do expect(described_class.new("admin").eq(Flipper.object("test"))).to eq(Flipper::Rules::Condition.new( - {"type" => "string", "value" => "admin"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "test"} + {"type" => "String", "value" => "admin"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "test"} )) end end @@ -158,9 +158,9 @@ context "with string" do it "returns not equal condition" do expect(described_class.new("plan").neq("basic")).to eq(Flipper::Rules::Condition.new( - {"type" => "string", "value" => "plan"}, - {"type" => "operator", "value" => "neq"}, - {"type" => "string", "value" => "basic"} + {"type" => "String", "value" => "plan"}, + {"type" => "Operator", "value" => "neq"}, + {"type" => "String", "value" => "basic"} )) end end @@ -168,9 +168,9 @@ context "with boolean" do it "returns not equal condition" do expect(described_class.new("admin").neq(true)).to eq(Flipper::Rules::Condition.new( - {"type" => "string", "value" => "admin"}, - {"type" => "operator", "value" => "neq"}, - {"type" => "boolean", "value" => true} + {"type" => "String", "value" => "admin"}, + {"type" => "Operator", "value" => "neq"}, + {"type" => "Boolean", "value" => true} )) end end @@ -178,9 +178,9 @@ context "with integer" do it "returns not equal condition" do expect(described_class.new("age").neq(21)).to eq(Flipper::Rules::Condition.new( - {"type" => "string", "value" => "age"}, - {"type" => "operator", "value" => "neq"}, - {"type" => "integer", "value" => 21} + {"type" => "String", "value" => "age"}, + {"type" => "Operator", "value" => "neq"}, + {"type" => "Integer", "value" => 21} )) end end @@ -188,9 +188,9 @@ context "with array" do it "returns not equal condition" do expect(described_class.new("roles").neq(["admin"])).to eq(Flipper::Rules::Condition.new( - {"type" => "string", "value" => "roles"}, - {"type" => "operator", "value" => "neq"}, - {"type" => "array", "value" => ["admin"]} + {"type" => "String", "value" => "roles"}, + {"type" => "Operator", "value" => "neq"}, + {"type" => "Array", "value" => ["admin"]} )) end end @@ -198,9 +198,9 @@ context "with nil" do it "returns not equal condition" do expect(described_class.new("admin").neq(nil)).to eq(Flipper::Rules::Condition.new( - {"type" => "string", "value" => "admin"}, - {"type" => "operator", "value" => "neq"}, - {"type" => "null", "value" => nil} + {"type" => "String", "value" => "admin"}, + {"type" => "Operator", "value" => "neq"}, + {"type" => "Null", "value" => nil} )) end end @@ -208,9 +208,9 @@ context "with property" do it "returns not equal condition" do expect(described_class.new("plan").neq(Flipper.property(:name))).to eq(Flipper::Rules::Condition.new( - {"type" => "string", "value" => "plan"}, - {"type" => "operator", "value" => "neq"}, - {"type" => "property", "value" => "name"} + {"type" => "String", "value" => "plan"}, + {"type" => "Operator", "value" => "neq"}, + {"type" => "Property", "value" => "name"} )) end end @@ -218,9 +218,9 @@ context "with object" do it "returns not equal condition" do expect(described_class.new("plan").neq(Flipper.object("test"))).to eq(Flipper::Rules::Condition.new( - {"type" => "string", "value" => "plan"}, - {"type" => "operator", "value" => "neq"}, - {"type" => "string", "value" => "test"} + {"type" => "String", "value" => "plan"}, + {"type" => "Operator", "value" => "neq"}, + {"type" => "String", "value" => "test"} )) end end @@ -230,9 +230,9 @@ context "with integer" do it "returns condition" do expect(described_class.new("age").gt(21)).to eq(Flipper::Rules::Condition.new( - {"type" => "string", "value" => "age"}, - {"type" => "operator", "value" => "gt"}, - {"type" => "integer", "value" => 21} + {"type" => "String", "value" => "age"}, + {"type" => "Operator", "value" => "gt"}, + {"type" => "Integer", "value" => 21} )) end end @@ -240,9 +240,9 @@ context "with property" do it "returns condition" do expect(described_class.new(21).gt(Flipper.property(:age))).to eq(Flipper::Rules::Condition.new( - {"type" => "integer", "value" => 21}, - {"type" => "operator", "value" => "gt"}, - {"type" => "property", "value" => "age"} + {"type" => "Integer", "value" => 21}, + {"type" => "Operator", "value" => "gt"}, + {"type" => "Property", "value" => "age"} )) end end @@ -250,9 +250,9 @@ context "with object" do it "returns condition" do expect(described_class.new(21).gt(Flipper.object(22))).to eq(Flipper::Rules::Condition.new( - {"type" => "integer", "value" => 21}, - {"type" => "operator", "value" => "gt"}, - {"type" => "integer", "value" => 22} + {"type" => "Integer", "value" => 21}, + {"type" => "Operator", "value" => "gt"}, + {"type" => "Integer", "value" => 22} )) end end @@ -286,9 +286,9 @@ context "with integer" do it "returns condition" do expect(described_class.new("age").gte(21)).to eq(Flipper::Rules::Condition.new( - {"type" => "string", "value" => "age"}, - {"type" => "operator", "value" => "gte"}, - {"type" => "integer", "value" => 21} + {"type" => "String", "value" => "age"}, + {"type" => "Operator", "value" => "gte"}, + {"type" => "Integer", "value" => 21} )) end end @@ -296,9 +296,9 @@ context "with property" do it "returns condition" do expect(described_class.new(21).gte(Flipper.property(:age))).to eq(Flipper::Rules::Condition.new( - {"type" => "integer", "value" => 21}, - {"type" => "operator", "value" => "gte"}, - {"type" => "property", "value" => "age"} + {"type" => "Integer", "value" => 21}, + {"type" => "Operator", "value" => "gte"}, + {"type" => "Property", "value" => "age"} )) end end @@ -306,9 +306,9 @@ context "with object" do it "returns condition" do expect(described_class.new(21).gte(Flipper.object(22))).to eq(Flipper::Rules::Condition.new( - {"type" => "integer", "value" => 21}, - {"type" => "operator", "value" => "gte"}, - {"type" => "integer", "value" => 22} + {"type" => "Integer", "value" => 21}, + {"type" => "Operator", "value" => "gte"}, + {"type" => "Integer", "value" => 22} )) end end @@ -342,9 +342,9 @@ context "with integer" do it "returns condition" do expect(described_class.new("age").lt(21)).to eq(Flipper::Rules::Condition.new( - {"type" => "string", "value" => "age"}, - {"type" => "operator", "value" => "lt"}, - {"type" => "integer", "value" => 21} + {"type" => "String", "value" => "age"}, + {"type" => "Operator", "value" => "lt"}, + {"type" => "Integer", "value" => 21} )) end end @@ -352,9 +352,9 @@ context "with property" do it "returns condition" do expect(described_class.new(21).lt(Flipper.property(:age))).to eq(Flipper::Rules::Condition.new( - {"type" => "integer", "value" => 21}, - {"type" => "operator", "value" => "lt"}, - {"type" => "property", "value" => "age"} + {"type" => "Integer", "value" => 21}, + {"type" => "Operator", "value" => "lt"}, + {"type" => "Property", "value" => "age"} )) end end @@ -362,9 +362,9 @@ context "with object" do it "returns condition" do expect(described_class.new(21).lt(Flipper.object(22))).to eq(Flipper::Rules::Condition.new( - {"type" => "integer", "value" => 21}, - {"type" => "operator", "value" => "lt"}, - {"type" => "integer", "value" => 22} + {"type" => "Integer", "value" => 21}, + {"type" => "Operator", "value" => "lt"}, + {"type" => "Integer", "value" => 22} )) end end @@ -398,9 +398,9 @@ context "with integer" do it "returns condition" do expect(described_class.new("age").lte(21)).to eq(Flipper::Rules::Condition.new( - {"type" => "string", "value" => "age"}, - {"type" => "operator", "value" => "lte"}, - {"type" => "integer", "value" => 21} + {"type" => "String", "value" => "age"}, + {"type" => "Operator", "value" => "lte"}, + {"type" => "Integer", "value" => 21} )) end end @@ -408,9 +408,9 @@ context "with property" do it "returns condition" do expect(described_class.new(21).lte(Flipper.property(:age))).to eq(Flipper::Rules::Condition.new( - {"type" => "integer", "value" => 21}, - {"type" => "operator", "value" => "lte"}, - {"type" => "property", "value" => "age"} + {"type" => "Integer", "value" => 21}, + {"type" => "Operator", "value" => "lte"}, + {"type" => "Property", "value" => "age"} )) end end @@ -418,9 +418,9 @@ context "with object" do it "returns condition" do expect(described_class.new(21).lte(Flipper.object(22))).to eq(Flipper::Rules::Condition.new( - {"type" => "integer", "value" => 21}, - {"type" => "operator", "value" => "lte"}, - {"type" => "integer", "value" => 22} + {"type" => "Integer", "value" => 21}, + {"type" => "Operator", "value" => "lte"}, + {"type" => "Integer", "value" => 22} )) end end @@ -454,9 +454,9 @@ context "with array" do it "returns condition" do expect(described_class.new("role").in(["admin"])).to eq(Flipper::Rules::Condition.new( - {"type" => "string", "value" => "role"}, - {"type" => "operator", "value" => "in"}, - {"type" => "array", "value" => ["admin"]} + {"type" => "String", "value" => "role"}, + {"type" => "Operator", "value" => "in"}, + {"type" => "Array", "value" => ["admin"]} )) end end @@ -464,9 +464,9 @@ context "with property" do it "returns condition" do expect(described_class.new("admin").in(Flipper.property(:roles))).to eq(Flipper::Rules::Condition.new( - {"type" => "string", "value" => "admin"}, - {"type" => "operator", "value" => "in"}, - {"type" => "property", "value" => "roles"} + {"type" => "String", "value" => "admin"}, + {"type" => "Operator", "value" => "in"}, + {"type" => "Property", "value" => "roles"} )) end end @@ -474,9 +474,9 @@ context "with object" do it "returns condition" do expect(described_class.new("admin").in(Flipper.object(["admin"]))).to eq(Flipper::Rules::Condition.new( - {"type" => "string", "value" => "admin"}, - {"type" => "operator", "value" => "in"}, - {"type" => "array", "value" => ["admin"]} + {"type" => "String", "value" => "admin"}, + {"type" => "Operator", "value" => "in"}, + {"type" => "Array", "value" => ["admin"]} )) end end @@ -510,9 +510,9 @@ context "with array" do it "returns condition" do expect(described_class.new("role").nin(["admin"])).to eq(Flipper::Rules::Condition.new( - {"type" => "string", "value" => "role"}, - {"type" => "operator", "value" => "nin"}, - {"type" => "array", "value" => ["admin"]} + {"type" => "String", "value" => "role"}, + {"type" => "Operator", "value" => "nin"}, + {"type" => "Array", "value" => ["admin"]} )) end end @@ -520,9 +520,9 @@ context "with property" do it "returns condition" do expect(described_class.new("admin").nin(Flipper.property(:roles))).to eq(Flipper::Rules::Condition.new( - {"type" => "string", "value" => "admin"}, - {"type" => "operator", "value" => "nin"}, - {"type" => "property", "value" => "roles"} + {"type" => "String", "value" => "admin"}, + {"type" => "Operator", "value" => "nin"}, + {"type" => "Property", "value" => "roles"} )) end end @@ -530,9 +530,9 @@ context "with object" do it "returns condition" do expect(described_class.new("admin").nin(Flipper.object(["admin"]))).to eq(Flipper::Rules::Condition.new( - {"type" => "string", "value" => "admin"}, - {"type" => "operator", "value" => "nin"}, - {"type" => "array", "value" => ["admin"]} + {"type" => "String", "value" => "admin"}, + {"type" => "Operator", "value" => "nin"}, + {"type" => "Array", "value" => ["admin"]} )) end end @@ -566,9 +566,9 @@ context "with integer" do it "returns condition" do expect(described_class.new("flipper_id").percentage(25)).to eq(Flipper::Rules::Condition.new( - {"type" => "string", "value" => "flipper_id"}, - {"type" => "operator", "value" => "percentage"}, - {"type" => "integer", "value" => 25} + {"type" => "String", "value" => "flipper_id"}, + {"type" => "Operator", "value" => "percentage"}, + {"type" => "Integer", "value" => 25} )) end end @@ -576,9 +576,9 @@ context "with property" do it "returns condition" do expect(described_class.new("flipper_id").percentage(Flipper.property(:percentage))).to eq(Flipper::Rules::Condition.new( - {"type" => "string", "value" => "flipper_id"}, - {"type" => "operator", "value" => "percentage"}, - {"type" => "property", "value" => "percentage"} + {"type" => "String", "value" => "flipper_id"}, + {"type" => "Operator", "value" => "percentage"}, + {"type" => "Property", "value" => "percentage"} )) end end @@ -586,9 +586,9 @@ context "with object" do it "returns condition" do expect(described_class.new("flipper_id").percentage(Flipper.object(21))).to eq(Flipper::Rules::Condition.new( - {"type" => "string", "value" => "flipper_id"}, - {"type" => "operator", "value" => "percentage"}, - {"type" => "integer", "value" => 21} + {"type" => "String", "value" => "flipper_id"}, + {"type" => "Operator", "value" => "percentage"}, + {"type" => "Integer", "value" => 21} )) end end diff --git a/spec/flipper/rules/operator_spec.rb b/spec/flipper/rules/operator_spec.rb index 545f6a9a8..55eaa37a7 100644 --- a/spec/flipper/rules/operator_spec.rb +++ b/spec/flipper/rules/operator_spec.rb @@ -16,7 +16,7 @@ describe "#to_h" do it "returns Hash with type and value" do expect(described_class.new("eq").to_h).to eq({ - "type" => "operator", + "type" => "Operator", "value" => "eq", }) end diff --git a/spec/flipper/rules/property_spec.rb b/spec/flipper/rules/property_spec.rb index cc9d8ba8b..8e28e2292 100644 --- a/spec/flipper/rules/property_spec.rb +++ b/spec/flipper/rules/property_spec.rb @@ -16,7 +16,7 @@ describe "#to_h" do it "returns Hash with type and value" do expect(described_class.new("plan").to_h).to eq({ - "type" => "property", + "type" => "Property", "value" => "plan", }) end @@ -40,9 +40,9 @@ context "with string" do it "returns equal condition" do expect(described_class.new(:plan).eq("basic")).to eq(Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "basic"} + {"type" => "Property", "value" => "plan"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "basic"} )) end end @@ -50,9 +50,9 @@ context "with boolean" do it "returns equal condition" do expect(described_class.new(:admin).eq(true)).to eq(Flipper::Rules::Condition.new( - {"type" => "property", "value" => "admin"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "boolean", "value" => true} + {"type" => "Property", "value" => "admin"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "Boolean", "value" => true} )) end end @@ -60,9 +60,9 @@ context "with integer" do it "returns equal condition" do expect(described_class.new(:age).eq(21)).to eq(Flipper::Rules::Condition.new( - {"type" => "property", "value" => "age"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "integer", "value" => 21} + {"type" => "Property", "value" => "age"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "Integer", "value" => 21} )) end end @@ -70,9 +70,9 @@ context "with array" do it "returns equal condition" do expect(described_class.new(:roles).eq(["admin"])).to eq(Flipper::Rules::Condition.new( - {"type" => "property", "value" => "roles"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "array", "value" => ["admin"]} + {"type" => "Property", "value" => "roles"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "Array", "value" => ["admin"]} )) end end @@ -80,9 +80,9 @@ context "with nil" do it "returns equal condition" do expect(described_class.new(:admin).eq(nil)).to eq(Flipper::Rules::Condition.new( - {"type" => "property", "value" => "admin"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "null", "value" => nil} + {"type" => "Property", "value" => "admin"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "Null", "value" => nil} )) end end @@ -92,9 +92,9 @@ context "with string" do it "returns not equal condition" do expect(described_class.new(:plan).neq("basic")).to eq(Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "neq"}, - {"type" => "string", "value" => "basic"} + {"type" => "Property", "value" => "plan"}, + {"type" => "Operator", "value" => "neq"}, + {"type" => "String", "value" => "basic"} )) end end @@ -102,9 +102,9 @@ context "with boolean" do it "returns not equal condition" do expect(described_class.new(:admin).neq(true)).to eq(Flipper::Rules::Condition.new( - {"type" => "property", "value" => "admin"}, - {"type" => "operator", "value" => "neq"}, - {"type" => "boolean", "value" => true} + {"type" => "Property", "value" => "admin"}, + {"type" => "Operator", "value" => "neq"}, + {"type" => "Boolean", "value" => true} )) end end @@ -112,9 +112,9 @@ context "with integer" do it "returns not equal condition" do expect(described_class.new(:age).neq(21)).to eq(Flipper::Rules::Condition.new( - {"type" => "property", "value" => "age"}, - {"type" => "operator", "value" => "neq"}, - {"type" => "integer", "value" => 21} + {"type" => "Property", "value" => "age"}, + {"type" => "Operator", "value" => "neq"}, + {"type" => "Integer", "value" => 21} )) end end @@ -122,9 +122,9 @@ context "with array" do it "returns not equal condition" do expect(described_class.new(:roles).neq(["admin"])).to eq(Flipper::Rules::Condition.new( - {"type" => "property", "value" => "roles"}, - {"type" => "operator", "value" => "neq"}, - {"type" => "array", "value" => ["admin"]} + {"type" => "Property", "value" => "roles"}, + {"type" => "Operator", "value" => "neq"}, + {"type" => "Array", "value" => ["admin"]} )) end end @@ -132,9 +132,9 @@ context "with nil" do it "returns not equal condition" do expect(described_class.new(:admin).neq(nil)).to eq(Flipper::Rules::Condition.new( - {"type" => "property", "value" => "admin"}, - {"type" => "operator", "value" => "neq"}, - {"type" => "null", "value" => nil} + {"type" => "Property", "value" => "admin"}, + {"type" => "Operator", "value" => "neq"}, + {"type" => "Null", "value" => nil} )) end end @@ -144,9 +144,9 @@ context "with integer" do it "returns condition" do expect(described_class.new(:age).gt(21)).to eq(Flipper::Rules::Condition.new( - {"type" => "property", "value" => "age"}, - {"type" => "operator", "value" => "gt"}, - {"type" => "integer", "value" => 21} + {"type" => "Property", "value" => "age"}, + {"type" => "Operator", "value" => "gt"}, + {"type" => "Integer", "value" => 21} )) end end @@ -180,9 +180,9 @@ context "with integer" do it "returns condition" do expect(described_class.new(:age).gte(21)).to eq(Flipper::Rules::Condition.new( - {"type" => "property", "value" => "age"}, - {"type" => "operator", "value" => "gte"}, - {"type" => "integer", "value" => 21} + {"type" => "Property", "value" => "age"}, + {"type" => "Operator", "value" => "gte"}, + {"type" => "Integer", "value" => 21} )) end end @@ -216,9 +216,9 @@ context "with integer" do it "returns condition" do expect(described_class.new(:age).lt(21)).to eq(Flipper::Rules::Condition.new( - {"type" => "property", "value" => "age"}, - {"type" => "operator", "value" => "lt"}, - {"type" => "integer", "value" => 21} + {"type" => "Property", "value" => "age"}, + {"type" => "Operator", "value" => "lt"}, + {"type" => "Integer", "value" => 21} )) end end @@ -252,9 +252,9 @@ context "with integer" do it "returns condition" do expect(described_class.new(:age).lte(21)).to eq(Flipper::Rules::Condition.new( - {"type" => "property", "value" => "age"}, - {"type" => "operator", "value" => "lte"}, - {"type" => "integer", "value" => 21} + {"type" => "Property", "value" => "age"}, + {"type" => "Operator", "value" => "lte"}, + {"type" => "Integer", "value" => 21} )) end end @@ -288,9 +288,9 @@ context "with array" do it "returns condition" do expect(described_class.new(:role).in(["admin"])).to eq(Flipper::Rules::Condition.new( - {"type" => "property", "value" => "role"}, - {"type" => "operator", "value" => "in"}, - {"type" => "array", "value" => ["admin"]} + {"type" => "Property", "value" => "role"}, + {"type" => "Operator", "value" => "in"}, + {"type" => "Array", "value" => ["admin"]} )) end end @@ -324,9 +324,9 @@ context "with array" do it "returns condition" do expect(described_class.new(:role).nin(["admin"])).to eq(Flipper::Rules::Condition.new( - {"type" => "property", "value" => "role"}, - {"type" => "operator", "value" => "nin"}, - {"type" => "array", "value" => ["admin"]} + {"type" => "Property", "value" => "role"}, + {"type" => "Operator", "value" => "nin"}, + {"type" => "Array", "value" => ["admin"]} )) end end @@ -360,9 +360,9 @@ context "with integer" do it "returns condition" do expect(described_class.new(:flipper_id).percentage(25)).to eq(Flipper::Rules::Condition.new( - {"type" => "property", "value" => "flipper_id"}, - {"type" => "operator", "value" => "percentage"}, - {"type" => "integer", "value" => 25} + {"type" => "Property", "value" => "flipper_id"}, + {"type" => "Operator", "value" => "percentage"}, + {"type" => "Integer", "value" => 25} )) end end diff --git a/spec/flipper_integration_spec.rb b/spec/flipper_integration_spec.rb index a36bf8328..448406a1c 100644 --- a/spec/flipper_integration_spec.rb +++ b/spec/flipper_integration_spec.rb @@ -553,9 +553,9 @@ context "for rule" do it "works" do rule = Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "basic"} + {"type" => "Property", "value" => "plan"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "basic"} ) feature.enable rule @@ -568,14 +568,14 @@ it "works" do rule = Flipper::Rules::Any.new( Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "basic"} + {"type" => "Property", "value" => "plan"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "basic"} ), Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "plus"} + {"type" => "Property", "value" => "plan"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "plus"} ) ) feature.enable rule @@ -597,14 +597,14 @@ }) rule = Flipper::Rules::All.new( Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "basic"} + {"type" => "Property", "value" => "plan"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "basic"} ), Flipper::Rules::Condition.new( - {"type" => "property", "value" => "age"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "integer", "value" => 21} + {"type" => "Property", "value" => "age"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "Integer", "value" => 21} ) ) feature.enable rule @@ -627,20 +627,20 @@ }) rule = Flipper::Rules::Any.new( Flipper::Rules::Condition.new( - {"type" => "property", "value" => "admin"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => true} + {"type" => "Property", "value" => "admin"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => true} ), Flipper::Rules::All.new( Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "basic"} + {"type" => "Property", "value" => "plan"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "basic"} ), Flipper::Rules::Condition.new( - {"type" => "property", "value" => "age"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "integer", "value" => 21} + {"type" => "Property", "value" => "age"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "Integer", "value" => 21} ) ) ) diff --git a/spec/flipper_spec.rb b/spec/flipper_spec.rb index 9aaa1d71a..2d0f024e1 100644 --- a/spec/flipper_spec.rb +++ b/spec/flipper_spec.rb @@ -72,9 +72,9 @@ } let(:rule) { Flipper::Rules::Condition.new( - {"type" => "property", "value" => "plan"}, - {"type" => "operator", "value" => "eq"}, - {"type" => "string", "value" => "basic"} + {"type" => "Property", "value" => "plan"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "basic"} ) } From 0cf9a6011d4e43dd75ef5c270dea4aabe897af42 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Tue, 7 Sep 2021 13:34:20 -0400 Subject: [PATCH 051/176] Port shared spec to shared tests --- lib/flipper/test/shared_adapter_test.rb | 27 +++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/lib/flipper/test/shared_adapter_test.rb b/lib/flipper/test/shared_adapter_test.rb index 292ab82dc..679f90ff6 100644 --- a/lib/flipper/test/shared_adapter_test.rb +++ b/lib/flipper/test/shared_adapter_test.rb @@ -7,6 +7,7 @@ def setup @feature = @flipper[:stats] @boolean_gate = @feature.gate(:boolean) @group_gate = @feature.gate(:group) + @rule_gate = @feature.gate(:rule) @actor_gate = @feature.gate(:actor) @actors_gate = @feature.gate(:percentage_of_actors) @time_gate = @feature.gate(:percentage_of_time) @@ -56,6 +57,32 @@ def test_fully_disables_all_enabled_things_when_boolean_gate_disabled assert_equal @adapter.default_config, @adapter.get(@feature) end + def test_can_enable_disable_and_get_value_for_rule_gate + basic_rule = Flipper::Rules::Condition.new( + {"type" => "Property", "value" => "plan"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "basic"} + ) + age_rule = Flipper::Rules::Condition.new( + {"type" => "Property", "value" => "age"}, + {"type" => "Operator", "value" => "gte"}, + {"type" => "Integer", "value" => 21} + ) + assert_equal true, @adapter.enable(@feature, @rule_gate, basic_rule) + assert_equal true, @adapter.enable(@feature, @rule_gate, age_rule) + result = @adapter.get(@feature) + assert_includes result[:rules], basic_rule.value + assert_includes result[:rules], age_rule.value + + assert_equal true, @adapter.disable(@feature, @rule_gate, basic_rule) + result = @adapter.get(@feature) + assert_includes result[:rules], age_rule.value + + assert_equal true, @adapter.disable(@feature, @rule_gate, age_rule) + result = @adapter.get(@feature) + assert result[:rules].empty? + end + def test_can_enable_disable_get_value_for_group_gate assert_equal true, @adapter.enable(@feature, @group_gate, @flipper.group(:admins)) assert_equal true, @adapter.enable(@feature, @group_gate, @flipper.group(:early_access)) From 23786a8d5b5d5fdcfbbf2207c37b2274f8593a55 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Tue, 7 Sep 2021 13:49:40 -0400 Subject: [PATCH 052/176] Add synchronization of rules --- .../adapters/sync/feature_synchronizer.rb | 17 ++++++++- .../sync/feature_synchronizer_spec.rb | 38 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/lib/flipper/adapters/sync/feature_synchronizer.rb b/lib/flipper/adapters/sync/feature_synchronizer.rb index f778755eb..4d533c0cd 100644 --- a/lib/flipper/adapters/sync/feature_synchronizer.rb +++ b/lib/flipper/adapters/sync/feature_synchronizer.rb @@ -9,6 +9,7 @@ class Sync class FeatureSynchronizer extend Forwardable + def_delegator :@local_gate_values, :rules, :local_rules def_delegator :@local_gate_values, :boolean, :local_boolean def_delegator :@local_gate_values, :actors, :local_actors def_delegator :@local_gate_values, :groups, :local_groups @@ -17,6 +18,7 @@ class FeatureSynchronizer def_delegator :@local_gate_values, :percentage_of_time, :local_percentage_of_time + def_delegator :@remote_gate_values, :rules, :remote_rules def_delegator :@remote_gate_values, :boolean, :remote_boolean def_delegator :@remote_gate_values, :actors, :remote_actors def_delegator :@remote_gate_values, :groups, :remote_groups @@ -40,8 +42,9 @@ def call @feature.enable else @feature.disable if local_boolean_enabled? - sync_actors sync_groups + sync_actors + sync_rules sync_percentage_of_actors sync_percentage_of_time end @@ -49,6 +52,18 @@ def call private + def sync_rules + remote_rules_added = remote_rules - local_rules + remote_rules_added.each do |rule_hash| + @feature.enable_rule Rules.build(rule_hash) + end + + remote_rules_removed = local_rules - remote_rules + remote_rules_removed.each do |rule_hash| + @feature.disable_rule Rules.build(rule_hash) + end + end + def sync_actors remote_actors_added = remote_actors - local_actors remote_actors_added.each do |flipper_id| diff --git a/spec/flipper/adapters/sync/feature_synchronizer_spec.rb b/spec/flipper/adapters/sync/feature_synchronizer_spec.rb index c474acbdf..e1a290c13 100644 --- a/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +++ b/spec/flipper/adapters/sync/feature_synchronizer_spec.rb @@ -9,6 +9,8 @@ end let(:flipper) { Flipper.new(adapter) } let(:feature) { flipper[:search] } + let(:plan_rule) { Flipper.property(:plan).eq("basic") } + let(:age_rule) { Flipper.property(:age).gte(21) } context "when remote disabled" do let(:remote) { Flipper::GateValues.new({}) } @@ -64,6 +66,7 @@ boolean: nil, actors: Set["1"], groups: Set["staff"], + rules: Set[plan_rule.value], percentage_of_time: 10, percentage_of_actors: 15, } @@ -75,10 +78,45 @@ expect(local_gate_values_hash.fetch(:boolean)).to be(nil) expect(local_gate_values_hash.fetch(:actors)).to eq(Set["1"]) expect(local_gate_values_hash.fetch(:groups)).to eq(Set["staff"]) + expect(local_gate_values_hash.fetch(:rules)).to eq(Set[plan_rule.value]) expect(local_gate_values_hash.fetch(:percentage_of_time)).to eq("10") expect(local_gate_values_hash.fetch(:percentage_of_actors)).to eq("15") end + it "adds remotely added rules" do + remote = Flipper::GateValues.new(rules: Set[plan_rule.value, age_rule.value]) + feature.enable_rule(age_rule) + adapter.reset + + described_class.new(feature, feature.gate_values, remote).call + + expect(feature.rules_value).to eq(Set[plan_rule.value, age_rule.value]) + expect_only_enable + end + + it "removes remotely removed rules" do + remote = Flipper::GateValues.new(rules: Set[plan_rule.value]) + feature.enable_rule(plan_rule) + feature.enable_rule(age_rule) + adapter.reset + + described_class.new(feature, feature.gate_values, remote).call + + expect(feature.rules_value).to eq(Set[plan_rule.value]) + expect_only_disable + end + + it "does nothing to rules if in sync" do + remote = Flipper::GateValues.new(rules: Set[plan_rule.value]) + feature.enable_rule(plan_rule) + adapter.reset + + described_class.new(feature, feature.gate_values, remote).call + + expect(feature.rules_value).to eq(Set[plan_rule.value]) + expect_no_enable_or_disable + end + it "adds remotely added actors" do remote = Flipper::GateValues.new(actors: Set["1", "2"]) feature.enable_actor(Flipper::Actor.new("1")) From d3adb005f876b2e6159993f1ed0a62864952d4f0 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Tue, 7 Sep 2021 13:56:17 -0400 Subject: [PATCH 053/176] Ignore spec in code climate --- .codeclimate.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.codeclimate.yml b/.codeclimate.yml index 225a9b8c1..61fd620f5 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,2 +1,3 @@ exclude_patterns: - "lib/flipper/ui/public" + - "spec/" From 34ada945822f3a5eff6c64e2d15ed5cf975084fb Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Tue, 7 Sep 2021 14:01:43 -0400 Subject: [PATCH 054/176] Consistent newlines for api markdown --- docs/api/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/api/README.md b/docs/api/README.md index 8bc3c5eee..cfc487c36 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -487,6 +487,7 @@ Successful disabling of the group will return a 200 HTTP status and the feature ] } ``` + ### Enable Actor **URL** @@ -542,6 +543,7 @@ Successful enabling of the actor will return a 200 HTTP status and the feature o ] } ``` + ### Disable Actor **URL** @@ -653,6 +655,7 @@ Successful enabling of a percentage of actors will return a 200 HTTP status and ] } ``` + ### Disable Percentage of Actors **URL** @@ -706,6 +709,7 @@ Successful disabling of a percentage of actors will set the percentage to 0 and ] } ``` + ### Enable Percentage of Time **URL** @@ -761,6 +765,7 @@ Successful enabling of a percentage of time will return a 200 HTTP status and th ] } ``` + ### Disable Percentage of Time **URL** From 6b28363f0d14b5b0e1abcfb050db3fbd8827460f Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Tue, 7 Sep 2021 14:18:05 -0400 Subject: [PATCH 055/176] Fix whitespace formatting in spec --- spec/flipper/adapters/read_only_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/flipper/adapters/read_only_spec.rb b/spec/flipper/adapters/read_only_spec.rb index c53c87e2f..df136a593 100644 --- a/spec/flipper/adapters/read_only_spec.rb +++ b/spec/flipper/adapters/read_only_spec.rb @@ -66,8 +66,8 @@ "left" => {"type" => "Property", "value" => "plan"}, "operator" => {"type" => "Operator", "value" => "eq"}, "right" => {"type" => "String", "value" => "basic"}, - } - } + } + } ], percentage_of_actors: '25', percentage_of_time: '45') From 72520fa6b22ee8e77e957beb88968d89bae6b3cd Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Tue, 7 Sep 2021 14:18:13 -0400 Subject: [PATCH 056/176] Add enable/disable rule to api markdown --- docs/api/README.md | 144 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/docs/api/README.md b/docs/api/README.md index cfc487c36..06685123f 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -376,6 +376,150 @@ Successful disabling of the boolean gate will return a 200 HTTP status and the f } ``` +### Enable Rule + +**URL** + +`POST /features/{feature_name}/rules` + +**Parameters** + +* `feature_name` - The name of the feature + +* `type` - The type of rule being enabled + +* `value` - The JSON representation of the rule being enabled. + +**Request** + +``` +curl -X POST -H "Content-Type: application/json" -d '{"type":"Condition","value":{"left":{"type":"Property","value":"plan"},"operator":{"type":"Operator","value":"eq"},"right":{"type":"String","value":"basic"}}}' http://example.com/flipper/api/features/reports/rules +``` + +**Response** + +Successful enabling of the group will return a 200 HTTP status and the feature object as the response body. + +```json +{ + "key": "reports", + "state": "conditional", + "gates": [ + { + "key": "boolean", + "name": "boolean", + "value": null + }, + { + "key": "actors", + "name": "actor", + "value": [] + }, + { + "key": "rules", + "name": "rule", + "value": [ + { + "type": "Condition", + "value": { + "left": { + "type": "Property", + "value": "plan" + }, + "operator": { + "type": "Operator", + "value": "eq" + }, + "right": { + "type": "String", + "value": "basic" + } + } + } + ] + }, + { + "key": "percentage_of_actors", + "name": "percentage_of_actors", + "value": null + }, + { + "key": "percentage_of_time", + "name": "percentage_of_time", + "value": null + }, + { + "key": "groups", + "name": "group", + "value": [] + } + ] +} +``` + +### Disable Rule + +**URL** + +`DELETE /features/{feature_name}/rules` + +**Parameters** + +* `feature_name` - The name of the feature + +* `type` - The type of rule being enabled + +* `value` - The JSON representation of the rule being enabled. + +**Request** + +``` +curl -X DELETE -H "Content-Type: application/json" -d '{"type":"Condition","value":{"left":{"type":"Property","value":"plan"},"operator":{"type":"Operator","value":"eq"},"right":{"type":"String","value":"basic"}}}' http://example.com/flipper/api/features/reports/rules +``` + +**Response** + +Successful disabling of the group will return a 200 HTTP status and the feature object as the response body. + +```json +{ + "key": "reports", + "state": "off", + "gates": [ + { + "key": "boolean", + "name": "boolean", + "value": null + }, + { + "key": "actors", + "name": "actor", + "value": [] + }, + { + "key": "rules", + "name": "rule", + "value": [] + }, + { + "key": "percentage_of_actors", + "name": "percentage_of_actors", + "value": null + }, + { + "key": "percentage_of_time", + "name": "percentage_of_time", + "value": null + }, + { + "key": "groups", + "name": "group", + "value": [] + } + ] +} +``` + ### Enable Group **URL** From 3648154a8706a834c7958828318ff50706a6fcc9 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Tue, 7 Sep 2021 14:20:24 -0400 Subject: [PATCH 057/176] Add rules to responses in api markdown --- docs/api/README.md | 65 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/docs/api/README.md b/docs/api/README.md index 06685123f..c2cb73b30 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -92,6 +92,11 @@ Returns an array of feature objects: "name": "actor", "value": [] }, + { + "key": "rules", + "name": "rule", + "value": [] + }, { "key": "percentage_of_actors", "name": "percentage_of_actors", @@ -123,6 +128,11 @@ Returns an array of feature objects: "name": "actor", "value": [] }, + { + "key": "rules", + "name": "rule", + "value": [] + }, { "key": "percentage_of_actors", "name": "percentage_of_actors", @@ -199,6 +209,11 @@ Returns an individual feature object: "name": "actor", "value": [] }, + { + "key": "rules", + "name": "rule", + "value": [] + }, { "key": "percentage_of_actors", "name": "percentage_of_actors", @@ -307,6 +322,11 @@ Successful enabling of the boolean gate will return a 200 HTTP status and the fe "name": "actor", "value": [] }, + { + "key": "rules", + "name": "rule", + "value": [] + }, { "key": "percentage_of_actors", "name": "percentage_of_actors", @@ -362,6 +382,11 @@ Successful disabling of the boolean gate will return a 200 HTTP status and the f "name": "actor", "value": [] }, + { + "key": "rules", + "name": "rule", + "value": [] + }, { "key": "percentage_of_actors", "name": "percentage_of_actors", @@ -562,6 +587,11 @@ Successful enabling of the group will return a 200 HTTP status and the feature o "name": "actor", "value": [] }, + { + "key": "rules", + "name": "rule", + "value": [] + }, { "key": "percentage_of_actors", "name": "percentage_of_actors", @@ -618,6 +648,11 @@ Successful disabling of the group will return a 200 HTTP status and the feature "name": "actor", "value": [] }, + { + "key": "rules", + "name": "rule", + "value": [] + }, { "key": "percentage_of_actors", "name": "percentage_of_actors", @@ -674,6 +709,11 @@ Successful enabling of the actor will return a 200 HTTP status and the feature o "name": "actor", "value": ["User;1"] }, + { + "key": "rules", + "name": "rule", + "value": [] + }, { "key": "percentage_of_actors", "name": "percentage_of_actors", @@ -730,6 +770,11 @@ Successful disabling of the actor will return a 200 HTTP status and the feature "name": "actor", "value": [] }, + { + "key": "rules", + "name": "rule", + "value": [] + }, { "key": "percentage_of_actors", "name": "percentage_of_actors", @@ -786,6 +831,11 @@ Successful enabling of a percentage of actors will return a 200 HTTP status and "name": "actor", "value": [] }, + { + "key": "rules", + "name": "rule", + "value": [] + }, { "key": "percentage_of_actors", "name": "percentage_of_actors", @@ -840,6 +890,11 @@ Successful disabling of a percentage of actors will set the percentage to 0 and "name": "actor", "value": [] }, + { + "key": "rules", + "name": "rule", + "value": [] + }, { "key": "percentage_of_actors", "name": "percentage_of_actors", @@ -896,6 +951,11 @@ Successful enabling of a percentage of time will return a 200 HTTP status and th "name": "actor", "value": [] }, + { + "key": "rules", + "name": "rule", + "value": [] + }, { "key": "percentage_of_actors", "name": "percentage_of_actors", @@ -950,6 +1010,11 @@ Successful disabling of a percentage of time will set the percentage to 0 and re "name": "actor", "value": [] }, + { + "key": "rules", + "name": "rule", + "value": [] + }, { "key": "percentage_of_actors", "name": "percentage_of_actors", From 8bd199107756ea5633c87b08bb2fe1440b87fce3 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Tue, 7 Sep 2021 14:23:57 -0400 Subject: [PATCH 058/176] Reorder gates --- lib/flipper/feature.rb | 2 +- spec/flipper/api/v1/actions/feature_spec.rb | 32 ++++++++++---------- spec/flipper/api/v1/actions/features_spec.rb | 18 +++++------ 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/lib/flipper/feature.rb b/lib/flipper/feature.rb index ca0e435c8..30652e19e 100644 --- a/lib/flipper/feature.rb +++ b/lib/flipper/feature.rb @@ -366,8 +366,8 @@ def inspect def gates @gates ||= [ Gates::Boolean.new, - Gates::Actor.new, Gates::Rule.new, + Gates::Actor.new, Gates::PercentageOfActors.new, Gates::PercentageOfTime.new, Gates::Group.new, diff --git a/spec/flipper/api/v1/actions/feature_spec.rb b/spec/flipper/api/v1/actions/feature_spec.rb index f5a3f544b..d22e41520 100644 --- a/spec/flipper/api/v1/actions/feature_spec.rb +++ b/spec/flipper/api/v1/actions/feature_spec.rb @@ -23,13 +23,13 @@ 'value' => 'true', }, { - 'key' => 'actors', - 'name' => 'actor', + 'key' => 'rules', + 'name' => 'rule', 'value' => [], }, { - 'key' => 'rules', - 'name' => 'rule', + 'key' => 'actors', + 'name' => 'actor', 'value' => [], }, { @@ -72,13 +72,13 @@ 'value' => nil, }, { - 'key' => 'actors', - 'name' => 'actor', + 'key' => 'rules', + 'name' => 'rule', 'value' => [], }, { - 'key' => 'rules', - 'name' => 'rule', + 'key' => 'actors', + 'name' => 'actor', 'value' => [], }, { @@ -137,13 +137,13 @@ 'value' => 'true', }, { - 'key' => 'actors', - 'name' => 'actor', + 'key' => 'rules', + 'name' => 'rule', 'value' => [], }, { - 'key' => 'rules', - 'name' => 'rule', + 'key' => 'actors', + 'name' => 'actor', 'value' => [], }, { @@ -186,13 +186,13 @@ 'value' => 'true', }, { - 'key' => 'actors', - 'name' => 'actor', + 'key' => 'rules', + 'name' => 'rule', 'value' => [], }, { - 'key' => 'rules', - 'name' => 'rule', + 'key' => 'actors', + 'name' => 'actor', 'value' => [], }, { diff --git a/spec/flipper/api/v1/actions/features_spec.rb b/spec/flipper/api/v1/actions/features_spec.rb index 5243287d7..448fed1a1 100644 --- a/spec/flipper/api/v1/actions/features_spec.rb +++ b/spec/flipper/api/v1/actions/features_spec.rb @@ -25,16 +25,16 @@ 'name' => 'boolean', 'value' => 'true', }, - { - 'key' => 'actors', - 'name' => 'actor', - 'value' => ['10'], - }, { 'key' => 'rules', 'name' => 'rule', 'value' => [], }, + { + 'key' => 'actors', + 'name' => 'actor', + 'value' => ['10'], + }, { 'key' => 'percentage_of_actors', 'name' => 'percentage_of_actors', @@ -126,13 +126,13 @@ 'value' => nil, }, { - 'key' => 'actors', - 'name' => 'actor', + 'key' => 'rules', + 'name' => 'rule', 'value' => [], }, { - 'key' => 'rules', - 'name' => 'rule', + 'key' => 'actors', + 'name' => 'actor', 'value' => [], }, { From 6f4ac7a3d9b566cb54d925ea0339d43818915aae Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Tue, 7 Sep 2021 14:44:48 -0400 Subject: [PATCH 059/176] Update gates markdown --- docs/Gates.md | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/docs/Gates.md b/docs/Gates.md index 3a451ef8c..ffda21791 100644 --- a/docs/Gates.md +++ b/docs/Gates.md @@ -12,7 +12,41 @@ Flipper.disable :stats # turn off Flipper.enabled? :stats # check ``` -## 2. Individual Actor +## 2. Rule + +Turn feature on for one or more rules. Rules have the same power and flexibility as groups. But the benefit is that they can be changed at runtime (because they are stored in adapter), whereas groups cannot (because they are defined in code). + +A rule is made up of a left value, operator and right value and can be combined with other rules to define pretty complex logic (and, or, eq, neq, gt, gte, lt, lte, in, nin, and percentage). + +**Note**: Eventually all other gates will be deprecated in favor of rules since rules can easily power all other gates. + +To make rules useful, you'll need to ensure that actors respond to `flipper_properties`. + +```ruby +class User < Struct.new(:id, :flipper_properties) + include Flipper::Identifier +end + +basic_user = User.new(1, {"plan" => "basic", "age" => 30}) +premium_user = User.new(2, {"plan" => "premium", "age" => 40}) + +# enable stats feature for anything where property == "basic" +Flipper.enable_rule :stats, Flipper.property(:plan).eq("basic") +Flipper.enabled? :stats, basic_user # true +Flipper.enabled? :stats, premium_user # false + +# enable stats for anyone on basic plan or age >= 40 +Flipper.enable_rule :stats, Flipper.any( + Flipper.property(:plan).eq("basic"), + Flipper.property(:age).gte(40), +) +Flipper.enabled? :stats, basic_user # true because plan == "basic" +Flipper.enabled? :stats, premium_user # true because age >= 40 +``` + +To learn more, check out the plethora of code samples in [examples/rules.rb](https://github.com/jnunemaker/flipper/blob/master/examples/rules.rb). + +## 3. Individual Actor Turn feature on for individual thing. Think enable feature for someone to test or for a buddy. @@ -60,7 +94,7 @@ Organization.new("DEB3D850-39FB-444B-A1E9-404A990FDBE0").flipper_id Just make sure each type of object has a unique `flipper_id`. -## 3. Percentage of Actors +## 4. Percentage of Actors Turn this on for a percentage of actors (think user, member, account, group, whatever). Consistently on or off for this user as long as percentage increases. Think slow rollout of a new feature to a percentage of things. @@ -85,7 +119,7 @@ feature.enabled? user feature.disable_percentage_of_actors # sets to 0 ``` -## 4. Percentage of Time +## 5. Percentage of Time Turn this on for a percentage of time. Think load testing new features behind the scenes and such. @@ -108,7 +142,7 @@ feature.disable_percentage_of_time Timeness is not a good idea for enabling new features in the UI. Most of the time you want a feature on or off for a user, but there are definitely times when I have found percentage of time to be very useful. -## 5. Group +## 6. Group Turn on feature based on the return value of block. Super flexible way to turn on a feature for multiple things (users, people, accounts, etc.) as long as the thing returns true when passed to the block. From e52875129d81654b2ddb51b71a9e2b813b72a4f5 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 8 Sep 2021 08:13:28 -0400 Subject: [PATCH 060/176] Make operator an object in condition instead of hash --- lib/flipper/rules/condition.rb | 9 ++++----- lib/flipper/rules/operator.rb | 12 ++++++++++++ spec/flipper/rules/operator_spec.rb | 26 ++++++++++++++++++++++---- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/lib/flipper/rules/condition.rb b/lib/flipper/rules/condition.rb index a85cb734d..3bd433b5e 100644 --- a/lib/flipper/rules/condition.rb +++ b/lib/flipper/rules/condition.rb @@ -28,7 +28,7 @@ def self.build(hash) def initialize(left, operator, right) @left = left - @operator = operator + @operator = Operator.wrap(operator) @right = right end @@ -37,7 +37,7 @@ def value "type" => "Condition", "value" => { "left" => @left, - "operator" => @operator, + "operator" => @operator.to_h, "right" => @right, } } @@ -55,9 +55,8 @@ def matches?(feature_name, actor = nil) properties = actor ? actor.flipper_properties.merge("flipper_id" => actor.flipper_id) : {}.freeze left_value = evaluate(@left, properties) right_value = evaluate(@right, properties) - operator_name = @operator.fetch("value") - operation = OPERATIONS.fetch(operator_name) do - raise "operator not implemented: #{operator_name}" + operation = OPERATIONS.fetch(@operator.name) do + raise "operator not implemented: #{@operator.name}" end !!operation.call(left: left_value, right: right_value, feature_name: feature_name) diff --git a/lib/flipper/rules/operator.rb b/lib/flipper/rules/operator.rb index 6c4e091f8..235821f3b 100644 --- a/lib/flipper/rules/operator.rb +++ b/lib/flipper/rules/operator.rb @@ -3,6 +3,18 @@ module Rules class Operator attr_reader :type, :value + # Ensures object is an Flipper::Rules::Operator.. + # + # object - The Hash or Flipper::Rules::Operator. instance. + # + # Returns Flipper::Rules::Operator. + # Raises Flipper::Errors::OperatorNotFound if not a known operator. + def self.wrap(object) + return object if object.is_a?(Flipper::Rules::Operator) + + new(object.fetch("value")) + end + def initialize(value) @type = "Operator".freeze @value = value.to_s diff --git a/spec/flipper/rules/operator_spec.rb b/spec/flipper/rules/operator_spec.rb index 55eaa37a7..f3960de7a 100644 --- a/spec/flipper/rules/operator_spec.rb +++ b/spec/flipper/rules/operator_spec.rb @@ -1,15 +1,33 @@ require 'helper' RSpec.describe Flipper::Rules::Operator do + describe ".wrap" do + context "with Hash" do + it "returns instance" do + instance = described_class.wrap({"type" => "Operator", "value" => "eq"}) + expect(instance).to be_instance_of(described_class) + expect(instance.name).to eq("eq") + end + end + + context "with instance" do + it "returns intance" do + instance = described_class.wrap(described_class.new("eq")) + expect(instance).to be_instance_of(described_class) + expect(instance.name).to eq("eq") + end + end + end + describe "#initialize" do it "works with string name" do - property = described_class.new("eq") - expect(property.name).to eq("eq") + instance = described_class.new("eq") + expect(instance.name).to eq("eq") end it "works with symbol name" do - property = described_class.new(:eq) - expect(property.name).to eq("eq") + instance = described_class.new(:eq) + expect(instance.name).to eq("eq") end end From 0a4bdbfe4b8f719651ba22a35c65db621b2ab037 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 8 Sep 2021 08:16:13 -0400 Subject: [PATCH 061/176] Ensure that operator can be found and raise if not --- lib/flipper/rules/operator.rb | 4 ++++ spec/flipper/rules/operator_spec.rb | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/lib/flipper/rules/operator.rb b/lib/flipper/rules/operator.rb index 235821f3b..9e7ffcd5b 100644 --- a/lib/flipper/rules/operator.rb +++ b/lib/flipper/rules/operator.rb @@ -18,6 +18,10 @@ def self.wrap(object) def initialize(value) @type = "Operator".freeze @value = value.to_s + + unless Condition::OPERATIONS.key?(@value) + raise ArgumentError, "Operator '#{@value}' could not be found" + end end def name diff --git a/spec/flipper/rules/operator_spec.rb b/spec/flipper/rules/operator_spec.rb index f3960de7a..9c0abf767 100644 --- a/spec/flipper/rules/operator_spec.rb +++ b/spec/flipper/rules/operator_spec.rb @@ -29,6 +29,10 @@ instance = described_class.new(:eq) expect(instance.name).to eq("eq") end + + it "raises error for unknown operator" do + expect { described_class.new("nope") }.to raise_error(ArgumentError, "Operator 'nope' could not be found") + end end describe "#to_h" do From e030518df0f14c54b0412da5bfb665448e8041ee Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 8 Sep 2021 08:21:01 -0400 Subject: [PATCH 062/176] Move call from Condition to Operator --- lib/flipper/rules/condition.rb | 23 +---------------------- lib/flipper/rules/operator.rb | 27 ++++++++++++++++++++++++--- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/lib/flipper/rules/condition.rb b/lib/flipper/rules/condition.rb index 3bd433b5e..42837be92 100644 --- a/lib/flipper/rules/condition.rb +++ b/lib/flipper/rules/condition.rb @@ -3,23 +3,6 @@ module Flipper module Rules class Condition < Rule - OPERATIONS = { - "eq" => -> (left:, right:, **) { left == right }, - "neq" => -> (left:, right:, **) { left != right }, - "gt" => -> (left:, right:, **) { left && right && left > right }, - "gte" => -> (left:, right:, **) { left && right && left >= right }, - "lt" => -> (left:, right:, **) { left && right && left < right }, - "lte" => -> (left:, right:, **) { left && right && left <= right }, - "in" => -> (left:, right:, **) { left && right && right.include?(left) }, - "nin" => -> (left:, right:, **) { left && right && !right.include?(left) }, - "percentage" => -> (left:, right:, feature_name:) do - # this is to support up to 3 decimal places in percentages - scaling_factor = 1_000 - id = "#{feature_name}#{left}" - left && right && (Zlib.crc32(id) % (100 * scaling_factor) < right * scaling_factor) - end - }.freeze - def self.build(hash) new(hash.fetch("left"), hash.fetch("operator"), hash.fetch("right")) end @@ -55,11 +38,7 @@ def matches?(feature_name, actor = nil) properties = actor ? actor.flipper_properties.merge("flipper_id" => actor.flipper_id) : {}.freeze left_value = evaluate(@left, properties) right_value = evaluate(@right, properties) - operation = OPERATIONS.fetch(@operator.name) do - raise "operator not implemented: #{@operator.name}" - end - - !!operation.call(left: left_value, right: right_value, feature_name: feature_name) + !!@operator.call(left: left_value, right: right_value, feature_name: feature_name) end private diff --git a/lib/flipper/rules/operator.rb b/lib/flipper/rules/operator.rb index 9e7ffcd5b..01c53022b 100644 --- a/lib/flipper/rules/operator.rb +++ b/lib/flipper/rules/operator.rb @@ -1,6 +1,24 @@ module Flipper module Rules class Operator + + OPERATIONS = { + "eq" => -> (left:, right:, **) { left == right }, + "neq" => -> (left:, right:, **) { left != right }, + "gt" => -> (left:, right:, **) { left && right && left > right }, + "gte" => -> (left:, right:, **) { left && right && left >= right }, + "lt" => -> (left:, right:, **) { left && right && left < right }, + "lte" => -> (left:, right:, **) { left && right && left <= right }, + "in" => -> (left:, right:, **) { left && right && right.include?(left) }, + "nin" => -> (left:, right:, **) { left && right && !right.include?(left) }, + "percentage" => -> (left:, right:, feature_name:) do + # this is to support up to 3 decimal places in percentages + scaling_factor = 1_000 + id = "#{feature_name}#{left}" + left && right && (Zlib.crc32(id) % (100 * scaling_factor) < right * scaling_factor) + end + }.freeze + attr_reader :type, :value # Ensures object is an Flipper::Rules::Operator.. @@ -18,10 +36,9 @@ def self.wrap(object) def initialize(value) @type = "Operator".freeze @value = value.to_s - - unless Condition::OPERATIONS.key?(@value) + @block = OPERATIONS.fetch(@value) { raise ArgumentError, "Operator '#{@value}' could not be found" - end + } end def name @@ -41,6 +58,10 @@ def eql?(other) @value == other.value end alias_method :==, :eql? + + def call(*args) + @block.call(*args) + end end end end From d989dd5016e9c0aae8d2f00206329cf726de7293 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 8 Sep 2021 08:33:37 -0400 Subject: [PATCH 063/176] Make condition checking of actors more resilient --- lib/flipper/rules/condition.rb | 25 +++++++++++++++++++++++-- spec/flipper/rules/condition_spec.rb | 24 ++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/lib/flipper/rules/condition.rb b/lib/flipper/rules/condition.rb index 42837be92..dcd4a0631 100644 --- a/lib/flipper/rules/condition.rb +++ b/lib/flipper/rules/condition.rb @@ -35,7 +35,7 @@ def eql?(other) alias_method :==, :eql? def matches?(feature_name, actor = nil) - properties = actor ? actor.flipper_properties.merge("flipper_id" => actor.flipper_id) : {}.freeze + properties = flipper_properties(actor) left_value = evaluate(@left, properties) right_value = evaluate(@right, properties) !!@operator.call(left: left_value, right: right_value, feature_name: feature_name) @@ -43,7 +43,28 @@ def matches?(feature_name, actor = nil) private - def evaluate(hash, properties) + DEFAULT_PROPERTIES = {}.freeze + + def flipper_properties(actor) + properties = if actor + if actor.respond_to?(:flipper_properties) + actor.flipper_properties + else + warn "#{actor.inspect} does not respond to `flipper_properties` but should." + DEFAULT_PROPERTIES + end + else + DEFAULT_PROPERTIES + end + + if actor.respond_to?(:flipper_id) + properties = properties.merge("flipper_id" => actor.flipper_id) + end + + properties + end + + def evaluate(hash, properties = DEFAULT_PROPERTIES) type = hash.fetch("type") case type diff --git a/spec/flipper/rules/condition_spec.rb b/spec/flipper/rules/condition_spec.rb index 0fd23e472..dab9a7de0 100644 --- a/spec/flipper/rules/condition_spec.rb +++ b/spec/flipper/rules/condition_spec.rb @@ -59,6 +59,30 @@ end end + context "with actor that does NOT respond to flipper_properties but does respond to flipper_id" do + it "does not error" do + user = Struct.new(:flipper_id).new("User;1") + rule = Flipper::Rules::Condition.new( + {"type" => "Boolean", "value" => true}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "Boolean", "value" => true}, + ) + expect(rule.matches?(feature_name, user)).to be(true) + end + end + + context "with actor that does respond to flipper_properties but does NOT respond to flipper_id" do + it "does not error" do + user = Struct.new(:flipper_properties).new({"id" => 1, "type" => "User"}) + rule = Flipper::Rules::Condition.new( + {"type" => "Boolean", "value" => true}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "Boolean", "value" => true}, + ) + expect(rule.matches?(feature_name, user)).to be(true) + end + end + context "with non-Flipper::Actor object that quacks like a duck" do it "works" do user_class = Class.new(Struct.new(:id, :flipper_properties)) do From 84b5abbe8c8ad3a03ffd0b6de113b6717db84c7e Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 8 Sep 2021 08:55:07 -0400 Subject: [PATCH 064/176] Change condition left/right to objects from hashes --- lib/flipper/rules/condition.rb | 55 ++++------------------------ lib/flipper/rules/object.rb | 29 ++++++++++++--- lib/flipper/rules/properties.rb | 25 +++++++++++++ lib/flipper/rules/property.rb | 4 ++ lib/flipper/rules/random.rb | 6 ++- spec/flipper/rules/condition_spec.rb | 18 +++++++++ 6 files changed, 84 insertions(+), 53 deletions(-) create mode 100644 lib/flipper/rules/properties.rb diff --git a/lib/flipper/rules/condition.rb b/lib/flipper/rules/condition.rb index dcd4a0631..f179ad87d 100644 --- a/lib/flipper/rules/condition.rb +++ b/lib/flipper/rules/condition.rb @@ -1,4 +1,5 @@ require 'flipper/rules/rule' +require 'flipper/rules/properties' module Flipper module Rules @@ -10,18 +11,18 @@ def self.build(hash) attr_reader :left, :operator, :right def initialize(left, operator, right) - @left = left + @left = Object.wrap(left) @operator = Operator.wrap(operator) - @right = right + @right = Object.wrap(right) end def value { "type" => "Condition", "value" => { - "left" => @left, + "left" => @left.to_h, "operator" => @operator.to_h, - "right" => @right, + "right" => @right.to_h, } } end @@ -35,51 +36,11 @@ def eql?(other) alias_method :==, :eql? def matches?(feature_name, actor = nil) - properties = flipper_properties(actor) - left_value = evaluate(@left, properties) - right_value = evaluate(@right, properties) + properties = Properties.from_actor(actor) + left_value = @left.evaluate(properties) + right_value = @right.evaluate(properties) !!@operator.call(left: left_value, right: right_value, feature_name: feature_name) end - - private - - DEFAULT_PROPERTIES = {}.freeze - - def flipper_properties(actor) - properties = if actor - if actor.respond_to?(:flipper_properties) - actor.flipper_properties - else - warn "#{actor.inspect} does not respond to `flipper_properties` but should." - DEFAULT_PROPERTIES - end - else - DEFAULT_PROPERTIES - end - - if actor.respond_to?(:flipper_id) - properties = properties.merge("flipper_id" => actor.flipper_id) - end - - properties - end - - def evaluate(hash, properties = DEFAULT_PROPERTIES) - type = hash.fetch("type") - - case type - when "Property" - properties[hash.fetch("value")] - when "Random" - rand hash.fetch("value") - when "Array", "String", "Integer", "Boolean" - hash.fetch("value") - when "Null" - nil - else - raise "type not found: #{type.inspect}" - end - end end end end diff --git a/lib/flipper/rules/object.rb b/lib/flipper/rules/object.rb index 2602c0d26..b9194772d 100644 --- a/lib/flipper/rules/object.rb +++ b/lib/flipper/rules/object.rb @@ -4,7 +4,7 @@ module Flipper module Rules class Object - SUPPORTED_VALUE_TYPES_MAP = { + SUPPORTED_TYPES_MAP = { String => "String", Integer => "Integer", NilClass => "Null", @@ -13,7 +13,21 @@ class Object Array => "Array", }.freeze - SUPPORTED_VALUE_TYPES = SUPPORTED_VALUE_TYPES_MAP.keys.freeze + SUPPORTED_TYPE_CLASSES = SUPPORTED_TYPES_MAP.keys.freeze + SUPPORTED_TYPE_NAMES = SUPPORTED_TYPES_MAP.values.freeze + + def self.wrap(object) + return object if object.is_a?(Flipper::Rules::Object) + + type = object.fetch("type") + value = object.fetch("value") + + if SUPPORTED_TYPE_NAMES.include?(type) + new(value) + else + Rules.const_get(type).new(value) + end + end attr_reader :type, :value @@ -36,6 +50,11 @@ def eql?(other) end alias_method :==, :eql? + def evaluate(properties) + return nil if type == "Null".freeze + value + end + def eq(object) Flipper::Rules::Condition.new( to_h, @@ -111,15 +130,15 @@ def percentage(object) private def type_of(object) - type_class = SUPPORTED_VALUE_TYPES.detect { |klass, type| object.is_a?(klass) } + type_class = SUPPORTED_TYPE_CLASSES.detect { |klass, type| object.is_a?(klass) } if type_class.nil? raise ArgumentError, "#{object.inspect} is not a supported primitive." + - " Object must be one of: #{SUPPORTED_VALUE_TYPES.join(", ")}." + " Object must be one of: #{SUPPORTED_TYPE_CLASSES.join(", ")}." end - SUPPORTED_VALUE_TYPES_MAP[type_class] + SUPPORTED_TYPES_MAP[type_class] end def self.primitive_or_object(object) diff --git a/lib/flipper/rules/properties.rb b/lib/flipper/rules/properties.rb new file mode 100644 index 000000000..efd069357 --- /dev/null +++ b/lib/flipper/rules/properties.rb @@ -0,0 +1,25 @@ +module Flipper + module Rules + module Properties + DEFAULT_PROPERTIES = {}.freeze + + def self.from_actor(actor) + return DEFAULT_PROPERTIES if actor.nil? + + properties = {} + + if actor.respond_to?(:flipper_properties) + properties.update(actor.flipper_properties) + else + warn "#{actor.inspect} does not respond to `flipper_properties` but should." + end + + if actor.respond_to?(:flipper_id) + properties["flipper_id".freeze] = actor.flipper_id + end + + properties + end + end + end +end diff --git a/lib/flipper/rules/property.rb b/lib/flipper/rules/property.rb index fc844e877..7f7994114 100644 --- a/lib/flipper/rules/property.rb +++ b/lib/flipper/rules/property.rb @@ -11,6 +11,10 @@ def initialize(value) def name @value end + + def evaluate(properties) + properties[value] + end end end end diff --git a/lib/flipper/rules/random.rb b/lib/flipper/rules/random.rb index a55bf964e..2471e6a93 100644 --- a/lib/flipper/rules/random.rb +++ b/lib/flipper/rules/random.rb @@ -4,9 +4,13 @@ module Flipper module Rules class Random < Object def initialize(value) - @type = "random".freeze + @type = "Random".freeze @value = value end + + def evaluate(properties) + rand value + end end end end diff --git a/spec/flipper/rules/condition_spec.rb b/spec/flipper/rules/condition_spec.rb index dab9a7de0..449f06ce5 100644 --- a/spec/flipper/rules/condition_spec.rb +++ b/spec/flipper/rules/condition_spec.rb @@ -3,6 +3,24 @@ RSpec.describe Flipper::Rules::Condition do let(:feature_name) { "search" } + describe "#value" do + it "returns Hash with type and value" do + rule = Flipper::Rules::Condition.new( + {"type" => "Property", "value" => "flipper_id"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "User;1"} + ) + expect(rule.value).to eq({ + "type" => "Condition", + "value" => { + "left" => {"type" => "Property", "value" => "flipper_id"}, + "operator" => {"type" => "Operator", "value" => "eq"}, + "right" => {"type" => "String", "value" => "User;1"}, + }, + }) + end + end + describe "#eql?" do let(:rule) { Flipper::Rules::Condition.new( From 5c867134b1f6bc7b1d4a09c25462f99422e537df Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 8 Sep 2021 09:11:37 -0400 Subject: [PATCH 065/176] Make creating object easier Now you can use primitives and it'll just work. Same for opeator. --- lib/flipper/rules/object.rb | 64 ++++++++++--------- lib/flipper/rules/operator.rb | 9 ++- spec/flipper/rules/object_spec.rb | 96 ++++++++++++++++++++++++++--- spec/flipper/rules/operator_spec.rb | 16 +++++ 4 files changed, 147 insertions(+), 38 deletions(-) diff --git a/lib/flipper/rules/object.rb b/lib/flipper/rules/object.rb index b9194772d..d9472ef0c 100644 --- a/lib/flipper/rules/object.rb +++ b/lib/flipper/rules/object.rb @@ -19,13 +19,17 @@ class Object def self.wrap(object) return object if object.is_a?(Flipper::Rules::Object) - type = object.fetch("type") - value = object.fetch("value") - - if SUPPORTED_TYPE_NAMES.include?(type) - new(value) + if object.is_a?(Hash) + type = object.fetch("type") + value = object.fetch("value") + + if SUPPORTED_TYPE_NAMES.include?(type) + new(value) + else + Rules.const_get(type).new(value) + end else - Rules.const_get(type).new(value) + new(object) end end @@ -57,72 +61,72 @@ def evaluate(properties) def eq(object) Flipper::Rules::Condition.new( - to_h, - Operator.new(:eq).to_h, - self.class.primitive_or_object(object).to_h + self, + Operator.new(:eq), + self.class.primitive_or_object(object) ) end def neq(object) Flipper::Rules::Condition.new( - to_h, - Operator.new(:neq).to_h, - self.class.primitive_or_object(object).to_h + self, + Operator.new(:neq), + self.class.primitive_or_object(object) ) end def gt(object) Flipper::Rules::Condition.new( - to_h, - Operator.new(:gt).to_h, + self, + Operator.new(:gt), self.class.integer_or_object(object) ) end def gte(object) Flipper::Rules::Condition.new( - to_h, - Operator.new(:gte).to_h, + self, + Operator.new(:gte), self.class.integer_or_object(object) ) end def lt(object) Flipper::Rules::Condition.new( - to_h, - Operator.new(:lt).to_h, + self, + Operator.new(:lt), self.class.integer_or_object(object) ) end def lte(object) Flipper::Rules::Condition.new( - to_h, - Operator.new(:lte).to_h, + self, + Operator.new(:lte), self.class.integer_or_object(object) ) end def in(object) Flipper::Rules::Condition.new( - to_h, - Operator.new(:in).to_h, + self, + Operator.new(:in), self.class.array_or_object(object) ) end def nin(object) Flipper::Rules::Condition.new( - to_h, - Operator.new(:nin).to_h, + self, + Operator.new(:nin), self.class.array_or_object(object) ) end def percentage(object) Flipper::Rules::Condition.new( - to_h, - Operator.new(:percentage).to_h, + self, + Operator.new(:percentage), self.class.integer_or_object(object) ) end @@ -152,9 +156,9 @@ def self.primitive_or_object(object) def self.integer_or_object(object) case object when Integer - {"type" => "Integer", "value" => object} + Object.new(object) when Flipper::Rules::Object - object.to_h + object else raise ArgumentError, "object must be integer or property" unless object.is_a?(Integer) end @@ -163,9 +167,9 @@ def self.integer_or_object(object) def self.array_or_object(object) case object when Array - {"type" => "Array", "value" => object} + Object.new(object) when Flipper::Rules::Object - object.to_h + object else raise ArgumentError, "object must be array or property" unless object.is_a?(Array) end diff --git a/lib/flipper/rules/operator.rb b/lib/flipper/rules/operator.rb index 01c53022b..47f12537b 100644 --- a/lib/flipper/rules/operator.rb +++ b/lib/flipper/rules/operator.rb @@ -30,7 +30,14 @@ class Operator def self.wrap(object) return object if object.is_a?(Flipper::Rules::Operator) - new(object.fetch("value")) + case object + when Hash + new(object.fetch("value")) + when String, Symbol + new(object) + else + raise ArgumentError, "#{object.inspect} cannot be converted into an operator" + end end def initialize(value) diff --git a/spec/flipper/rules/object_spec.rb b/spec/flipper/rules/object_spec.rb index e53fbc297..8a285420a 100644 --- a/spec/flipper/rules/object_spec.rb +++ b/spec/flipper/rules/object_spec.rb @@ -1,9 +1,91 @@ require 'helper' RSpec.describe Flipper::Rules::Object do + describe ".wrap" do + context "with Hash" do + it "returns instance" do + instance = described_class.wrap({"type" => "Integer", "value" => 2}) + expect(instance).to be_instance_of(described_class) + expect(instance.type).to eq("Integer") + expect(instance.value).to eq(2) + end + end + + context "with instance" do + it "returns instance" do + instance = described_class.wrap(described_class.new(2)) + expect(instance).to be_instance_of(described_class) + expect(instance.type).to eq("Integer") + expect(instance.value).to eq(2) + end + end + + context "with string" do + it "returns instance" do + instance = described_class.wrap("test") + expect(instance).to be_instance_of(described_class) + expect(instance.type).to eq("String") + expect(instance.value).to eq("test") + end + end + + context "with integer" do + it "returns instance" do + instance = described_class.wrap(21) + expect(instance).to be_instance_of(described_class) + expect(instance.type).to eq("Integer") + expect(instance.value).to eq(21) + end + end + + context "with nil" do + it "returns instance" do + instance = described_class.wrap(nil) + expect(instance).to be_instance_of(described_class) + expect(instance.type).to eq("Null") + expect(instance.value).to be(nil) + end + end + + context "with true" do + it "returns instance" do + instance = described_class.wrap(true) + expect(instance).to be_instance_of(described_class) + expect(instance.type).to eq("Boolean") + expect(instance.value).to be(true) + end + end + + context "with false" do + it "returns instance" do + instance = described_class.wrap(false) + expect(instance).to be_instance_of(described_class) + expect(instance.type).to eq("Boolean") + expect(instance.value).to be(false) + end + end + + context "with array" do + it "returns instance" do + instance = described_class.wrap(["test"]) + expect(instance).to be_instance_of(described_class) + expect(instance.type).to eq("Array") + expect(instance.value).to eq(["test"]) + end + end + + context "with unsupported type" do + it "raises ArgumentError" do + expect { + described_class.wrap(Set.new) + }.to raise_error(ArgumentError, /is not a supported primitive\. Object must be one of: String, Integer, NilClass, TrueClass, FalseClass, Array\./) + end + end + end + describe "#initialize" do context "with string" do - it "returns array of type and value" do + it "returns instance" do instance = described_class.new("test") expect(instance.type).to eq("String") expect(instance.value).to eq("test") @@ -11,7 +93,7 @@ end context "with integer" do - it "returns array of type and value" do + it "returns instance" do instance = described_class.new(21) expect(instance.type).to eq("Integer") expect(instance.value).to eq(21) @@ -19,7 +101,7 @@ end context "with nil" do - it "returns array of type and value" do + it "returns instance" do instance = described_class.new(nil) expect(instance.type).to eq("Null") expect(instance.value).to be(nil) @@ -27,7 +109,7 @@ end context "with true" do - it "returns array of type and value" do + it "returns instance" do instance = described_class.new(true) expect(instance.type).to eq("Boolean") expect(instance.value).to be(true) @@ -35,7 +117,7 @@ end context "with false" do - it "returns array of type and value" do + it "returns instance" do instance = described_class.new(false) expect(instance.type).to eq("Boolean") expect(instance.value).to be(false) @@ -43,7 +125,7 @@ end context "with array" do - it "returns array of type and value" do + it "returns instance" do instance = described_class.new(["test"]) expect(instance.type).to eq("Array") expect(instance.value).to eq(["test"]) @@ -51,7 +133,7 @@ end context "with unsupported type" do - it "returns array of type and value" do + it "raises ArgumentError" do expect { described_class.new({}) }.to raise_error(ArgumentError, /{} is not a supported primitive\. Object must be one of: String, Integer, NilClass, TrueClass, FalseClass, Array\./) diff --git a/spec/flipper/rules/operator_spec.rb b/spec/flipper/rules/operator_spec.rb index 9c0abf767..f5ea24113 100644 --- a/spec/flipper/rules/operator_spec.rb +++ b/spec/flipper/rules/operator_spec.rb @@ -10,6 +10,22 @@ end end + context "with String" do + it "returns instance" do + instance = described_class.wrap("eq") + expect(instance).to be_instance_of(described_class) + expect(instance.name).to eq("eq") + end + end + + context "with Symbol" do + it "returns instance" do + instance = described_class.wrap(:eq) + expect(instance).to be_instance_of(described_class) + expect(instance.name).to eq("eq") + end + end + context "with instance" do it "returns intance" do instance = described_class.wrap(described_class.new("eq")) From b51d12686cccff7b05f098ab5cf950be9640f787 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Wed, 8 Sep 2021 11:14:16 -0500 Subject: [PATCH 066/176] Add #flipper_properties to ActiveRecord::Base --- lib/flipper/model/active_record.rb | 12 ++++++ lib/flipper/railtie.rb | 3 +- spec/flipper/model/active_record_spec.rb | 49 ++++++++++++++++++++++++ spec/flipper/railtie_spec.rb | 6 +++ 4 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 lib/flipper/model/active_record.rb create mode 100644 spec/flipper/model/active_record_spec.rb diff --git a/lib/flipper/model/active_record.rb b/lib/flipper/model/active_record.rb new file mode 100644 index 000000000..76eee215f --- /dev/null +++ b/lib/flipper/model/active_record.rb @@ -0,0 +1,12 @@ +module Flipper + module Model + module ActiveRecord + include Flipper::Identifier + + # Properties used to evaluate rules + def flipper_properties + {"type" => self.class.name}.merge(attributes) + end + end + end +end diff --git a/lib/flipper/railtie.rb b/lib/flipper/railtie.rb index 8bbe27e97..a23f4b25b 100644 --- a/lib/flipper/railtie.rb +++ b/lib/flipper/railtie.rb @@ -39,7 +39,8 @@ class Railtie < Rails::Railtie initializer "flipper.identifier" do ActiveSupport.on_load(:active_record) do - ActiveRecord::Base.include Flipper::Identifier + require "flipper/model/active_record" + ActiveRecord::Base.include Flipper::Model::ActiveRecord end end end diff --git a/spec/flipper/model/active_record_spec.rb b/spec/flipper/model/active_record_spec.rb new file mode 100644 index 000000000..b2f86c652 --- /dev/null +++ b/spec/flipper/model/active_record_spec.rb @@ -0,0 +1,49 @@ +require 'helper' +require 'active_record' +require 'flipper/model/active_record' + +# Turn off migration logging for specs +ActiveRecord::Migration.verbose = false + +RSpec.describe Flipper::Model::ActiveRecord do + before(:all) do + ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') + end + + before(:each) do + ActiveRecord::Base.connection.execute <<-SQL + CREATE TABLE users ( + id integer PRIMARY KEY, + name string NOT NULL, + age integer, + is_confirmed boolean, + created_at datetime NOT NULL, + updated_at datetime NOT NULL + ) + SQL + end + + after(:each) do + ActiveRecord::Base.connection.execute("DROP table IF EXISTS `users`") + end + + class User < ActiveRecord::Base + include Flipper::Model::ActiveRecord + end + + describe "flipper_properties" do + subject { User.create!(name: "Test", age: 22, is_confirmed: true) } + + it "includes all attributes" do + expect(subject.flipper_properties).to eq({ + "type" => "User", + "id" => subject.id, + "name" => "Test", + "age" => 22, + "is_confirmed" => true, + "created_at" => subject.created_at, + "updated_at" => subject.updated_at + }) + end + end +end diff --git a/spec/flipper/railtie_spec.rb b/spec/flipper/railtie_spec.rb index f3b0672c6..bfcd0c119 100644 --- a/spec/flipper/railtie_spec.rb +++ b/spec/flipper/railtie_spec.rb @@ -65,5 +65,11 @@ require 'active_record' expect(ActiveRecord::Base.ancestors).to include(Flipper::Identifier) end + + it "defines #flipper_properties on AR::Base" do + subject + require 'active_record' + expect(ActiveRecord::Base.ancestors).to include(Flipper::Model::ActiveRecord) + end end end From 55073d18a2ae90594c91a62c8164c36ff5fcd1c7 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Wed, 8 Sep 2021 11:41:01 -0500 Subject: [PATCH 067/176] Add #flipper_properties to Sequel::Model --- lib/flipper/adapters/sequel.rb | 3 +- lib/flipper/model/sequel.rb | 12 +++++++ spec/flipper/adapters/sequel_spec.rb | 4 +++ spec/flipper/model/sequel_spec.rb | 50 ++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 lib/flipper/model/sequel.rb create mode 100644 spec/flipper/model/sequel_spec.rb diff --git a/lib/flipper/adapters/sequel.rb b/lib/flipper/adapters/sequel.rb index 31ad3ebcf..986069afd 100644 --- a/lib/flipper/adapters/sequel.rb +++ b/lib/flipper/adapters/sequel.rb @@ -1,6 +1,7 @@ require 'set' require 'flipper' require 'sequel' +require 'flipper/model/sequel' module Flipper module Adapters @@ -224,4 +225,4 @@ def result_for_feature(feature, db_gates) config.adapter { Flipper::Adapters::Sequel.new } end -Sequel::Model.include Flipper::Identifier +Sequel::Model.include Flipper::Model::Sequel diff --git a/lib/flipper/model/sequel.rb b/lib/flipper/model/sequel.rb new file mode 100644 index 000000000..344cc15af --- /dev/null +++ b/lib/flipper/model/sequel.rb @@ -0,0 +1,12 @@ +module Flipper + module Model + module Sequel + include Flipper::Identifier + + # Properties used to evaluate rules + def flipper_properties + {"type" => self.class.name}.update(to_hash.transform_keys(&:to_s)) + end + end + end +end diff --git a/spec/flipper/adapters/sequel_spec.rb b/spec/flipper/adapters/sequel_spec.rb index 0c5f123ef..5117123c3 100644 --- a/spec/flipper/adapters/sequel_spec.rb +++ b/spec/flipper/adapters/sequel_spec.rb @@ -44,5 +44,9 @@ it "defines #flipper_id on Sequel::Model" do expect(Sequel::Model.ancestors).to include(Flipper::Identifier) end + + it "defines #flipper_properties on Sequel::Model" do + expect(Sequel::Model.ancestors).to include(Flipper::Model::Sequel) + end end end diff --git a/spec/flipper/model/sequel_spec.rb b/spec/flipper/model/sequel_spec.rb new file mode 100644 index 000000000..a5e631aef --- /dev/null +++ b/spec/flipper/model/sequel_spec.rb @@ -0,0 +1,50 @@ +require 'helper' +require 'sequel' +require 'flipper/model/sequel' + +Sequel::Model.db = Sequel.sqlite(':memory:') + +RSpec.describe Flipper::Model::Sequel do + before(:each) do + Sequel::Model.db.run <<-SQL + CREATE TABLE users ( + id integer PRIMARY KEY, + name string NOT NULL, + age integer, + is_confirmed boolean, + created_at datetime NOT NULL, + updated_at datetime NOT NULL + ) + SQL + + @User = Class.new(::Sequel::Model(:users)) do + include Flipper::Model::Sequel + plugin :timestamps, update_on_create: true + + + def self.name + 'User' + end + end + end + + after(:each) do + Sequel::Model.db.run("DROP table IF EXISTS `users`") + end + + describe "flipper_properties" do + subject { @User.create(name: "Test", age: 22, is_confirmed: true) } + + it "includes all attributes" do + expect(subject.flipper_properties).to eq({ + "type" => "User", + "id" => subject.id, + "name" => "Test", + "age" => 22, + "is_confirmed" => true, + "created_at" => subject.created_at, + "updated_at" => subject.updated_at + }) + end + end +end From 250dd4864c437ce2d51991a54e0a1f9e0143bcc8 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 8 Sep 2021 20:39:03 -0400 Subject: [PATCH 068/176] Add subclasses for each operator --- lib/flipper/rules/condition.rb | 2 +- lib/flipper/rules/eq.rb | 15 +++++++++ lib/flipper/rules/gt.rb | 15 +++++++++ lib/flipper/rules/gte.rb | 15 +++++++++ lib/flipper/rules/in.rb | 15 +++++++++ lib/flipper/rules/lt.rb | 15 +++++++++ lib/flipper/rules/lte.rb | 15 +++++++++ lib/flipper/rules/neq.rb | 15 +++++++++ lib/flipper/rules/nin.rb | 15 +++++++++ lib/flipper/rules/object.rb | 36 ++++++---------------- lib/flipper/rules/operator.rb | 48 ++++++++++++----------------- lib/flipper/rules/percentage.rb | 19 ++++++++++++ spec/flipper/rules/operator_spec.rb | 20 +++++------- 13 files changed, 177 insertions(+), 68 deletions(-) create mode 100644 lib/flipper/rules/eq.rb create mode 100644 lib/flipper/rules/gt.rb create mode 100644 lib/flipper/rules/gte.rb create mode 100644 lib/flipper/rules/in.rb create mode 100644 lib/flipper/rules/lt.rb create mode 100644 lib/flipper/rules/lte.rb create mode 100644 lib/flipper/rules/neq.rb create mode 100644 lib/flipper/rules/nin.rb create mode 100644 lib/flipper/rules/percentage.rb diff --git a/lib/flipper/rules/condition.rb b/lib/flipper/rules/condition.rb index f179ad87d..7b99f7bb9 100644 --- a/lib/flipper/rules/condition.rb +++ b/lib/flipper/rules/condition.rb @@ -12,7 +12,7 @@ def self.build(hash) def initialize(left, operator, right) @left = Object.wrap(left) - @operator = Operator.wrap(operator) + @operator = Operator.build(operator) @right = Object.wrap(right) end diff --git a/lib/flipper/rules/eq.rb b/lib/flipper/rules/eq.rb new file mode 100644 index 000000000..ecf2a6bdd --- /dev/null +++ b/lib/flipper/rules/eq.rb @@ -0,0 +1,15 @@ +require "flipper/rules/operator" + +module Flipper + module Rules + class Eq < Operator + def initialize + super :eq + end + + def call(left:, right:, **) + left == right + end + end + end +end diff --git a/lib/flipper/rules/gt.rb b/lib/flipper/rules/gt.rb new file mode 100644 index 000000000..f494dea52 --- /dev/null +++ b/lib/flipper/rules/gt.rb @@ -0,0 +1,15 @@ +require "flipper/rules/operator" + +module Flipper + module Rules + class Gt < Operator + def initialize + super :gt + end + + def call(left:, right:, **) + left && right && left > right + end + end + end +end diff --git a/lib/flipper/rules/gte.rb b/lib/flipper/rules/gte.rb new file mode 100644 index 000000000..53a9db5db --- /dev/null +++ b/lib/flipper/rules/gte.rb @@ -0,0 +1,15 @@ +require "flipper/rules/operator" + +module Flipper + module Rules + class Gte < Operator + def initialize + super :gte + end + + def call(left:, right:, **) + left && right && left >= right + end + end + end +end diff --git a/lib/flipper/rules/in.rb b/lib/flipper/rules/in.rb new file mode 100644 index 000000000..536591b05 --- /dev/null +++ b/lib/flipper/rules/in.rb @@ -0,0 +1,15 @@ +require "flipper/rules/operator" + +module Flipper + module Rules + class In < Operator + def initialize + super :in + end + + def call(left:, right:, **) + left && right && right.include?(left) + end + end + end +end diff --git a/lib/flipper/rules/lt.rb b/lib/flipper/rules/lt.rb new file mode 100644 index 000000000..e5ddef902 --- /dev/null +++ b/lib/flipper/rules/lt.rb @@ -0,0 +1,15 @@ +require "flipper/rules/operator" + +module Flipper + module Rules + class Lt < Operator + def initialize + super :lt + end + + def call(left:, right:, **) + left && right && left < right + end + end + end +end diff --git a/lib/flipper/rules/lte.rb b/lib/flipper/rules/lte.rb new file mode 100644 index 000000000..dca1506f4 --- /dev/null +++ b/lib/flipper/rules/lte.rb @@ -0,0 +1,15 @@ +require "flipper/rules/operator" + +module Flipper + module Rules + class Lte < Operator + def initialize + super :lte + end + + def call(left:, right:, **) + left && right && left <= right + end + end + end +end diff --git a/lib/flipper/rules/neq.rb b/lib/flipper/rules/neq.rb new file mode 100644 index 000000000..639fe1f54 --- /dev/null +++ b/lib/flipper/rules/neq.rb @@ -0,0 +1,15 @@ +require "flipper/rules/operator" + +module Flipper + module Rules + class Neq < Operator + def initialize + super :neq + end + + def call(left:, right:, **) + left != right + end + end + end +end diff --git a/lib/flipper/rules/nin.rb b/lib/flipper/rules/nin.rb new file mode 100644 index 000000000..8a61b4d28 --- /dev/null +++ b/lib/flipper/rules/nin.rb @@ -0,0 +1,15 @@ +require "flipper/rules/operator" + +module Flipper + module Rules + class Nin < Operator + def initialize + super :nin + end + + def call(left:, right:, **) + left && right && !right.include?(left) + end + end + end +end diff --git a/lib/flipper/rules/object.rb b/lib/flipper/rules/object.rb index d9472ef0c..aee481f56 100644 --- a/lib/flipper/rules/object.rb +++ b/lib/flipper/rules/object.rb @@ -61,73 +61,55 @@ def evaluate(properties) def eq(object) Flipper::Rules::Condition.new( - self, - Operator.new(:eq), - self.class.primitive_or_object(object) + self, Eq.new, self.class.primitive_or_object(object) ) end def neq(object) Flipper::Rules::Condition.new( - self, - Operator.new(:neq), - self.class.primitive_or_object(object) + self, Neq.new, self.class.primitive_or_object(object) ) end def gt(object) Flipper::Rules::Condition.new( - self, - Operator.new(:gt), - self.class.integer_or_object(object) + self, Gt.new, self.class.integer_or_object(object) ) end def gte(object) Flipper::Rules::Condition.new( - self, - Operator.new(:gte), - self.class.integer_or_object(object) + self, Gte.new, self.class.integer_or_object(object) ) end def lt(object) Flipper::Rules::Condition.new( - self, - Operator.new(:lt), - self.class.integer_or_object(object) + self, Lt.new, self.class.integer_or_object(object) ) end def lte(object) Flipper::Rules::Condition.new( - self, - Operator.new(:lte), - self.class.integer_or_object(object) + self, Lte.new, self.class.integer_or_object(object) ) end def in(object) Flipper::Rules::Condition.new( - self, - Operator.new(:in), - self.class.array_or_object(object) + self, In.new, self.class.array_or_object(object) ) end def nin(object) Flipper::Rules::Condition.new( - self, - Operator.new(:nin), - self.class.array_or_object(object) + self, Nin.new, self.class.array_or_object(object) ) end def percentage(object) Flipper::Rules::Condition.new( - self, - Operator.new(:percentage), - self.class.integer_or_object(object) + self, Percentage.new, self.class.integer_or_object(object) ) end diff --git a/lib/flipper/rules/operator.rb b/lib/flipper/rules/operator.rb index 47f12537b..3bb0e3473 100644 --- a/lib/flipper/rules/operator.rb +++ b/lib/flipper/rules/operator.rb @@ -1,51 +1,33 @@ module Flipper module Rules class Operator - - OPERATIONS = { - "eq" => -> (left:, right:, **) { left == right }, - "neq" => -> (left:, right:, **) { left != right }, - "gt" => -> (left:, right:, **) { left && right && left > right }, - "gte" => -> (left:, right:, **) { left && right && left >= right }, - "lt" => -> (left:, right:, **) { left && right && left < right }, - "lte" => -> (left:, right:, **) { left && right && left <= right }, - "in" => -> (left:, right:, **) { left && right && right.include?(left) }, - "nin" => -> (left:, right:, **) { left && right && !right.include?(left) }, - "percentage" => -> (left:, right:, feature_name:) do - # this is to support up to 3 decimal places in percentages - scaling_factor = 1_000 - id = "#{feature_name}#{left}" - left && right && (Zlib.crc32(id) % (100 * scaling_factor) < right * scaling_factor) - end - }.freeze - attr_reader :type, :value - # Ensures object is an Flipper::Rules::Operator.. + # Builds a Flipper::Rules::Operator based on an object. # - # object - The Hash or Flipper::Rules::Operator. instance. + # object - The Hash, String, Symbol or Flipper::Rules::Operator + # representation of an operator. # # Returns Flipper::Rules::Operator. # Raises Flipper::Errors::OperatorNotFound if not a known operator. - def self.wrap(object) + def self.build(object) return object if object.is_a?(Flipper::Rules::Operator) - case object + operator_class = case object when Hash - new(object.fetch("value")) + object.fetch("value") when String, Symbol - new(object) + object else raise ArgumentError, "#{object.inspect} cannot be converted into an operator" end + + Rules.const_get(operator_class.to_s.capitalize).new end def initialize(value) @type = "Operator".freeze @value = value.to_s - @block = OPERATIONS.fetch(@value) { - raise ArgumentError, "Operator '#{@value}' could not be found" - } end def name @@ -67,8 +49,18 @@ def eql?(other) alias_method :==, :eql? def call(*args) - @block.call(*args) + raise NotImplementedError end end end end + +require "flipper/rules/eq" +require "flipper/rules/neq" +require "flipper/rules/gt" +require "flipper/rules/gte" +require "flipper/rules/lt" +require "flipper/rules/lte" +require "flipper/rules/in" +require "flipper/rules/nin" +require "flipper/rules/percentage" diff --git a/lib/flipper/rules/percentage.rb b/lib/flipper/rules/percentage.rb new file mode 100644 index 000000000..4c1d3f5b4 --- /dev/null +++ b/lib/flipper/rules/percentage.rb @@ -0,0 +1,19 @@ +require "zlib" +require "flipper/rules/operator" + +module Flipper + module Rules + class Percentage < Operator + SCALING_FACTOR = 1_000 + + def initialize + super :percentage + end + + def call(left:, right:, feature_name:, **) + return false unless left && right + Zlib.crc32("#{feature_name}#{left}") % (100 * SCALING_FACTOR) < right * SCALING_FACTOR + end + end + end +end diff --git a/spec/flipper/rules/operator_spec.rb b/spec/flipper/rules/operator_spec.rb index f5ea24113..0def8d974 100644 --- a/spec/flipper/rules/operator_spec.rb +++ b/spec/flipper/rules/operator_spec.rb @@ -1,34 +1,34 @@ require 'helper' RSpec.describe Flipper::Rules::Operator do - describe ".wrap" do + describe ".build" do context "with Hash" do it "returns instance" do - instance = described_class.wrap({"type" => "Operator", "value" => "eq"}) - expect(instance).to be_instance_of(described_class) + instance = described_class.build({"type" => "Operator", "value" => "eq"}) + expect(instance).to be_a(described_class) expect(instance.name).to eq("eq") end end context "with String" do it "returns instance" do - instance = described_class.wrap("eq") - expect(instance).to be_instance_of(described_class) + instance = described_class.build("eq") + expect(instance).to be_a(described_class) expect(instance.name).to eq("eq") end end context "with Symbol" do it "returns instance" do - instance = described_class.wrap(:eq) - expect(instance).to be_instance_of(described_class) + instance = described_class.build(:eq) + expect(instance).to be_a(described_class) expect(instance.name).to eq("eq") end end context "with instance" do it "returns intance" do - instance = described_class.wrap(described_class.new("eq")) + instance = described_class.build(described_class.new("eq")) expect(instance).to be_instance_of(described_class) expect(instance.name).to eq("eq") end @@ -45,10 +45,6 @@ instance = described_class.new(:eq) expect(instance.name).to eq("eq") end - - it "raises error for unknown operator" do - expect { described_class.new("nope") }.to raise_error(ArgumentError, "Operator 'nope' could not be found") - end end describe "#to_h" do From 78b8e102ad460c5b275f0a0f24fbb03b63c2a174 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 8 Sep 2021 21:02:25 -0400 Subject: [PATCH 069/176] Move operators to plural namespace Leave singular just for building. Goal is to make it real obvious that you shouldn't use superclass and instead use the subclasses. --- lib/flipper/rules/eq.rb | 15 ------ lib/flipper/rules/gt.rb | 15 ------ lib/flipper/rules/gte.rb | 15 ------ lib/flipper/rules/in.rb | 15 ------ lib/flipper/rules/lt.rb | 15 ------ lib/flipper/rules/lte.rb | 15 ------ lib/flipper/rules/neq.rb | 15 ------ lib/flipper/rules/nin.rb | 15 ------ lib/flipper/rules/object.rb | 18 +++---- lib/flipper/rules/operator.rb | 60 ++++++----------------- lib/flipper/rules/operators/base.rb | 36 ++++++++++++++ lib/flipper/rules/operators/eq.rb | 17 +++++++ lib/flipper/rules/operators/gt.rb | 17 +++++++ lib/flipper/rules/operators/gte.rb | 17 +++++++ lib/flipper/rules/operators/in.rb | 17 +++++++ lib/flipper/rules/operators/lt.rb | 17 +++++++ lib/flipper/rules/operators/lte.rb | 17 +++++++ lib/flipper/rules/operators/neq.rb | 17 +++++++ lib/flipper/rules/operators/nin.rb | 17 +++++++ lib/flipper/rules/operators/percentage.rb | 21 ++++++++ lib/flipper/rules/percentage.rb | 19 ------- spec/flipper/identifier_spec.rb | 9 ++-- spec/flipper/rules/operator_spec.rb | 22 ++++----- 23 files changed, 232 insertions(+), 209 deletions(-) delete mode 100644 lib/flipper/rules/eq.rb delete mode 100644 lib/flipper/rules/gt.rb delete mode 100644 lib/flipper/rules/gte.rb delete mode 100644 lib/flipper/rules/in.rb delete mode 100644 lib/flipper/rules/lt.rb delete mode 100644 lib/flipper/rules/lte.rb delete mode 100644 lib/flipper/rules/neq.rb delete mode 100644 lib/flipper/rules/nin.rb create mode 100644 lib/flipper/rules/operators/base.rb create mode 100644 lib/flipper/rules/operators/eq.rb create mode 100644 lib/flipper/rules/operators/gt.rb create mode 100644 lib/flipper/rules/operators/gte.rb create mode 100644 lib/flipper/rules/operators/in.rb create mode 100644 lib/flipper/rules/operators/lt.rb create mode 100644 lib/flipper/rules/operators/lte.rb create mode 100644 lib/flipper/rules/operators/neq.rb create mode 100644 lib/flipper/rules/operators/nin.rb create mode 100644 lib/flipper/rules/operators/percentage.rb delete mode 100644 lib/flipper/rules/percentage.rb diff --git a/lib/flipper/rules/eq.rb b/lib/flipper/rules/eq.rb deleted file mode 100644 index ecf2a6bdd..000000000 --- a/lib/flipper/rules/eq.rb +++ /dev/null @@ -1,15 +0,0 @@ -require "flipper/rules/operator" - -module Flipper - module Rules - class Eq < Operator - def initialize - super :eq - end - - def call(left:, right:, **) - left == right - end - end - end -end diff --git a/lib/flipper/rules/gt.rb b/lib/flipper/rules/gt.rb deleted file mode 100644 index f494dea52..000000000 --- a/lib/flipper/rules/gt.rb +++ /dev/null @@ -1,15 +0,0 @@ -require "flipper/rules/operator" - -module Flipper - module Rules - class Gt < Operator - def initialize - super :gt - end - - def call(left:, right:, **) - left && right && left > right - end - end - end -end diff --git a/lib/flipper/rules/gte.rb b/lib/flipper/rules/gte.rb deleted file mode 100644 index 53a9db5db..000000000 --- a/lib/flipper/rules/gte.rb +++ /dev/null @@ -1,15 +0,0 @@ -require "flipper/rules/operator" - -module Flipper - module Rules - class Gte < Operator - def initialize - super :gte - end - - def call(left:, right:, **) - left && right && left >= right - end - end - end -end diff --git a/lib/flipper/rules/in.rb b/lib/flipper/rules/in.rb deleted file mode 100644 index 536591b05..000000000 --- a/lib/flipper/rules/in.rb +++ /dev/null @@ -1,15 +0,0 @@ -require "flipper/rules/operator" - -module Flipper - module Rules - class In < Operator - def initialize - super :in - end - - def call(left:, right:, **) - left && right && right.include?(left) - end - end - end -end diff --git a/lib/flipper/rules/lt.rb b/lib/flipper/rules/lt.rb deleted file mode 100644 index e5ddef902..000000000 --- a/lib/flipper/rules/lt.rb +++ /dev/null @@ -1,15 +0,0 @@ -require "flipper/rules/operator" - -module Flipper - module Rules - class Lt < Operator - def initialize - super :lt - end - - def call(left:, right:, **) - left && right && left < right - end - end - end -end diff --git a/lib/flipper/rules/lte.rb b/lib/flipper/rules/lte.rb deleted file mode 100644 index dca1506f4..000000000 --- a/lib/flipper/rules/lte.rb +++ /dev/null @@ -1,15 +0,0 @@ -require "flipper/rules/operator" - -module Flipper - module Rules - class Lte < Operator - def initialize - super :lte - end - - def call(left:, right:, **) - left && right && left <= right - end - end - end -end diff --git a/lib/flipper/rules/neq.rb b/lib/flipper/rules/neq.rb deleted file mode 100644 index 639fe1f54..000000000 --- a/lib/flipper/rules/neq.rb +++ /dev/null @@ -1,15 +0,0 @@ -require "flipper/rules/operator" - -module Flipper - module Rules - class Neq < Operator - def initialize - super :neq - end - - def call(left:, right:, **) - left != right - end - end - end -end diff --git a/lib/flipper/rules/nin.rb b/lib/flipper/rules/nin.rb deleted file mode 100644 index 8a61b4d28..000000000 --- a/lib/flipper/rules/nin.rb +++ /dev/null @@ -1,15 +0,0 @@ -require "flipper/rules/operator" - -module Flipper - module Rules - class Nin < Operator - def initialize - super :nin - end - - def call(left:, right:, **) - left && right && !right.include?(left) - end - end - end -end diff --git a/lib/flipper/rules/object.rb b/lib/flipper/rules/object.rb index aee481f56..f56f453c4 100644 --- a/lib/flipper/rules/object.rb +++ b/lib/flipper/rules/object.rb @@ -61,55 +61,55 @@ def evaluate(properties) def eq(object) Flipper::Rules::Condition.new( - self, Eq.new, self.class.primitive_or_object(object) + self, Operators::Eq.new, self.class.primitive_or_object(object) ) end def neq(object) Flipper::Rules::Condition.new( - self, Neq.new, self.class.primitive_or_object(object) + self, Operators::Neq.new, self.class.primitive_or_object(object) ) end def gt(object) Flipper::Rules::Condition.new( - self, Gt.new, self.class.integer_or_object(object) + self, Operators::Gt.new, self.class.integer_or_object(object) ) end def gte(object) Flipper::Rules::Condition.new( - self, Gte.new, self.class.integer_or_object(object) + self, Operators::Gte.new, self.class.integer_or_object(object) ) end def lt(object) Flipper::Rules::Condition.new( - self, Lt.new, self.class.integer_or_object(object) + self, Operators::Lt.new, self.class.integer_or_object(object) ) end def lte(object) Flipper::Rules::Condition.new( - self, Lte.new, self.class.integer_or_object(object) + self, Operators::Lte.new, self.class.integer_or_object(object) ) end def in(object) Flipper::Rules::Condition.new( - self, In.new, self.class.array_or_object(object) + self, Operators::In.new, self.class.array_or_object(object) ) end def nin(object) Flipper::Rules::Condition.new( - self, Nin.new, self.class.array_or_object(object) + self, Operators::Nin.new, self.class.array_or_object(object) ) end def percentage(object) Flipper::Rules::Condition.new( - self, Percentage.new, self.class.integer_or_object(object) + self, Operators::Percentage.new, self.class.integer_or_object(object) ) end diff --git a/lib/flipper/rules/operator.rb b/lib/flipper/rules/operator.rb index 3bb0e3473..8107cdab3 100644 --- a/lib/flipper/rules/operator.rb +++ b/lib/flipper/rules/operator.rb @@ -1,17 +1,14 @@ module Flipper module Rules - class Operator - attr_reader :type, :value - - # Builds a Flipper::Rules::Operator based on an object. + module Operator + # Builds a flipper operator based on an object. # - # object - The Hash, String, Symbol or Flipper::Rules::Operator + # object - The Hash, String, Symbol or Flipper::Rules::Operators::* # representation of an operator. # - # Returns Flipper::Rules::Operator. - # Raises Flipper::Errors::OperatorNotFound if not a known operator. + # Returns Flipper::Rules::Operator::* instance. def self.build(object) - return object if object.is_a?(Flipper::Rules::Operator) + return object if object.is_a?(Flipper::Rules::Operators::Base) operator_class = case object when Hash @@ -22,45 +19,18 @@ def self.build(object) raise ArgumentError, "#{object.inspect} cannot be converted into an operator" end - Rules.const_get(operator_class.to_s.capitalize).new - end - - def initialize(value) - @type = "Operator".freeze - @value = value.to_s - end - - def name - @value - end - - def to_h - { - "type" => @type, - "value" => @value, - } - end - - def eql?(other) - self.class.eql?(other.class) && - @type == other.type && - @value == other.value - end - alias_method :==, :eql? - - def call(*args) - raise NotImplementedError + Operators.const_get(operator_class.to_s.capitalize).new end end end end -require "flipper/rules/eq" -require "flipper/rules/neq" -require "flipper/rules/gt" -require "flipper/rules/gte" -require "flipper/rules/lt" -require "flipper/rules/lte" -require "flipper/rules/in" -require "flipper/rules/nin" -require "flipper/rules/percentage" +require "flipper/rules/operators/eq" +require "flipper/rules/operators/neq" +require "flipper/rules/operators/gt" +require "flipper/rules/operators/gte" +require "flipper/rules/operators/lt" +require "flipper/rules/operators/lte" +require "flipper/rules/operators/in" +require "flipper/rules/operators/nin" +require "flipper/rules/operators/percentage" diff --git a/lib/flipper/rules/operators/base.rb b/lib/flipper/rules/operators/base.rb new file mode 100644 index 000000000..cafad5933 --- /dev/null +++ b/lib/flipper/rules/operators/base.rb @@ -0,0 +1,36 @@ +module Flipper + module Rules + module Operators + class Base + attr_reader :type, :value + + def initialize(value) + @type = "Operator".freeze + @value = value.to_s + end + + def name + @value + end + + def to_h + { + "type" => @type, + "value" => @value, + } + end + + def eql?(other) + self.class.eql?(other.class) && + @type == other.type && + @value == other.value + end + alias_method :==, :eql? + + def call(*args) + raise NotImplementedError + end + end + end + end +end diff --git a/lib/flipper/rules/operators/eq.rb b/lib/flipper/rules/operators/eq.rb new file mode 100644 index 000000000..fd75cce16 --- /dev/null +++ b/lib/flipper/rules/operators/eq.rb @@ -0,0 +1,17 @@ +require "flipper/rules/operators/base" + +module Flipper + module Rules + module Operators + class Eq < Base + def initialize + super :eq + end + + def call(left:, right:, **) + left == right + end + end + end + end +end diff --git a/lib/flipper/rules/operators/gt.rb b/lib/flipper/rules/operators/gt.rb new file mode 100644 index 000000000..46a74bee5 --- /dev/null +++ b/lib/flipper/rules/operators/gt.rb @@ -0,0 +1,17 @@ +require "flipper/rules/operators/base" + +module Flipper + module Rules + module Operators + class Gt < Base + def initialize + super :gt + end + + def call(left:, right:, **) + left && right && left > right + end + end + end + end +end diff --git a/lib/flipper/rules/operators/gte.rb b/lib/flipper/rules/operators/gte.rb new file mode 100644 index 000000000..83db568eb --- /dev/null +++ b/lib/flipper/rules/operators/gte.rb @@ -0,0 +1,17 @@ +require "flipper/rules/operators/base" + +module Flipper + module Rules + module Operators + class Gte < Base + def initialize + super :gte + end + + def call(left:, right:, **) + left && right && left >= right + end + end + end + end +end diff --git a/lib/flipper/rules/operators/in.rb b/lib/flipper/rules/operators/in.rb new file mode 100644 index 000000000..d7ca9b6d5 --- /dev/null +++ b/lib/flipper/rules/operators/in.rb @@ -0,0 +1,17 @@ +require "flipper/rules/operators/base" + +module Flipper + module Rules + module Operators + class In < Base + def initialize + super :in + end + + def call(left:, right:, **) + left && right && right.include?(left) + end + end + end + end +end diff --git a/lib/flipper/rules/operators/lt.rb b/lib/flipper/rules/operators/lt.rb new file mode 100644 index 000000000..0e659d52b --- /dev/null +++ b/lib/flipper/rules/operators/lt.rb @@ -0,0 +1,17 @@ +require "flipper/rules/operators/base" + +module Flipper + module Rules + module Operators + class Lt < Base + def initialize + super :lt + end + + def call(left:, right:, **) + left && right && left < right + end + end + end + end +end diff --git a/lib/flipper/rules/operators/lte.rb b/lib/flipper/rules/operators/lte.rb new file mode 100644 index 000000000..c005f98c4 --- /dev/null +++ b/lib/flipper/rules/operators/lte.rb @@ -0,0 +1,17 @@ +require "flipper/rules/operators/base" + +module Flipper + module Rules + module Operators + class Lte < Base + def initialize + super :lte + end + + def call(left:, right:, **) + left && right && left <= right + end + end + end + end +end diff --git a/lib/flipper/rules/operators/neq.rb b/lib/flipper/rules/operators/neq.rb new file mode 100644 index 000000000..c98daa320 --- /dev/null +++ b/lib/flipper/rules/operators/neq.rb @@ -0,0 +1,17 @@ +require "flipper/rules/operators/base" + +module Flipper + module Rules + module Operators + class Neq < Base + def initialize + super :neq + end + + def call(left:, right:, **) + left != right + end + end + end + end +end diff --git a/lib/flipper/rules/operators/nin.rb b/lib/flipper/rules/operators/nin.rb new file mode 100644 index 000000000..6355dcfdc --- /dev/null +++ b/lib/flipper/rules/operators/nin.rb @@ -0,0 +1,17 @@ +require "flipper/rules/operators/base" + +module Flipper + module Rules + module Operators + class Nin < Base + def initialize + super :nin + end + + def call(left:, right:, **) + left && right && !right.include?(left) + end + end + end + end +end diff --git a/lib/flipper/rules/operators/percentage.rb b/lib/flipper/rules/operators/percentage.rb new file mode 100644 index 000000000..b08653340 --- /dev/null +++ b/lib/flipper/rules/operators/percentage.rb @@ -0,0 +1,21 @@ +require "zlib" +require "flipper/rules/operators/base" + +module Flipper + module Rules + module Operators + class Percentage < Base + SCALING_FACTOR = 1_000 + + def initialize + super :percentage + end + + def call(left:, right:, feature_name:, **) + return false unless left && right + Zlib.crc32("#{feature_name}#{left}") % (100 * SCALING_FACTOR) < right * SCALING_FACTOR + end + end + end + end +end diff --git a/lib/flipper/rules/percentage.rb b/lib/flipper/rules/percentage.rb deleted file mode 100644 index 4c1d3f5b4..000000000 --- a/lib/flipper/rules/percentage.rb +++ /dev/null @@ -1,19 +0,0 @@ -require "zlib" -require "flipper/rules/operator" - -module Flipper - module Rules - class Percentage < Operator - SCALING_FACTOR = 1_000 - - def initialize - super :percentage - end - - def call(left:, right:, feature_name:, **) - return false unless left && right - Zlib.crc32("#{feature_name}#{left}") % (100 * SCALING_FACTOR) < right * SCALING_FACTOR - end - end - end -end diff --git a/spec/flipper/identifier_spec.rb b/spec/flipper/identifier_spec.rb index 2ac3fb80c..728899046 100644 --- a/spec/flipper/identifier_spec.rb +++ b/spec/flipper/identifier_spec.rb @@ -3,12 +3,11 @@ RSpec.describe Flipper::Identifier do describe '#flipper_id' do - class User < Struct.new(:id) - include Flipper::Identifier - end - it 'uses class name and id' do - expect(User.new(5).flipper_id).to eq('User;5') + class BlahBlah < Struct.new(:id) + include Flipper::Identifier + end + expect(BlahBlah.new(5).flipper_id).to eq('BlahBlah;5') end end end diff --git a/spec/flipper/rules/operator_spec.rb b/spec/flipper/rules/operator_spec.rb index 0def8d974..cf8df9ee9 100644 --- a/spec/flipper/rules/operator_spec.rb +++ b/spec/flipper/rules/operator_spec.rb @@ -5,7 +5,7 @@ context "with Hash" do it "returns instance" do instance = described_class.build({"type" => "Operator", "value" => "eq"}) - expect(instance).to be_a(described_class) + expect(instance).to be_a(Flipper::Rules::Operators::Base) expect(instance.name).to eq("eq") end end @@ -13,7 +13,7 @@ context "with String" do it "returns instance" do instance = described_class.build("eq") - expect(instance).to be_a(described_class) + expect(instance).to be_a(Flipper::Rules::Operators::Base) expect(instance.name).to eq("eq") end end @@ -21,15 +21,15 @@ context "with Symbol" do it "returns instance" do instance = described_class.build(:eq) - expect(instance).to be_a(described_class) + expect(instance).to be_a(Flipper::Rules::Operators::Base) expect(instance.name).to eq("eq") end end context "with instance" do it "returns intance" do - instance = described_class.build(described_class.new("eq")) - expect(instance).to be_instance_of(described_class) + instance = described_class.build(described_class.build(:eq)) + expect(instance).to be_instance_of(Flipper::Rules::Operators::Eq) expect(instance.name).to eq("eq") end end @@ -37,19 +37,19 @@ describe "#initialize" do it "works with string name" do - instance = described_class.new("eq") + instance = described_class.build("eq") expect(instance.name).to eq("eq") end it "works with symbol name" do - instance = described_class.new(:eq) + instance = described_class.build(:eq) expect(instance.name).to eq("eq") end end describe "#to_h" do it "returns Hash with type and value" do - expect(described_class.new("eq").to_h).to eq({ + expect(described_class.build("eq").to_h).to eq({ "type" => "Operator", "value" => "eq", }) @@ -58,15 +58,15 @@ describe "equality" do it "returns true if equal" do - expect(described_class.new("eq").eql?(described_class.new("eq"))).to be(true) + expect(described_class.build("eq").eql?(described_class.build("eq"))).to be(true) end it "returns false if name does not match" do - expect(described_class.new("eq").eql?(described_class.new("neq"))).to be(false) + expect(described_class.build("eq").eql?(described_class.build("neq"))).to be(false) end it "returns false for different class" do - expect(described_class.new("eq").eql?(Object.new)).to be(false) + expect(described_class.build("eq").eql?(Object.new)).to be(false) end end end From 03e4268e355f8d3910ab4fec23c03afff7769380 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 8 Sep 2021 21:03:55 -0400 Subject: [PATCH 070/176] Rename wrap to build --- lib/flipper/rules/condition.rb | 4 ++-- lib/flipper/rules/object.rb | 2 +- spec/flipper/rules/object_spec.rb | 18 +++++++++--------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/flipper/rules/condition.rb b/lib/flipper/rules/condition.rb index 7b99f7bb9..5bcc34edc 100644 --- a/lib/flipper/rules/condition.rb +++ b/lib/flipper/rules/condition.rb @@ -11,9 +11,9 @@ def self.build(hash) attr_reader :left, :operator, :right def initialize(left, operator, right) - @left = Object.wrap(left) + @left = Object.build(left) @operator = Operator.build(operator) - @right = Object.wrap(right) + @right = Object.build(right) end def value diff --git a/lib/flipper/rules/object.rb b/lib/flipper/rules/object.rb index f56f453c4..147298222 100644 --- a/lib/flipper/rules/object.rb +++ b/lib/flipper/rules/object.rb @@ -16,7 +16,7 @@ class Object SUPPORTED_TYPE_CLASSES = SUPPORTED_TYPES_MAP.keys.freeze SUPPORTED_TYPE_NAMES = SUPPORTED_TYPES_MAP.values.freeze - def self.wrap(object) + def self.build(object) return object if object.is_a?(Flipper::Rules::Object) if object.is_a?(Hash) diff --git a/spec/flipper/rules/object_spec.rb b/spec/flipper/rules/object_spec.rb index 8a285420a..a9cfbd7f7 100644 --- a/spec/flipper/rules/object_spec.rb +++ b/spec/flipper/rules/object_spec.rb @@ -4,7 +4,7 @@ describe ".wrap" do context "with Hash" do it "returns instance" do - instance = described_class.wrap({"type" => "Integer", "value" => 2}) + instance = described_class.build({"type" => "Integer", "value" => 2}) expect(instance).to be_instance_of(described_class) expect(instance.type).to eq("Integer") expect(instance.value).to eq(2) @@ -13,7 +13,7 @@ context "with instance" do it "returns instance" do - instance = described_class.wrap(described_class.new(2)) + instance = described_class.build(described_class.new(2)) expect(instance).to be_instance_of(described_class) expect(instance.type).to eq("Integer") expect(instance.value).to eq(2) @@ -22,7 +22,7 @@ context "with string" do it "returns instance" do - instance = described_class.wrap("test") + instance = described_class.build("test") expect(instance).to be_instance_of(described_class) expect(instance.type).to eq("String") expect(instance.value).to eq("test") @@ -31,7 +31,7 @@ context "with integer" do it "returns instance" do - instance = described_class.wrap(21) + instance = described_class.build(21) expect(instance).to be_instance_of(described_class) expect(instance.type).to eq("Integer") expect(instance.value).to eq(21) @@ -40,7 +40,7 @@ context "with nil" do it "returns instance" do - instance = described_class.wrap(nil) + instance = described_class.build(nil) expect(instance).to be_instance_of(described_class) expect(instance.type).to eq("Null") expect(instance.value).to be(nil) @@ -49,7 +49,7 @@ context "with true" do it "returns instance" do - instance = described_class.wrap(true) + instance = described_class.build(true) expect(instance).to be_instance_of(described_class) expect(instance.type).to eq("Boolean") expect(instance.value).to be(true) @@ -58,7 +58,7 @@ context "with false" do it "returns instance" do - instance = described_class.wrap(false) + instance = described_class.build(false) expect(instance).to be_instance_of(described_class) expect(instance.type).to eq("Boolean") expect(instance.value).to be(false) @@ -67,7 +67,7 @@ context "with array" do it "returns instance" do - instance = described_class.wrap(["test"]) + instance = described_class.build(["test"]) expect(instance).to be_instance_of(described_class) expect(instance.type).to eq("Array") expect(instance.value).to eq(["test"]) @@ -77,7 +77,7 @@ context "with unsupported type" do it "raises ArgumentError" do expect { - described_class.wrap(Set.new) + described_class.build(Set.new) }.to raise_error(ArgumentError, /is not a supported primitive\. Object must be one of: String, Integer, NilClass, TrueClass, FalseClass, Array\./) end end From 41157f1b9deb0a2fd0ee8473f1f3055107e272c1 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Thu, 9 Sep 2021 08:24:25 -0400 Subject: [PATCH 071/176] Get sequel model and adapter specs to get along --- spec/flipper/adapters/sequel_spec.rb | 5 ----- spec/flipper/model/sequel_spec.rb | 3 --- spec/support/sequel_setup.rb | 5 +++++ 3 files changed, 5 insertions(+), 8 deletions(-) create mode 100644 spec/support/sequel_setup.rb diff --git a/spec/flipper/adapters/sequel_spec.rb b/spec/flipper/adapters/sequel_spec.rb index 5117123c3..d1a02a42c 100644 --- a/spec/flipper/adapters/sequel_spec.rb +++ b/spec/flipper/adapters/sequel_spec.rb @@ -1,9 +1,4 @@ require 'helper' -require 'sequel' - -Sequel::Model.db = Sequel.sqlite(':memory:') -Sequel.extension :migration, :core_extensions - require 'flipper/adapters/sequel' require 'generators/flipper/templates/sequel_migration' require 'flipper/spec/shared_adapter_specs' diff --git a/spec/flipper/model/sequel_spec.rb b/spec/flipper/model/sequel_spec.rb index a5e631aef..9a453e3cf 100644 --- a/spec/flipper/model/sequel_spec.rb +++ b/spec/flipper/model/sequel_spec.rb @@ -1,9 +1,6 @@ require 'helper' -require 'sequel' require 'flipper/model/sequel' -Sequel::Model.db = Sequel.sqlite(':memory:') - RSpec.describe Flipper::Model::Sequel do before(:each) do Sequel::Model.db.run <<-SQL diff --git a/spec/support/sequel_setup.rb b/spec/support/sequel_setup.rb new file mode 100644 index 000000000..131776a45 --- /dev/null +++ b/spec/support/sequel_setup.rb @@ -0,0 +1,5 @@ +# Setup sequel for sequel adapter and model specs. We don't want this to happen +# multiple times or it causes failures. So it lives here. +require "sequel" +Sequel::Model.db = Sequel.sqlite(':memory:') +Sequel.extension :migration, :core_extensions From 8bcb12cb7d82a862918e115f7d80377af95ad888 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Thu, 9 Sep 2021 08:11:17 -0500 Subject: [PATCH 072/176] Warn if value column is not text --- lib/flipper/adapters/active_record.rb | 22 +++++++++++--- lib/flipper/adapters/sequel.rb | 30 +++++++++++++++---- .../flipper/templates/sequel_migration.rb | 2 +- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/lib/flipper/adapters/active_record.rb b/lib/flipper/adapters/active_record.rb index 7b7174f7d..d0e5a9cde 100644 --- a/lib/flipper/adapters/active_record.rb +++ b/lib/flipper/adapters/active_record.rb @@ -29,6 +29,11 @@ class Gate < ::ActiveRecord::Base serialize :value, JSON end + VALUE_TO_TEXT_WARNING = <<-EOS + Your database needs migrated to use the latest Flipper features. + See https://github.com/jnunemaker/flipper/issues/557 + EOS + # Public: The name of the adapter. attr_reader :name @@ -48,6 +53,8 @@ def initialize(options = {}) @name = options.fetch(:name, :active_record) @feature_class = options.fetch(:feature_class) { Feature } @gate_class = options.fetch(:gate_class) { Gate } + + warn VALUE_TO_TEXT_WARNING unless value_is_text? end # Public: The set of known features. @@ -128,7 +135,10 @@ def enable(feature, gate, thing) set(feature, gate, thing, clear: true) when :integer set(feature, gate, thing) - when :set, :json + when :set + enable_multi(feature, gate, thing) + when :json + raise VALUE_TO_TEXT_WARNING unless value_is_text? enable_multi(feature, gate, thing) else unsupported_data_type gate.data_type @@ -150,9 +160,7 @@ def disable(feature, gate, thing) clear(feature) when :integer set(feature, gate, thing) - when :set - @gate_class.where(feature_key: feature.key, key: gate.key, value: thing.value).destroy_all - when :json + when :set, :json @gate_class.where(feature_key: feature.key, key: gate.key, value: thing.value).destroy_all else unsupported_data_type gate.data_type @@ -222,6 +230,12 @@ def result_for_feature(feature, db_gates) end result end + + # Check if value column is text instead of string + # See TODO:link/to/PR + def value_is_text? + @gate_class.column_for_attribute(:value).type == :text + end end end end diff --git a/lib/flipper/adapters/sequel.rb b/lib/flipper/adapters/sequel.rb index 986069afd..d75818ffb 100644 --- a/lib/flipper/adapters/sequel.rb +++ b/lib/flipper/adapters/sequel.rb @@ -29,6 +29,11 @@ class Gate < ::Sequel::Model(:flipper_gates) ::Sequel::Model.require_valid_table = old end + VALUE_TO_TEXT_WARNING = <<-EOS + Your database needs migrated to use the latest Flipper features. + See https://github.com/jnunemaker/flipper/issues/557 + EOS + # Public: The name of the adapter. attr_reader :name @@ -48,6 +53,8 @@ def initialize(options = {}) @name = options.fetch(:name, :sequel) @feature_class = options.fetch(:feature_class) { Feature } @gate_class = options.fetch(:gate_class) { Gate } + + warn VALUE_TO_TEXT_WARNING unless value_is_text? end # Public: The set of known features. @@ -129,11 +136,11 @@ def enable(feature, gate, thing) set(feature, gate, thing, clear: true) when :integer set(feature, gate, thing) - when :set, :json - begin - @gate_class.create(gate_attrs(feature, gate, thing, json: gate.data_type == :json)) - rescue ::Sequel::UniqueConstraintViolation - end + when :set + enable_multi(feature, gate, thing) + when :json + raise VALUE_TO_TEXT_WARNING unless value_is_text? + enable_multi(feature, gate, thing) else unsupported_data_type gate.data_type end @@ -187,6 +194,13 @@ def set(feature, gate, thing, options = {}) end end + def enable_multi(feature, gate, thing) + begin + @gate_class.create(gate_attrs(feature, gate, thing, json: gate.data_type == :json)) + rescue ::Sequel::UniqueConstraintViolation + end + end + def gate_attrs(feature, gate, thing, json: false) { feature_key: feature.key.to_s, @@ -217,6 +231,12 @@ def result_for_feature(feature, db_gates) end end end + + # Check if value column is text instead of string + # See TODO:link/to/PR + def value_is_text? + @gate_class.db_schema[:value][:db_type] == "text" + end end end end diff --git a/lib/generators/flipper/templates/sequel_migration.rb b/lib/generators/flipper/templates/sequel_migration.rb index 83e772219..0ed08b7f0 100644 --- a/lib/generators/flipper/templates/sequel_migration.rb +++ b/lib/generators/flipper/templates/sequel_migration.rb @@ -9,7 +9,7 @@ def up create_table :flipper_gates do |_t| String :feature_key, null: false String :key, null: false - Text :value + String :value, text: true DateTime :created_at, null: false DateTime :updated_at, null: false primary_key [:feature_key, :key, :value] From 0d522eacffc9fd3212d37531fb2a0b36ef93c4e5 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Thu, 9 Sep 2021 08:48:44 -0500 Subject: [PATCH 073/176] Initialize adapter after creating tables in tests --- test/adapters/active_record_test.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/adapters/active_record_test.rb b/test/adapters/active_record_test.rb index 6ee3aadb3..698ba67c4 100644 --- a/test/adapters/active_record_test.rb +++ b/test/adapters/active_record_test.rb @@ -11,8 +11,6 @@ class ActiveRecordTest < MiniTest::Test database: ':memory:') def setup - @adapter = Flipper::Adapters::ActiveRecord.new - ActiveRecord::Base.connection.execute <<-SQL CREATE TABLE flipper_features ( id integer PRIMARY KEY, @@ -36,6 +34,8 @@ def setup ActiveRecord::Base.connection.execute <<-SQL CREATE UNIQUE INDEX index_gates_on_keys_and_value on flipper_gates (feature_key, key, value) SQL + + @adapter = Flipper::Adapters::ActiveRecord.new end def teardown From 73fc82eb06da4283dc0ddde80cf051a8cd3f064e Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Thu, 9 Sep 2021 09:15:24 -0500 Subject: [PATCH 074/176] Silence warnings when re-requiring adapter --- spec/flipper/adapters/active_record_spec.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/flipper/adapters/active_record_spec.rb b/spec/flipper/adapters/active_record_spec.rb index 82765db96..98c7116f8 100644 --- a/spec/flipper/adapters/active_record_spec.rb +++ b/spec/flipper/adapters/active_record_spec.rb @@ -51,8 +51,7 @@ Flipper.configuration = nil Flipper.instance = nil - load 'flipper/adapters/active_record.rb' - ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base) + silence_warnings { load 'flipper/adapters/active_record.rb' } end it 'configures itself' do From 33ff1c4925feec3e87940b5d605701463feb8332 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Thu, 23 Sep 2021 19:51:55 -0400 Subject: [PATCH 075/176] Remove arrays They complicate updating rules so for now we'll remove in an effort to get something shipped. Then, we can come back later and determine if arrays are really helpful and how to make them easy. --- examples/rules.rb | 14 ++- lib/flipper/rules/object.rb | 13 --- spec/flipper/rules/condition_spec.rb | 48 --------- spec/flipper/rules/object_spec.rb | 153 +-------------------------- spec/flipper/rules/property_spec.rb | 92 ---------------- 5 files changed, 11 insertions(+), 309 deletions(-) diff --git a/examples/rules.rb b/examples/rules.rb index 15f745f9e..cb3613ae1 100644 --- a/examples/rules.rb +++ b/examples/rules.rb @@ -35,13 +35,14 @@ class Org < Struct.new(:id, :flipper_properties) "id" => 1, "plan" => "basic", "age" => 39, - "roles" => ["team_user"] + "team_user" => true, }) admin_user = User.new(2, { "type" => "User", "id" => 2, - "roles" => ["admin", "team_user"], + "admin" => true, + "team_user" => true, }) other_user = User.new(3, { @@ -49,12 +50,12 @@ class Org < Struct.new(:id, :flipper_properties) "id" => 3, "plan" => "plus", "age" => 18, - "roles" => ["org_admin"] + "org_admin" => true, }) age_rule = Flipper.property(:age).gte(21) plan_rule = Flipper.property(:plan).eq("basic") -admin_rule = Flipper.object("admin").in(Flipper.property(:roles)) +admin_rule = Flipper.property(:admin).eq(true) puts "Single Rule" refute Flipper.enabled?(:something, user) @@ -123,7 +124,10 @@ class Org < Struct.new(:id, :flipper_properties) Flipper.disable_rule :something, boolean_rule puts "\n\nSet of Actors Rule" -set_of_actors_rule = Flipper.property(:flipper_id).in(["User;1", "User;3"]) +set_of_actors_rule = Flipper.any( + Flipper.property(:flipper_id).eq("User;1"), + Flipper.property(:flipper_id).eq("User;3"), +) Flipper.enable_rule :something, set_of_actors_rule assert Flipper.enabled?(:something, user) assert Flipper.enabled?(:something, other_user) diff --git a/lib/flipper/rules/object.rb b/lib/flipper/rules/object.rb index 147298222..8c958724b 100644 --- a/lib/flipper/rules/object.rb +++ b/lib/flipper/rules/object.rb @@ -10,7 +10,6 @@ class Object NilClass => "Null", TrueClass => "Boolean", FalseClass => "Boolean", - Array => "Array", }.freeze SUPPORTED_TYPE_CLASSES = SUPPORTED_TYPES_MAP.keys.freeze @@ -95,18 +94,6 @@ def lte(object) ) end - def in(object) - Flipper::Rules::Condition.new( - self, Operators::In.new, self.class.array_or_object(object) - ) - end - - def nin(object) - Flipper::Rules::Condition.new( - self, Operators::Nin.new, self.class.array_or_object(object) - ) - end - def percentage(object) Flipper::Rules::Condition.new( self, Operators::Percentage.new, self.class.integer_or_object(object) diff --git a/spec/flipper/rules/condition_spec.rb b/spec/flipper/rules/condition_spec.rb index 449f06ce5..6f4051414 100644 --- a/spec/flipper/rules/condition_spec.rb +++ b/spec/flipper/rules/condition_spec.rb @@ -285,54 +285,6 @@ def flipper_id end end - context "in" do - let(:rule) { - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "age"}, - {"type" => "Operator", "value" => "in"}, - {"type" => "Array", "value" => [20, 21, 22]} - ) - } - - it "returns true when property matches" do - actor = Flipper::Actor.new("User;1", { - "age" => 21, - }) - expect(rule.matches?(feature_name, actor)).to be(true) - end - - it "returns false when property does NOT match" do - actor = Flipper::Actor.new("User;1", { - "age" => 10, - }) - expect(rule.matches?(feature_name, actor)).to be(false) - end - end - - context "nin" do - let(:rule) { - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "age"}, - {"type" => "Operator", "value" => "nin"}, - {"type" => "Array", "value" => [20, 21, 22]} - ) - } - - it "returns true when property matches" do - actor = Flipper::Actor.new("User;1", { - "age" => 10, - }) - expect(rule.matches?(feature_name, actor)).to be(true) - end - - it "returns false when property does NOT match" do - actor = Flipper::Actor.new("User;1", { - "age" => 20, - }) - expect(rule.matches?(feature_name, actor)).to be(false) - end - end - context "percentage" do let(:rule) { Flipper::Rules::Condition.new( diff --git a/spec/flipper/rules/object_spec.rb b/spec/flipper/rules/object_spec.rb index a9cfbd7f7..28e9c9843 100644 --- a/spec/flipper/rules/object_spec.rb +++ b/spec/flipper/rules/object_spec.rb @@ -65,20 +65,11 @@ end end - context "with array" do - it "returns instance" do - instance = described_class.build(["test"]) - expect(instance).to be_instance_of(described_class) - expect(instance.type).to eq("Array") - expect(instance.value).to eq(["test"]) - end - end - context "with unsupported type" do it "raises ArgumentError" do expect { described_class.build(Set.new) - }.to raise_error(ArgumentError, /is not a supported primitive\. Object must be one of: String, Integer, NilClass, TrueClass, FalseClass, Array\./) + }.to raise_error(ArgumentError, /is not a supported primitive\. Object must be one of: String, Integer, NilClass, TrueClass, FalseClass\./) end end end @@ -124,19 +115,11 @@ end end - context "with array" do - it "returns instance" do - instance = described_class.new(["test"]) - expect(instance.type).to eq("Array") - expect(instance.value).to eq(["test"]) - end - end - context "with unsupported type" do it "raises ArgumentError" do expect { described_class.new({}) - }.to raise_error(ArgumentError, /{} is not a supported primitive\. Object must be one of: String, Integer, NilClass, TrueClass, FalseClass, Array\./) + }.to raise_error(ArgumentError, /{} is not a supported primitive\. Object must be one of: String, Integer, NilClass, TrueClass, FalseClass\./) end end end @@ -195,16 +178,6 @@ end end - context "with array" do - it "returns equal condition" do - expect(described_class.new("roles").eq(["admin"])).to eq(Flipper::Rules::Condition.new( - {"type" => "String", "value" => "roles"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "Array", "value" => ["admin"]} - )) - end - end - context "with nil" do it "returns equal condition" do expect(described_class.new("admin").eq(nil)).to eq(Flipper::Rules::Condition.new( @@ -267,16 +240,6 @@ end end - context "with array" do - it "returns not equal condition" do - expect(described_class.new("roles").neq(["admin"])).to eq(Flipper::Rules::Condition.new( - {"type" => "String", "value" => "roles"}, - {"type" => "Operator", "value" => "neq"}, - {"type" => "Array", "value" => ["admin"]} - )) - end - end - context "with nil" do it "returns not equal condition" do expect(described_class.new("admin").neq(nil)).to eq(Flipper::Rules::Condition.new( @@ -532,118 +495,6 @@ end end - describe "#in" do - context "with array" do - it "returns condition" do - expect(described_class.new("role").in(["admin"])).to eq(Flipper::Rules::Condition.new( - {"type" => "String", "value" => "role"}, - {"type" => "Operator", "value" => "in"}, - {"type" => "Array", "value" => ["admin"]} - )) - end - end - - context "with property" do - it "returns condition" do - expect(described_class.new("admin").in(Flipper.property(:roles))).to eq(Flipper::Rules::Condition.new( - {"type" => "String", "value" => "admin"}, - {"type" => "Operator", "value" => "in"}, - {"type" => "Property", "value" => "roles"} - )) - end - end - - context "with object" do - it "returns condition" do - expect(described_class.new("admin").in(Flipper.object(["admin"]))).to eq(Flipper::Rules::Condition.new( - {"type" => "String", "value" => "admin"}, - {"type" => "Operator", "value" => "in"}, - {"type" => "Array", "value" => ["admin"]} - )) - end - end - - context "with string" do - it "raises error" do - expect { described_class.new("role").in("231") }.to raise_error(ArgumentError) - end - end - - context "with boolean" do - it "raises error" do - expect { described_class.new("role").in(true) }.to raise_error(ArgumentError) - end - end - - context "with integer" do - it "raises error" do - expect { described_class.new("role").in(21) }.to raise_error(ArgumentError) - end - end - - context "with nil" do - it "raises error" do - expect { described_class.new("role").in(nil) }.to raise_error(ArgumentError) - end - end - end - - describe "#nin" do - context "with array" do - it "returns condition" do - expect(described_class.new("role").nin(["admin"])).to eq(Flipper::Rules::Condition.new( - {"type" => "String", "value" => "role"}, - {"type" => "Operator", "value" => "nin"}, - {"type" => "Array", "value" => ["admin"]} - )) - end - end - - context "with property" do - it "returns condition" do - expect(described_class.new("admin").nin(Flipper.property(:roles))).to eq(Flipper::Rules::Condition.new( - {"type" => "String", "value" => "admin"}, - {"type" => "Operator", "value" => "nin"}, - {"type" => "Property", "value" => "roles"} - )) - end - end - - context "with object" do - it "returns condition" do - expect(described_class.new("admin").nin(Flipper.object(["admin"]))).to eq(Flipper::Rules::Condition.new( - {"type" => "String", "value" => "admin"}, - {"type" => "Operator", "value" => "nin"}, - {"type" => "Array", "value" => ["admin"]} - )) - end - end - - context "with string" do - it "raises error" do - expect { described_class.new("role").nin("231") }.to raise_error(ArgumentError) - end - end - - context "with boolean" do - it "raises error" do - expect { described_class.new("role").nin(true) }.to raise_error(ArgumentError) - end - end - - context "with integer" do - it "raises error" do - expect { described_class.new("role").nin(21) }.to raise_error(ArgumentError) - end - end - - context "with nil" do - it "raises error" do - expect { described_class.new("role").nin(nil) }.to raise_error(ArgumentError) - end - end - end - describe "#percentage" do context "with integer" do it "returns condition" do diff --git a/spec/flipper/rules/property_spec.rb b/spec/flipper/rules/property_spec.rb index 8e28e2292..8820ece4e 100644 --- a/spec/flipper/rules/property_spec.rb +++ b/spec/flipper/rules/property_spec.rb @@ -67,16 +67,6 @@ end end - context "with array" do - it "returns equal condition" do - expect(described_class.new(:roles).eq(["admin"])).to eq(Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "roles"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "Array", "value" => ["admin"]} - )) - end - end - context "with nil" do it "returns equal condition" do expect(described_class.new(:admin).eq(nil)).to eq(Flipper::Rules::Condition.new( @@ -119,16 +109,6 @@ end end - context "with array" do - it "returns not equal condition" do - expect(described_class.new(:roles).neq(["admin"])).to eq(Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "roles"}, - {"type" => "Operator", "value" => "neq"}, - {"type" => "Array", "value" => ["admin"]} - )) - end - end - context "with nil" do it "returns not equal condition" do expect(described_class.new(:admin).neq(nil)).to eq(Flipper::Rules::Condition.new( @@ -284,78 +264,6 @@ end end - describe "#in" do - context "with array" do - it "returns condition" do - expect(described_class.new(:role).in(["admin"])).to eq(Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "role"}, - {"type" => "Operator", "value" => "in"}, - {"type" => "Array", "value" => ["admin"]} - )) - end - end - - context "with string" do - it "raises error" do - expect { described_class.new(:role).in("231") }.to raise_error(ArgumentError) - end - end - - context "with boolean" do - it "raises error" do - expect { described_class.new(:role).in(true) }.to raise_error(ArgumentError) - end - end - - context "with integer" do - it "raises error" do - expect { described_class.new(:role).in(21) }.to raise_error(ArgumentError) - end - end - - context "with nil" do - it "raises error" do - expect { described_class.new(:role).in(nil) }.to raise_error(ArgumentError) - end - end - end - - describe "#nin" do - context "with array" do - it "returns condition" do - expect(described_class.new(:role).nin(["admin"])).to eq(Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "role"}, - {"type" => "Operator", "value" => "nin"}, - {"type" => "Array", "value" => ["admin"]} - )) - end - end - - context "with string" do - it "raises error" do - expect { described_class.new(:role).nin("231") }.to raise_error(ArgumentError) - end - end - - context "with boolean" do - it "raises error" do - expect { described_class.new(:role).nin(true) }.to raise_error(ArgumentError) - end - end - - context "with integer" do - it "raises error" do - expect { described_class.new(:role).nin(21) }.to raise_error(ArgumentError) - end - end - - context "with nil" do - it "raises error" do - expect { described_class.new(:role).nin(nil) }.to raise_error(ArgumentError) - end - end - end - describe "#percentage" do context "with integer" do it "returns condition" do From fa4ba5924ff6b294939d29c0cdde068d297d283c Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Thu, 23 Sep 2021 21:19:12 -0400 Subject: [PATCH 076/176] Switch to single rule per feature instead of set of rules per feature --- docs/api/README.md | 100 +++++++++--------- lib/flipper/adapter.rb | 2 +- lib/flipper/adapters/active_record.rb | 35 +++--- lib/flipper/adapters/http.rb | 6 +- lib/flipper/adapters/memory.rb | 4 +- lib/flipper/adapters/moneta.rb | 4 +- lib/flipper/adapters/mongo.rb | 17 ++- lib/flipper/adapters/pstore.rb | 12 ++- lib/flipper/adapters/redis.rb | 15 +-- lib/flipper/adapters/rollout.rb | 2 +- lib/flipper/adapters/sequel.rb | 32 +++--- .../adapters/sync/feature_synchronizer.rb | 18 ++-- lib/flipper/api/v1/actions/rules_gate.rb | 7 +- lib/flipper/api/v1/decorators/gate.rb | 2 +- lib/flipper/dsl.rb | 2 +- lib/flipper/feature.rb | 16 +-- lib/flipper/gate_values.rb | 8 +- lib/flipper/gates/rule.rb | 13 ++- lib/flipper/rules/object.rb | 11 -- lib/flipper/rules/operator.rb | 2 - lib/flipper/spec/shared_adapter_specs.rb | 25 ++--- lib/flipper/test/shared_adapter_test.rb | 25 ++--- spec/flipper/adapter_spec.rb | 2 +- spec/flipper/adapters/read_only_spec.rb | 30 +++--- spec/flipper/adapters/rollout_spec.rb | 6 +- .../sync/feature_synchronizer_spec.rb | 29 ++--- spec/flipper/api/v1/actions/feature_spec.rb | 16 +-- spec/flipper/api/v1/actions/features_spec.rb | 8 +- .../flipper/api/v1/actions/rules_gate_spec.rb | 64 ++++++----- spec/flipper/cloud_spec.rb | 24 ++--- spec/flipper/feature_spec.rb | 22 ++-- spec/flipper_spec.rb | 2 +- 32 files changed, 261 insertions(+), 300 deletions(-) diff --git a/docs/api/README.md b/docs/api/README.md index c2cb73b30..5debfc4d7 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -93,9 +93,9 @@ Returns an array of feature objects: "value": [] }, { - "key": "rules", + "key": "rule", "name": "rule", - "value": [] + "value": nil }, { "key": "percentage_of_actors", @@ -129,9 +129,9 @@ Returns an array of feature objects: "value": [] }, { - "key": "rules", + "key": "rule", "name": "rule", - "value": [] + "value": nil }, { "key": "percentage_of_actors", @@ -210,9 +210,9 @@ Returns an individual feature object: "value": [] }, { - "key": "rules", + "key": "rule", "name": "rule", - "value": [] + "value": nil }, { "key": "percentage_of_actors", @@ -323,9 +323,9 @@ Successful enabling of the boolean gate will return a 200 HTTP status and the fe "value": [] }, { - "key": "rules", + "key": "rule", "name": "rule", - "value": [] + "value": nil }, { "key": "percentage_of_actors", @@ -383,9 +383,9 @@ Successful disabling of the boolean gate will return a 200 HTTP status and the f "value": [] }, { - "key": "rules", + "key": "rule", "name": "rule", - "value": [] + "value": nil }, { "key": "percentage_of_actors", @@ -405,7 +405,7 @@ Successful disabling of the boolean gate will return a 200 HTTP status and the f **URL** -`POST /features/{feature_name}/rules` +`POST /features/{feature_name}/rule` **Parameters** @@ -413,12 +413,12 @@ Successful disabling of the boolean gate will return a 200 HTTP status and the f * `type` - The type of rule being enabled -* `value` - The JSON representation of the rule being enabled. +* `value` - The JSON representation of the rule. **Request** ``` -curl -X POST -H "Content-Type: application/json" -d '{"type":"Condition","value":{"left":{"type":"Property","value":"plan"},"operator":{"type":"Operator","value":"eq"},"right":{"type":"String","value":"basic"}}}' http://example.com/flipper/api/features/reports/rules +curl -X POST -H "Content-Type: application/json" -d '{"type":"Condition","value":{"left":{"type":"Property","value":"plan"},"operator":{"type":"Operator","value":"eq"},"right":{"type":"String","value":"basic"}}}' http://example.com/flipper/api/features/reports/rule ``` **Response** @@ -441,24 +441,22 @@ Successful enabling of the group will return a 200 HTTP status and the feature o "value": [] }, { - "key": "rules", + "key": "rule", "name": "rule", - "value": [ - { - "type": "Condition", - "value": { - "left": { - "type": "Property", - "value": "plan" - }, - "operator": { - "type": "Operator", - "value": "eq" - }, - "right": { - "type": "String", - "value": "basic" - } + "value": { + "type": "Condition", + "value": { + "left": { + "type": "Property", + "value": "plan" + }, + "operator": { + "type": "Operator", + "value": "eq" + }, + "right": { + "type": "String", + "value": "basic" } } ] @@ -486,7 +484,7 @@ Successful enabling of the group will return a 200 HTTP status and the feature o **URL** -`DELETE /features/{feature_name}/rules` +`DELETE /features/{feature_name}/rule` **Parameters** @@ -494,12 +492,12 @@ Successful enabling of the group will return a 200 HTTP status and the feature o * `type` - The type of rule being enabled -* `value` - The JSON representation of the rule being enabled. +* `value` - The JSON representation of the rule. **Request** ``` -curl -X DELETE -H "Content-Type: application/json" -d '{"type":"Condition","value":{"left":{"type":"Property","value":"plan"},"operator":{"type":"Operator","value":"eq"},"right":{"type":"String","value":"basic"}}}' http://example.com/flipper/api/features/reports/rules +curl -X DELETE -H "Content-Type: application/json" -d '{"type":"Condition","value":{"left":{"type":"Property","value":"plan"},"operator":{"type":"Operator","value":"eq"},"right":{"type":"String","value":"basic"}}}' http://example.com/flipper/api/features/reports/rule ``` **Response** @@ -522,9 +520,9 @@ Successful disabling of the group will return a 200 HTTP status and the feature "value": [] }, { - "key": "rules", + "key": "rule", "name": "rule", - "value": [] + "value": nil }, { "key": "percentage_of_actors", @@ -588,9 +586,9 @@ Successful enabling of the group will return a 200 HTTP status and the feature o "value": [] }, { - "key": "rules", + "key": "rule", "name": "rule", - "value": [] + "value": nil }, { "key": "percentage_of_actors", @@ -649,9 +647,9 @@ Successful disabling of the group will return a 200 HTTP status and the feature "value": [] }, { - "key": "rules", + "key": "rule", "name": "rule", - "value": [] + "value": nil }, { "key": "percentage_of_actors", @@ -710,9 +708,9 @@ Successful enabling of the actor will return a 200 HTTP status and the feature o "value": ["User;1"] }, { - "key": "rules", + "key": "rule", "name": "rule", - "value": [] + "value": nil }, { "key": "percentage_of_actors", @@ -771,9 +769,9 @@ Successful disabling of the actor will return a 200 HTTP status and the feature "value": [] }, { - "key": "rules", + "key": "rule", "name": "rule", - "value": [] + "value": nil }, { "key": "percentage_of_actors", @@ -832,9 +830,9 @@ Successful enabling of a percentage of actors will return a 200 HTTP status and "value": [] }, { - "key": "rules", + "key": "rule", "name": "rule", - "value": [] + "value": nil }, { "key": "percentage_of_actors", @@ -891,9 +889,9 @@ Successful disabling of a percentage of actors will set the percentage to 0 and "value": [] }, { - "key": "rules", + "key": "rule", "name": "rule", - "value": [] + "value": nil }, { "key": "percentage_of_actors", @@ -952,9 +950,9 @@ Successful enabling of a percentage of time will return a 200 HTTP status and th "value": [] }, { - "key": "rules", + "key": "rule", "name": "rule", - "value": [] + "value": nil }, { "key": "percentage_of_actors", @@ -1011,9 +1009,9 @@ Successful disabling of a percentage of time will set the percentage to 0 and re "value": [] }, { - "key": "rules", + "key": "rule", "name": "rule", - "value": [] + "value": nil }, { "key": "percentage_of_actors", diff --git a/lib/flipper/adapter.rb b/lib/flipper/adapter.rb index 39f1563d4..f56857fed 100644 --- a/lib/flipper/adapter.rb +++ b/lib/flipper/adapter.rb @@ -16,7 +16,7 @@ def default_config boolean: nil, groups: Set.new, actors: Set.new, - rules: Set.new, + rule: nil, percentage_of_actors: nil, percentage_of_time: nil, } diff --git a/lib/flipper/adapters/active_record.rb b/lib/flipper/adapters/active_record.rb index d0e5a9cde..a4d3060f0 100644 --- a/lib/flipper/adapters/active_record.rb +++ b/lib/flipper/adapters/active_record.rb @@ -54,7 +54,7 @@ def initialize(options = {}) @feature_class = options.fetch(:feature_class) { Feature } @gate_class = options.fetch(:gate_class) { Gate } - warn VALUE_TO_TEXT_WARNING unless value_is_text? + warn VALUE_TO_TEXT_WARNING if value_not_text? end # Public: The set of known features. @@ -135,10 +135,9 @@ def enable(feature, gate, thing) set(feature, gate, thing, clear: true) when :integer set(feature, gate, thing) - when :set - enable_multi(feature, gate, thing) when :json - raise VALUE_TO_TEXT_WARNING unless value_is_text? + set(feature, gate, thing, json: true) + when :set enable_multi(feature, gate, thing) else unsupported_data_type gate.data_type @@ -160,7 +159,9 @@ def disable(feature, gate, thing) clear(feature) when :integer set(feature, gate, thing) - when :set, :json + when :json + delete(feature, gate) + when :set @gate_class.where(feature_key: feature.key, key: gate.key, value: thing.value).destroy_all else unsupported_data_type gate.data_type @@ -178,14 +179,18 @@ def unsupported_data_type(data_type) def set(feature, gate, thing, options = {}) clear_feature = options.fetch(:clear, false) + json_feature = options.fetch(:json, false) + + raise VALUE_TO_TEXT_WARNING if json_feature && value_not_text? + @gate_class.transaction do clear(feature) if clear_feature - @gate_class.where(feature_key: feature.key, key: gate.key).destroy_all + delete(feature, gate) begin @gate_class.create! do |g| g.feature_key = feature.key g.key = gate.key - g.value = thing.value.to_s + g.value = json_feature ? JSON.dump(thing.value) : thing.value.to_s end rescue ::ActiveRecord::RecordNotUnique # assume this happened concurrently with the same thing and its fine @@ -196,6 +201,10 @@ def set(feature, gate, thing, options = {}) nil end + def delete(feature, gate) + @gate_class.where(feature_key: feature.key, key: gate.key).destroy_all + end + def enable_multi(feature, gate, thing) @gate_class.create! do |g| g.feature_key = feature.key @@ -214,15 +223,15 @@ def result_for_feature(feature, db_gates) feature.gates.each do |gate| result[gate.key] = case gate.data_type - when :boolean + when :boolean, :integer if detected_db_gate = db_gates.detect { |db_gate| db_gate.key == gate.key.to_s } detected_db_gate.value end - when :integer + when :json if detected_db_gate = db_gates.detect { |db_gate| db_gate.key == gate.key.to_s } - detected_db_gate.value + JSON.parse(detected_db_gate.value) end - when :set, :json + when :set db_gates.select { |db_gate| db_gate.key == gate.key.to_s }.map(&:value).to_set else unsupported_data_type gate.data_type @@ -233,8 +242,8 @@ def result_for_feature(feature, db_gates) # Check if value column is text instead of string # See TODO:link/to/PR - def value_is_text? - @gate_class.column_for_attribute(:value).type == :text + def value_not_text? + @gate_class.column_for_attribute(:value).type != :text end end end diff --git a/lib/flipper/adapters/http.rb b/lib/flipper/adapters/http.rb index 14ba5ed4f..f9e6a20b1 100644 --- a/lib/flipper/adapters/http.rb +++ b/lib/flipper/adapters/http.rb @@ -133,7 +133,7 @@ def request_body_for_gate(gate, value) { flipper_id: value.to_s } when :percentage_of_actors, :percentage_of_time { percentage: value.to_s } - when :rules + when :rule value else raise "#{gate.key} is not a valid flipper gate key" @@ -158,7 +158,9 @@ def value_for_gate(gate, api_gate) case gate.data_type when :boolean, :integer value ? value.to_s : value - when :set, :json + when :json + value + when :set value ? value.to_set : Set.new else unsupported_data_type gate.data_type diff --git a/lib/flipper/adapters/memory.rb b/lib/flipper/adapters/memory.rb index d5f0c42da..ac4b55596 100644 --- a/lib/flipper/adapters/memory.rb +++ b/lib/flipper/adapters/memory.rb @@ -72,7 +72,7 @@ def enable(feature, gate, thing) when :set @source[feature.key][gate.key] << thing.value.to_s when :json - @source[feature.key][gate.key] << thing.value + @source[feature.key][gate.key] = thing.value else raise "#{gate} is not supported by this adapter yet" end @@ -92,7 +92,7 @@ def disable(feature, gate, thing) when :set @source[feature.key][gate.key].delete thing.value.to_s when :json - @source[feature.key][gate.key].delete thing.value + @source[feature.key].delete(gate.key) else raise "#{gate} is not supported by this adapter yet" end diff --git a/lib/flipper/adapters/moneta.rb b/lib/flipper/adapters/moneta.rb index 1c67ad2c3..21f70da27 100644 --- a/lib/flipper/adapters/moneta.rb +++ b/lib/flipper/adapters/moneta.rb @@ -72,7 +72,7 @@ def enable(feature, gate, thing) moneta[key(feature.key)] = result when :json result = get(feature) - result[gate.key] << thing.value + result[gate.key] = thing.value moneta[key(feature.key)] = result end true @@ -99,7 +99,7 @@ def disable(feature, gate, thing) moneta[key(feature.key)] = result when :json result = get(feature) - result[gate.key] = result[gate.key].delete(thing.value) + result[gate.key] = nil moneta[key(feature.key)] = result end true diff --git a/lib/flipper/adapters/mongo.rb b/lib/flipper/adapters/mongo.rb index 088edd48e..1f8e61362 100644 --- a/lib/flipper/adapters/mongo.rb +++ b/lib/flipper/adapters/mongo.rb @@ -85,7 +85,7 @@ def enable(feature, gate, thing) gate.key.to_s => thing.value.to_s, } when :json - update feature.key, '$addToSet' => { + update feature.key, '$set' => { gate.key.to_s => JSON.dump(thing.value), } else @@ -107,11 +107,17 @@ def disable(feature, gate, thing) when :boolean delete feature.key when :integer - update feature.key, '$set' => { gate.key.to_s => thing.value.to_s } + update feature.key, '$set' => { + gate.key.to_s => thing.value.to_s, + } when :set - update feature.key, '$pull' => { gate.key.to_s => thing.value.to_s } + update feature.key, '$pull' => { + gate.key.to_s => thing.value.to_s, + } when :json - update feature.key, '$pull' => { gate.key.to_s => JSON.dump(thing.value) } + update feature.key, '$set' => { + gate.key.to_s => nil, + } else unsupported_data_type gate.data_type end @@ -174,7 +180,8 @@ def result_for_feature(feature, doc) when :set doc.fetch(gate.key.to_s) { Set.new }.to_set when :json - doc.fetch(gate.key.to_s) { Set.new }.map! { |member| JSON.parse(member) }.to_set + value = doc[gate.key.to_s] + JSON.parse(value) if value else unsupported_data_type gate.data_type end diff --git a/lib/flipper/adapters/pstore.rb b/lib/flipper/adapters/pstore.rb index f53ec6dcb..52f9415f0 100644 --- a/lib/flipper/adapters/pstore.rb +++ b/lib/flipper/adapters/pstore.rb @@ -93,7 +93,7 @@ def enable(feature, gate, thing) when :set set_add key(feature, gate), thing.value.to_s when :json - set_add key(feature, gate), JSON.dump(thing.value) + write key(feature, gate), JSON.dump(thing.value) else raise "#{gate} is not supported by this adapter yet" end @@ -117,7 +117,7 @@ def disable(feature, gate, thing) end when :json @store.transaction do - set_delete key(feature, gate), JSON.dump(thing.value) + delete key(feature, gate) end else raise "#{gate} is not supported by this adapter yet" @@ -160,14 +160,16 @@ def result_for_feature(feature) result = {} feature.gates.each do |gate| + key = key(feature, gate) result[gate.key] = case gate.data_type when :boolean, :integer - read key(feature, gate) + read key when :set - set_members key(feature, gate) + set_members key when :json - set_members(key(feature, gate)).map { |member| JSON.parse(member) }.to_set + value = read(key) + JSON.parse(value) if value else raise "#{gate} is not supported by this adapter yet" end diff --git a/lib/flipper/adapters/redis.rb b/lib/flipper/adapters/redis.rb index aaa889678..48cb22fde 100644 --- a/lib/flipper/adapters/redis.rb +++ b/lib/flipper/adapters/redis.rb @@ -79,7 +79,7 @@ def enable(feature, gate, thing) when :set @client.hset feature.key, to_field(gate, thing), 1 when :json - @client.hset feature.key, to_json_field(gate, thing), 1 + @client.hset feature.key, gate.key, JSON.dump(thing.value) else unsupported_data_type gate.data_type end @@ -103,7 +103,7 @@ def disable(feature, gate, thing) when :set @client.hdel feature.key, to_field(gate, thing) when :json - @client.hdel feature.key, to_json_field(gate, thing) + @client.hdel feature.key, gate.key else unsupported_data_type gate.data_type end @@ -153,7 +153,8 @@ def result_for_feature(feature, doc) when :set fields_to_gate_value fields, gate when :json - json_fields_to_gate_value fields, gate + value = doc[gate.key.to_s] + JSON.parse(value) if value else unsupported_data_type gate.data_type end @@ -167,10 +168,6 @@ def to_field(gate, thing) "#{gate.key}/#{thing.value}" end - def to_json_field(gate, thing) - "#{gate.key}/#{JSON.dump(thing.value)}" - end - # Private: Returns a set of values given an array of fields and a gate. # # Returns a Set of the values enabled for the gate. @@ -181,10 +178,6 @@ def fields_to_gate_value(fields, gate) values.to_set end - def json_fields_to_gate_value(fields, gate) - fields_to_gate_value(fields, gate).map! { |member| JSON.parse(member) } - end - # Private def unsupported_data_type(data_type) raise "#{data_type} is not supported by this adapter" diff --git a/lib/flipper/adapters/rollout.rb b/lib/flipper/adapters/rollout.rb index e0161adb1..e56d702ea 100644 --- a/lib/flipper/adapters/rollout.rb +++ b/lib/flipper/adapters/rollout.rb @@ -53,7 +53,7 @@ def get(feature) actors: actors, percentage_of_actors: percentage_of_actors, percentage_of_time: nil, - rules: Set.new, + rule: nil, } end diff --git a/lib/flipper/adapters/sequel.rb b/lib/flipper/adapters/sequel.rb index d75818ffb..738dd644e 100644 --- a/lib/flipper/adapters/sequel.rb +++ b/lib/flipper/adapters/sequel.rb @@ -54,7 +54,7 @@ def initialize(options = {}) @feature_class = options.fetch(:feature_class) { Feature } @gate_class = options.fetch(:gate_class) { Gate } - warn VALUE_TO_TEXT_WARNING unless value_is_text? + warn VALUE_TO_TEXT_WARNING if value_not_text? end # Public: The set of known features. @@ -139,8 +139,7 @@ def enable(feature, gate, thing) when :set enable_multi(feature, gate, thing) when :json - raise VALUE_TO_TEXT_WARNING unless value_is_text? - enable_multi(feature, gate, thing) + set(feature, gate, thing, json: true) else unsupported_data_type gate.data_type end @@ -161,7 +160,9 @@ def disable(feature, gate, thing) clear(feature) when :integer set(feature, gate, thing) - when :set, :json + when :json + delete(feature, gate) + when :set @gate_class.where(gate_attrs(feature, gate, thing, json: gate.data_type == :json)).delete else unsupported_data_type gate.data_type @@ -178,22 +179,25 @@ def unsupported_data_type(data_type) def set(feature, gate, thing, options = {}) clear_feature = options.fetch(:clear, false) - args = { - feature_key: feature.key, - key: gate.key.to_s, - } + json_feature = options.fetch(:json, false) + + raise VALUE_TO_TEXT_WARNING if json_feature && value_not_text? @gate_class.db.transaction do clear(feature) if clear_feature - @gate_class.where(args).delete + delete(feature, gate) begin - @gate_class.create(gate_attrs(feature, gate, thing)) + @gate_class.create(gate_attrs(feature, gate, thing, json: json_feature)) rescue ::Sequel::UniqueConstraintViolation end end end + def delete(feature, gate) + @gate_class.where(feature_key: feature.key, key: gate.key.to_s).delete + end + def enable_multi(feature, gate, thing) begin @gate_class.create(gate_attrs(feature, gate, thing, json: gate.data_type == :json)) @@ -225,7 +229,9 @@ def result_for_feature(feature, db_gates) when :set db_gates.select { |db_gate| db_gate.key == gate.key.to_s }.map(&:value).to_set when :json - db_gates.select { |db_gate| db_gate.key == gate.key.to_s }.map {|gate| JSON.load(gate.value) }.to_set + if detected_db_gate = db_gates.detect { |db_gate| db_gate.key == gate.key.to_s } + JSON.parse(detected_db_gate.value) + end else unsupported_data_type gate.data_type end @@ -234,8 +240,8 @@ def result_for_feature(feature, db_gates) # Check if value column is text instead of string # See TODO:link/to/PR - def value_is_text? - @gate_class.db_schema[:value][:db_type] == "text" + def value_not_text? + @gate_class.db_schema[:value][:db_type] != "text" end end end diff --git a/lib/flipper/adapters/sync/feature_synchronizer.rb b/lib/flipper/adapters/sync/feature_synchronizer.rb index 4d533c0cd..60cc213e8 100644 --- a/lib/flipper/adapters/sync/feature_synchronizer.rb +++ b/lib/flipper/adapters/sync/feature_synchronizer.rb @@ -9,7 +9,7 @@ class Sync class FeatureSynchronizer extend Forwardable - def_delegator :@local_gate_values, :rules, :local_rules + def_delegator :@local_gate_values, :rule, :local_rule def_delegator :@local_gate_values, :boolean, :local_boolean def_delegator :@local_gate_values, :actors, :local_actors def_delegator :@local_gate_values, :groups, :local_groups @@ -18,7 +18,7 @@ class FeatureSynchronizer def_delegator :@local_gate_values, :percentage_of_time, :local_percentage_of_time - def_delegator :@remote_gate_values, :rules, :remote_rules + def_delegator :@remote_gate_values, :rule, :remote_rule def_delegator :@remote_gate_values, :boolean, :remote_boolean def_delegator :@remote_gate_values, :actors, :remote_actors def_delegator :@remote_gate_values, :groups, :remote_groups @@ -44,7 +44,7 @@ def call @feature.disable if local_boolean_enabled? sync_groups sync_actors - sync_rules + sync_rule sync_percentage_of_actors sync_percentage_of_time end @@ -52,16 +52,10 @@ def call private - def sync_rules - remote_rules_added = remote_rules - local_rules - remote_rules_added.each do |rule_hash| - @feature.enable_rule Rules.build(rule_hash) - end + def sync_rule + return if local_rule == remote_rule - remote_rules_removed = local_rules - remote_rules - remote_rules_removed.each do |rule_hash| - @feature.disable_rule Rules.build(rule_hash) - end + @feature.enable_rule remote_rule end def sync_actors diff --git a/lib/flipper/api/v1/actions/rules_gate.rb b/lib/flipper/api/v1/actions/rules_gate.rb index 778702c11..d6396360c 100644 --- a/lib/flipper/api/v1/actions/rules_gate.rb +++ b/lib/flipper/api/v1/actions/rules_gate.rb @@ -8,21 +8,20 @@ module Actions class RulesGate < Api::Action include FeatureNameFromRoute - route %r{\A/features/(?.*)/rules/?\Z} + route %r{\A/features/(?.*)/rule/?\Z} def post ensure_valid_params feature = flipper[feature_name] - feature.enable Flipper::Rules.build(rule_hash) + feature.enable_rule Flipper::Rules.build(rule_hash) decorated_feature = Decorators::Feature.new(feature) json_response(decorated_feature.as_json, 200) end def delete - ensure_valid_params feature = flipper[feature_name] - feature.disable Flipper::Rules.build(rule_hash) + feature.disable_rule decorated_feature = Decorators::Feature.new(feature) json_response(decorated_feature.as_json, 200) diff --git a/lib/flipper/api/v1/decorators/gate.rb b/lib/flipper/api/v1/decorators/gate.rb index eb9e5f238..1d8cacabf 100644 --- a/lib/flipper/api/v1/decorators/gate.rb +++ b/lib/flipper/api/v1/decorators/gate.rb @@ -25,7 +25,7 @@ def as_json private # Set of types that should be represented as Array in JSON. - JSON_ARRAY_TYPES = Set[:set, :json].freeze + JSON_ARRAY_TYPES = Set[:set].freeze # json doesn't like sets def value_as_json diff --git a/lib/flipper/dsl.rb b/lib/flipper/dsl.rb index 60133a073..9b525ee53 100644 --- a/lib/flipper/dsl.rb +++ b/lib/flipper/dsl.rb @@ -114,7 +114,7 @@ def disable(name, *args) # rule - a Flipper::Rules::Rule instance or a Hash. # # Returns result of Feature#disable. - def disable_rule(name, rule) + def disable_rule(name, rule = nil) feature(name).disable_rule(rule) end diff --git a/lib/flipper/feature.rb b/lib/flipper/feature.rb index 30652e19e..c4ae0369f 100644 --- a/lib/flipper/feature.rb +++ b/lib/flipper/feature.rb @@ -173,8 +173,12 @@ def enable_percentage_of_actors(percentage) # rule - a rule or Hash that can be converted to a rule. # # Returns result of disable. - def disable_rule(rule) - disable Rules.wrap(rule) + def disable_rule(rule = nil) + if rule + disable Rules.wrap(rule) + else + disable Flipper.object(true).eq(false) + end end # Public: Disables a feature for an actor. @@ -275,11 +279,11 @@ def groups_value gate_values.groups end - # Public: Get the adapter value for the rules gate. + # Public: Get the adapter value for the rule gate. # - # Returns Set of rules. - def rules_value - gate_values.rules + # Returns rule. + def rule_value + gate_values.rule end # Public: Get the adapter value for the actors gate. diff --git a/lib/flipper/gate_values.rb b/lib/flipper/gate_values.rb index 3ed7b0680..4c65d1cc4 100644 --- a/lib/flipper/gate_values.rb +++ b/lib/flipper/gate_values.rb @@ -9,7 +9,7 @@ class GateValues 'boolean' => '@boolean', 'actors' => '@actors', 'groups' => '@groups', - 'rules' => '@rules', + 'rule' => '@rule', 'percentage_of_time' => '@percentage_of_time', 'percentage_of_actors' => '@percentage_of_actors', }.freeze @@ -17,7 +17,7 @@ class GateValues attr_reader :boolean attr_reader :actors attr_reader :groups - attr_reader :rules + attr_reader :rule attr_reader :percentage_of_actors attr_reader :percentage_of_time @@ -25,7 +25,7 @@ def initialize(adapter_values) @boolean = Typecast.to_boolean(adapter_values[:boolean]) @actors = Typecast.to_set(adapter_values[:actors]) @groups = Typecast.to_set(adapter_values[:groups]) - @rules = Typecast.to_set(adapter_values[:rules]) + @rule = adapter_values[:rule] @percentage_of_actors = Typecast.to_percentage(adapter_values[:percentage_of_actors]) @percentage_of_time = Typecast.to_percentage(adapter_values[:percentage_of_time]) end @@ -41,7 +41,7 @@ def eql?(other) boolean == other.boolean && actors == other.actors && groups == other.groups && - rules == other.rules && + rule == other.rule && percentage_of_actors == other.percentage_of_actors && percentage_of_time == other.percentage_of_time end diff --git a/lib/flipper/gates/rule.rb b/lib/flipper/gates/rule.rb index c5dee99cc..1de8160c8 100644 --- a/lib/flipper/gates/rule.rb +++ b/lib/flipper/gates/rule.rb @@ -8,7 +8,7 @@ def name # Internal: Name converted to value safe for adapter. def key - :rules + :rule end def data_type @@ -16,18 +16,17 @@ def data_type end def enabled?(value) - !value.empty? + value && !value.empty? end # Internal: Checks if the gate is open for a thing. # # Returns true if gate open for thing, false if not. def open?(context) - rules = context.values[key] - rules.any? { |hash| - rule = Flipper::Rules.build(hash) - rule.matches?(context.feature_name, context.thing) - } + data = context.values[key] + return false if data.nil? || data.empty? + rule = Flipper::Rules.build(data) + rule.matches?(context.feature_name, context.thing) end def protects?(thing) diff --git a/lib/flipper/rules/object.rb b/lib/flipper/rules/object.rb index 8c958724b..dd560bab1 100644 --- a/lib/flipper/rules/object.rb +++ b/lib/flipper/rules/object.rb @@ -132,17 +132,6 @@ def self.integer_or_object(object) raise ArgumentError, "object must be integer or property" unless object.is_a?(Integer) end end - - def self.array_or_object(object) - case object - when Array - Object.new(object) - when Flipper::Rules::Object - object - else - raise ArgumentError, "object must be array or property" unless object.is_a?(Array) - end - end end end end diff --git a/lib/flipper/rules/operator.rb b/lib/flipper/rules/operator.rb index 8107cdab3..6e5d911d0 100644 --- a/lib/flipper/rules/operator.rb +++ b/lib/flipper/rules/operator.rb @@ -31,6 +31,4 @@ def self.build(object) require "flipper/rules/operators/gte" require "flipper/rules/operators/lt" require "flipper/rules/operators/lte" -require "flipper/rules/operators/in" -require "flipper/rules/operators/nin" require "flipper/rules/operators/percentage" diff --git a/lib/flipper/spec/shared_adapter_specs.rb b/lib/flipper/spec/shared_adapter_specs.rb index 972d0d703..8db69f928 100644 --- a/lib/flipper/spec/shared_adapter_specs.rb +++ b/lib/flipper/spec/shared_adapter_specs.rb @@ -63,29 +63,16 @@ end it 'can enable, disable and get value for rule gate' do - basic_rule = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "plan"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "basic"} - ) - age_rule = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "age"}, - {"type" => "Operator", "value" => "gte"}, - {"type" => "Integer", "value" => 21} - ) - expect(subject.enable(feature, rule_gate, basic_rule)).to eq(true) - expect(subject.enable(feature, rule_gate, age_rule)).to eq(true) + basic_rule = Flipper.property(:plan).eq("basic") + age_rule = Flipper.property(:age).gte(21) + any_rule = Flipper.any(basic_rule, age_rule) + expect(subject.enable(feature, rule_gate, any_rule)).to eq(true) result = subject.get(feature) - expect(result[:rules]).to include(basic_rule.value) - expect(result[:rules]).to include(age_rule.value) + expect(result[:rule]).to eq(any_rule.value) expect(subject.disable(feature, rule_gate, basic_rule)).to eq(true) result = subject.get(feature) - expect(result[:rules]).to include(age_rule.value) - - expect(subject.disable(feature, rule_gate, age_rule)).to eq(true) - result = subject.get(feature) - expect(result[:rules]).to be_empty + expect(result[:rule]).to be(nil) end it 'can enable, disable and get value for group gate' do diff --git a/lib/flipper/test/shared_adapter_test.rb b/lib/flipper/test/shared_adapter_test.rb index 679f90ff6..1dac6263e 100644 --- a/lib/flipper/test/shared_adapter_test.rb +++ b/lib/flipper/test/shared_adapter_test.rb @@ -58,29 +58,16 @@ def test_fully_disables_all_enabled_things_when_boolean_gate_disabled end def test_can_enable_disable_and_get_value_for_rule_gate - basic_rule = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "plan"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "basic"} - ) - age_rule = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "age"}, - {"type" => "Operator", "value" => "gte"}, - {"type" => "Integer", "value" => 21} - ) - assert_equal true, @adapter.enable(@feature, @rule_gate, basic_rule) - assert_equal true, @adapter.enable(@feature, @rule_gate, age_rule) + basic_rule = Flipper.property(:plan).eq("basic") + age_rule = Flipper.property(:age).gte(21) + any_rule = Flipper.any(basic_rule, age_rule) + assert_equal true, @adapter.enable(@feature, @rule_gate, any_rule) result = @adapter.get(@feature) - assert_includes result[:rules], basic_rule.value - assert_includes result[:rules], age_rule.value + assert_equal any_rule.value, result[:rule] assert_equal true, @adapter.disable(@feature, @rule_gate, basic_rule) result = @adapter.get(@feature) - assert_includes result[:rules], age_rule.value - - assert_equal true, @adapter.disable(@feature, @rule_gate, age_rule) - result = @adapter.get(@feature) - assert result[:rules].empty? + assert_nil result[:rule] end def test_can_enable_disable_get_value_for_group_gate diff --git a/spec/flipper/adapter_spec.rb b/spec/flipper/adapter_spec.rb index 40ab4cf48..12888b5d3 100644 --- a/spec/flipper/adapter_spec.rb +++ b/spec/flipper/adapter_spec.rb @@ -8,7 +8,7 @@ boolean: nil, groups: Set.new, actors: Set.new, - rules: Set.new, + rule: nil, percentage_of_actors: nil, percentage_of_time: nil, } diff --git a/spec/flipper/adapters/read_only_spec.rb b/spec/flipper/adapters/read_only_spec.rb index df136a593..6bd46bc6b 100644 --- a/spec/flipper/adapters/read_only_spec.rb +++ b/spec/flipper/adapters/read_only_spec.rb @@ -56,21 +56,21 @@ adapter.enable(feature, time_gate, flipper.time(45)) adapter.enable(feature, rule_gate, rule) - expect(subject.get(feature)).to eq(boolean: 'true', - groups: Set['admins'], - actors: Set['22'], - rules: Set[ - { - "type" => "Condition", - "value" => { - "left" => {"type" => "Property", "value" => "plan"}, - "operator" => {"type" => "Operator", "value" => "eq"}, - "right" => {"type" => "String", "value" => "basic"}, - } - } - ], - percentage_of_actors: '25', - percentage_of_time: '45') + expect(subject.get(feature)).to eq({ + boolean: 'true', + groups: Set['admins'], + actors: Set['22'], + rule: { + "type" => "Condition", + "value" => { + "left" => {"type" => "Property", "value" => "plan"}, + "operator" => {"type" => "Operator", "value" => "eq"}, + "right" => {"type" => "String", "value" => "basic"}, + } + }, + percentage_of_actors: '25', + percentage_of_time: '45', + }) end it 'can get features' do diff --git a/spec/flipper/adapters/rollout_spec.rb b/spec/flipper/adapters/rollout_spec.rb index 3734f7520..0c729ab0b 100644 --- a/spec/flipper/adapters/rollout_spec.rb +++ b/spec/flipper/adapters/rollout_spec.rb @@ -37,7 +37,7 @@ boolean: nil, groups: Set.new([:admins]), actors: Set.new(["1"]), - rules: Set.new, + rule: nil, percentage_of_actors: 20.0, percentage_of_time: nil, } @@ -51,7 +51,7 @@ boolean: true, groups: Set.new, actors: Set.new, - rules: Set.new, + rule: nil, percentage_of_actors: nil, percentage_of_time: nil, } @@ -67,7 +67,7 @@ boolean: true, groups: Set.new, actors: Set.new, - rules: Set.new, + rule: nil, percentage_of_actors: nil, percentage_of_time: nil, } diff --git a/spec/flipper/adapters/sync/feature_synchronizer_spec.rb b/spec/flipper/adapters/sync/feature_synchronizer_spec.rb index e1a290c13..331b4c0af 100644 --- a/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +++ b/spec/flipper/adapters/sync/feature_synchronizer_spec.rb @@ -66,7 +66,7 @@ boolean: nil, actors: Set["1"], groups: Set["staff"], - rules: Set[plan_rule.value], + rule: plan_rule.value, percentage_of_time: 10, percentage_of_actors: 15, } @@ -78,42 +78,31 @@ expect(local_gate_values_hash.fetch(:boolean)).to be(nil) expect(local_gate_values_hash.fetch(:actors)).to eq(Set["1"]) expect(local_gate_values_hash.fetch(:groups)).to eq(Set["staff"]) - expect(local_gate_values_hash.fetch(:rules)).to eq(Set[plan_rule.value]) + expect(local_gate_values_hash.fetch(:rule)).to eq(plan_rule.value) expect(local_gate_values_hash.fetch(:percentage_of_time)).to eq("10") expect(local_gate_values_hash.fetch(:percentage_of_actors)).to eq("15") end - it "adds remotely added rules" do - remote = Flipper::GateValues.new(rules: Set[plan_rule.value, age_rule.value]) + it "updates rule when remote is updated" do + any_rule = Flipper.any(plan_rule, age_rule) + remote = Flipper::GateValues.new(rule: any_rule.value) feature.enable_rule(age_rule) adapter.reset described_class.new(feature, feature.gate_values, remote).call - expect(feature.rules_value).to eq(Set[plan_rule.value, age_rule.value]) + expect(feature.rule_value).to eq(any_rule.value) expect_only_enable end - it "removes remotely removed rules" do - remote = Flipper::GateValues.new(rules: Set[plan_rule.value]) - feature.enable_rule(plan_rule) - feature.enable_rule(age_rule) - adapter.reset - - described_class.new(feature, feature.gate_values, remote).call - - expect(feature.rules_value).to eq(Set[plan_rule.value]) - expect_only_disable - end - - it "does nothing to rules if in sync" do - remote = Flipper::GateValues.new(rules: Set[plan_rule.value]) + it "does nothing to rule if in sync" do + remote = Flipper::GateValues.new(rule: plan_rule.value) feature.enable_rule(plan_rule) adapter.reset described_class.new(feature, feature.gate_values, remote).call - expect(feature.rules_value).to eq(Set[plan_rule.value]) + expect(feature.rule_value).to eq(plan_rule.value) expect_no_enable_or_disable end diff --git a/spec/flipper/api/v1/actions/feature_spec.rb b/spec/flipper/api/v1/actions/feature_spec.rb index d22e41520..0bd2435df 100644 --- a/spec/flipper/api/v1/actions/feature_spec.rb +++ b/spec/flipper/api/v1/actions/feature_spec.rb @@ -23,9 +23,9 @@ 'value' => 'true', }, { - 'key' => 'rules', + 'key' => 'rule', 'name' => 'rule', - 'value' => [], + 'value' => nil, }, { 'key' => 'actors', @@ -72,9 +72,9 @@ 'value' => nil, }, { - 'key' => 'rules', + 'key' => 'rule', 'name' => 'rule', - 'value' => [], + 'value' => nil, }, { 'key' => 'actors', @@ -137,9 +137,9 @@ 'value' => 'true', }, { - 'key' => 'rules', + 'key' => 'rule', 'name' => 'rule', - 'value' => [], + 'value' => nil, }, { 'key' => 'actors', @@ -186,9 +186,9 @@ 'value' => 'true', }, { - 'key' => 'rules', + 'key' => 'rule', 'name' => 'rule', - 'value' => [], + 'value' => nil, }, { 'key' => 'actors', diff --git a/spec/flipper/api/v1/actions/features_spec.rb b/spec/flipper/api/v1/actions/features_spec.rb index 448fed1a1..43be9c50e 100644 --- a/spec/flipper/api/v1/actions/features_spec.rb +++ b/spec/flipper/api/v1/actions/features_spec.rb @@ -26,9 +26,9 @@ 'value' => 'true', }, { - 'key' => 'rules', + 'key' => 'rule', 'name' => 'rule', - 'value' => [], + 'value' => nil, }, { 'key' => 'actors', @@ -126,9 +126,9 @@ 'value' => nil, }, { - 'key' => 'rules', + 'key' => 'rule', 'name' => 'rule', - 'value' => [], + 'value' => nil, }, { 'key' => 'actors', diff --git a/spec/flipper/api/v1/actions/rules_gate_spec.rb b/spec/flipper/api/v1/actions/rules_gate_spec.rb index 0a13221b9..6f60599e6 100644 --- a/spec/flipper/api/v1/actions/rules_gate_spec.rb +++ b/spec/flipper/api/v1/actions/rules_gate_spec.rb @@ -19,7 +19,7 @@ describe 'enable' do before do flipper[:my_feature].disable_rule(rule) - post '/features/my_feature/rules', JSON.dump(rule.value), "CONTENT_TYPE" => "application/json" + post '/features/my_feature/rule', JSON.dump(rule.value), "CONTENT_TYPE" => "application/json" end it 'enables feature for rule' do @@ -29,15 +29,15 @@ end it 'returns decorated feature with rule enabled' do - gate = json_response['gates'].find { |gate| gate['key'] == 'rules' } - expect(gate['value']).to eq([rule.value]) + gate = json_response['gates'].find { |gate| gate['key'] == 'rule' } + expect(gate['value']).to eq(rule.value) end end describe 'disable' do before do flipper[:my_feature].enable_rule(rule) - delete '/features/my_feature/rules', JSON.dump(rule.value), "CONTENT_TYPE" => "application/json" + delete '/features/my_feature/rule', JSON.dump(rule.value), "CONTENT_TYPE" => "application/json" end it 'disables rule for feature' do @@ -47,15 +47,15 @@ end it 'returns decorated feature with rule gate disabled' do - gate = json_response['gates'].find { |gate| gate['key'] == 'rules' } - expect(gate['value']).to be_empty + gate = json_response['gates'].find { |gate| gate['key'] == 'rule' } + expect(gate['value']).to be(nil) end end describe 'enable feature with slash in name' do before do flipper["my/feature"].disable_rule(rule) - post '/features/my/feature/rules', JSON.dump(rule.value), "CONTENT_TYPE" => "application/json" + post '/features/my/feature/rule', JSON.dump(rule.value), "CONTENT_TYPE" => "application/json" end it 'enables feature for rule' do @@ -65,15 +65,15 @@ end it 'returns decorated feature with rule enabled' do - gate = json_response['gates'].find { |gate| gate['key'] == 'rules' } - expect(gate['value']).to eq([rule.value]) + gate = json_response['gates'].find { |gate| gate['key'] == 'rule' } + expect(gate['value']).to eq(rule.value) end end describe 'enable feature with space in name' do before do flipper["sp ace"].disable_rule(rule) - post '/features/sp%20ace/rules', JSON.dump(rule.value), "CONTENT_TYPE" => "application/json" + post '/features/sp%20ace/rule', JSON.dump(rule.value), "CONTENT_TYPE" => "application/json" end it 'enables feature for rule' do @@ -83,8 +83,8 @@ end it 'returns decorated feature with rule enabled' do - gate = json_response['gates'].find { |gate| gate['key'] == 'rules' } - expect(gate['value']).to eq([rule.value]) + gate = json_response['gates'].find { |gate| gate['key'] == 'rule' } + expect(gate['value']).to eq(rule.value) end end @@ -92,7 +92,7 @@ before do data = rule.value data.delete("type") - post '/features/my_feature/rules', JSON.dump(data), "CONTENT_TYPE" => "application/json" + post '/features/my_feature/rule', JSON.dump(data), "CONTENT_TYPE" => "application/json" end it 'returns correct error response' do @@ -105,12 +105,11 @@ before do data = rule.value data.delete("type") - delete '/features/my_feature/rules' + delete '/features/my_feature/rule' end it 'returns correct error response' do - expect(last_response.status).to eq(422) - expect(json_response).to eq(api_rule_type_invalid_response) + expect(last_response.status).to eq(200) end end @@ -118,7 +117,7 @@ before do data = rule.value data.delete("value") - post '/features/my_feature/rules', JSON.dump(data), "CONTENT_TYPE" => "application/json" + post '/features/my_feature/rule', JSON.dump(data), "CONTENT_TYPE" => "application/json" end it 'returns correct error response' do @@ -131,12 +130,11 @@ before do data = rule.value data.delete("value") - delete '/features/my_feature/rules', JSON.dump(data), "CONTENT_TYPE" => "application/json" + delete '/features/my_feature/rule', JSON.dump(data), "CONTENT_TYPE" => "application/json" end it 'returns correct error response' do - expect(last_response.status).to eq(422) - expect(json_response).to eq(api_rule_value_invalid_response) + expect(last_response.status).to eq(200) end end @@ -144,7 +142,7 @@ before do data = rule.value data["type"] = nil - post '/features/my_feature/rules', JSON.dump(data), "CONTENT_TYPE" => "application/json" + post '/features/my_feature/rule', JSON.dump(data), "CONTENT_TYPE" => "application/json" end it 'returns correct error response' do @@ -157,12 +155,11 @@ before do data = rule.value data["type"] = nil - delete '/features/my_feature/rules', JSON.dump(data), "CONTENT_TYPE" => "application/json" + delete '/features/my_feature/rule', JSON.dump(data), "CONTENT_TYPE" => "application/json" end it 'returns correct error response' do - expect(last_response.status).to eq(422) - expect(json_response).to eq(api_rule_type_invalid_response) + expect(last_response.status).to eq(200) end end @@ -170,7 +167,7 @@ before do data = rule.value data["value"] = nil - post '/features/my_feature/rules', JSON.dump(data), "CONTENT_TYPE" => "application/json" + post '/features/my_feature/rule', JSON.dump(data), "CONTENT_TYPE" => "application/json" end it 'returns correct error response' do @@ -183,18 +180,17 @@ before do data = rule.value data["value"] = nil - delete '/features/my_feature/rules', JSON.dump(data), "CONTENT_TYPE" => "application/json" + delete '/features/my_feature/rule', JSON.dump(data), "CONTENT_TYPE" => "application/json" end it 'returns correct error response' do - expect(last_response.status).to eq(422) - expect(json_response).to eq(api_rule_value_invalid_response) + expect(last_response.status).to eq(200) end end describe 'enable missing feature' do before do - post '/features/my_feature/rules', JSON.dump(rule.value), "CONTENT_TYPE" => "application/json" + post '/features/my_feature/rule', JSON.dump(rule.value), "CONTENT_TYPE" => "application/json" end it 'enables rule for feature' do @@ -204,14 +200,14 @@ end it 'returns decorated feature with rule enabled' do - gate = json_response['gates'].find { |gate| gate['key'] == 'rules' } - expect(gate['value']).to eq([rule.value]) + gate = json_response['gates'].find { |gate| gate['key'] == 'rule' } + expect(gate['value']).to eq(rule.value) end end describe 'disable missing feature' do before do - delete '/features/my_feature/rules', JSON.dump(rule.value), "CONTENT_TYPE" => "application/json" + delete '/features/my_feature/rule', "CONTENT_TYPE" => "application/json" end it 'disables rule for feature' do @@ -221,8 +217,8 @@ end it 'returns decorated feature with rule gate disabled' do - gate = json_response['gates'].find { |gate| gate['key'] == 'rules' } - expect(gate['value']).to be_empty + gate = json_response['gates'].find { |gate| gate['key'] == 'rule' } + expect(gate['value']).to be(nil) end end end diff --git a/spec/flipper/cloud_spec.rb b/spec/flipper/cloud_spec.rb index 9af07ff2c..18c4a377f 100644 --- a/spec/flipper/cloud_spec.rb +++ b/spec/flipper/cloud_spec.rb @@ -132,10 +132,10 @@ cloud_flipper = Flipper::Cloud.new(token: "asdf") get_all = { - "logging" => {actors: Set.new, boolean: nil, groups: Set.new, rules: Set.new, percentage_of_actors: nil, percentage_of_time: "5"}, - "search" => {actors: Set.new, boolean: "true", groups: Set.new, rules: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, - "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, rules: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, - "test" => {actors: Set.new, boolean: "true", groups: Set.new, rules: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, + "logging" => {actors: Set.new, boolean: nil, groups: Set.new, rule: nil, percentage_of_actors: nil, percentage_of_time: "5"}, + "search" => {actors: Set.new, boolean: "true", groups: Set.new, rule: nil, percentage_of_actors: nil, percentage_of_time: nil}, + "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, rule: nil, percentage_of_actors: nil, percentage_of_time: nil}, + "test" => {actors: Set.new, boolean: "true", groups: Set.new, rule: nil, percentage_of_actors: nil, percentage_of_time: nil}, } expect(flipper.adapter.get_all).to eq(get_all) @@ -161,10 +161,10 @@ cloud_flipper = Flipper::Cloud.new(token: "asdf") get_all = { - "logging" => {actors: Set.new, boolean: nil, groups: Set.new, rules: Set.new, percentage_of_actors: nil, percentage_of_time: "5"}, - "search" => {actors: Set.new, boolean: "true", groups: Set.new, rules: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, - "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, rules: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, - "test" => {actors: Set.new, boolean: "true", groups: Set.new, rules: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, + "logging" => {actors: Set.new, boolean: nil, groups: Set.new, rule: nil, percentage_of_actors: nil, percentage_of_time: "5"}, + "search" => {actors: Set.new, boolean: "true", groups: Set.new, rule: nil, percentage_of_actors: nil, percentage_of_time: nil}, + "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, rule: nil, percentage_of_actors: nil, percentage_of_time: nil}, + "test" => {actors: Set.new, boolean: "true", groups: Set.new, rule: nil, percentage_of_actors: nil, percentage_of_time: nil}, } expect(flipper.adapter.get_all).to eq(get_all) @@ -189,10 +189,10 @@ cloud_flipper = Flipper::Cloud.new(token: "asdf") get_all = { - "logging" => {actors: Set.new, boolean: nil, groups: Set.new, rules: Set.new, percentage_of_actors: nil, percentage_of_time: "5"}, - "search" => {actors: Set.new, boolean: "true", groups: Set.new, rules: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, - "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, rules: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, - "test" => {actors: Set.new, boolean: "true", groups: Set.new, rules: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, + "logging" => {actors: Set.new, boolean: nil, groups: Set.new, rule: nil, percentage_of_actors: nil, percentage_of_time: "5"}, + "search" => {actors: Set.new, boolean: "true", groups: Set.new, rule: nil, percentage_of_actors: nil, percentage_of_time: nil}, + "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, rule: nil, percentage_of_actors: nil, percentage_of_time: nil}, + "test" => {actors: Set.new, boolean: "true", groups: Set.new, rule: nil, percentage_of_actors: nil, percentage_of_time: nil}, } expect(flipper.adapter.get_all).to eq(get_all) diff --git a/spec/flipper/feature_spec.rb b/spec/flipper/feature_spec.rb index 45f764366..fa499462e 100644 --- a/spec/flipper/feature_spec.rb +++ b/spec/flipper/feature_spec.rb @@ -652,32 +652,34 @@ describe '#enable_rule/disable_rule' do context "with rule instance" do - it "updates gate values to include rule" do + it "updates gate values to equal rule" do rule = Flipper::Rules::Condition.new( {"type" => "Property", "value" => "plan"}, {"type" => "Operator", "value" => "eq"}, {"type" => "String", "value" => "basic"} ) - expect(subject.gate_values.rules).to be_empty + other_rule = Flipper.property(:age).gte(21) + expect(subject.gate_values.rule).to be(nil) subject.enable_rule(rule) - expect(subject.gate_values.rules).to eq(Set[rule.value]) - subject.disable_rule(rule) - expect(subject.gate_values.rules).to be_empty + expect(subject.gate_values.rule).to eq(rule.value) + subject.disable_rule(other_rule) + expect(subject.gate_values.rule).to be(nil) end end context "with Hash" do - it "updates gate values to include rule" do + it "updates gate values to equal rule" do rule = Flipper::Rules::Condition.new( {"type" => "Property", "value" => "plan"}, {"type" => "Operator", "value" => "eq"}, {"type" => "String", "value" => "basic"} ) - expect(subject.gate_values.rules).to be_empty + other_rule = Flipper.property(:age).gte(21) + expect(subject.gate_values.rule).to be(nil) subject.enable_rule(rule.value) - expect(subject.gate_values.rules).to eq(Set[rule.value]) - subject.disable_rule(rule.value) - expect(subject.gate_values.rules).to be_empty + expect(subject.gate_values.rule).to eq(rule.value) + subject.disable_rule(other_rule.value) + expect(subject.gate_values.rule).to be(nil) end end end diff --git a/spec/flipper_spec.rb b/spec/flipper_spec.rb index 2d0f024e1..ab1a00a47 100644 --- a/spec/flipper_spec.rb +++ b/spec/flipper_spec.rb @@ -114,7 +114,7 @@ end it 'delegates disable_rule to instance' do - described_class.disable_rule(:search, rule) + described_class.disable_rule(:search, Flipper.object(true).eq(false)) expect(described_class.instance.enabled?(:search, actor)).to be(false) end From 159717bf082c7ed6270cd01974b68c4fefa16153 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Fri, 24 Sep 2021 08:03:38 -0400 Subject: [PATCH 077/176] Add test for updating rule to shared specs/tests --- lib/flipper/spec/shared_adapter_specs.rb | 5 +++++ lib/flipper/test/shared_adapter_test.rb | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/lib/flipper/spec/shared_adapter_specs.rb b/lib/flipper/spec/shared_adapter_specs.rb index 8db69f928..5c46e6572 100644 --- a/lib/flipper/spec/shared_adapter_specs.rb +++ b/lib/flipper/spec/shared_adapter_specs.rb @@ -66,10 +66,15 @@ basic_rule = Flipper.property(:plan).eq("basic") age_rule = Flipper.property(:age).gte(21) any_rule = Flipper.any(basic_rule, age_rule) + expect(subject.enable(feature, rule_gate, any_rule)).to eq(true) result = subject.get(feature) expect(result[:rule]).to eq(any_rule.value) + expect(subject.enable(feature, rule_gate, basic_rule)).to eq(true) + result = subject.get(feature) + expect(result[:rule]).to eq(basic_rule.value) + expect(subject.disable(feature, rule_gate, basic_rule)).to eq(true) result = subject.get(feature) expect(result[:rule]).to be(nil) diff --git a/lib/flipper/test/shared_adapter_test.rb b/lib/flipper/test/shared_adapter_test.rb index 1dac6263e..ef2fd065b 100644 --- a/lib/flipper/test/shared_adapter_test.rb +++ b/lib/flipper/test/shared_adapter_test.rb @@ -61,10 +61,15 @@ def test_can_enable_disable_and_get_value_for_rule_gate basic_rule = Flipper.property(:plan).eq("basic") age_rule = Flipper.property(:age).gte(21) any_rule = Flipper.any(basic_rule, age_rule) + assert_equal true, @adapter.enable(@feature, @rule_gate, any_rule) result = @adapter.get(@feature) assert_equal any_rule.value, result[:rule] + assert_equal true, @adapter.enable(@feature, @rule_gate, basic_rule) + result = @adapter.get(@feature) + assert_equal basic_rule.value, result[:rule] + assert_equal true, @adapter.disable(@feature, @rule_gate, basic_rule) result = @adapter.get(@feature) assert_nil result[:rule] From 10bdf5ace290dcda39f7e73be597230ec299de80 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Fri, 24 Sep 2021 08:07:58 -0400 Subject: [PATCH 078/176] Allow wrapping condition with multi-condition --- lib/flipper/rules/condition.rb | 8 ++++++++ spec/flipper/rules/condition_spec.rb | 26 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/lib/flipper/rules/condition.rb b/lib/flipper/rules/condition.rb index 5bcc34edc..2047baf5f 100644 --- a/lib/flipper/rules/condition.rb +++ b/lib/flipper/rules/condition.rb @@ -16,6 +16,14 @@ def initialize(left, operator, right) @right = Object.build(right) end + def all + Flipper::Rules::All.new(self) + end + + def any + Flipper::Rules::Any.new(self) + end + def value { "type" => "Condition", diff --git a/spec/flipper/rules/condition_spec.rb b/spec/flipper/rules/condition_spec.rb index 6f4051414..43cbe3623 100644 --- a/spec/flipper/rules/condition_spec.rb +++ b/spec/flipper/rules/condition_spec.rb @@ -3,6 +3,32 @@ RSpec.describe Flipper::Rules::Condition do let(:feature_name) { "search" } + describe "#all" do + it "wraps self with all" do + rule = Flipper::Rules::Condition.new( + {"type" => "Property", "value" => "flipper_id"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "User;1"} + ) + result = rule.all + expect(result).to be_instance_of(Flipper::Rules::All) + expect(result.rules).to eq([rule]) + end + end + + describe "#any" do + it "wraps self with any" do + rule = Flipper::Rules::Condition.new( + {"type" => "Property", "value" => "flipper_id"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "User;1"} + ) + result = rule.any + expect(result).to be_instance_of(Flipper::Rules::Any) + expect(result.rules).to eq([rule]) + end + end + describe "#value" do it "returns Hash with type and value" do rule = Flipper::Rules::Condition.new( From dd03f5e874b0924d39c6b311112644fba8b23bb5 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Fri, 24 Sep 2021 08:09:23 -0400 Subject: [PATCH 079/176] Fix typo --- spec/flipper/rules/all_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/flipper/rules/all_spec.rb b/spec/flipper/rules/all_spec.rb index e9c63ddb0..fccd0328a 100644 --- a/spec/flipper/rules/all_spec.rb +++ b/spec/flipper/rules/all_spec.rb @@ -62,7 +62,7 @@ end end - context "for Array with All rule" do + context "for Array with Any rule" do it "builds instance" do instance = Flipper::Rules::All.build(any_rule.value) expect(instance).to be_instance_of(Flipper::Rules::All) From 35efe2fc268da88a70247bcab56992d5971a8940 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Fri, 24 Sep 2021 08:13:53 -0400 Subject: [PATCH 080/176] Add any and all to Any/All --- lib/flipper/rules/all.rb | 8 ++++++++ lib/flipper/rules/any.rb | 8 ++++++++ spec/flipper/rules/all_spec.rb | 14 ++++++++++++++ spec/flipper/rules/any_spec.rb | 15 +++++++++++++++ 4 files changed, 45 insertions(+) diff --git a/lib/flipper/rules/all.rb b/lib/flipper/rules/all.rb index 90cf4f6f8..82ee9f9ab 100644 --- a/lib/flipper/rules/all.rb +++ b/lib/flipper/rules/all.rb @@ -3,6 +3,14 @@ module Flipper module Rules class All < Any + def all + self + end + + def any + Flipper::Rules::Any.new(self) + end + def matches?(feature_name, actor) @rules.all? { |rule| rule.matches?(feature_name, actor) } end diff --git a/lib/flipper/rules/any.rb b/lib/flipper/rules/any.rb index 8d2a85d7f..920fbec9f 100644 --- a/lib/flipper/rules/any.rb +++ b/lib/flipper/rules/any.rb @@ -13,6 +13,14 @@ def initialize(*rules) @rules = rules.flatten end + def all + Flipper::Rules::All.new(self) + end + + def any + self + end + def value { "type" => self.class.name.split('::').last, diff --git a/spec/flipper/rules/all_spec.rb b/spec/flipper/rules/all_spec.rb index fccd0328a..ab39e373a 100644 --- a/spec/flipper/rules/all_spec.rb +++ b/spec/flipper/rules/all_spec.rb @@ -71,6 +71,20 @@ end end + describe "#all" do + it "returns self" do + expect(rule.all).to be(rule) + end + end + + describe "#any" do + it "wraps self with any" do + result = rule.any + expect(result).to be_instance_of(Flipper::Rules::Any) + expect(result.rules).to eq([rule]) + end + end + describe "#value" do it "returns type and value" do expect(rule.value).to eq({ diff --git a/spec/flipper/rules/any_spec.rb b/spec/flipper/rules/any_spec.rb index f5ea16b91..6aa64004d 100644 --- a/spec/flipper/rules/any_spec.rb +++ b/spec/flipper/rules/any_spec.rb @@ -71,6 +71,21 @@ end end + describe "#all" do + it "wraps self with all" do + result = rule.all + expect(result).to be_instance_of(Flipper::Rules::All) + expect(result.rules).to eq([rule]) + end + end + + describe "#any" do + it "returns self" do + result = rule.any + expect(result).to be(rule) + end + end + describe "#value" do it "returns type and value" do expect(rule.value).to eq({ From 8c529998b2d25607c16d3c5c567d647f5324d1ef Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Fri, 24 Sep 2021 08:30:04 -0400 Subject: [PATCH 081/176] Add add/remove to Condition, Any and All This will make it easier to modify/update rules over time. --- lib/flipper/rules/any.rb | 8 +++ lib/flipper/rules/condition.rb | 8 +++ spec/flipper/rules/all_spec.rb | 79 ++++++++++++++++++++++++++++ spec/flipper/rules/any_spec.rb | 79 ++++++++++++++++++++++++++++ spec/flipper/rules/condition_spec.rb | 69 ++++++++++++++++++++++++ 5 files changed, 243 insertions(+) diff --git a/lib/flipper/rules/any.rb b/lib/flipper/rules/any.rb index 920fbec9f..2992ca487 100644 --- a/lib/flipper/rules/any.rb +++ b/lib/flipper/rules/any.rb @@ -21,6 +21,14 @@ def any self end + def add(*rules) + self.class.new(@rules + rules.flatten) + end + + def remove(*rules) + self.class.new(@rules - rules.flatten) + end + def value { "type" => self.class.name.split('::').last, diff --git a/lib/flipper/rules/condition.rb b/lib/flipper/rules/condition.rb index 2047baf5f..42bf8b782 100644 --- a/lib/flipper/rules/condition.rb +++ b/lib/flipper/rules/condition.rb @@ -24,6 +24,14 @@ def any Flipper::Rules::Any.new(self) end + def add(*rules) + any.add(*rules) + end + + def remove(*rules) + any.remove(*rules) + end + def value { "type" => "Condition", diff --git a/spec/flipper/rules/all_spec.rb b/spec/flipper/rules/all_spec.rb index ab39e373a..848f8dee0 100644 --- a/spec/flipper/rules/all_spec.rb +++ b/spec/flipper/rules/all_spec.rb @@ -85,6 +85,85 @@ end end + describe "#add" do + context "with single rule" do + it "returns new instance with rule added" do + rule2 = Flipper::Rules::Condition.new( + {"type" => "Property", "value" => "flipper_id"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "User;2"} + ) + result = rule.add rule2 + expect(result).not_to be(rule) + expect(result.rules).to eq([rule.rules, rule2].flatten) + end + end + + context "with multiple rules" do + it "returns new instance with rule added" do + rule2 = Flipper::Rules::Condition.new( + {"type" => "Property", "value" => "flipper_id"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "User;2"} + ) + rule3 = Flipper::Rules::Condition.new( + {"type" => "Property", "value" => "flipper_id"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "User;3"} + ) + + result = rule.add rule2, rule3 + expect(result).not_to be(rule) + expect(result.rules).to eq([rule.rules, rule2, rule3].flatten) + end + end + + context "with array of rules" do + it "returns new instance with rule added" do + rule2 = Flipper::Rules::Condition.new( + {"type" => "Property", "value" => "flipper_id"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "User;2"} + ) + rule3 = Flipper::Rules::Condition.new( + {"type" => "Property", "value" => "flipper_id"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "User;3"} + ) + + result = rule.add [rule2, rule3] + expect(result).not_to be(rule) + expect(result.rules).to eq([rule.rules, rule2, rule3].flatten) + end + end + end + + describe "#remove" do + context "with single rule" do + it "returns new instance with rule removed" do + result = rule.remove age_condition + expect(result).not_to be(rule) + expect(result.rules).to eq([plan_condition]) + end + end + + context "with multiple rules" do + it "returns new instance with rules removed" do + result = rule.remove age_condition, plan_condition + expect(result).not_to be(rule) + expect(result.rules).to eq([]) + end + end + + context "with array of rules" do + it "returns new instance with rules removed" do + result = rule.remove [age_condition, plan_condition] + expect(result).not_to be(rule) + expect(result.rules).to eq([]) + end + end + end + describe "#value" do it "returns type and value" do expect(rule.value).to eq({ diff --git a/spec/flipper/rules/any_spec.rb b/spec/flipper/rules/any_spec.rb index 6aa64004d..f123d5d42 100644 --- a/spec/flipper/rules/any_spec.rb +++ b/spec/flipper/rules/any_spec.rb @@ -86,6 +86,85 @@ end end + describe "#add" do + context "with single rule" do + it "returns new instance with rule added" do + rule2 = Flipper::Rules::Condition.new( + {"type" => "Property", "value" => "flipper_id"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "User;2"} + ) + result = rule.add rule2 + expect(result).not_to be(rule) + expect(result.rules).to eq([rule.rules, rule2].flatten) + end + end + + context "with multiple rules" do + it "returns new instance with rule added" do + rule2 = Flipper::Rules::Condition.new( + {"type" => "Property", "value" => "flipper_id"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "User;2"} + ) + rule3 = Flipper::Rules::Condition.new( + {"type" => "Property", "value" => "flipper_id"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "User;3"} + ) + + result = rule.add rule2, rule3 + expect(result).not_to be(rule) + expect(result.rules).to eq([rule.rules, rule2, rule3].flatten) + end + end + + context "with array of rules" do + it "returns new instance with rule added" do + rule2 = Flipper::Rules::Condition.new( + {"type" => "Property", "value" => "flipper_id"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "User;2"} + ) + rule3 = Flipper::Rules::Condition.new( + {"type" => "Property", "value" => "flipper_id"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "User;3"} + ) + + result = rule.add [rule2, rule3] + expect(result).not_to be(rule) + expect(result.rules).to eq([rule.rules, rule2, rule3].flatten) + end + end + end + + describe "#remove" do + context "with single rule" do + it "returns new instance with rule removed" do + result = rule.remove age_condition + expect(result).not_to be(rule) + expect(result.rules).to eq([plan_condition]) + end + end + + context "with multiple rules" do + it "returns new instance with rules removed" do + result = rule.remove age_condition, plan_condition + expect(result).not_to be(rule) + expect(result.rules).to eq([]) + end + end + + context "with array of rules" do + it "returns new instance with rules removed" do + result = rule.remove [age_condition, plan_condition] + expect(result).not_to be(rule) + expect(result.rules).to eq([]) + end + end + end + describe "#value" do it "returns type and value" do expect(rule.value).to eq({ diff --git a/spec/flipper/rules/condition_spec.rb b/spec/flipper/rules/condition_spec.rb index 43cbe3623..bd28d6220 100644 --- a/spec/flipper/rules/condition_spec.rb +++ b/spec/flipper/rules/condition_spec.rb @@ -29,6 +29,75 @@ end end + describe "#add" do + context "with single rule" do + it "wraps self with any and adds new rule" do + rule = Flipper::Rules::Condition.new( + {"type" => "Property", "value" => "flipper_id"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "User;1"} + ) + rule2 = Flipper::Rules::Condition.new( + {"type" => "Property", "value" => "flipper_id"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "User;2"} + ) + + result = rule.add(rule2) + expect(result).to be_instance_of(Flipper::Rules::Any) + expect(result.rules).to eq([rule, rule2]) + end + end + + context "with multiple rules" do + it "wraps self with any and adds new rules" do + rule = Flipper::Rules::Condition.new( + {"type" => "Property", "value" => "flipper_id"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "User;1"} + ) + rule2 = Flipper::Rules::Condition.new( + {"type" => "Property", "value" => "flipper_id"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "User;2"} + ) + rule3 = Flipper::Rules::Condition.new( + {"type" => "Property", "value" => "flipper_id"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "User;3"} + ) + + result = rule.add(rule2, rule3) + expect(result).to be_instance_of(Flipper::Rules::Any) + expect(result.rules).to eq([rule, rule2, rule3]) + end + end + + context "with array of rules" do + it "wraps self with any and adds new rules" do + rule = Flipper::Rules::Condition.new( + {"type" => "Property", "value" => "flipper_id"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "User;1"} + ) + rule2 = Flipper::Rules::Condition.new( + {"type" => "Property", "value" => "flipper_id"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "User;2"} + ) + rule3 = Flipper::Rules::Condition.new( + {"type" => "Property", "value" => "flipper_id"}, + {"type" => "Operator", "value" => "eq"}, + {"type" => "String", "value" => "User;3"} + ) + + result = rule.add([rule2, rule3]) + expect(result).to be_instance_of(Flipper::Rules::Any) + expect(result.rules).to eq([rule, rule2, rule3]) + end + end + end + describe "#value" do it "returns Hash with type and value" do rule = Flipper::Rules::Condition.new( From b7b843fb006c838e5e4ed74c6f4d9065553c74dd Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Fri, 24 Sep 2021 08:42:23 -0400 Subject: [PATCH 082/176] Add top level rule method to get rule for feature This makes it easy to then update and save feature. --- lib/flipper/dsl.rb | 8 ++++---- lib/flipper/feature.rb | 4 ++++ spec/flipper/dsl_spec.rb | 19 ++++++++----------- spec/flipper/feature_spec.rb | 12 ++++++++++++ spec/flipper_spec.rb | 9 +++++++++ 5 files changed, 37 insertions(+), 15 deletions(-) diff --git a/lib/flipper/dsl.rb b/lib/flipper/dsl.rb index 9b525ee53..228cd5c41 100644 --- a/lib/flipper/dsl.rb +++ b/lib/flipper/dsl.rb @@ -263,13 +263,13 @@ def actor(thing) Types::Actor.new(thing) end - # Public: Wraps an object as a Flipper::Rules::Rule. + # Public: Gets the rule for the feature. # - # thing - The rule or Hash that you would like to wrap. + # name - The String or Symbol name of the feature. # # Returns an instance of Flipper::Rules::Rule. - def rule(thing) - Rules.wrap(thing) + def rule(name) + feature(name).rule end # Public: Shortcut for getting a percentage of time instance. diff --git a/lib/flipper/feature.rb b/lib/flipper/feature.rb index c4ae0369f..9795f3567 100644 --- a/lib/flipper/feature.rb +++ b/lib/flipper/feature.rb @@ -272,6 +272,10 @@ def disabled_groups Flipper.groups - enabled_groups end + def rule + Flipper::Rules.build(rule_value) if rule_value + end + # Public: Get the adapter value for the groups gate. # # Returns Set of String group names. diff --git a/spec/flipper/dsl_spec.rb b/spec/flipper/dsl_spec.rb index beecd0273..64a83ee05 100644 --- a/spec/flipper/dsl_spec.rb +++ b/spec/flipper/dsl_spec.rb @@ -140,17 +140,14 @@ end describe '#rule' do - context 'for Hash' do - it 'returns rule instance' do - rule = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "plan"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "basic"} - ) - result = subject.rule(rule.value) - expect(result).to be_instance_of(Flipper::Rules::Condition) - expect(result.value).to eq(rule.value) - end + it "returns nil if feature has no rule" do + expect(subject.rule(:stats)).to be(nil) + end + + it "returns rule if feature has rule" do + rule = Flipper.property(:plan).eq("basic") + subject[:stats].enable_rule rule + expect(subject.rule(:stats)).to eq(rule) end end diff --git a/spec/flipper/feature_spec.rb b/spec/flipper/feature_spec.rb index fa499462e..581d4260c 100644 --- a/spec/flipper/feature_spec.rb +++ b/spec/flipper/feature_spec.rb @@ -650,6 +650,18 @@ end end + describe '#rule' do + it "returns nil if feature has no rule" do + expect(subject.rule).to be(nil) + end + + it "returns rule if feature has rule" do + rule = Flipper.property(:plan).eq("basic") + subject.enable_rule rule + expect(subject.rule).to eq(rule) + end + end + describe '#enable_rule/disable_rule' do context "with rule instance" do it "updates gate values to equal rule" do diff --git a/spec/flipper_spec.rb b/spec/flipper_spec.rb index ab1a00a47..d1c6a0d5e 100644 --- a/spec/flipper_spec.rb +++ b/spec/flipper_spec.rb @@ -108,6 +108,15 @@ expect(described_class.boolean).to eq(described_class.instance.boolean) end + it 'delegates rule to instance' do + expect(described_class.rule(:search)).to be(nil) + + rule = Flipper.property(:plan).eq("basic") + Flipper.instance.enable_rule :search, rule + + expect(described_class.rule(:search)).to eq(rule) + end + it 'delegates enable_rule to instance' do described_class.enable_rule(:search, rule) expect(described_class.instance.enabled?(:search, actor)).to be(true) From 84591edfaf8e31e3548ba6a04012818114f33fde Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Fri, 24 Sep 2021 08:54:04 -0400 Subject: [PATCH 083/176] Add some examples that show how to go from simple condition to more complex --- examples/rules.rb | 43 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/examples/rules.rb b/examples/rules.rb index cb3613ae1..9411c1ade 100644 --- a/examples/rules.rb +++ b/examples/rules.rb @@ -17,6 +17,10 @@ def refute(value) end end +def reset + Flipper.disable_rule :something +end + class User < Struct.new(:id, :flipper_properties) include Flipper::Identifier end @@ -67,7 +71,7 @@ class Org < Struct.new(:id, :flipper_properties) refute Flipper.enabled?(:something, other_user) puts "Disabling single rule" -Flipper.disable_rule :something, plan_rule +reset refute Flipper.enabled?(:something, user) puts "\n\nAny Rule" @@ -81,7 +85,7 @@ class Org < Struct.new(:id, :flipper_properties) refute Flipper.enabled?(:something, other_user) puts "Disabling any rule" -Flipper.disable_rule :something, any_rule +reset refute Flipper.enabled?(:something, user) puts "\n\nAll Rule" @@ -95,7 +99,7 @@ class Org < Struct.new(:id, :flipper_properties) refute Flipper.enabled?(:something, other_user) puts "Disabling all rule" -Flipper.disable_rule :something, all_rule +reset refute Flipper.enabled?(:something, user) puts "\n\nNested Rule" @@ -111,7 +115,7 @@ class Org < Struct.new(:id, :flipper_properties) refute Flipper.enabled?(:something, other_user) puts "Disabling nested rule" -Flipper.disable_rule :something, nested_rule +reset refute Flipper.enabled?(:something, user) refute Flipper.enabled?(:something, admin_user) refute Flipper.enabled?(:something, other_user) @@ -121,7 +125,7 @@ class Org < Struct.new(:id, :flipper_properties) Flipper.enable_rule :something, boolean_rule assert Flipper.enabled?(:something) assert Flipper.enabled?(:something, user) -Flipper.disable_rule :something, boolean_rule +reset puts "\n\nSet of Actors Rule" set_of_actors_rule = Flipper.any( @@ -132,7 +136,7 @@ class Org < Struct.new(:id, :flipper_properties) assert Flipper.enabled?(:something, user) assert Flipper.enabled?(:something, other_user) refute Flipper.enabled?(:something, admin_user) -Flipper.disable_rule :something, set_of_actors_rule +reset puts "\n\n% of Actors Rule" percentage_of_actors = Flipper.property(:flipper_id).percentage(30) @@ -140,7 +144,7 @@ class Org < Struct.new(:id, :flipper_properties) refute Flipper.enabled?(:something, user) refute Flipper.enabled?(:something, other_user) assert Flipper.enabled?(:something, admin_user) -Flipper.disable_rule :something, percentage_of_actors +reset puts "\n\n% of Actors Per Type Rule" percentage_of_actors_per_type = Flipper.any( @@ -158,7 +162,7 @@ class Org < Struct.new(:id, :flipper_properties) assert Flipper.enabled?(:something, other_user) assert Flipper.enabled?(:something, admin_user) refute Flipper.enabled?(:something, org) # not in the 10% of enabled for Orgs -Flipper.disable_rule :something, percentage_of_actors_per_type +reset puts "\n\nPercentage of Time Rule" percentage_of_time_rule = Flipper.random(100).lt(50) @@ -169,4 +173,25 @@ class Org < Struct.new(:id, :flipper_properties) p disabled: disabled.size assert (4_700..5_200).include?(enabled.size) assert (4_700..5_200).include?(disabled.size) -Flipper.disable_rule :something, percentage_of_time_rule +reset + +puts "\n\nChanging single rule to all rule" +Flipper.enable_rule :something, plan_rule +Flipper.enable_rule :something, Flipper.rule(:something).all.add(age_rule) +assert Flipper.enabled?(:something, user) +refute Flipper.enabled?(:something, admin_user) +refute Flipper.enabled?(:something, other_user) + +puts "\n\nChanging single rule to any rule" +Flipper.enable_rule :something, plan_rule +Flipper.enable_rule :something, Flipper.rule(:something).any.add(age_rule, admin_rule) +assert Flipper.enabled?(:something, user) +assert Flipper.enabled?(:something, admin_user) +refute Flipper.enabled?(:something, other_user) + +puts "\n\nChanging single rule to any rule by adding to condition" +Flipper.enable_rule :something, plan_rule +Flipper.enable_rule :something, Flipper.rule(:something).add(admin_rule) +assert Flipper.enabled?(:something, user) +assert Flipper.enabled?(:something, admin_user) +refute Flipper.enabled?(:something, other_user) From e80cca1b617b9f51f8e5927e3c1d86c184e4540a Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Fri, 24 Sep 2021 08:56:12 -0400 Subject: [PATCH 084/176] Increase range for random spec --- spec/flipper/rules/condition_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/flipper/rules/condition_spec.rb b/spec/flipper/rules/condition_spec.rb index bd28d6220..7a12abc10 100644 --- a/spec/flipper/rules/condition_spec.rb +++ b/spec/flipper/rules/condition_spec.rb @@ -352,7 +352,7 @@ def flipper_id end enabled, disabled = results.partition { |r| r } - expect(enabled.size).to be_within(30).of(250) + expect(enabled.size).to be_within(50).of(250) end end From d0ddb1b2d56d2ea5b73f1866e6b8250ccdd4a91c Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Fri, 1 Oct 2021 08:24:55 -0400 Subject: [PATCH 085/176] Remove parameter from disable_rule to make it more clear what this does --- lib/flipper/dsl.rb | 7 +++---- lib/flipper/feature.rb | 8 ++------ spec/flipper/feature_spec.rb | 8 ++++---- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/lib/flipper/dsl.rb b/lib/flipper/dsl.rb index 228cd5c41..e2f790725 100644 --- a/lib/flipper/dsl.rb +++ b/lib/flipper/dsl.rb @@ -108,14 +108,13 @@ def disable(name, *args) feature(name).disable(*args) end - # Public: Disable a feature for a rule. + # Public: Disable rule for feature. # # name - The String or Symbol name of the feature. - # rule - a Flipper::Rules::Rule instance or a Hash. # # Returns result of Feature#disable. - def disable_rule(name, rule = nil) - feature(name).disable_rule(rule) + def disable_rule(name) + feature(name).disable_rule end # Public: Disable a feature for an actor. diff --git a/lib/flipper/feature.rb b/lib/flipper/feature.rb index 9795f3567..28dc1fe0a 100644 --- a/lib/flipper/feature.rb +++ b/lib/flipper/feature.rb @@ -173,12 +173,8 @@ def enable_percentage_of_actors(percentage) # rule - a rule or Hash that can be converted to a rule. # # Returns result of disable. - def disable_rule(rule = nil) - if rule - disable Rules.wrap(rule) - else - disable Flipper.object(true).eq(false) - end + def disable_rule + disable Flipper.all # just need a rule to clear end # Public: Disables a feature for an actor. diff --git a/spec/flipper/feature_spec.rb b/spec/flipper/feature_spec.rb index 581d4260c..107777081 100644 --- a/spec/flipper/feature_spec.rb +++ b/spec/flipper/feature_spec.rb @@ -664,7 +664,7 @@ describe '#enable_rule/disable_rule' do context "with rule instance" do - it "updates gate values to equal rule" do + it "updates gate values to equal rule or clears rule" do rule = Flipper::Rules::Condition.new( {"type" => "Property", "value" => "plan"}, {"type" => "Operator", "value" => "eq"}, @@ -674,13 +674,13 @@ expect(subject.gate_values.rule).to be(nil) subject.enable_rule(rule) expect(subject.gate_values.rule).to eq(rule.value) - subject.disable_rule(other_rule) + subject.disable_rule expect(subject.gate_values.rule).to be(nil) end end context "with Hash" do - it "updates gate values to equal rule" do + it "updates gate values to equal rule or clears rule" do rule = Flipper::Rules::Condition.new( {"type" => "Property", "value" => "plan"}, {"type" => "Operator", "value" => "eq"}, @@ -690,7 +690,7 @@ expect(subject.gate_values.rule).to be(nil) subject.enable_rule(rule.value) expect(subject.gate_values.rule).to eq(rule.value) - subject.disable_rule(other_rule.value) + subject.disable_rule expect(subject.gate_values.rule).to be(nil) end end From cc7094dc5d822aa7a2ae7971341beb1f6eb9491c Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Fri, 1 Oct 2021 08:25:03 -0400 Subject: [PATCH 086/176] Change rules gate to rule gate --- lib/flipper/api/middleware.rb | 2 +- .../api/v1/actions/{rules_gate.rb => rule_gate.rb} | 2 +- .../v1/actions/{rules_gate_spec.rb => rule_gate_spec.rb} | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) rename lib/flipper/api/v1/actions/{rules_gate.rb => rule_gate.rb} (97%) rename spec/flipper/api/v1/actions/{rules_gate_spec.rb => rule_gate_spec.rb} (97%) diff --git a/lib/flipper/api/middleware.rb b/lib/flipper/api/middleware.rb index eb741a8de..3dfc7eb13 100644 --- a/lib/flipper/api/middleware.rb +++ b/lib/flipper/api/middleware.rb @@ -14,7 +14,7 @@ def initialize(app, options = {}) @env_key = options.fetch(:env_key, 'flipper') @action_collection = ActionCollection.new - @action_collection.add Api::V1::Actions::RulesGate + @action_collection.add Api::V1::Actions::RuleGate @action_collection.add Api::V1::Actions::PercentageOfTimeGate @action_collection.add Api::V1::Actions::PercentageOfActorsGate @action_collection.add Api::V1::Actions::ActorsGate diff --git a/lib/flipper/api/v1/actions/rules_gate.rb b/lib/flipper/api/v1/actions/rule_gate.rb similarity index 97% rename from lib/flipper/api/v1/actions/rules_gate.rb rename to lib/flipper/api/v1/actions/rule_gate.rb index d6396360c..fd0776ad8 100644 --- a/lib/flipper/api/v1/actions/rules_gate.rb +++ b/lib/flipper/api/v1/actions/rule_gate.rb @@ -5,7 +5,7 @@ module Flipper module Api module V1 module Actions - class RulesGate < Api::Action + class RuleGate < Api::Action include FeatureNameFromRoute route %r{\A/features/(?.*)/rule/?\Z} diff --git a/spec/flipper/api/v1/actions/rules_gate_spec.rb b/spec/flipper/api/v1/actions/rule_gate_spec.rb similarity index 97% rename from spec/flipper/api/v1/actions/rules_gate_spec.rb rename to spec/flipper/api/v1/actions/rule_gate_spec.rb index 6f60599e6..8182b33f4 100644 --- a/spec/flipper/api/v1/actions/rules_gate_spec.rb +++ b/spec/flipper/api/v1/actions/rule_gate_spec.rb @@ -1,6 +1,6 @@ require 'helper' -RSpec.describe Flipper::Api::V1::Actions::RulesGate do +RSpec.describe Flipper::Api::V1::Actions::RuleGate do let(:app) { build_api(flipper) } let(:actor) { Flipper::Actor.new('1', { @@ -18,7 +18,7 @@ describe 'enable' do before do - flipper[:my_feature].disable_rule(rule) + flipper[:my_feature].disable_rule post '/features/my_feature/rule', JSON.dump(rule.value), "CONTENT_TYPE" => "application/json" end @@ -54,7 +54,7 @@ describe 'enable feature with slash in name' do before do - flipper["my/feature"].disable_rule(rule) + flipper["my/feature"].disable_rule post '/features/my/feature/rule', JSON.dump(rule.value), "CONTENT_TYPE" => "application/json" end @@ -72,7 +72,7 @@ describe 'enable feature with space in name' do before do - flipper["sp ace"].disable_rule(rule) + flipper["sp ace"].disable_rule post '/features/sp%20ace/rule', JSON.dump(rule.value), "CONTENT_TYPE" => "application/json" end From 49119d4ba7d7317f232c10ec83eea7ba9b777e75 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Fri, 1 Oct 2021 08:28:31 -0400 Subject: [PATCH 087/176] Fix failing spec due to parameter removal --- spec/flipper_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/flipper_spec.rb b/spec/flipper_spec.rb index d1c6a0d5e..8d8987970 100644 --- a/spec/flipper_spec.rb +++ b/spec/flipper_spec.rb @@ -123,7 +123,7 @@ end it 'delegates disable_rule to instance' do - described_class.disable_rule(:search, Flipper.object(true).eq(false)) + described_class.disable_rule(:search) expect(described_class.instance.enabled?(:search, actor)).to be(false) end From 5f43d2df9e16e02dc017537ca7951b5351c4fb7c Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 6 Oct 2021 08:52:06 -0400 Subject: [PATCH 088/176] Add add_rule/remove_rule To make it easier to add and remove rules from the current rule. --- lib/flipper.rb | 2 +- lib/flipper/dsl.rb | 18 ++ lib/flipper/feature.rb | 25 +++ spec/flipper/dsl_spec.rb | 27 +++ spec/flipper/feature_spec.rb | 319 +++++++++++++++++++++++++++++++++++ spec/flipper_spec.rb | 12 ++ 6 files changed, 402 insertions(+), 1 deletion(-) diff --git a/lib/flipper.rb b/lib/flipper.rb index 6c5d505c9..cc0a84476 100644 --- a/lib/flipper.rb +++ b/lib/flipper.rb @@ -57,7 +57,7 @@ def instance=(flipper) # interface of Flipper::DSL. def_delegators :instance, :enabled?, :enable, :disable, :bool, :boolean, - :enable_rule, :disable_rule, :rule, + :enable_rule, :disable_rule, :rule, :add_rule, :remove_rule, :enable_actor, :disable_actor, :actor, :enable_group, :disable_group, :enable_percentage_of_actors, :disable_percentage_of_actors, diff --git a/lib/flipper/dsl.rb b/lib/flipper/dsl.rb index e2f790725..7866172a7 100644 --- a/lib/flipper/dsl.rb +++ b/lib/flipper/dsl.rb @@ -54,6 +54,15 @@ def enable_rule(name, rule) feature(name).enable_rule(rule) end + # Public: Add a rule to a feature. + # + # rule - a rule or Hash that can be converted to a rule. + # + # Returns result of enable. + def add_rule(name, rule) + feature(name).add_rule(rule) + end + # Public: Enable a feature for an actor. # # name - The String or Symbol name of the feature. @@ -117,6 +126,15 @@ def disable_rule(name) feature(name).disable_rule end + # Public: Remove a rule from a feature. + # + # rule - a rule or Hash that can be converted to a rule. + # + # Returns result of enable. + def remove_rule(name, rule) + feature(name).remove_rule(rule) + end + # Public: Disable a feature for an actor. # # name - The String or Symbol name of the feature. diff --git a/lib/flipper/feature.rb b/lib/flipper/feature.rb index 28dc1fe0a..ef9f12d59 100644 --- a/lib/flipper/feature.rb +++ b/lib/flipper/feature.rb @@ -128,6 +128,19 @@ def enable_rule(rule) enable Rules.wrap(rule) end + # Public: Add a rule for a feature. + # + # rule_to_add - a rule or Hash that can be converted to a rule. + # + # Returns result of enable. + def add_rule(rule_to_add) + if (current_rule = rule) + enable current_rule.add(rule_to_add) + else + enable rule_to_add + end + end + # Public: Enables a feature for an actor. # # actor - a Flipper::Types::Actor instance or an object that responds @@ -177,6 +190,18 @@ def disable_rule disable Flipper.all # just need a rule to clear end + # Public: Remove a rule from a feature. Does nothing if no rule is + # currently enabled. + # + # rule_to_remove - a rule or Hash that can be converted to a rule. + # + # Returns result of enable or nil (if no rule enabled). + def remove_rule(rule_to_remove) + if (current_rule = rule) + enable current_rule.remove(rule_to_remove) + end + end + # Public: Disables a feature for an actor. # # actor - a Flipper::Types::Actor instance or an object that responds diff --git a/spec/flipper/dsl_spec.rb b/spec/flipper/dsl_spec.rb index 64a83ee05..4b93bead8 100644 --- a/spec/flipper/dsl_spec.rb +++ b/spec/flipper/dsl_spec.rb @@ -249,6 +249,33 @@ end end + describe '#enable_rule/disable_rule' do + it 'enables and disables the feature for the rule' do + rule = Flipper.property(:plan).eq("basic") + + expect(subject[:stats].rule).to be(nil) + subject.enable_rule(:stats, rule) + expect(subject[:stats].rule).to eq(rule) + + subject.disable_rule(:stats) + expect(subject[:stats].rule).to be(nil) + end + end + + describe '#add_rule/remove_rule' do + it 'enables and disables the feature for the rule' do + condition = Flipper.property(:plan).eq("basic") + rule = Flipper.any(condition) + + expect(subject[:stats].rule).to be(nil) + subject.add_rule(:stats, rule) + expect(subject[:stats].rule).to eq(rule) + + subject.remove_rule(:stats, condition) + expect(subject[:stats].rule).to eq(Flipper.any) + end + end + describe '#enable_actor/disable_actor' do it 'enables and disables the feature for actor' do actor = Flipper::Actor.new(5) diff --git a/spec/flipper/feature_spec.rb b/spec/flipper/feature_spec.rb index 107777081..1dfeb9863 100644 --- a/spec/flipper/feature_spec.rb +++ b/spec/flipper/feature_spec.rb @@ -696,6 +696,325 @@ end end + describe "#add_rule" do + context "when nothing enabled" do + context "with Condition instance" do + it "sets rule to Condition" do + rule = Flipper.property(:plan).eq("basic") + subject.add_rule(rule) + expect(subject.rule).to be_instance_of(Flipper::Rules::Condition) + expect(subject.rule).to eq(rule) + end + end + + context "with Any instance" do + it "sets rule to Any" do + rule = Flipper.any(Flipper.property(:plan).eq("basic")) + subject.add_rule(rule) + expect(subject.rule).to be_instance_of(Flipper::Rules::Any) + expect(subject.rule).to eq(rule) + end + end + + context "with All instance" do + it "sets rule to All" do + rule = Flipper.all(Flipper.property(:plan).eq("basic")) + subject.add_rule(rule) + expect(subject.rule).to be_instance_of(Flipper::Rules::All) + expect(subject.rule).to eq(rule) + end + end + end + + context "when Condition enabled" do + let(:rule) { Flipper.property(:plan).eq("basic") } + + before do + subject.enable_rule rule + end + + context "with Condition instance" do + it "changes rule to Any and adds new Condition" do + new_rule = Flipper.property(:age).gte(21) + subject.add_rule(new_rule) + expect(subject.rule).to be_instance_of(Flipper::Rules::Any) + expect(subject.rule.rules).to include(rule) + expect(subject.rule.rules).to include(new_rule) + end + end + + context "with Any instance" do + it "changes rule to Any and adds new Any" do + new_rule = Flipper.any(Flipper.property(:age).eq(21)) + subject.add_rule new_rule + expect(subject.rule).to be_instance_of(Flipper::Rules::Any) + expect(subject.rule.rules).to include(rule) + expect(subject.rule.rules).to include(new_rule) + end + end + + context "with All instance" do + it "changes rule to Any and adds new All" do + new_rule = Flipper.all(Flipper.property(:plan).eq("basic")) + subject.add_rule new_rule + expect(subject.rule).to be_instance_of(Flipper::Rules::Any) + expect(subject.rule.rules).to include(rule) + expect(subject.rule.rules).to include(new_rule) + end + end + end + + context "when Any enabled" do + let(:condition) { Flipper.property(:plan).eq("basic") } + let(:rule) { Flipper.any(condition) } + + before do + subject.enable_rule rule + end + + context "with Condition instance" do + it "adds Condition to Any" do + new_rule = Flipper.property(:age).gte(21) + subject.add_rule(new_rule) + expect(subject.rule).to be_instance_of(Flipper::Rules::Any) + expect(subject.rule.rules).to include(condition) + expect(subject.rule.rules).to include(new_rule) + end + end + + context "with Any instance" do + it "adds Any to Any" do + new_rule = Flipper.any(Flipper.property(:age).gte(21)) + subject.add_rule(new_rule) + expect(subject.rule).to be_instance_of(Flipper::Rules::Any) + expect(subject.rule.rules).to include(condition) + expect(subject.rule.rules).to include(new_rule) + end + end + + context "with All instance" do + it "adds All to Any" do + new_rule = Flipper.all(Flipper.property(:age).gte(21)) + subject.add_rule(new_rule) + expect(subject.rule).to be_instance_of(Flipper::Rules::Any) + expect(subject.rule.rules).to include(condition) + expect(subject.rule.rules).to include(new_rule) + end + end + end + + context "when All enabled" do + let(:condition) { Flipper.property(:plan).eq("basic") } + let(:rule) { Flipper.all(condition) } + + before do + subject.enable_rule rule + end + + context "with Condition instance" do + it "adds Condition to All" do + new_rule = Flipper.property(:age).gte(21) + subject.add_rule(new_rule) + expect(subject.rule).to be_instance_of(Flipper::Rules::All) + expect(subject.rule.rules).to include(condition) + expect(subject.rule.rules).to include(new_rule) + end + end + + context "with Any instance" do + it "adds Any to All" do + new_rule = Flipper.any(Flipper.property(:age).gte(21)) + subject.add_rule(new_rule) + expect(subject.rule).to be_instance_of(Flipper::Rules::All) + expect(subject.rule.rules).to include(condition) + expect(subject.rule.rules).to include(new_rule) + end + end + + context "with All instance" do + it "adds All to All" do + new_rule = Flipper.all(Flipper.property(:age).gte(21)) + subject.add_rule(new_rule) + expect(subject.rule).to be_instance_of(Flipper::Rules::All) + expect(subject.rule.rules).to include(condition) + expect(subject.rule.rules).to include(new_rule) + end + end + end + end + + describe '#remove_rule' do + context "when nothing enabled" do + context "with Condition instance" do + it "does nothing" do + rule = Flipper.property(:plan).eq("basic") + subject.remove_rule(rule) + expect(subject.rule).to be(nil) + end + end + + context "with Any instance" do + it "does nothing" do + rule = Flipper.any(Flipper.property(:plan).eq("basic")) + subject.remove_rule rule + expect(subject.rule).to be(nil) + end + end + + context "with All instance" do + it "does nothing" do + rule = Flipper.all(Flipper.property(:plan).eq("basic")) + subject.remove_rule rule + expect(subject.rule).to be(nil) + end + end + end + + context "when Condition enabled" do + let(:rule) { Flipper.property(:plan).eq("basic") } + + before do + subject.enable_rule rule + end + + context "with Condition instance" do + it "changes rule to Any and removes Condition if it matches" do + new_rule = Flipper.property(:plan).eq("basic") + subject.remove_rule new_rule + expect(subject.rule).to eq(Flipper.any) + end + + it "changes rule to Any if Condition doesn't match" do + new_rule = Flipper.property(:plan).eq("premium") + subject.remove_rule new_rule + expect(subject.rule).to eq(Flipper.any(rule)) + end + end + + context "with Any instance" do + it "changes rule to Any and does nothing" do + new_rule = Flipper.any(Flipper.property(:plan).eq("basic")) + subject.remove_rule new_rule + expect(subject.rule).to eq(Flipper.any(rule)) + end + end + + context "with All instance" do + it "changes rule to Any and does nothing" do + new_rule = Flipper.all(Flipper.property(:plan).eq("basic")) + subject.remove_rule new_rule + expect(subject.rule).to eq(Flipper.any(rule)) + end + end + end + + context "when Any enabled" do + let(:condition) { Flipper.property(:plan).eq("basic") } + let(:rule) { Flipper.any condition } + + before do + subject.enable_rule rule + end + + context "with Condition instance" do + it "removes Condition if it matches" do + subject.remove_rule condition + expect(subject.rule).to eq(Flipper.any) + end + + it "does nothing if Condition does not match" do + subject.remove_rule Flipper.property(:plan).eq("premium") + expect(subject.rule).to eq(rule) + end + end + + context "with Any instance" do + it "removes Any if it matches" do + new_rule = Flipper.any(Flipper.property(:plan).eq("premium")) + subject.add_rule new_rule + expect(subject.rule.rules.size).to be(2) + subject.remove_rule new_rule + expect(subject.rule).to eq(rule) + end + + it "does nothing if Any does not match" do + new_rule = Flipper.any(Flipper.property(:plan).eq("premium")) + subject.remove_rule new_rule + expect(subject.rule).to eq(rule) + end + end + + context "with All instance" do + it "removes All if it matches" do + new_rule = Flipper.all(Flipper.property(:plan).eq("premium")) + subject.add_rule new_rule + expect(subject.rule.rules.size).to be(2) + subject.remove_rule new_rule + expect(subject.rule).to eq(rule) + end + + it "does nothing if All does not match" do + new_rule = Flipper.all(Flipper.property(:plan).eq("premium")) + subject.remove_rule new_rule + expect(subject.rule).to eq(rule) + end + end + end + + context "when All enabled" do + let(:condition) { Flipper.property(:plan).eq("basic") } + let(:rule) { Flipper.all condition } + + before do + subject.enable_rule rule + end + + context "with Condition instance" do + it "removes Condition if it matches" do + subject.remove_rule condition + expect(subject.rule).to eq(Flipper.all) + end + + it "does nothing if Condition does not match" do + subject.remove_rule Flipper.property(:plan).eq("premium") + expect(subject.rule).to eq(rule) + end + end + + context "with Any instance" do + it "removes Any if it matches" do + new_rule = Flipper.any(Flipper.property(:plan).eq("premium")) + subject.add_rule new_rule + expect(subject.rule.rules.size).to be(2) + subject.remove_rule new_rule + expect(subject.rule).to eq(rule) + end + + it "does nothing if Any does not match" do + new_rule = Flipper.any(Flipper.property(:plan).eq("premium")) + subject.remove_rule new_rule + expect(subject.rule).to eq(rule) + end + end + + context "with All instance" do + it "removes All if it matches" do + new_rule = Flipper.all(Flipper.property(:plan).eq("premium")) + subject.add_rule new_rule + expect(subject.rule.rules.size).to be(2) + subject.remove_rule new_rule + expect(subject.rule).to eq(rule) + end + + it "does nothing if All does not match" do + new_rule = Flipper.all(Flipper.property(:plan).eq("premium")) + subject.remove_rule new_rule + expect(subject.rule).to eq(rule) + end + end + end + end + describe '#enable_actor/disable_actor' do context 'with object that responds to flipper_id' do it 'updates the gate values to include the actor' do diff --git a/spec/flipper_spec.rb b/spec/flipper_spec.rb index 8d8987970..d4d90ec56 100644 --- a/spec/flipper_spec.rb +++ b/spec/flipper_spec.rb @@ -127,6 +127,18 @@ expect(described_class.instance.enabled?(:search, actor)).to be(false) end + it 'delegates add_rule to instance' do + described_class.add_rule(:search, rule) + expect(described_class.instance.enabled?(:search, actor)).to be(true) + end + + it 'delegates remove_rule to instance' do + described_class.enable_rule(:search, Flipper.any(rule)) + expect(described_class.instance.enabled?(:search, actor)).to be(true) + described_class.remove_rule(:search, rule) + expect(described_class.instance.enabled?(:search, actor)).to be(false) + end + it 'delegates enable_actor to instance' do described_class.enable_actor(:search, actor) expect(described_class.instance.enabled?(:search, actor)).to be(true) From b5ca1b843cc0d2055097811fc763d7bca2d6edde Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sat, 6 Nov 2021 21:09:12 -0400 Subject: [PATCH 089/176] Add example of scheduling a feature to release at a particular point in time --- examples/rules.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/examples/rules.rb b/examples/rules.rb index 9411c1ade..281c48791 100644 --- a/examples/rules.rb +++ b/examples/rules.rb @@ -29,9 +29,13 @@ class Org < Struct.new(:id, :flipper_properties) include Flipper::Identifier end +NOW = Time.now.to_i +DAY = 60 * 60 * 24 + org = Org.new(1, { "type" => "Org", "id" => 1, + "now" => NOW, }) user = User.new(1, { @@ -40,6 +44,7 @@ class Org < Struct.new(:id, :flipper_properties) "plan" => "basic", "age" => 39, "team_user" => true, + "now" => NOW, }) admin_user = User.new(2, { @@ -47,6 +52,7 @@ class Org < Struct.new(:id, :flipper_properties) "id" => 2, "admin" => true, "team_user" => true, + "now" => NOW, }) other_user = User.new(3, { @@ -55,6 +61,7 @@ class Org < Struct.new(:id, :flipper_properties) "plan" => "plus", "age" => 18, "org_admin" => true, + "now" => NOW + DAY, }) age_rule = Flipper.property(:age).gte(21) @@ -195,3 +202,10 @@ class Org < Struct.new(:id, :flipper_properties) assert Flipper.enabled?(:something, user) assert Flipper.enabled?(:something, admin_user) refute Flipper.enabled?(:something, other_user) + +puts "\n\nEnabling based on time" +scheduled_time_rule = Flipper.property(:now).gte(NOW) +Flipper.enable_rule :something, scheduled_time_rule +assert Flipper.enabled?(:something, user) +assert Flipper.enabled?(:something, admin_user) +refute Flipper.enabled?(:something, other_user) From aab0224cb152a80ea7d6fd6ef7bb25d84fe01e2b Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 10 Nov 2021 10:24:55 -0500 Subject: [PATCH 090/176] Add spec that demonstrates the failure --- spec/flipper/adapters/active_record_spec.rb | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/spec/flipper/adapters/active_record_spec.rb b/spec/flipper/adapters/active_record_spec.rb index 98c7116f8..34cd81e1b 100644 --- a/spec/flipper/adapters/active_record_spec.rb +++ b/spec/flipper/adapters/active_record_spec.rb @@ -17,7 +17,7 @@ ActiveRecord::Base.connection.execute <<-SQL CREATE TABLE flipper_features ( id integer PRIMARY KEY, - key text NOT NULL UNIQUE, + key string NOT NULL UNIQUE, created_at datetime NOT NULL, updated_at datetime NOT NULL ) @@ -46,6 +46,18 @@ it_should_behave_like 'a flipper adapter' + it "should load actor ids fine" do + flipper.enable_percentage_of_time(:foo, 1) + + ActiveRecord::Base.connection.execute <<-SQL + INSERT INTO flipper_gates (feature_key, key, value, created_at, updated_at) + VALUES ("foo", "actors", "Organization;4", time(), time()) + SQL + + flipper = Flipper.new(subject) + flipper.preload([:foo]) + end + context 'requiring "flipper-active_record"' do before do Flipper.configuration = nil From ac58c439384f05867936813fdf782fd0577bb767 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 10 Nov 2021 10:36:41 -0500 Subject: [PATCH 091/176] Fix the json serialize issue --- lib/flipper/adapters/active_record.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/flipper/adapters/active_record.rb b/lib/flipper/adapters/active_record.rb index a4d3060f0..ace1c34db 100644 --- a/lib/flipper/adapters/active_record.rb +++ b/lib/flipper/adapters/active_record.rb @@ -25,8 +25,6 @@ class Gate < ::ActiveRecord::Base "flipper_gates", ::ActiveRecord::Base.table_name_suffix, ].join - - serialize :value, JSON end VALUE_TO_TEXT_WARNING = <<-EOS From 6854b2defe4d91ad8c542ddac27ffa41d42736af Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 17 Nov 2021 14:50:44 -0500 Subject: [PATCH 092/176] Switch to more simple, consistent and flexible data structure for rules --- lib/flipper.rb | 22 +- lib/flipper/api/v1/actions/rule_gate.rb | 2 +- lib/flipper/dsl.rb | 4 +- lib/flipper/expression.rb | 97 +++ lib/flipper/expressions/all.rb | 27 + lib/flipper/expressions/any.rb | 27 + lib/flipper/expressions/boolean.rb | 15 + lib/flipper/expressions/equal.rb | 16 + lib/flipper/expressions/greater_than.rb | 16 + .../expressions/greater_than_or_equal_to.rb | 16 + lib/flipper/expressions/less_than.rb | 16 + .../expressions/less_than_or_equal_to.rb | 16 + lib/flipper/expressions/not_equal.rb | 16 + lib/flipper/expressions/number.rb | 15 + lib/flipper/expressions/percentage.rb | 20 + lib/flipper/expressions/property.rb | 16 + lib/flipper/expressions/random.rb | 15 + lib/flipper/expressions/string.rb | 15 + lib/flipper/feature.rb | 4 +- lib/flipper/gates/rule.rb | 35 +- lib/flipper/rules.rb | 25 - lib/flipper/rules/all.rb | 19 - lib/flipper/rules/any.rb | 49 -- lib/flipper/rules/condition.rb | 62 -- lib/flipper/rules/object.rb | 137 ----- lib/flipper/rules/operator.rb | 34 -- lib/flipper/rules/operators/base.rb | 36 -- lib/flipper/rules/operators/eq.rb | 17 - lib/flipper/rules/operators/gt.rb | 17 - lib/flipper/rules/operators/gte.rb | 17 - lib/flipper/rules/operators/in.rb | 17 - lib/flipper/rules/operators/lt.rb | 17 - lib/flipper/rules/operators/lte.rb | 17 - lib/flipper/rules/operators/neq.rb | 17 - lib/flipper/rules/operators/nin.rb | 17 - lib/flipper/rules/operators/percentage.rb | 21 - lib/flipper/rules/properties.rb | 25 - lib/flipper/rules/property.rb | 20 - lib/flipper/rules/random.rb | 16 - lib/flipper/rules/rule.rb | 8 - spec/flipper/adapters/read_only_spec.rb | 6 +- spec/flipper/api/v1/actions/rule_gate_spec.rb | 8 +- spec/flipper/dsl_spec.rb | 6 +- spec/flipper/feature_spec.rb | 121 ++-- spec/flipper/rules/all_spec.rb | 252 -------- spec/flipper/rules/any_spec.rb | 250 -------- spec/flipper/rules/condition_spec.rb | 409 ------------- spec/flipper/rules/expression_spec.rb | 128 ++++ spec/flipper/rules/expressions/equal_spec.rb | 38 ++ .../greater_than_or_equal_to_spec.rb | 32 + .../rules/expressions/greater_than_spec.rb | 32 + .../expressions/less_than_or_equal_to_spec.rb | 32 + .../rules/expressions/less_than_spec.rb | 32 + .../rules/expressions/not_equal_spec.rb | 23 + spec/flipper/rules/expressions/number_spec.rb | 20 + .../rules/expressions/percentage_spec.rb | 33 ++ .../rules/expressions/property_spec.rb | 34 ++ spec/flipper/rules/expressions/random_spec.rb | 20 + spec/flipper/rules/expressions/string_spec.rb | 18 + spec/flipper/rules/object_spec.rb | 553 ------------------ spec/flipper/rules/operator_spec.rb | 72 --- spec/flipper/rules/property_spec.rb | 302 ---------- spec/flipper/rules_spec.rb | 4 - spec/flipper_integration_spec.rb | 89 ++- spec/flipper_spec.rb | 35 +- spec/helper.rb | 1 + 66 files changed, 949 insertions(+), 2599 deletions(-) create mode 100644 lib/flipper/expression.rb create mode 100644 lib/flipper/expressions/all.rb create mode 100644 lib/flipper/expressions/any.rb create mode 100644 lib/flipper/expressions/boolean.rb create mode 100644 lib/flipper/expressions/equal.rb create mode 100644 lib/flipper/expressions/greater_than.rb create mode 100644 lib/flipper/expressions/greater_than_or_equal_to.rb create mode 100644 lib/flipper/expressions/less_than.rb create mode 100644 lib/flipper/expressions/less_than_or_equal_to.rb create mode 100644 lib/flipper/expressions/not_equal.rb create mode 100644 lib/flipper/expressions/number.rb create mode 100644 lib/flipper/expressions/percentage.rb create mode 100644 lib/flipper/expressions/property.rb create mode 100644 lib/flipper/expressions/random.rb create mode 100644 lib/flipper/expressions/string.rb delete mode 100644 lib/flipper/rules.rb delete mode 100644 lib/flipper/rules/all.rb delete mode 100644 lib/flipper/rules/any.rb delete mode 100644 lib/flipper/rules/condition.rb delete mode 100644 lib/flipper/rules/object.rb delete mode 100644 lib/flipper/rules/operator.rb delete mode 100644 lib/flipper/rules/operators/base.rb delete mode 100644 lib/flipper/rules/operators/eq.rb delete mode 100644 lib/flipper/rules/operators/gt.rb delete mode 100644 lib/flipper/rules/operators/gte.rb delete mode 100644 lib/flipper/rules/operators/in.rb delete mode 100644 lib/flipper/rules/operators/lt.rb delete mode 100644 lib/flipper/rules/operators/lte.rb delete mode 100644 lib/flipper/rules/operators/neq.rb delete mode 100644 lib/flipper/rules/operators/nin.rb delete mode 100644 lib/flipper/rules/operators/percentage.rb delete mode 100644 lib/flipper/rules/properties.rb delete mode 100644 lib/flipper/rules/property.rb delete mode 100644 lib/flipper/rules/random.rb delete mode 100644 lib/flipper/rules/rule.rb delete mode 100644 spec/flipper/rules/all_spec.rb delete mode 100644 spec/flipper/rules/any_spec.rb delete mode 100644 spec/flipper/rules/condition_spec.rb create mode 100644 spec/flipper/rules/expression_spec.rb create mode 100644 spec/flipper/rules/expressions/equal_spec.rb create mode 100644 spec/flipper/rules/expressions/greater_than_or_equal_to_spec.rb create mode 100644 spec/flipper/rules/expressions/greater_than_spec.rb create mode 100644 spec/flipper/rules/expressions/less_than_or_equal_to_spec.rb create mode 100644 spec/flipper/rules/expressions/less_than_spec.rb create mode 100644 spec/flipper/rules/expressions/not_equal_spec.rb create mode 100644 spec/flipper/rules/expressions/number_spec.rb create mode 100644 spec/flipper/rules/expressions/percentage_spec.rb create mode 100644 spec/flipper/rules/expressions/property_spec.rb create mode 100644 spec/flipper/rules/expressions/random_spec.rb create mode 100644 spec/flipper/rules/expressions/string_spec.rb delete mode 100644 spec/flipper/rules/object_spec.rb delete mode 100644 spec/flipper/rules/operator_spec.rb delete mode 100644 spec/flipper/rules/property_spec.rb delete mode 100644 spec/flipper/rules_spec.rb diff --git a/lib/flipper.rb b/lib/flipper.rb index cc0a84476..32af1045f 100644 --- a/lib/flipper.rb +++ b/lib/flipper.rb @@ -70,27 +70,19 @@ def instance=(flipper) :sync, :sync_secret # For Flipper::Cloud. Will error for OSS Flipper. def property(name) - Flipper::Rules::Property.new(name) + Flipper::Expressions::Property.new(name) end def random(name) - Flipper::Rules::Random.new(name) + Flipper::Expressions::Random.new(name) end - def object(object) - Flipper::Rules::Object.new(object) + def any(*args) + Flipper::Expressions::Any.new(args) end - def operator(name) - Flipper::Rules::Object.new(name) - end - - def any(*rules) - Flipper::Rules::Any.new(*rules) - end - - def all(*rules) - Flipper::Rules::All.new(*rules) + def all(*args) + Flipper::Expressions::All.new(args) end # Public: Use this to register a group by name. @@ -181,7 +173,7 @@ def groups_registry=(registry) require 'flipper/middleware/memoizer' require 'flipper/middleware/setup_env' require 'flipper/registry' -require 'flipper/rules' +require 'flipper/expression' require 'flipper/type' require 'flipper/types/actor' require 'flipper/types/boolean' diff --git a/lib/flipper/api/v1/actions/rule_gate.rb b/lib/flipper/api/v1/actions/rule_gate.rb index fd0776ad8..9f79e38a5 100644 --- a/lib/flipper/api/v1/actions/rule_gate.rb +++ b/lib/flipper/api/v1/actions/rule_gate.rb @@ -13,7 +13,7 @@ class RuleGate < Api::Action def post ensure_valid_params feature = flipper[feature_name] - feature.enable_rule Flipper::Rules.build(rule_hash) + feature.enable_rule Flipper::Expression.build(rule_hash) decorated_feature = Decorators::Feature.new(feature) json_response(decorated_feature.as_json, 200) diff --git a/lib/flipper/dsl.rb b/lib/flipper/dsl.rb index 7866172a7..c627a3f73 100644 --- a/lib/flipper/dsl.rb +++ b/lib/flipper/dsl.rb @@ -47,7 +47,7 @@ def enable(name, *args) # Public: Enable a feature for a rule. # # name - The String or Symbol name of the feature. - # rule - a Flipper::Rules::Rule instance or a Hash. + # rule - a Flipper::Expression instance or a Hash. # # Returns result of Feature#enable. def enable_rule(name, rule) @@ -284,7 +284,7 @@ def actor(thing) # # name - The String or Symbol name of the feature. # - # Returns an instance of Flipper::Rules::Rule. + # Returns an instance of Flipper::Expression. def rule(name) feature(name).rule end diff --git a/lib/flipper/expression.rb b/lib/flipper/expression.rb new file mode 100644 index 000000000..93c9d53e8 --- /dev/null +++ b/lib/flipper/expression.rb @@ -0,0 +1,97 @@ +module Flipper + class Expression + def self.build(object) + return object if object.is_a?(Flipper::Expression) + + case object + when Array + object.map { |o| build(o) } + when Hash + type = object.keys.first + args = object.values.first + Expressions.const_get(type).new(args) + when String, Symbol, Numeric, TrueClass, FalseClass + object + else + raise ArgumentError, "#{object.inspect} cannot be converted into a rule expression" + end + end + + attr_reader :args + + def initialize(args) + @args = self.class.build(args) + end + + def eql?(other) + self.class.eql?(other.class) && @args == other.args + end + alias_method :==, :eql? + + def value + { + self.class.name => args.map { |arg| + arg.is_a?(Expression) ? arg.value : arg + } + } + end + + def add(*expressions) + any.add(*expressions) + end + + def remove(*expressions) + any.remove(*expressions) + end + + def any + Expressions::Any.new([self]) + end + + def all + Expressions::All.new([self]) + end + + def eq(*args) + Expressions::Equal.new([self].concat(args)) + end + + def neq(*args) + Expressions::NotEqual.new([self].concat(args)) + end + + ##################################################################### + # TODO: convert naked primitive to Number, String, Boolean, etc. + ##################################################################### + def gt(*args) + Expressions::GreaterThan.new([self].concat(args)) + end + + def gte(*args) + Expressions::GreaterThan.new([self].concat(args)) + end + + def lt(*args) + Expressions::GreaterThan.new([self].concat(args)) + end + + def lte(*args) + Expressions::GreaterThan.new([self].concat(args)) + end + end +end + +require "flipper/expressions/any" +require "flipper/expressions/all" +require "flipper/expressions/boolean" +require "flipper/expressions/equal" +require "flipper/expressions/greater_than_or_equal_to" +require "flipper/expressions/greater_than" +require "flipper/expressions/less_than_or_equal_to" +require "flipper/expressions/less_than" +require "flipper/expressions/not_equal" +require "flipper/expressions/number" +require "flipper/expressions/percentage" +require "flipper/expressions/property" +require "flipper/expressions/random" +require "flipper/expressions/string" diff --git a/lib/flipper/expressions/all.rb b/lib/flipper/expressions/all.rb new file mode 100644 index 000000000..bac1c9340 --- /dev/null +++ b/lib/flipper/expressions/all.rb @@ -0,0 +1,27 @@ +require "flipper/expression" + +module Flipper + module Expressions + class All < Expression + def evaluate(feature_name: "", properties: {}) + args.all? { |arg| arg.evaluate(feature_name: feature_name, properties: properties) == true } + end + + def any + Expressions::Any.new([self]) + end + + def all + self + end + + def add(*expressions) + self.class.new(args + expressions.flatten) + end + + def remove(*expressions) + self.class.new(args - expressions.flatten) + end + end + end +end diff --git a/lib/flipper/expressions/any.rb b/lib/flipper/expressions/any.rb new file mode 100644 index 000000000..a7fb6e2b3 --- /dev/null +++ b/lib/flipper/expressions/any.rb @@ -0,0 +1,27 @@ +require "flipper/expression" + +module Flipper + module Expressions + class Any < Expression + def evaluate(feature_name: "", properties: {}) + args.any? { |arg| arg.evaluate(feature_name: feature_name, properties: properties) == true } + end + + def any + self + end + + def all + Expressions::All.new([self]) + end + + def add(*expressions) + self.class.new(args + expressions.flatten) + end + + def remove(*expressions) + self.class.new(args - expressions.flatten) + end + end + end +end diff --git a/lib/flipper/expressions/boolean.rb b/lib/flipper/expressions/boolean.rb new file mode 100644 index 000000000..66c857182 --- /dev/null +++ b/lib/flipper/expressions/boolean.rb @@ -0,0 +1,15 @@ +require "flipper/expression" + +module Flipper + module Expressions + class Boolean < Expression + def initialize(args) + super Array(args) + end + + def evaluate(feature_name: "", properties: {}) + args[0] + end + end + end +end diff --git a/lib/flipper/expressions/equal.rb b/lib/flipper/expressions/equal.rb new file mode 100644 index 000000000..a4f0ade8b --- /dev/null +++ b/lib/flipper/expressions/equal.rb @@ -0,0 +1,16 @@ +require "flipper/expression" + +module Flipper + module Expressions + class Equal < Expression + def evaluate(feature_name: "", properties: {}) + return false unless args[0] && args[1] + + left = args[0].evaluate(feature_name: feature_name, properties: properties) + right = args[1].evaluate(feature_name: feature_name, properties: properties) + + left == right + end + end + end +end diff --git a/lib/flipper/expressions/greater_than.rb b/lib/flipper/expressions/greater_than.rb new file mode 100644 index 000000000..86eb2fa65 --- /dev/null +++ b/lib/flipper/expressions/greater_than.rb @@ -0,0 +1,16 @@ +require "flipper/expression" + +module Flipper + module Expressions + class GreaterThan < Expression + def evaluate(feature_name: "", properties: {}) + return false unless args[0] && args[1] + + left = args[0].evaluate(feature_name: feature_name, properties: properties) + right = args[1].evaluate(feature_name: feature_name, properties: properties) + + left && right && left > right + end + end + end +end diff --git a/lib/flipper/expressions/greater_than_or_equal_to.rb b/lib/flipper/expressions/greater_than_or_equal_to.rb new file mode 100644 index 000000000..c0dc8b562 --- /dev/null +++ b/lib/flipper/expressions/greater_than_or_equal_to.rb @@ -0,0 +1,16 @@ +require "flipper/expression" + +module Flipper + module Expressions + class GreaterThanOrEqualTo < Expression + def evaluate(feature_name: "", properties: {}) + return false unless args[0] && args[1] + + left = args[0].evaluate(feature_name: feature_name, properties: properties) + right = args[1].evaluate(feature_name: feature_name, properties: properties) + + left && right && left >= right + end + end + end +end diff --git a/lib/flipper/expressions/less_than.rb b/lib/flipper/expressions/less_than.rb new file mode 100644 index 000000000..a786dec84 --- /dev/null +++ b/lib/flipper/expressions/less_than.rb @@ -0,0 +1,16 @@ +require "flipper/expression" + +module Flipper + module Expressions + class LessThan < Expression + def evaluate(feature_name: "", properties: {}) + return false unless args[0] && args[1] + + left = args[0].evaluate(feature_name: feature_name, properties: properties) + right = args[1].evaluate(feature_name: feature_name, properties: properties) + + left && right && left < right + end + end + end +end diff --git a/lib/flipper/expressions/less_than_or_equal_to.rb b/lib/flipper/expressions/less_than_or_equal_to.rb new file mode 100644 index 000000000..8948dacd0 --- /dev/null +++ b/lib/flipper/expressions/less_than_or_equal_to.rb @@ -0,0 +1,16 @@ +require "flipper/expression" + +module Flipper + module Expressions + class LessThanOrEqualTo < Expression + def evaluate(feature_name: "", properties: {}) + return false unless args[0] && args[1] + + left = args[0].evaluate(feature_name: feature_name, properties: properties) + right = args[1].evaluate(feature_name: feature_name, properties: properties) + + left && right && left <= right + end + end + end +end diff --git a/lib/flipper/expressions/not_equal.rb b/lib/flipper/expressions/not_equal.rb new file mode 100644 index 000000000..39977d0dd --- /dev/null +++ b/lib/flipper/expressions/not_equal.rb @@ -0,0 +1,16 @@ +require "flipper/expression" + +module Flipper + module Expressions + class NotEqual < Expression + def evaluate(feature_name: "", properties: {}) + return false unless args[0] && args[1] + + left = args[0].evaluate(feature_name: feature_name, properties: properties) + right = args[1].evaluate(feature_name: feature_name, properties: properties) + + left != right + end + end + end +end diff --git a/lib/flipper/expressions/number.rb b/lib/flipper/expressions/number.rb new file mode 100644 index 000000000..ba22abd92 --- /dev/null +++ b/lib/flipper/expressions/number.rb @@ -0,0 +1,15 @@ +require "flipper/expression" + +module Flipper + module Expressions + class Number < Expression + def initialize(args) + super Array(args) + end + + def evaluate(feature_name: "", properties: {}) + args[0] + end + end + end +end diff --git a/lib/flipper/expressions/percentage.rb b/lib/flipper/expressions/percentage.rb new file mode 100644 index 000000000..398a894b9 --- /dev/null +++ b/lib/flipper/expressions/percentage.rb @@ -0,0 +1,20 @@ +require "flipper/expression" + +module Flipper + module Expressions + class Percentage < Expression + SCALING_FACTOR = 1_000 + + def evaluate(feature_name: "", properties: {}) + return false unless args[0] && args[1] + + left = args[0].evaluate(feature_name: feature_name, properties: properties) + right = args[1].evaluate(feature_name: feature_name, properties: properties) + + return false unless left && right + + Zlib.crc32("#{feature_name}#{left}") % (100 * SCALING_FACTOR) < right * SCALING_FACTOR + end + end + end +end diff --git a/lib/flipper/expressions/property.rb b/lib/flipper/expressions/property.rb new file mode 100644 index 000000000..3c03eda41 --- /dev/null +++ b/lib/flipper/expressions/property.rb @@ -0,0 +1,16 @@ +require "flipper/expression" + +module Flipper + module Expressions + class Property < Expression + def initialize(args) + super Array(args).map(&:to_s) + end + + def evaluate(feature_name: "", properties: {}) + key = args[0] + properties[key] + end + end + end +end diff --git a/lib/flipper/expressions/random.rb b/lib/flipper/expressions/random.rb new file mode 100644 index 000000000..0effdbc9d --- /dev/null +++ b/lib/flipper/expressions/random.rb @@ -0,0 +1,15 @@ +require "flipper/expression" + +module Flipper + module Expressions + class Random < Expression + def initialize(args) + super Array(args) + end + + def evaluate(feature_name: "", properties: {}) + rand args[0] + end + end + end +end diff --git a/lib/flipper/expressions/string.rb b/lib/flipper/expressions/string.rb new file mode 100644 index 000000000..42d77a2e5 --- /dev/null +++ b/lib/flipper/expressions/string.rb @@ -0,0 +1,15 @@ +require "flipper/expression" + +module Flipper + module Expressions + class String < Expression + def initialize(args) + super Array(args) + end + + def evaluate(feature_name: "", properties: {}) + args[0] + end + end + end +end diff --git a/lib/flipper/feature.rb b/lib/flipper/feature.rb index ef9f12d59..d1174b634 100644 --- a/lib/flipper/feature.rb +++ b/lib/flipper/feature.rb @@ -125,7 +125,7 @@ def enabled?(thing = nil) # # Returns result of enable. def enable_rule(rule) - enable Rules.wrap(rule) + enable Expression.build(rule) end # Public: Add a rule for a feature. @@ -294,7 +294,7 @@ def disabled_groups end def rule - Flipper::Rules.build(rule_value) if rule_value + Flipper::Expression.build(rule_value) if rule_value end # Public: Get the adapter value for the groups gate. diff --git a/lib/flipper/gates/rule.rb b/lib/flipper/gates/rule.rb index 1de8160c8..5ffdb2a12 100644 --- a/lib/flipper/gates/rule.rb +++ b/lib/flipper/gates/rule.rb @@ -1,3 +1,5 @@ +require "flipper/expression" + module Flipper module Gates class Rule < Gate @@ -25,12 +27,39 @@ def enabled?(value) def open?(context) data = context.values[key] return false if data.nil? || data.empty? - rule = Flipper::Rules.build(data) - rule.matches?(context.feature_name, context.thing) + expression = Flipper::Expression.build(data) + result = expression.evaluate( + feature_name: context.feature_name, + properties: properties(context.thing) + ) + !!result end def protects?(thing) - thing.is_a?(Flipper::Rules::Rule) + thing.is_a?(Flipper::Expression) + end + + private + + # Internal + DEFAULT_PROPERTIES = {}.freeze + + def properties(actor) + return DEFAULT_PROPERTIES if actor.nil? + + properties = {} + + if actor.respond_to?(:flipper_properties) + properties.update(actor.flipper_properties) + else + warn "#{actor.inspect} does not respond to `flipper_properties` but should." + end + + if actor.respond_to?(:flipper_id) + properties["flipper_id".freeze] = actor.flipper_id + end + + properties end end end diff --git a/lib/flipper/rules.rb b/lib/flipper/rules.rb deleted file mode 100644 index c9a2252a5..000000000 --- a/lib/flipper/rules.rb +++ /dev/null @@ -1,25 +0,0 @@ -require 'flipper/rules/condition' -require 'flipper/rules/any' -require 'flipper/rules/all' - -require 'flipper/rules/operator' -require 'flipper/rules/object' -require 'flipper/rules/property' -require 'flipper/rules/random' - -module Flipper - module Rules - def self.wrap(thing) - if thing.is_a?(Flipper::Rules::Rule) - thing - else - build(thing) - end - end - - def self.build(hash) - type = const_get(hash.fetch("type")) - type.build(hash.fetch("value")) - end - end -end diff --git a/lib/flipper/rules/all.rb b/lib/flipper/rules/all.rb deleted file mode 100644 index 82ee9f9ab..000000000 --- a/lib/flipper/rules/all.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'flipper/rules/any' - -module Flipper - module Rules - class All < Any - def all - self - end - - def any - Flipper::Rules::Any.new(self) - end - - def matches?(feature_name, actor) - @rules.all? { |rule| rule.matches?(feature_name, actor) } - end - end - end -end diff --git a/lib/flipper/rules/any.rb b/lib/flipper/rules/any.rb deleted file mode 100644 index 2992ca487..000000000 --- a/lib/flipper/rules/any.rb +++ /dev/null @@ -1,49 +0,0 @@ -require 'flipper/rules/rule' - -module Flipper - module Rules - class Any < Rule - def self.build(*rules) - new(rules.flatten.map { |rule| Flipper::Rules.build(rule) }) - end - - attr_reader :rules - - def initialize(*rules) - @rules = rules.flatten - end - - def all - Flipper::Rules::All.new(self) - end - - def any - self - end - - def add(*rules) - self.class.new(@rules + rules.flatten) - end - - def remove(*rules) - self.class.new(@rules - rules.flatten) - end - - def value - { - "type" => self.class.name.split('::').last, - "value" => @rules.map(&:value), - } - end - - def eql?(other) - self.class.eql?(other.class) && @rules == other.rules - end - alias_method :==, :eql? - - def matches?(feature_name, actor) - @rules.any? { |rule| rule.matches?(feature_name, actor) } - end - end - end -end diff --git a/lib/flipper/rules/condition.rb b/lib/flipper/rules/condition.rb deleted file mode 100644 index 42bf8b782..000000000 --- a/lib/flipper/rules/condition.rb +++ /dev/null @@ -1,62 +0,0 @@ -require 'flipper/rules/rule' -require 'flipper/rules/properties' - -module Flipper - module Rules - class Condition < Rule - def self.build(hash) - new(hash.fetch("left"), hash.fetch("operator"), hash.fetch("right")) - end - - attr_reader :left, :operator, :right - - def initialize(left, operator, right) - @left = Object.build(left) - @operator = Operator.build(operator) - @right = Object.build(right) - end - - def all - Flipper::Rules::All.new(self) - end - - def any - Flipper::Rules::Any.new(self) - end - - def add(*rules) - any.add(*rules) - end - - def remove(*rules) - any.remove(*rules) - end - - def value - { - "type" => "Condition", - "value" => { - "left" => @left.to_h, - "operator" => @operator.to_h, - "right" => @right.to_h, - } - } - end - - def eql?(other) - self.class.eql?(other.class) && - @left == other.left && - @operator == other.operator && - @right == other.right - end - alias_method :==, :eql? - - def matches?(feature_name, actor = nil) - properties = Properties.from_actor(actor) - left_value = @left.evaluate(properties) - right_value = @right.evaluate(properties) - !!@operator.call(left: left_value, right: right_value, feature_name: feature_name) - end - end - end -end diff --git a/lib/flipper/rules/object.rb b/lib/flipper/rules/object.rb deleted file mode 100644 index dd560bab1..000000000 --- a/lib/flipper/rules/object.rb +++ /dev/null @@ -1,137 +0,0 @@ -require "flipper/rules/condition" -require "flipper/rules/operator" - -module Flipper - module Rules - class Object - SUPPORTED_TYPES_MAP = { - String => "String", - Integer => "Integer", - NilClass => "Null", - TrueClass => "Boolean", - FalseClass => "Boolean", - }.freeze - - SUPPORTED_TYPE_CLASSES = SUPPORTED_TYPES_MAP.keys.freeze - SUPPORTED_TYPE_NAMES = SUPPORTED_TYPES_MAP.values.freeze - - def self.build(object) - return object if object.is_a?(Flipper::Rules::Object) - - if object.is_a?(Hash) - type = object.fetch("type") - value = object.fetch("value") - - if SUPPORTED_TYPE_NAMES.include?(type) - new(value) - else - Rules.const_get(type).new(value) - end - else - new(object) - end - end - - attr_reader :type, :value - - def initialize(value) - @type = type_of(value) - @value = value - end - - def to_h - { - "type" => @type, - "value" => @value, - } - end - - def eql?(other) - self.class.eql?(other.class) && - @type == other.type && - @value == other.value - end - alias_method :==, :eql? - - def evaluate(properties) - return nil if type == "Null".freeze - value - end - - def eq(object) - Flipper::Rules::Condition.new( - self, Operators::Eq.new, self.class.primitive_or_object(object) - ) - end - - def neq(object) - Flipper::Rules::Condition.new( - self, Operators::Neq.new, self.class.primitive_or_object(object) - ) - end - - def gt(object) - Flipper::Rules::Condition.new( - self, Operators::Gt.new, self.class.integer_or_object(object) - ) - end - - def gte(object) - Flipper::Rules::Condition.new( - self, Operators::Gte.new, self.class.integer_or_object(object) - ) - end - - def lt(object) - Flipper::Rules::Condition.new( - self, Operators::Lt.new, self.class.integer_or_object(object) - ) - end - - def lte(object) - Flipper::Rules::Condition.new( - self, Operators::Lte.new, self.class.integer_or_object(object) - ) - end - - def percentage(object) - Flipper::Rules::Condition.new( - self, Operators::Percentage.new, self.class.integer_or_object(object) - ) - end - - private - - def type_of(object) - type_class = SUPPORTED_TYPE_CLASSES.detect { |klass, type| object.is_a?(klass) } - - if type_class.nil? - raise ArgumentError, - "#{object.inspect} is not a supported primitive." + - " Object must be one of: #{SUPPORTED_TYPE_CLASSES.join(", ")}." - end - - SUPPORTED_TYPES_MAP[type_class] - end - - def self.primitive_or_object(object) - if object.is_a?(Flipper::Rules::Object) - object - else - Object.new(object) - end - end - - def self.integer_or_object(object) - case object - when Integer - Object.new(object) - when Flipper::Rules::Object - object - else - raise ArgumentError, "object must be integer or property" unless object.is_a?(Integer) - end - end - end - end -end diff --git a/lib/flipper/rules/operator.rb b/lib/flipper/rules/operator.rb deleted file mode 100644 index 6e5d911d0..000000000 --- a/lib/flipper/rules/operator.rb +++ /dev/null @@ -1,34 +0,0 @@ -module Flipper - module Rules - module Operator - # Builds a flipper operator based on an object. - # - # object - The Hash, String, Symbol or Flipper::Rules::Operators::* - # representation of an operator. - # - # Returns Flipper::Rules::Operator::* instance. - def self.build(object) - return object if object.is_a?(Flipper::Rules::Operators::Base) - - operator_class = case object - when Hash - object.fetch("value") - when String, Symbol - object - else - raise ArgumentError, "#{object.inspect} cannot be converted into an operator" - end - - Operators.const_get(operator_class.to_s.capitalize).new - end - end - end -end - -require "flipper/rules/operators/eq" -require "flipper/rules/operators/neq" -require "flipper/rules/operators/gt" -require "flipper/rules/operators/gte" -require "flipper/rules/operators/lt" -require "flipper/rules/operators/lte" -require "flipper/rules/operators/percentage" diff --git a/lib/flipper/rules/operators/base.rb b/lib/flipper/rules/operators/base.rb deleted file mode 100644 index cafad5933..000000000 --- a/lib/flipper/rules/operators/base.rb +++ /dev/null @@ -1,36 +0,0 @@ -module Flipper - module Rules - module Operators - class Base - attr_reader :type, :value - - def initialize(value) - @type = "Operator".freeze - @value = value.to_s - end - - def name - @value - end - - def to_h - { - "type" => @type, - "value" => @value, - } - end - - def eql?(other) - self.class.eql?(other.class) && - @type == other.type && - @value == other.value - end - alias_method :==, :eql? - - def call(*args) - raise NotImplementedError - end - end - end - end -end diff --git a/lib/flipper/rules/operators/eq.rb b/lib/flipper/rules/operators/eq.rb deleted file mode 100644 index fd75cce16..000000000 --- a/lib/flipper/rules/operators/eq.rb +++ /dev/null @@ -1,17 +0,0 @@ -require "flipper/rules/operators/base" - -module Flipper - module Rules - module Operators - class Eq < Base - def initialize - super :eq - end - - def call(left:, right:, **) - left == right - end - end - end - end -end diff --git a/lib/flipper/rules/operators/gt.rb b/lib/flipper/rules/operators/gt.rb deleted file mode 100644 index 46a74bee5..000000000 --- a/lib/flipper/rules/operators/gt.rb +++ /dev/null @@ -1,17 +0,0 @@ -require "flipper/rules/operators/base" - -module Flipper - module Rules - module Operators - class Gt < Base - def initialize - super :gt - end - - def call(left:, right:, **) - left && right && left > right - end - end - end - end -end diff --git a/lib/flipper/rules/operators/gte.rb b/lib/flipper/rules/operators/gte.rb deleted file mode 100644 index 83db568eb..000000000 --- a/lib/flipper/rules/operators/gte.rb +++ /dev/null @@ -1,17 +0,0 @@ -require "flipper/rules/operators/base" - -module Flipper - module Rules - module Operators - class Gte < Base - def initialize - super :gte - end - - def call(left:, right:, **) - left && right && left >= right - end - end - end - end -end diff --git a/lib/flipper/rules/operators/in.rb b/lib/flipper/rules/operators/in.rb deleted file mode 100644 index d7ca9b6d5..000000000 --- a/lib/flipper/rules/operators/in.rb +++ /dev/null @@ -1,17 +0,0 @@ -require "flipper/rules/operators/base" - -module Flipper - module Rules - module Operators - class In < Base - def initialize - super :in - end - - def call(left:, right:, **) - left && right && right.include?(left) - end - end - end - end -end diff --git a/lib/flipper/rules/operators/lt.rb b/lib/flipper/rules/operators/lt.rb deleted file mode 100644 index 0e659d52b..000000000 --- a/lib/flipper/rules/operators/lt.rb +++ /dev/null @@ -1,17 +0,0 @@ -require "flipper/rules/operators/base" - -module Flipper - module Rules - module Operators - class Lt < Base - def initialize - super :lt - end - - def call(left:, right:, **) - left && right && left < right - end - end - end - end -end diff --git a/lib/flipper/rules/operators/lte.rb b/lib/flipper/rules/operators/lte.rb deleted file mode 100644 index c005f98c4..000000000 --- a/lib/flipper/rules/operators/lte.rb +++ /dev/null @@ -1,17 +0,0 @@ -require "flipper/rules/operators/base" - -module Flipper - module Rules - module Operators - class Lte < Base - def initialize - super :lte - end - - def call(left:, right:, **) - left && right && left <= right - end - end - end - end -end diff --git a/lib/flipper/rules/operators/neq.rb b/lib/flipper/rules/operators/neq.rb deleted file mode 100644 index c98daa320..000000000 --- a/lib/flipper/rules/operators/neq.rb +++ /dev/null @@ -1,17 +0,0 @@ -require "flipper/rules/operators/base" - -module Flipper - module Rules - module Operators - class Neq < Base - def initialize - super :neq - end - - def call(left:, right:, **) - left != right - end - end - end - end -end diff --git a/lib/flipper/rules/operators/nin.rb b/lib/flipper/rules/operators/nin.rb deleted file mode 100644 index 6355dcfdc..000000000 --- a/lib/flipper/rules/operators/nin.rb +++ /dev/null @@ -1,17 +0,0 @@ -require "flipper/rules/operators/base" - -module Flipper - module Rules - module Operators - class Nin < Base - def initialize - super :nin - end - - def call(left:, right:, **) - left && right && !right.include?(left) - end - end - end - end -end diff --git a/lib/flipper/rules/operators/percentage.rb b/lib/flipper/rules/operators/percentage.rb deleted file mode 100644 index b08653340..000000000 --- a/lib/flipper/rules/operators/percentage.rb +++ /dev/null @@ -1,21 +0,0 @@ -require "zlib" -require "flipper/rules/operators/base" - -module Flipper - module Rules - module Operators - class Percentage < Base - SCALING_FACTOR = 1_000 - - def initialize - super :percentage - end - - def call(left:, right:, feature_name:, **) - return false unless left && right - Zlib.crc32("#{feature_name}#{left}") % (100 * SCALING_FACTOR) < right * SCALING_FACTOR - end - end - end - end -end diff --git a/lib/flipper/rules/properties.rb b/lib/flipper/rules/properties.rb deleted file mode 100644 index efd069357..000000000 --- a/lib/flipper/rules/properties.rb +++ /dev/null @@ -1,25 +0,0 @@ -module Flipper - module Rules - module Properties - DEFAULT_PROPERTIES = {}.freeze - - def self.from_actor(actor) - return DEFAULT_PROPERTIES if actor.nil? - - properties = {} - - if actor.respond_to?(:flipper_properties) - properties.update(actor.flipper_properties) - else - warn "#{actor.inspect} does not respond to `flipper_properties` but should." - end - - if actor.respond_to?(:flipper_id) - properties["flipper_id".freeze] = actor.flipper_id - end - - properties - end - end - end -end diff --git a/lib/flipper/rules/property.rb b/lib/flipper/rules/property.rb deleted file mode 100644 index 7f7994114..000000000 --- a/lib/flipper/rules/property.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'flipper/rules/object' - -module Flipper - module Rules - class Property < Object - def initialize(value) - @type = "Property".freeze - @value = value.to_s - end - - def name - @value - end - - def evaluate(properties) - properties[value] - end - end - end -end diff --git a/lib/flipper/rules/random.rb b/lib/flipper/rules/random.rb deleted file mode 100644 index 2471e6a93..000000000 --- a/lib/flipper/rules/random.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'flipper/rules/object' - -module Flipper - module Rules - class Random < Object - def initialize(value) - @type = "Random".freeze - @value = value - end - - def evaluate(properties) - rand value - end - end - end -end diff --git a/lib/flipper/rules/rule.rb b/lib/flipper/rules/rule.rb deleted file mode 100644 index 9d7ce2ea6..000000000 --- a/lib/flipper/rules/rule.rb +++ /dev/null @@ -1,8 +0,0 @@ -module Flipper - module Rules - # Base class for the various rules. Just makes it easier to detect if a - # rule is being used or not. - class Rule - end - end -end diff --git a/spec/flipper/adapters/read_only_spec.rb b/spec/flipper/adapters/read_only_spec.rb index 6bd46bc6b..aa0770691 100644 --- a/spec/flipper/adapters/read_only_spec.rb +++ b/spec/flipper/adapters/read_only_spec.rb @@ -43,11 +43,7 @@ end it 'can get feature' do - rule = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "plan"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "basic"} - ) + rule = Flipper.property(:plan).eq("basic") actor22 = Flipper::Actor.new('22') adapter.enable(feature, boolean_gate, flipper.boolean) adapter.enable(feature, group_gate, flipper.group(:admins)) diff --git a/spec/flipper/api/v1/actions/rule_gate_spec.rb b/spec/flipper/api/v1/actions/rule_gate_spec.rb index 8182b33f4..204eac5f4 100644 --- a/spec/flipper/api/v1/actions/rule_gate_spec.rb +++ b/spec/flipper/api/v1/actions/rule_gate_spec.rb @@ -8,13 +8,7 @@ "age" => 21, }) } - let(:rule) { - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "plan"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "basic"} - ) - } + let(:rule) { Flipper.property(:plan).eq("basic") } describe 'enable' do before do diff --git a/spec/flipper/dsl_spec.rb b/spec/flipper/dsl_spec.rb index 4b93bead8..637af58b7 100644 --- a/spec/flipper/dsl_spec.rb +++ b/spec/flipper/dsl_spec.rb @@ -264,14 +264,14 @@ describe '#add_rule/remove_rule' do it 'enables and disables the feature for the rule' do - condition = Flipper.property(:plan).eq("basic") - rule = Flipper.any(condition) + expression = Flipper.property(:plan).eq("basic") + rule = Flipper.any(expression) expect(subject[:stats].rule).to be(nil) subject.add_rule(:stats, rule) expect(subject[:stats].rule).to eq(rule) - subject.remove_rule(:stats, condition) + subject.remove_rule(:stats, expression) expect(subject[:stats].rule).to eq(Flipper.any) end end diff --git a/spec/flipper/feature_spec.rb b/spec/flipper/feature_spec.rb index 1dfeb9863..579bd5419 100644 --- a/spec/flipper/feature_spec.rb +++ b/spec/flipper/feature_spec.rb @@ -665,11 +665,7 @@ describe '#enable_rule/disable_rule' do context "with rule instance" do it "updates gate values to equal rule or clears rule" do - rule = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "plan"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "basic"} - ) + rule = Flipper.property(:plan).eq("basic") other_rule = Flipper.property(:age).gte(21) expect(subject.gate_values.rule).to be(nil) subject.enable_rule(rule) @@ -681,11 +677,7 @@ context "with Hash" do it "updates gate values to equal rule or clears rule" do - rule = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "plan"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "basic"} - ) + rule = Flipper.property(:plan).eq("basic") other_rule = Flipper.property(:age).gte(21) expect(subject.gate_values.rule).to be(nil) subject.enable_rule(rule.value) @@ -698,11 +690,11 @@ describe "#add_rule" do context "when nothing enabled" do - context "with Condition instance" do - it "sets rule to Condition" do + context "with Expression instance" do + it "sets rule to Expression" do rule = Flipper.property(:plan).eq("basic") subject.add_rule(rule) - expect(subject.rule).to be_instance_of(Flipper::Rules::Condition) + expect(subject.rule).to be_instance_of(Flipper::Expressions::Equal) expect(subject.rule).to eq(rule) end end @@ -711,7 +703,7 @@ it "sets rule to Any" do rule = Flipper.any(Flipper.property(:plan).eq("basic")) subject.add_rule(rule) - expect(subject.rule).to be_instance_of(Flipper::Rules::Any) + expect(subject.rule).to be_instance_of(Flipper::Expressions::Any) expect(subject.rule).to eq(rule) end end @@ -720,26 +712,26 @@ it "sets rule to All" do rule = Flipper.all(Flipper.property(:plan).eq("basic")) subject.add_rule(rule) - expect(subject.rule).to be_instance_of(Flipper::Rules::All) + expect(subject.rule).to be_instance_of(Flipper::Expressions::All) expect(subject.rule).to eq(rule) end end end - context "when Condition enabled" do + context "when Expression enabled" do let(:rule) { Flipper.property(:plan).eq("basic") } before do subject.enable_rule rule end - context "with Condition instance" do - it "changes rule to Any and adds new Condition" do + context "with Expression instance" do + it "changes rule to Any and adds new Expression" do new_rule = Flipper.property(:age).gte(21) subject.add_rule(new_rule) - expect(subject.rule).to be_instance_of(Flipper::Rules::Any) - expect(subject.rule.rules).to include(rule) - expect(subject.rule.rules).to include(new_rule) + expect(subject.rule).to be_instance_of(Flipper::Expressions::Any) + expect(subject.rule.args).to include(rule) + expect(subject.rule.args).to include(new_rule) end end @@ -747,9 +739,9 @@ it "changes rule to Any and adds new Any" do new_rule = Flipper.any(Flipper.property(:age).eq(21)) subject.add_rule new_rule - expect(subject.rule).to be_instance_of(Flipper::Rules::Any) - expect(subject.rule.rules).to include(rule) - expect(subject.rule.rules).to include(new_rule) + expect(subject.rule).to be_instance_of(Flipper::Expressions::Any) + expect(subject.rule.args).to include(rule) + expect(subject.rule.args).to include(new_rule) end end @@ -757,9 +749,9 @@ it "changes rule to Any and adds new All" do new_rule = Flipper.all(Flipper.property(:plan).eq("basic")) subject.add_rule new_rule - expect(subject.rule).to be_instance_of(Flipper::Rules::Any) - expect(subject.rule.rules).to include(rule) - expect(subject.rule.rules).to include(new_rule) + expect(subject.rule).to be_instance_of(Flipper::Expressions::Any) + expect(subject.rule.args).to include(rule) + expect(subject.rule.args).to include(new_rule) end end end @@ -772,13 +764,13 @@ subject.enable_rule rule end - context "with Condition instance" do - it "adds Condition to Any" do + context "with Expression instance" do + it "adds Expression to Any" do new_rule = Flipper.property(:age).gte(21) subject.add_rule(new_rule) - expect(subject.rule).to be_instance_of(Flipper::Rules::Any) - expect(subject.rule.rules).to include(condition) - expect(subject.rule.rules).to include(new_rule) + expect(subject.rule).to be_instance_of(Flipper::Expressions::Any) + expect(subject.rule.args).to include(condition) + expect(subject.rule.args).to include(new_rule) end end @@ -786,9 +778,9 @@ it "adds Any to Any" do new_rule = Flipper.any(Flipper.property(:age).gte(21)) subject.add_rule(new_rule) - expect(subject.rule).to be_instance_of(Flipper::Rules::Any) - expect(subject.rule.rules).to include(condition) - expect(subject.rule.rules).to include(new_rule) + expect(subject.rule).to be_instance_of(Flipper::Expressions::Any) + expect(subject.rule.args).to include(condition) + expect(subject.rule.args).to include(new_rule) end end @@ -796,9 +788,9 @@ it "adds All to Any" do new_rule = Flipper.all(Flipper.property(:age).gte(21)) subject.add_rule(new_rule) - expect(subject.rule).to be_instance_of(Flipper::Rules::Any) - expect(subject.rule.rules).to include(condition) - expect(subject.rule.rules).to include(new_rule) + expect(subject.rule).to be_instance_of(Flipper::Expressions::Any) + expect(subject.rule.args).to include(condition) + expect(subject.rule.args).to include(new_rule) end end end @@ -811,13 +803,13 @@ subject.enable_rule rule end - context "with Condition instance" do - it "adds Condition to All" do + context "with Expression instance" do + it "adds Expression to All" do new_rule = Flipper.property(:age).gte(21) subject.add_rule(new_rule) - expect(subject.rule).to be_instance_of(Flipper::Rules::All) - expect(subject.rule.rules).to include(condition) - expect(subject.rule.rules).to include(new_rule) + expect(subject.rule).to be_instance_of(Flipper::Expressions::All) + expect(subject.rule.args).to include(condition) + expect(subject.rule.args).to include(new_rule) end end @@ -825,9 +817,9 @@ it "adds Any to All" do new_rule = Flipper.any(Flipper.property(:age).gte(21)) subject.add_rule(new_rule) - expect(subject.rule).to be_instance_of(Flipper::Rules::All) - expect(subject.rule.rules).to include(condition) - expect(subject.rule.rules).to include(new_rule) + expect(subject.rule).to be_instance_of(Flipper::Expressions::All) + expect(subject.rule.args).to include(condition) + expect(subject.rule.args).to include(new_rule) end end @@ -835,9 +827,9 @@ it "adds All to All" do new_rule = Flipper.all(Flipper.property(:age).gte(21)) subject.add_rule(new_rule) - expect(subject.rule).to be_instance_of(Flipper::Rules::All) - expect(subject.rule.rules).to include(condition) - expect(subject.rule.rules).to include(new_rule) + expect(subject.rule).to be_instance_of(Flipper::Expressions::All) + expect(subject.rule.args).to include(condition) + expect(subject.rule.args).to include(new_rule) end end end @@ -845,7 +837,7 @@ describe '#remove_rule' do context "when nothing enabled" do - context "with Condition instance" do + context "with Expression instance" do it "does nothing" do rule = Flipper.property(:plan).eq("basic") subject.remove_rule(rule) @@ -870,21 +862,21 @@ end end - context "when Condition enabled" do + context "when Expression enabled" do let(:rule) { Flipper.property(:plan).eq("basic") } before do subject.enable_rule rule end - context "with Condition instance" do - it "changes rule to Any and removes Condition if it matches" do + context "with Expression instance" do + it "changes rule to Any and removes Expression if it matches" do new_rule = Flipper.property(:plan).eq("basic") subject.remove_rule new_rule expect(subject.rule).to eq(Flipper.any) end - it "changes rule to Any if Condition doesn't match" do + it "changes rule to Any if Expression doesn't match" do new_rule = Flipper.property(:plan).eq("premium") subject.remove_rule new_rule expect(subject.rule).to eq(Flipper.any(rule)) @@ -916,13 +908,13 @@ subject.enable_rule rule end - context "with Condition instance" do - it "removes Condition if it matches" do + context "with Expression instance" do + it "removes Expression if it matches" do subject.remove_rule condition expect(subject.rule).to eq(Flipper.any) end - it "does nothing if Condition does not match" do + it "does nothing if Expression does not match" do subject.remove_rule Flipper.property(:plan).eq("premium") expect(subject.rule).to eq(rule) end @@ -932,7 +924,8 @@ it "removes Any if it matches" do new_rule = Flipper.any(Flipper.property(:plan).eq("premium")) subject.add_rule new_rule - expect(subject.rule.rules.size).to be(2) + p subject.rule + expect(subject.rule.args.size).to be(2) subject.remove_rule new_rule expect(subject.rule).to eq(rule) end @@ -948,7 +941,7 @@ it "removes All if it matches" do new_rule = Flipper.all(Flipper.property(:plan).eq("premium")) subject.add_rule new_rule - expect(subject.rule.rules.size).to be(2) + expect(subject.rule.args.size).to be(2) subject.remove_rule new_rule expect(subject.rule).to eq(rule) end @@ -969,13 +962,13 @@ subject.enable_rule rule end - context "with Condition instance" do - it "removes Condition if it matches" do + context "with Expression instance" do + it "removes Expression if it matches" do subject.remove_rule condition expect(subject.rule).to eq(Flipper.all) end - it "does nothing if Condition does not match" do + it "does nothing if Expression does not match" do subject.remove_rule Flipper.property(:plan).eq("premium") expect(subject.rule).to eq(rule) end @@ -985,7 +978,7 @@ it "removes Any if it matches" do new_rule = Flipper.any(Flipper.property(:plan).eq("premium")) subject.add_rule new_rule - expect(subject.rule.rules.size).to be(2) + expect(subject.rule.args.size).to be(2) subject.remove_rule new_rule expect(subject.rule).to eq(rule) end @@ -1001,7 +994,7 @@ it "removes All if it matches" do new_rule = Flipper.all(Flipper.property(:plan).eq("premium")) subject.add_rule new_rule - expect(subject.rule.rules.size).to be(2) + expect(subject.rule.args.size).to be(2) subject.remove_rule new_rule expect(subject.rule).to eq(rule) end diff --git a/spec/flipper/rules/all_spec.rb b/spec/flipper/rules/all_spec.rb deleted file mode 100644 index 848f8dee0..000000000 --- a/spec/flipper/rules/all_spec.rb +++ /dev/null @@ -1,252 +0,0 @@ -require 'helper' - -RSpec.describe Flipper::Rules::All do - let(:feature_name) { "search" } - let(:plan_condition) { - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "plan"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "basic"} - ) - } - let(:age_condition) { - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "age"}, - {"type" => "Operator", "value" => "gte"}, - {"type" => "Integer", "value" => 21} - ) - } - let(:any_rule) { - Flipper::Rules::Any.new( - plan_condition, - age_condition - ) - } - let(:rule) { - Flipper::Rules::All.new( - plan_condition, - age_condition - ) - } - - describe "#initialize" do - it "flattens rules" do - instance = Flipper::Rules::Any.new([[plan_condition, age_condition]]) - expect(instance.rules).to eq([ - plan_condition, - age_condition, - ]) - end - end - - describe ".build" do - context "for Array of Hashes" do - it "builds instance" do - instance = Flipper::Rules::All.build([plan_condition.value, age_condition.value]) - expect(instance).to be_instance_of(Flipper::Rules::All) - expect(instance.rules).to eq([ - plan_condition, - age_condition, - ]) - end - end - - context "for nested Array of Hashes" do - it "builds instance" do - instance = Flipper::Rules::All.build([[plan_condition.value, age_condition.value]]) - expect(instance).to be_instance_of(Flipper::Rules::All) - expect(instance.rules).to eq([ - plan_condition, - age_condition, - ]) - end - end - - context "for Array with Any rule" do - it "builds instance" do - instance = Flipper::Rules::All.build(any_rule.value) - expect(instance).to be_instance_of(Flipper::Rules::All) - expect(instance.rules).to eq([any_rule]) - end - end - end - - describe "#all" do - it "returns self" do - expect(rule.all).to be(rule) - end - end - - describe "#any" do - it "wraps self with any" do - result = rule.any - expect(result).to be_instance_of(Flipper::Rules::Any) - expect(result.rules).to eq([rule]) - end - end - - describe "#add" do - context "with single rule" do - it "returns new instance with rule added" do - rule2 = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "flipper_id"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "User;2"} - ) - result = rule.add rule2 - expect(result).not_to be(rule) - expect(result.rules).to eq([rule.rules, rule2].flatten) - end - end - - context "with multiple rules" do - it "returns new instance with rule added" do - rule2 = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "flipper_id"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "User;2"} - ) - rule3 = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "flipper_id"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "User;3"} - ) - - result = rule.add rule2, rule3 - expect(result).not_to be(rule) - expect(result.rules).to eq([rule.rules, rule2, rule3].flatten) - end - end - - context "with array of rules" do - it "returns new instance with rule added" do - rule2 = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "flipper_id"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "User;2"} - ) - rule3 = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "flipper_id"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "User;3"} - ) - - result = rule.add [rule2, rule3] - expect(result).not_to be(rule) - expect(result.rules).to eq([rule.rules, rule2, rule3].flatten) - end - end - end - - describe "#remove" do - context "with single rule" do - it "returns new instance with rule removed" do - result = rule.remove age_condition - expect(result).not_to be(rule) - expect(result.rules).to eq([plan_condition]) - end - end - - context "with multiple rules" do - it "returns new instance with rules removed" do - result = rule.remove age_condition, plan_condition - expect(result).not_to be(rule) - expect(result.rules).to eq([]) - end - end - - context "with array of rules" do - it "returns new instance with rules removed" do - result = rule.remove [age_condition, plan_condition] - expect(result).not_to be(rule) - expect(result.rules).to eq([]) - end - end - end - - describe "#value" do - it "returns type and value" do - expect(rule.value).to eq({ - "type" => "All", - "value" => [ - plan_condition.value, - age_condition.value, - ], - }) - end - end - - describe "#eql?" do - it "returns true if equal" do - other_rule = Flipper::Rules::All.new( - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "plan"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "basic"} - ), - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "age"}, - {"type" => "Operator", "value" => "gte"}, - {"type" => "Integer", "value" => 21} - ) - ) - expect(rule).to eql(other_rule) - expect(rule == other_rule).to be(true) - end - - it "returns false if not equal" do - other_rule = Flipper::Rules::All.new( - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "plan"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "premium"} - ), - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "age"}, - {"type" => "Operator", "value" => "gte"}, - {"type" => "Integer", "value" => 21} - ) - ) - expect(rule).not_to eql(other_rule) - expect(rule == other_rule).to be(false) - end - - it "returns false if not rule" do - expect(rule).not_to eql(Object.new) - expect(rule == Object.new).to be(false) - end - end - - describe "#matches?" do - let(:rule) { - Flipper::Rules::All.new( - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "plan"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "basic"} - ), - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "age"}, - {"type" => "Operator", "value" => "gte"}, - {"type" => "Integer", "value" => 21} - ) - ) - } - - it "returns true when all conditions match" do - actor = Flipper::Actor.new("User;1", "plan" => "basic", "age" => 21) - expect(rule.matches?(feature_name, actor)).to be(true) - end - - it "returns false when any condition does NOT match" do - actor = Flipper::Actor.new("User;1", "plan" => "premium", "age" => 18) - expect(rule.matches?(feature_name, actor)).to be(false) - - actor = Flipper::Actor.new("User;1", "plan" => "basic", "age" => 20) - expect(rule.matches?(feature_name, actor)).to be(false) - - actor = Flipper::Actor.new("User;1", "plan" => "premium", "age" => 21) - expect(rule.matches?(feature_name, actor)).to be(false) - end - end -end diff --git a/spec/flipper/rules/any_spec.rb b/spec/flipper/rules/any_spec.rb deleted file mode 100644 index f123d5d42..000000000 --- a/spec/flipper/rules/any_spec.rb +++ /dev/null @@ -1,250 +0,0 @@ -require 'helper' - -RSpec.describe Flipper::Rules::Any do - let(:feature_name) { "search" } - let(:plan_condition) { - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "plan"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "basic"} - ) - } - let(:age_condition) { - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "age"}, - {"type" => "Operator", "value" => "gte"}, - {"type" => "Integer", "value" => 21} - ) - } - let(:all_rule) { - Flipper::Rules::All.new( - plan_condition, - age_condition - ) - } - let(:rule) { - Flipper::Rules::Any.new( - plan_condition, - age_condition - ) - } - - describe "#initialize" do - it "flattens rules" do - instance = Flipper::Rules::Any.new([[plan_condition, age_condition]]) - expect(instance.rules).to eq([ - plan_condition, - age_condition, - ]) - end - end - - describe ".build" do - context "for Array of Hashes" do - it "builds instance" do - instance = Flipper::Rules::Any.build([plan_condition.value, age_condition.value]) - expect(instance).to be_instance_of(Flipper::Rules::Any) - expect(instance.rules).to eq([ - plan_condition, - age_condition, - ]) - end - end - - context "for nested Array of Hashes" do - it "builds instance" do - instance = Flipper::Rules::Any.build([[plan_condition.value, age_condition.value]]) - expect(instance).to be_instance_of(Flipper::Rules::Any) - expect(instance.rules).to eq([ - plan_condition, - age_condition, - ]) - end - end - - context "for Array with All rule" do - it "builds instance" do - instance = Flipper::Rules::Any.build(all_rule.value) - expect(instance).to be_instance_of(Flipper::Rules::Any) - expect(instance.rules).to eq([all_rule]) - end - end - end - - describe "#all" do - it "wraps self with all" do - result = rule.all - expect(result).to be_instance_of(Flipper::Rules::All) - expect(result.rules).to eq([rule]) - end - end - - describe "#any" do - it "returns self" do - result = rule.any - expect(result).to be(rule) - end - end - - describe "#add" do - context "with single rule" do - it "returns new instance with rule added" do - rule2 = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "flipper_id"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "User;2"} - ) - result = rule.add rule2 - expect(result).not_to be(rule) - expect(result.rules).to eq([rule.rules, rule2].flatten) - end - end - - context "with multiple rules" do - it "returns new instance with rule added" do - rule2 = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "flipper_id"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "User;2"} - ) - rule3 = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "flipper_id"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "User;3"} - ) - - result = rule.add rule2, rule3 - expect(result).not_to be(rule) - expect(result.rules).to eq([rule.rules, rule2, rule3].flatten) - end - end - - context "with array of rules" do - it "returns new instance with rule added" do - rule2 = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "flipper_id"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "User;2"} - ) - rule3 = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "flipper_id"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "User;3"} - ) - - result = rule.add [rule2, rule3] - expect(result).not_to be(rule) - expect(result.rules).to eq([rule.rules, rule2, rule3].flatten) - end - end - end - - describe "#remove" do - context "with single rule" do - it "returns new instance with rule removed" do - result = rule.remove age_condition - expect(result).not_to be(rule) - expect(result.rules).to eq([plan_condition]) - end - end - - context "with multiple rules" do - it "returns new instance with rules removed" do - result = rule.remove age_condition, plan_condition - expect(result).not_to be(rule) - expect(result.rules).to eq([]) - end - end - - context "with array of rules" do - it "returns new instance with rules removed" do - result = rule.remove [age_condition, plan_condition] - expect(result).not_to be(rule) - expect(result.rules).to eq([]) - end - end - end - - describe "#value" do - it "returns type and value" do - expect(rule.value).to eq({ - "type" => "Any", - "value" => [ - plan_condition.value, - age_condition.value, - ], - }) - end - end - - describe "#eql?" do - it "returns true if equal" do - other_rule = Flipper::Rules::Any.new( - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "plan"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "basic"} - ), - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "age"}, - {"type" => "Operator", "value" => "gte"}, - {"type" => "Integer", "value" => 21} - ) - ) - expect(rule).to eql(other_rule) - expect(rule == other_rule).to be(true) - end - - it "returns false if not equal" do - other_rule = Flipper::Rules::Any.new( - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "plan"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "premium"} - ), - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "age"}, - {"type" => "Operator", "value" => "gte"}, - {"type" => "Integer", "value" => 21} - ) - ) - expect(rule).not_to eql(other_rule) - expect(rule == other_rule).to be(false) - end - - it "returns false if not rule" do - expect(rule).not_to eql(Object.new) - expect(rule == Object.new).to be(false) - end - end - - describe "#matches?" do - let(:rule) { - Flipper::Rules::Any.new( - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "plan"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "basic"} - ), - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "age"}, - {"type" => "Operator", "value" => "gte"}, - {"type" => "Integer", "value" => 21} - ) - ) - } - - it "returns true when any condition matches" do - plan_actor = Flipper::Actor.new("User;1", "plan" => "basic") - expect(rule.matches?(feature_name, plan_actor)).to be(true) - - age_actor = Flipper::Actor.new("User;1", "age" => 21) - expect(rule.matches?(feature_name, age_actor)).to be(true) - end - - it "returns false when no condition matches" do - actor = Flipper::Actor.new("User;1", "plan" => "premium", "age" => 18) - expect(rule.matches?(feature_name, actor)).to be(false) - end - end -end diff --git a/spec/flipper/rules/condition_spec.rb b/spec/flipper/rules/condition_spec.rb deleted file mode 100644 index 7a12abc10..000000000 --- a/spec/flipper/rules/condition_spec.rb +++ /dev/null @@ -1,409 +0,0 @@ -require 'helper' - -RSpec.describe Flipper::Rules::Condition do - let(:feature_name) { "search" } - - describe "#all" do - it "wraps self with all" do - rule = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "flipper_id"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "User;1"} - ) - result = rule.all - expect(result).to be_instance_of(Flipper::Rules::All) - expect(result.rules).to eq([rule]) - end - end - - describe "#any" do - it "wraps self with any" do - rule = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "flipper_id"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "User;1"} - ) - result = rule.any - expect(result).to be_instance_of(Flipper::Rules::Any) - expect(result.rules).to eq([rule]) - end - end - - describe "#add" do - context "with single rule" do - it "wraps self with any and adds new rule" do - rule = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "flipper_id"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "User;1"} - ) - rule2 = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "flipper_id"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "User;2"} - ) - - result = rule.add(rule2) - expect(result).to be_instance_of(Flipper::Rules::Any) - expect(result.rules).to eq([rule, rule2]) - end - end - - context "with multiple rules" do - it "wraps self with any and adds new rules" do - rule = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "flipper_id"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "User;1"} - ) - rule2 = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "flipper_id"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "User;2"} - ) - rule3 = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "flipper_id"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "User;3"} - ) - - result = rule.add(rule2, rule3) - expect(result).to be_instance_of(Flipper::Rules::Any) - expect(result.rules).to eq([rule, rule2, rule3]) - end - end - - context "with array of rules" do - it "wraps self with any and adds new rules" do - rule = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "flipper_id"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "User;1"} - ) - rule2 = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "flipper_id"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "User;2"} - ) - rule3 = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "flipper_id"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "User;3"} - ) - - result = rule.add([rule2, rule3]) - expect(result).to be_instance_of(Flipper::Rules::Any) - expect(result.rules).to eq([rule, rule2, rule3]) - end - end - end - - describe "#value" do - it "returns Hash with type and value" do - rule = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "flipper_id"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "User;1"} - ) - expect(rule.value).to eq({ - "type" => "Condition", - "value" => { - "left" => {"type" => "Property", "value" => "flipper_id"}, - "operator" => {"type" => "Operator", "value" => "eq"}, - "right" => {"type" => "String", "value" => "User;1"}, - }, - }) - end - end - - describe "#eql?" do - let(:rule) { - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "plan"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "basic"} - ) - } - - it "returns true if equal" do - other_rule = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "plan"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "basic"} - ) - expect(rule).to eql(other_rule) - expect(rule == other_rule).to be(true) - end - - it "returns false if not equal" do - other_rule = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "plan"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "premium"} - ) - expect(rule).not_to eql(other_rule) - expect(rule == other_rule).to be(false) - end - - it "returns false if not rule" do - expect(rule).not_to eql(Object.new) - expect(rule == Object.new).to be(false) - end - end - - describe "#matches?" do - context "with no actor" do - it "does not error for condition that returns true" do - rule = Flipper::Rules::Condition.new( - {"type" => "Boolean", "value" => true}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "Boolean", "value" => true}, - ) - expect(rule.matches?(feature_name, nil)).to be(true) - end - - it "does not error for condition that returns false" do - rule = Flipper::Rules::Condition.new( - {"type" => "Boolean", "value" => true}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "Boolean", "value" => false}, - ) - expect(rule.matches?(feature_name, nil)).to be(false) - end - end - - context "with actor that does NOT respond to flipper_properties but does respond to flipper_id" do - it "does not error" do - user = Struct.new(:flipper_id).new("User;1") - rule = Flipper::Rules::Condition.new( - {"type" => "Boolean", "value" => true}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "Boolean", "value" => true}, - ) - expect(rule.matches?(feature_name, user)).to be(true) - end - end - - context "with actor that does respond to flipper_properties but does NOT respond to flipper_id" do - it "does not error" do - user = Struct.new(:flipper_properties).new({"id" => 1, "type" => "User"}) - rule = Flipper::Rules::Condition.new( - {"type" => "Boolean", "value" => true}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "Boolean", "value" => true}, - ) - expect(rule.matches?(feature_name, user)).to be(true) - end - end - - context "with non-Flipper::Actor object that quacks like a duck" do - it "works" do - user_class = Class.new(Struct.new(:id, :flipper_properties)) do - def flipper_id - "User;#{id}" - end - end - user = user_class.new(1, {}) - - rule = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "flipper_id"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "User;1"} - ) - expect(rule.matches?(feature_name, user)).to be(true) - expect(rule.matches?(feature_name, user_class.new(2, {}))).to be(false) - end - end - - context "eq" do - let(:rule) { - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "plan"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "basic"} - ) - } - - it "returns true when property matches" do - actor = Flipper::Actor.new("User;1", { - "plan" => "basic", - }) - expect(rule.matches?(feature_name, actor)).to be(true) - end - - it "returns false when property does not match" do - actor = Flipper::Actor.new("User;1", { - "plan" => "premium", - }) - expect(rule.matches?(feature_name, actor)).to be(false) - end - end - - context "neq" do - let(:rule) { - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "plan"}, - {"type" => "Operator", "value" => "neq"}, - {"type" => "String", "value" => "basic"} - ) - } - - it "returns true when property does NOT match" do - actor = Flipper::Actor.new("User;1", { - "plan" => "premium", - }) - expect(rule.matches?(feature_name, actor)).to be(true) - end - - it "returns false when property does match" do - actor = Flipper::Actor.new("User;1", { - "plan" => "basic", - }) - expect(rule.matches?(feature_name, actor)).to be(false) - end - end - - context "gt" do - let(:rule) { - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "age"}, - {"type" => "Operator", "value" => "gt"}, - {"type" => "Integer", "value" => 20} - ) - } - - it "returns true when property matches" do - actor = Flipper::Actor.new("User;1", { - "age" => 21, - }) - expect(rule.matches?(feature_name, actor)).to be(true) - end - - it "returns false when property does NOT match" do - actor = Flipper::Actor.new("User;1", { - "age" => 20, - }) - expect(rule.matches?(feature_name, actor)).to be(false) - end - end - - context "gte" do - let(:rule) { - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "age"}, - {"type" => "Operator", "value" => "gte"}, - {"type" => "Integer", "value" => 20} - ) - } - - it "returns true when property matches" do - actor = Flipper::Actor.new("User;1", { - "age" => 20, - }) - expect(rule.matches?(feature_name, actor)).to be(true) - end - - it "returns false when property does NOT match" do - actor = Flipper::Actor.new("User;1", { - "age" => 19, - }) - expect(rule.matches?(feature_name, actor)).to be(false) - end - end - - context "lt" do - let(:rule) { - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "age"}, - {"type" => "Operator", "value" => "lt"}, - {"type" => "Integer", "value" => 21} - ) - } - - it "returns true when property matches" do - actor = Flipper::Actor.new("User;1", { - "age" => 20, - }) - expect(rule.matches?(feature_name, actor)).to be(true) - end - - it "returns false when property does NOT match" do - actor = Flipper::Actor.new("User;1", { - "age" => 21, - }) - expect(rule.matches?(feature_name, actor)).to be(false) - end - end - - context "lt with rand type" do - let(:rule) { - Flipper::Rules::Condition.new( - {"type" => "Random", "value" => 100}, - {"type" => "Operator", "value" => "lt"}, - {"type" => "Integer", "value" => 25} - ) - } - - it "returns true when property matches" do - results = [] - (1..1000).to_a.each do |n| - actor = Flipper::Actor.new("User;#{n}") - results << rule.matches?(feature_name, actor) - end - - enabled, disabled = results.partition { |r| r } - expect(enabled.size).to be_within(50).of(250) - end - end - - context "lte" do - let(:rule) { - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "age"}, - {"type" => "Operator", "value" => "lte"}, - {"type" => "Integer", "value" => 21} - ) - } - - it "returns true when property matches" do - actor = Flipper::Actor.new("User;1", { - "age" => 21, - }) - expect(rule.matches?(feature_name, actor)).to be(true) - end - - it "returns false when property does NOT match" do - actor = Flipper::Actor.new("User;1", { - "age" => 22, - }) - expect(rule.matches?(feature_name, actor)).to be(false) - end - end - - context "percentage" do - let(:rule) { - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "flipper_id"}, - {"type" => "Operator", "value" => "percentage"}, - {"type" => "Integer", "value" => 25} - ) - } - - it "returns true when property matches" do - results = [] - (1..1000).to_a.each do |n| - actor = Flipper::Actor.new("User;#{n}") - results << rule.matches?(feature_name, actor) - end - - enabled, disabled = results.partition { |r| r } - expect(enabled.size).to be_within(10).of(250) - end - - it "returns false when property does NOT match" do - actor = Flipper::Actor.new("User;1") - expect(rule.matches?(feature_name, actor)).to be(false) - end - end - end -end diff --git a/spec/flipper/rules/expression_spec.rb b/spec/flipper/rules/expression_spec.rb new file mode 100644 index 000000000..f20f0b9a6 --- /dev/null +++ b/spec/flipper/rules/expression_spec.rb @@ -0,0 +1,128 @@ +require 'helper' + +RSpec.describe Flipper::Expression do + describe "#build" do + it "can build Equal" do + expression = Flipper::Expression.build({ + "Equal" => [ + {"String" => ["basic"]}, + {"String" => ["basic"]}, + ] + }) + + expect(expression).to be_instance_of(Flipper::Expressions::Equal) + expect(expression.args).to eq([ + Flipper::Expressions::String.new(["basic"]), + Flipper::Expressions::String.new(["basic"]), + ]) + end + + it "can build GreaterThanOrEqualTo" do + expression = Flipper::Expression.build({ + "GreaterThanOrEqualTo" => [ + {"Number" => [2]}, + {"Number" => [1]}, + ] + }) + + expect(expression).to be_instance_of(Flipper::Expressions::GreaterThanOrEqualTo) + expect(expression.args).to eq([ + Flipper::Expressions::Number.new([2]), + Flipper::Expressions::Number.new([1]), + ]) + end + + it "can build GreaterThan" do + expression = Flipper::Expression.build({ + "GreaterThan" => [ + {"Number" => [2]}, + {"Number" => [1]}, + ] + }) + + expect(expression).to be_instance_of(Flipper::Expressions::GreaterThan) + expect(expression.args).to eq([ + Flipper::Expressions::Number.new([2]), + Flipper::Expressions::Number.new([1]), + ]) + end + + it "can build LessThanOrEqualTo" do + expression = Flipper::Expression.build({ + "LessThanOrEqualTo" => [ + {"Number" => [2]}, + {"Number" => [1]}, + ] + }) + + expect(expression).to be_instance_of(Flipper::Expressions::LessThanOrEqualTo) + expect(expression.args).to eq([ + Flipper::Expressions::Number.new([2]), + Flipper::Expressions::Number.new([1]), + ]) + end + + it "can build LessThan" do + expression = Flipper::Expression.build({ + "LessThan" => [ + {"Number" => [2]}, + {"Number" => [1]}, + ] + }) + + expect(expression).to be_instance_of(Flipper::Expressions::LessThan) + expect(expression.args).to eq([ + Flipper::Expressions::Number.new([2]), + Flipper::Expressions::Number.new([1]), + ]) + end + + it "can build NotEqual" do + expression = Flipper::Expression.build({ + "NotEqual" => [ + {"String" => ["basic"]}, + {"String" => ["plus"]}, + ] + }) + + expect(expression).to be_instance_of(Flipper::Expressions::NotEqual) + expect(expression.args).to eq([ + Flipper::Expressions::String.new(["basic"]), + Flipper::Expressions::String.new(["plus"]), + ]) + end + + it "can build Number" do + expression = Flipper::Expression.build({ + "Number" => [1] + }) + + expect(expression).to be_instance_of(Flipper::Expressions::Number) + expect(expression.args).to eq([1]) + end + + it "can build Percentage" do + expression = Flipper::Expression.build({ + "Percentage" => [ + {"String" => ["User;1"]}, + {"Number" => [40]}, + ] + }) + + expect(expression).to be_instance_of(Flipper::Expressions::Percentage) + expect(expression.args).to eq([ + Flipper::Expressions::String.new(["User;1"]), + Flipper::Expressions::Number.new([40]), + ]) + end + + it "can build String" do + expression = Flipper::Expression.build({ + "String" => ["basic"] + }) + + expect(expression).to be_instance_of(Flipper::Expressions::String) + expect(expression.args).to eq(["basic"]) + end + end +end diff --git a/spec/flipper/rules/expressions/equal_spec.rb b/spec/flipper/rules/expressions/equal_spec.rb new file mode 100644 index 000000000..a8df8e268 --- /dev/null +++ b/spec/flipper/rules/expressions/equal_spec.rb @@ -0,0 +1,38 @@ +require 'helper' + +RSpec.describe Flipper::Expressions::Equal do + it "can be built" do + expression = described_class.build({ + "Equal" => [ + {"String" => ["basic"]}, + {"String" => ["basic"]}, + ] + }) + + expect(expression).to be_instance_of(Flipper::Expressions::Equal) + expect(expression.args).to eq([ + Flipper::Expressions::String.new(["basic"]), + Flipper::Expressions::String.new(["basic"]), + ]) + end + + describe "#evaluate" do + it "returns true when equal" do + expression = Flipper::Expressions::Equal.new([ + Flipper::Expressions::String.new(["basic"]), + Flipper::Expressions::String.new(["basic"]), + ]) + + expect(expression.evaluate).to be(true) + end + + it "returns false when not equal" do + expression = Flipper::Expressions::Equal.new([ + Flipper::Expressions::String.new(["basic"]), + Flipper::Expressions::String.new(["plus"]), + ]) + + expect(expression.evaluate).to be(false) + end + end +end diff --git a/spec/flipper/rules/expressions/greater_than_or_equal_to_spec.rb b/spec/flipper/rules/expressions/greater_than_or_equal_to_spec.rb new file mode 100644 index 000000000..f8a55e858 --- /dev/null +++ b/spec/flipper/rules/expressions/greater_than_or_equal_to_spec.rb @@ -0,0 +1,32 @@ +require 'helper' + +RSpec.describe Flipper::Expressions::GreaterThanOrEqualTo do + describe "#evaluate" do + it "returns true when equal" do + expression = described_class.new([ + Flipper::Expressions::Number.new([2]), + Flipper::Expressions::Number.new([2]), + ]) + + expect(expression.evaluate).to be(true) + end + + it "returns true when greater" do + expression = described_class.new([ + Flipper::Expressions::Number.new([2]), + Flipper::Expressions::Number.new([1]), + ]) + + expect(expression.evaluate).to be(true) + end + + it "returns false when less" do + expression = described_class.new([ + Flipper::Expressions::Number.new([1]), + Flipper::Expressions::Number.new([2]), + ]) + + expect(expression.evaluate).to be(false) + end + end +end diff --git a/spec/flipper/rules/expressions/greater_than_spec.rb b/spec/flipper/rules/expressions/greater_than_spec.rb new file mode 100644 index 000000000..f5c648b16 --- /dev/null +++ b/spec/flipper/rules/expressions/greater_than_spec.rb @@ -0,0 +1,32 @@ +require 'helper' + +RSpec.describe Flipper::Expressions::GreaterThan do + describe "#evaluate" do + it "returns false when equal" do + expression = described_class.new([ + Flipper::Expressions::Number.new([2]), + Flipper::Expressions::Number.new([2]), + ]) + + expect(expression.evaluate).to be(false) + end + + it "returns true when greater" do + expression = described_class.new([ + Flipper::Expressions::Number.new([2]), + Flipper::Expressions::Number.new([1]), + ]) + + expect(expression.evaluate).to be(true) + end + + it "returns false when less" do + expression = described_class.new([ + Flipper::Expressions::Number.new([1]), + Flipper::Expressions::Number.new([2]), + ]) + + expect(expression.evaluate).to be(false) + end + end +end diff --git a/spec/flipper/rules/expressions/less_than_or_equal_to_spec.rb b/spec/flipper/rules/expressions/less_than_or_equal_to_spec.rb new file mode 100644 index 000000000..b5ff859c7 --- /dev/null +++ b/spec/flipper/rules/expressions/less_than_or_equal_to_spec.rb @@ -0,0 +1,32 @@ +require 'helper' + +RSpec.describe Flipper::Expressions::LessThanOrEqualTo do + describe "#evaluate" do + it "returns true when equal" do + expression = described_class.new([ + Flipper::Expressions::Number.new([2]), + Flipper::Expressions::Number.new([2]), + ]) + + expect(expression.evaluate).to be(true) + end + + it "returns true when less" do + expression = described_class.new([ + Flipper::Expressions::Number.new([1]), + Flipper::Expressions::Number.new([2]), + ]) + + expect(expression.evaluate).to be(true) + end + + it "returns false when greater" do + expression = described_class.new([ + Flipper::Expressions::Number.new([2]), + Flipper::Expressions::Number.new([1]), + ]) + + expect(expression.evaluate).to be(false) + end + end +end diff --git a/spec/flipper/rules/expressions/less_than_spec.rb b/spec/flipper/rules/expressions/less_than_spec.rb new file mode 100644 index 000000000..baff7a08d --- /dev/null +++ b/spec/flipper/rules/expressions/less_than_spec.rb @@ -0,0 +1,32 @@ +require 'helper' + +RSpec.describe Flipper::Expressions::LessThan do + describe "#evaluate" do + it "returns false when equal" do + expression = described_class.new([ + Flipper::Expressions::Number.new(2), + Flipper::Expressions::Number.new(2), + ]) + + expect(expression.evaluate).to be(false) + end + + it "returns true when less" do + expression = described_class.new([ + Flipper::Expressions::Number.new([1]), + Flipper::Expressions::Number.new([2]), + ]) + + expect(expression.evaluate).to be(true) + end + + it "returns false when greater" do + expression = described_class.new([ + Flipper::Expressions::Number.new([2]), + Flipper::Expressions::Number.new([1]), + ]) + + expect(expression.evaluate).to be(false) + end + end +end diff --git a/spec/flipper/rules/expressions/not_equal_spec.rb b/spec/flipper/rules/expressions/not_equal_spec.rb new file mode 100644 index 000000000..220bbc967 --- /dev/null +++ b/spec/flipper/rules/expressions/not_equal_spec.rb @@ -0,0 +1,23 @@ +require 'helper' + +RSpec.describe Flipper::Expressions::NotEqual do + describe "#evaluate" do + it "returns true when not equal" do + expression = described_class.new([ + Flipper::Expressions::String.new("basic"), + Flipper::Expressions::String.new("plus"), + ]) + + expect(expression.evaluate).to be(true) + end + + it "returns false when equal" do + expression = described_class.new([ + Flipper::Expressions::String.new("basic"), + Flipper::Expressions::String.new("basic"), + ]) + + expect(expression.evaluate).to be(false) + end + end +end diff --git a/spec/flipper/rules/expressions/number_spec.rb b/spec/flipper/rules/expressions/number_spec.rb new file mode 100644 index 000000000..16970e451 --- /dev/null +++ b/spec/flipper/rules/expressions/number_spec.rb @@ -0,0 +1,20 @@ +require 'helper' + +RSpec.describe Flipper::Expressions::Number do + it "can initialize with number" do + expect(described_class.new(1).args).to eq([1]) + end + + it "can initialize with array" do + expect(described_class.new([1]).args).to eq([1]) + end + + describe "#evaluate" do + [1, 1.1, 1_000].each do |value| + it "returns first arg for #{value}" do + expression = described_class.new([value]) + expect(expression.evaluate).to eq(value) + end + end + end +end diff --git a/spec/flipper/rules/expressions/percentage_spec.rb b/spec/flipper/rules/expressions/percentage_spec.rb new file mode 100644 index 000000000..23d03b815 --- /dev/null +++ b/spec/flipper/rules/expressions/percentage_spec.rb @@ -0,0 +1,33 @@ +require 'helper' + +RSpec.describe Flipper::Expressions::Percentage do + describe "#evaluate" do + it "returns true when string in percentage enabled" do + expression = described_class.new([ + Flipper::Expressions::String.new("User;1"), + Flipper::Expressions::Number.new(100), + ]) + + expect(expression.evaluate).to be(true) + end + + it "returns false when string in percentage enabled" do + expression = described_class.new([ + Flipper::Expressions::String.new("User;1"), + Flipper::Expressions::Number.new(0), + ]) + + expect(expression.evaluate).to be(false) + end + + it "changes value based on feature_name so not all actors get all features first" do + expression = described_class.new([ + Flipper::Expressions::String.new("User;1"), + Flipper::Expressions::Number.new(70), + ]) + + expect(expression.evaluate(feature_name: "a")).to be(true) + expect(expression.evaluate(feature_name: "b")).to be(false) + end + end +end diff --git a/spec/flipper/rules/expressions/property_spec.rb b/spec/flipper/rules/expressions/property_spec.rb new file mode 100644 index 000000000..a33994757 --- /dev/null +++ b/spec/flipper/rules/expressions/property_spec.rb @@ -0,0 +1,34 @@ +require 'helper' + +RSpec.describe Flipper::Expressions::Property do + it "can initialize with string" do + expect(described_class.new("flipper_id").args).to eq(["flipper_id"]) + end + + it "can initialize with symbol" do + expect(described_class.new(:flipper_id).args).to eq(["flipper_id"]) + end + + it "can initialize with array of string" do + expect(described_class.new(["flipper_id"]).args).to eq(["flipper_id"]) + end + + it "can initialize with array of symbol" do + expect(described_class.new([:flipper_id]).args).to eq(["flipper_id"]) + end + + describe "#evaluate" do + it "returns value for property key" do + expression = described_class.new("flipper_id") + properties = { + "flipper_id" => "User;1", + } + expect(expression.evaluate(properties: properties)).to eq("User;1") + end + + it "returns nil if key not found in properties" do + expression = described_class.new("flipper_id") + expect(expression.evaluate).to be(nil) + end + end +end diff --git a/spec/flipper/rules/expressions/random_spec.rb b/spec/flipper/rules/expressions/random_spec.rb new file mode 100644 index 000000000..a570449a4 --- /dev/null +++ b/spec/flipper/rules/expressions/random_spec.rb @@ -0,0 +1,20 @@ +require 'helper' + +RSpec.describe Flipper::Expressions::Random do + it "can initialize with number" do + expect(described_class.new(1).args).to eq([1]) + end + + it "can initialize with array" do + expect(described_class.new([1]).args).to eq([1]) + end + + describe "#evaluate" do + it "returns random number based on seed" do + expression = described_class.new([10]) + result = expression.evaluate + expect(result).to be >= 0 + expect(result).to be <= 10 + end + end +end diff --git a/spec/flipper/rules/expressions/string_spec.rb b/spec/flipper/rules/expressions/string_spec.rb new file mode 100644 index 000000000..20a25ddba --- /dev/null +++ b/spec/flipper/rules/expressions/string_spec.rb @@ -0,0 +1,18 @@ +require 'helper' + +RSpec.describe Flipper::Expressions::String do + it "can initialize with string" do + expect(described_class.new("basic").args).to eq(["basic"]) + end + + it "can initialize with array" do + expect(described_class.new(["basic"]).args).to eq(["basic"]) + end + + describe "#evaluate" do + it "returns arg" do + expression = described_class.new(["basic"]) + expect(expression.evaluate).to eq("basic") + end + end +end diff --git a/spec/flipper/rules/object_spec.rb b/spec/flipper/rules/object_spec.rb deleted file mode 100644 index 28e9c9843..000000000 --- a/spec/flipper/rules/object_spec.rb +++ /dev/null @@ -1,553 +0,0 @@ -require 'helper' - -RSpec.describe Flipper::Rules::Object do - describe ".wrap" do - context "with Hash" do - it "returns instance" do - instance = described_class.build({"type" => "Integer", "value" => 2}) - expect(instance).to be_instance_of(described_class) - expect(instance.type).to eq("Integer") - expect(instance.value).to eq(2) - end - end - - context "with instance" do - it "returns instance" do - instance = described_class.build(described_class.new(2)) - expect(instance).to be_instance_of(described_class) - expect(instance.type).to eq("Integer") - expect(instance.value).to eq(2) - end - end - - context "with string" do - it "returns instance" do - instance = described_class.build("test") - expect(instance).to be_instance_of(described_class) - expect(instance.type).to eq("String") - expect(instance.value).to eq("test") - end - end - - context "with integer" do - it "returns instance" do - instance = described_class.build(21) - expect(instance).to be_instance_of(described_class) - expect(instance.type).to eq("Integer") - expect(instance.value).to eq(21) - end - end - - context "with nil" do - it "returns instance" do - instance = described_class.build(nil) - expect(instance).to be_instance_of(described_class) - expect(instance.type).to eq("Null") - expect(instance.value).to be(nil) - end - end - - context "with true" do - it "returns instance" do - instance = described_class.build(true) - expect(instance).to be_instance_of(described_class) - expect(instance.type).to eq("Boolean") - expect(instance.value).to be(true) - end - end - - context "with false" do - it "returns instance" do - instance = described_class.build(false) - expect(instance).to be_instance_of(described_class) - expect(instance.type).to eq("Boolean") - expect(instance.value).to be(false) - end - end - - context "with unsupported type" do - it "raises ArgumentError" do - expect { - described_class.build(Set.new) - }.to raise_error(ArgumentError, /is not a supported primitive\. Object must be one of: String, Integer, NilClass, TrueClass, FalseClass\./) - end - end - end - - describe "#initialize" do - context "with string" do - it "returns instance" do - instance = described_class.new("test") - expect(instance.type).to eq("String") - expect(instance.value).to eq("test") - end - end - - context "with integer" do - it "returns instance" do - instance = described_class.new(21) - expect(instance.type).to eq("Integer") - expect(instance.value).to eq(21) - end - end - - context "with nil" do - it "returns instance" do - instance = described_class.new(nil) - expect(instance.type).to eq("Null") - expect(instance.value).to be(nil) - end - end - - context "with true" do - it "returns instance" do - instance = described_class.new(true) - expect(instance.type).to eq("Boolean") - expect(instance.value).to be(true) - end - end - - context "with false" do - it "returns instance" do - instance = described_class.new(false) - expect(instance.type).to eq("Boolean") - expect(instance.value).to be(false) - end - end - - context "with unsupported type" do - it "raises ArgumentError" do - expect { - described_class.new({}) - }.to raise_error(ArgumentError, /{} is not a supported primitive\. Object must be one of: String, Integer, NilClass, TrueClass, FalseClass\./) - end - end - end - - describe "equality" do - it "returns true if equal" do - expect(described_class.new("test").eql?(described_class.new("test"))).to be(true) - end - - it "returns false if value does not match" do - expect(described_class.new("test").eql?(described_class.new("age"))).to be(false) - end - - it "returns false for different class" do - expect(described_class.new("test").eql?(Object.new)).to be(false) - end - end - - describe "#to_h" do - it "returns Hash with type and value" do - expect(described_class.new("test").to_h).to eq({ - "type" => "String", - "value" => "test", - }) - end - end - - describe "#eq" do - context "with string" do - it "returns equal condition" do - expect(described_class.new("plan").eq("basic")).to eq(Flipper::Rules::Condition.new( - {"type" => "String", "value" => "plan"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "basic"} - )) - end - end - - context "with boolean" do - it "returns equal condition" do - expect(described_class.new("admin").eq(true)).to eq(Flipper::Rules::Condition.new( - {"type" => "String", "value" => "admin"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "Boolean", "value" => true} - )) - end - end - - context "with integer" do - it "returns equal condition" do - expect(described_class.new("age").eq(21)).to eq(Flipper::Rules::Condition.new( - {"type" => "String", "value" => "age"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "Integer", "value" => 21} - )) - end - end - - context "with nil" do - it "returns equal condition" do - expect(described_class.new("admin").eq(nil)).to eq(Flipper::Rules::Condition.new( - {"type" => "String", "value" => "admin"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "Null", "value" => nil} - )) - end - end - - context "with property" do - it "returns equal condition" do - expect(described_class.new("admin").eq(Flipper.property(:name))).to eq(Flipper::Rules::Condition.new( - {"type" => "String", "value" => "admin"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "Property", "value" => "name"} - )) - end - end - - context "with object" do - it "returns equal condition" do - expect(described_class.new("admin").eq(Flipper.object("test"))).to eq(Flipper::Rules::Condition.new( - {"type" => "String", "value" => "admin"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "test"} - )) - end - end - end - - describe "#neq" do - context "with string" do - it "returns not equal condition" do - expect(described_class.new("plan").neq("basic")).to eq(Flipper::Rules::Condition.new( - {"type" => "String", "value" => "plan"}, - {"type" => "Operator", "value" => "neq"}, - {"type" => "String", "value" => "basic"} - )) - end - end - - context "with boolean" do - it "returns not equal condition" do - expect(described_class.new("admin").neq(true)).to eq(Flipper::Rules::Condition.new( - {"type" => "String", "value" => "admin"}, - {"type" => "Operator", "value" => "neq"}, - {"type" => "Boolean", "value" => true} - )) - end - end - - context "with integer" do - it "returns not equal condition" do - expect(described_class.new("age").neq(21)).to eq(Flipper::Rules::Condition.new( - {"type" => "String", "value" => "age"}, - {"type" => "Operator", "value" => "neq"}, - {"type" => "Integer", "value" => 21} - )) - end - end - - context "with nil" do - it "returns not equal condition" do - expect(described_class.new("admin").neq(nil)).to eq(Flipper::Rules::Condition.new( - {"type" => "String", "value" => "admin"}, - {"type" => "Operator", "value" => "neq"}, - {"type" => "Null", "value" => nil} - )) - end - end - - context "with property" do - it "returns not equal condition" do - expect(described_class.new("plan").neq(Flipper.property(:name))).to eq(Flipper::Rules::Condition.new( - {"type" => "String", "value" => "plan"}, - {"type" => "Operator", "value" => "neq"}, - {"type" => "Property", "value" => "name"} - )) - end - end - - context "with object" do - it "returns not equal condition" do - expect(described_class.new("plan").neq(Flipper.object("test"))).to eq(Flipper::Rules::Condition.new( - {"type" => "String", "value" => "plan"}, - {"type" => "Operator", "value" => "neq"}, - {"type" => "String", "value" => "test"} - )) - end - end - end - - describe "#gt" do - context "with integer" do - it "returns condition" do - expect(described_class.new("age").gt(21)).to eq(Flipper::Rules::Condition.new( - {"type" => "String", "value" => "age"}, - {"type" => "Operator", "value" => "gt"}, - {"type" => "Integer", "value" => 21} - )) - end - end - - context "with property" do - it "returns condition" do - expect(described_class.new(21).gt(Flipper.property(:age))).to eq(Flipper::Rules::Condition.new( - {"type" => "Integer", "value" => 21}, - {"type" => "Operator", "value" => "gt"}, - {"type" => "Property", "value" => "age"} - )) - end - end - - context "with object" do - it "returns condition" do - expect(described_class.new(21).gt(Flipper.object(22))).to eq(Flipper::Rules::Condition.new( - {"type" => "Integer", "value" => 21}, - {"type" => "Operator", "value" => "gt"}, - {"type" => "Integer", "value" => 22} - )) - end - end - - context "with string" do - it "raises error" do - expect { described_class.new("age").gt("231") }.to raise_error(ArgumentError) - end - end - - context "with boolean" do - it "raises error" do - expect { described_class.new("age").gt(true) }.to raise_error(ArgumentError) - end - end - - context "with array" do - it "raises error" do - expect { described_class.new("age").gt(["admin"]) }.to raise_error(ArgumentError) - end - end - - context "with nil" do - it "raises error" do - expect { described_class.new("age").gt(nil) }.to raise_error(ArgumentError) - end - end - end - - describe "#gte" do - context "with integer" do - it "returns condition" do - expect(described_class.new("age").gte(21)).to eq(Flipper::Rules::Condition.new( - {"type" => "String", "value" => "age"}, - {"type" => "Operator", "value" => "gte"}, - {"type" => "Integer", "value" => 21} - )) - end - end - - context "with property" do - it "returns condition" do - expect(described_class.new(21).gte(Flipper.property(:age))).to eq(Flipper::Rules::Condition.new( - {"type" => "Integer", "value" => 21}, - {"type" => "Operator", "value" => "gte"}, - {"type" => "Property", "value" => "age"} - )) - end - end - - context "with object" do - it "returns condition" do - expect(described_class.new(21).gte(Flipper.object(22))).to eq(Flipper::Rules::Condition.new( - {"type" => "Integer", "value" => 21}, - {"type" => "Operator", "value" => "gte"}, - {"type" => "Integer", "value" => 22} - )) - end - end - - context "with string" do - it "raises error" do - expect { described_class.new("age").gte("231") }.to raise_error(ArgumentError) - end - end - - context "with boolean" do - it "raises error" do - expect { described_class.new("age").gte(true) }.to raise_error(ArgumentError) - end - end - - context "with array" do - it "raises error" do - expect { described_class.new("age").gte(["admin"]) }.to raise_error(ArgumentError) - end - end - - context "with nil" do - it "raises error" do - expect { described_class.new("age").gte(nil) }.to raise_error(ArgumentError) - end - end - end - - describe "#lt" do - context "with integer" do - it "returns condition" do - expect(described_class.new("age").lt(21)).to eq(Flipper::Rules::Condition.new( - {"type" => "String", "value" => "age"}, - {"type" => "Operator", "value" => "lt"}, - {"type" => "Integer", "value" => 21} - )) - end - end - - context "with property" do - it "returns condition" do - expect(described_class.new(21).lt(Flipper.property(:age))).to eq(Flipper::Rules::Condition.new( - {"type" => "Integer", "value" => 21}, - {"type" => "Operator", "value" => "lt"}, - {"type" => "Property", "value" => "age"} - )) - end - end - - context "with object" do - it "returns condition" do - expect(described_class.new(21).lt(Flipper.object(22))).to eq(Flipper::Rules::Condition.new( - {"type" => "Integer", "value" => 21}, - {"type" => "Operator", "value" => "lt"}, - {"type" => "Integer", "value" => 22} - )) - end - end - - context "with string" do - it "raises error" do - expect { described_class.new("age").lt("231") }.to raise_error(ArgumentError) - end - end - - context "with boolean" do - it "raises error" do - expect { described_class.new("age").lt(true) }.to raise_error(ArgumentError) - end - end - - context "with array" do - it "raises error" do - expect { described_class.new("age").lt(["admin"]) }.to raise_error(ArgumentError) - end - end - - context "with nil" do - it "raises error" do - expect { described_class.new("age").lt(nil) }.to raise_error(ArgumentError) - end - end - end - - describe "#lte" do - context "with integer" do - it "returns condition" do - expect(described_class.new("age").lte(21)).to eq(Flipper::Rules::Condition.new( - {"type" => "String", "value" => "age"}, - {"type" => "Operator", "value" => "lte"}, - {"type" => "Integer", "value" => 21} - )) - end - end - - context "with property" do - it "returns condition" do - expect(described_class.new(21).lte(Flipper.property(:age))).to eq(Flipper::Rules::Condition.new( - {"type" => "Integer", "value" => 21}, - {"type" => "Operator", "value" => "lte"}, - {"type" => "Property", "value" => "age"} - )) - end - end - - context "with object" do - it "returns condition" do - expect(described_class.new(21).lte(Flipper.object(22))).to eq(Flipper::Rules::Condition.new( - {"type" => "Integer", "value" => 21}, - {"type" => "Operator", "value" => "lte"}, - {"type" => "Integer", "value" => 22} - )) - end - end - - context "with string" do - it "raises error" do - expect { described_class.new("age").lte("231") }.to raise_error(ArgumentError) - end - end - - context "with boolean" do - it "raises error" do - expect { described_class.new("age").lte(true) }.to raise_error(ArgumentError) - end - end - - context "with array" do - it "raises error" do - expect { described_class.new("age").lte(["admin"]) }.to raise_error(ArgumentError) - end - end - - context "with nil" do - it "raises error" do - expect { described_class.new("age").lte(nil) }.to raise_error(ArgumentError) - end - end - end - - describe "#percentage" do - context "with integer" do - it "returns condition" do - expect(described_class.new("flipper_id").percentage(25)).to eq(Flipper::Rules::Condition.new( - {"type" => "String", "value" => "flipper_id"}, - {"type" => "Operator", "value" => "percentage"}, - {"type" => "Integer", "value" => 25} - )) - end - end - - context "with property" do - it "returns condition" do - expect(described_class.new("flipper_id").percentage(Flipper.property(:percentage))).to eq(Flipper::Rules::Condition.new( - {"type" => "String", "value" => "flipper_id"}, - {"type" => "Operator", "value" => "percentage"}, - {"type" => "Property", "value" => "percentage"} - )) - end - end - - context "with object" do - it "returns condition" do - expect(described_class.new("flipper_id").percentage(Flipper.object(21))).to eq(Flipper::Rules::Condition.new( - {"type" => "String", "value" => "flipper_id"}, - {"type" => "Operator", "value" => "percentage"}, - {"type" => "Integer", "value" => 21} - )) - end - end - - context "with string" do - it "raises error" do - expect { described_class.new("flipper_id").percentage("231") }.to raise_error(ArgumentError) - end - end - - context "with boolean" do - it "raises error" do - expect { described_class.new("flipper_id").percentage(true) }.to raise_error(ArgumentError) - end - end - - context "with array" do - it "raises error" do - expect { described_class.new("flipper_id").percentage(["admin"]) }.to raise_error(ArgumentError) - end - end - - context "with nil" do - it "raises error" do - expect { described_class.new("flipper_id").percentage(nil) }.to raise_error(ArgumentError) - end - end - end -end diff --git a/spec/flipper/rules/operator_spec.rb b/spec/flipper/rules/operator_spec.rb deleted file mode 100644 index cf8df9ee9..000000000 --- a/spec/flipper/rules/operator_spec.rb +++ /dev/null @@ -1,72 +0,0 @@ -require 'helper' - -RSpec.describe Flipper::Rules::Operator do - describe ".build" do - context "with Hash" do - it "returns instance" do - instance = described_class.build({"type" => "Operator", "value" => "eq"}) - expect(instance).to be_a(Flipper::Rules::Operators::Base) - expect(instance.name).to eq("eq") - end - end - - context "with String" do - it "returns instance" do - instance = described_class.build("eq") - expect(instance).to be_a(Flipper::Rules::Operators::Base) - expect(instance.name).to eq("eq") - end - end - - context "with Symbol" do - it "returns instance" do - instance = described_class.build(:eq) - expect(instance).to be_a(Flipper::Rules::Operators::Base) - expect(instance.name).to eq("eq") - end - end - - context "with instance" do - it "returns intance" do - instance = described_class.build(described_class.build(:eq)) - expect(instance).to be_instance_of(Flipper::Rules::Operators::Eq) - expect(instance.name).to eq("eq") - end - end - end - - describe "#initialize" do - it "works with string name" do - instance = described_class.build("eq") - expect(instance.name).to eq("eq") - end - - it "works with symbol name" do - instance = described_class.build(:eq) - expect(instance.name).to eq("eq") - end - end - - describe "#to_h" do - it "returns Hash with type and value" do - expect(described_class.build("eq").to_h).to eq({ - "type" => "Operator", - "value" => "eq", - }) - end - end - - describe "equality" do - it "returns true if equal" do - expect(described_class.build("eq").eql?(described_class.build("eq"))).to be(true) - end - - it "returns false if name does not match" do - expect(described_class.build("eq").eql?(described_class.build("neq"))).to be(false) - end - - it "returns false for different class" do - expect(described_class.build("eq").eql?(Object.new)).to be(false) - end - end -end diff --git a/spec/flipper/rules/property_spec.rb b/spec/flipper/rules/property_spec.rb deleted file mode 100644 index 8820ece4e..000000000 --- a/spec/flipper/rules/property_spec.rb +++ /dev/null @@ -1,302 +0,0 @@ -require 'helper' - -RSpec.describe Flipper::Rules::Property do - describe "#initialize" do - it "works with string name" do - property = described_class.new("plan") - expect(property.name).to eq("plan") - end - - it "works with symbol name" do - property = described_class.new(:plan) - expect(property.name).to eq("plan") - end - end - - describe "#to_h" do - it "returns Hash with type and value" do - expect(described_class.new("plan").to_h).to eq({ - "type" => "Property", - "value" => "plan", - }) - end - end - - describe "equality" do - it "returns true if equal" do - expect(described_class.new("name").eql?(described_class.new("name"))).to be(true) - end - - it "returns false if name does not match" do - expect(described_class.new("name").eql?(described_class.new("age"))).to be(false) - end - - it "returns false for different class" do - expect(described_class.new("name").eql?(Object.new)).to be(false) - end - end - - describe "#eq" do - context "with string" do - it "returns equal condition" do - expect(described_class.new(:plan).eq("basic")).to eq(Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "plan"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "basic"} - )) - end - end - - context "with boolean" do - it "returns equal condition" do - expect(described_class.new(:admin).eq(true)).to eq(Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "admin"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "Boolean", "value" => true} - )) - end - end - - context "with integer" do - it "returns equal condition" do - expect(described_class.new(:age).eq(21)).to eq(Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "age"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "Integer", "value" => 21} - )) - end - end - - context "with nil" do - it "returns equal condition" do - expect(described_class.new(:admin).eq(nil)).to eq(Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "admin"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "Null", "value" => nil} - )) - end - end - end - - describe "#neq" do - context "with string" do - it "returns not equal condition" do - expect(described_class.new(:plan).neq("basic")).to eq(Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "plan"}, - {"type" => "Operator", "value" => "neq"}, - {"type" => "String", "value" => "basic"} - )) - end - end - - context "with boolean" do - it "returns not equal condition" do - expect(described_class.new(:admin).neq(true)).to eq(Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "admin"}, - {"type" => "Operator", "value" => "neq"}, - {"type" => "Boolean", "value" => true} - )) - end - end - - context "with integer" do - it "returns not equal condition" do - expect(described_class.new(:age).neq(21)).to eq(Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "age"}, - {"type" => "Operator", "value" => "neq"}, - {"type" => "Integer", "value" => 21} - )) - end - end - - context "with nil" do - it "returns not equal condition" do - expect(described_class.new(:admin).neq(nil)).to eq(Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "admin"}, - {"type" => "Operator", "value" => "neq"}, - {"type" => "Null", "value" => nil} - )) - end - end - end - - describe "#gt" do - context "with integer" do - it "returns condition" do - expect(described_class.new(:age).gt(21)).to eq(Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "age"}, - {"type" => "Operator", "value" => "gt"}, - {"type" => "Integer", "value" => 21} - )) - end - end - - context "with string" do - it "raises error" do - expect { described_class.new(:age).gt("231") }.to raise_error(ArgumentError) - end - end - - context "with boolean" do - it "raises error" do - expect { described_class.new(:age).gt(true) }.to raise_error(ArgumentError) - end - end - - context "with array" do - it "raises error" do - expect { described_class.new(:age).gt(["admin"]) }.to raise_error(ArgumentError) - end - end - - context "with nil" do - it "raises error" do - expect { described_class.new(:age).gt(nil) }.to raise_error(ArgumentError) - end - end - end - - describe "#gte" do - context "with integer" do - it "returns condition" do - expect(described_class.new(:age).gte(21)).to eq(Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "age"}, - {"type" => "Operator", "value" => "gte"}, - {"type" => "Integer", "value" => 21} - )) - end - end - - context "with string" do - it "raises error" do - expect { described_class.new(:age).gte("231") }.to raise_error(ArgumentError) - end - end - - context "with boolean" do - it "raises error" do - expect { described_class.new(:age).gte(true) }.to raise_error(ArgumentError) - end - end - - context "with array" do - it "raises error" do - expect { described_class.new(:age).gte(["admin"]) }.to raise_error(ArgumentError) - end - end - - context "with nil" do - it "raises error" do - expect { described_class.new(:age).gte(nil) }.to raise_error(ArgumentError) - end - end - end - - describe "#lt" do - context "with integer" do - it "returns condition" do - expect(described_class.new(:age).lt(21)).to eq(Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "age"}, - {"type" => "Operator", "value" => "lt"}, - {"type" => "Integer", "value" => 21} - )) - end - end - - context "with string" do - it "raises error" do - expect { described_class.new(:age).lt("231") }.to raise_error(ArgumentError) - end - end - - context "with boolean" do - it "raises error" do - expect { described_class.new(:age).lt(true) }.to raise_error(ArgumentError) - end - end - - context "with array" do - it "raises error" do - expect { described_class.new(:age).lt(["admin"]) }.to raise_error(ArgumentError) - end - end - - context "with nil" do - it "raises error" do - expect { described_class.new(:age).lt(nil) }.to raise_error(ArgumentError) - end - end - end - - describe "#lte" do - context "with integer" do - it "returns condition" do - expect(described_class.new(:age).lte(21)).to eq(Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "age"}, - {"type" => "Operator", "value" => "lte"}, - {"type" => "Integer", "value" => 21} - )) - end - end - - context "with string" do - it "raises error" do - expect { described_class.new(:age).lte("231") }.to raise_error(ArgumentError) - end - end - - context "with boolean" do - it "raises error" do - expect { described_class.new(:age).lte(true) }.to raise_error(ArgumentError) - end - end - - context "with array" do - it "raises error" do - expect { described_class.new(:age).lte(["admin"]) }.to raise_error(ArgumentError) - end - end - - context "with nil" do - it "raises error" do - expect { described_class.new(:age).lte(nil) }.to raise_error(ArgumentError) - end - end - end - - describe "#percentage" do - context "with integer" do - it "returns condition" do - expect(described_class.new(:flipper_id).percentage(25)).to eq(Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "flipper_id"}, - {"type" => "Operator", "value" => "percentage"}, - {"type" => "Integer", "value" => 25} - )) - end - end - - context "with string" do - it "raises error" do - expect { described_class.new(:flipper_id).percentage("231") }.to raise_error(ArgumentError) - end - end - - context "with boolean" do - it "raises error" do - expect { described_class.new(:flipper_id).percentage(true) }.to raise_error(ArgumentError) - end - end - - context "with array" do - it "raises error" do - expect { described_class.new(:flipper_id).percentage(["admin"]) }.to raise_error(ArgumentError) - end - end - - context "with nil" do - it "raises error" do - expect { described_class.new(:flipper_id).percentage(nil) }.to raise_error(ArgumentError) - end - end - end -end diff --git a/spec/flipper/rules_spec.rb b/spec/flipper/rules_spec.rb deleted file mode 100644 index d3e57ade0..000000000 --- a/spec/flipper/rules_spec.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'helper' - -RSpec.describe Flipper::Rules do -end diff --git a/spec/flipper_integration_spec.rb b/spec/flipper_integration_spec.rb index 448406a1c..87906ee5d 100644 --- a/spec/flipper_integration_spec.rb +++ b/spec/flipper_integration_spec.rb @@ -552,11 +552,10 @@ context "for rule" do it "works" do - rule = Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "plan"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "basic"} - ) + rule = Flipper::Expressions::Equal.new([ + Flipper::Expressions::Property.new("plan"), + Flipper::Expressions::String.new("basic"), + ]) feature.enable rule expect(feature.enabled?(basic_plan_thing)).to be(true) @@ -566,18 +565,16 @@ context "for Any" do it "works" do - rule = Flipper::Rules::Any.new( - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "plan"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "basic"} - ), - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "plan"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "plus"} - ) - ) + rule = Flipper::Expressions::Any.new([ + Flipper::Expressions::Equal.new([ + Flipper::Expressions::Property.new("plan"), + Flipper::Expressions::String.new("basic"), + ]), + Flipper::Expressions::Equal.new([ + Flipper::Expressions::Property.new("plan"), + Flipper::Expressions::String.new("plus"), + ]) + ]) feature.enable rule expect(feature.enabled?(basic_plan_thing)).to be(true) @@ -595,18 +592,16 @@ "plan" => "basic", "age" => 20, }) - rule = Flipper::Rules::All.new( - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "plan"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "basic"} - ), - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "age"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "Integer", "value" => 21} - ) - ) + rule = Flipper::Expressions::All.new([ + Flipper::Expressions::Equal.new([ + Flipper::Expressions::Property.new("plan"), + Flipper::Expressions::String.new("basic"), + ]), + Flipper::Expressions::Equal.new([ + Flipper::Expressions::Property.new("age"), + Flipper::Expressions::Number.new(21), + ]) + ]) feature.enable rule expect(feature.enabled?(true_actor)).to be(true) @@ -625,25 +620,23 @@ "plan" => "basic", "age" => 20, }) - rule = Flipper::Rules::Any.new( - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "admin"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => true} - ), - Flipper::Rules::All.new( - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "plan"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "basic"} - ), - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "age"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "Integer", "value" => 21} - ) - ) - ) + rule = Flipper::Expressions::Any.new([ + Flipper::Expressions::Equal.new([ + Flipper::Expressions::Property.new("admin"), + Flipper::Expressions::Boolean.new(true), + ]), + Flipper::Expressions::All.new([ + Flipper::Expressions::Equal.new([ + Flipper::Expressions::Property.new("plan"), + Flipper::Expressions::String.new("basic"), + ]), + Flipper::Expressions::Equal.new([ + Flipper::Expressions::Property.new("age"), + Flipper::Expressions::Number.new(21), + ]) + ]) + ]) + feature.enable rule expect(feature.enabled?(admin_actor)).to be(true) diff --git a/spec/flipper_spec.rb b/spec/flipper_spec.rb index d4d90ec56..494013504 100644 --- a/spec/flipper_spec.rb +++ b/spec/flipper_spec.rb @@ -71,11 +71,10 @@ }) } let(:rule) { - Flipper::Rules::Condition.new( - {"type" => "Property", "value" => "plan"}, - {"type" => "Operator", "value" => "eq"}, - {"type" => "String", "value" => "basic"} - ) + Flipper::Expressions::Equal.new([ + Flipper::Expressions::Property.new("plan"), + Flipper::Expressions::String.new("basic"), + ]) } before do @@ -395,20 +394,14 @@ end describe ".property" do - it "returns Flipper::Rules::Property instance" do - expect(Flipper.property("name")).to eq(Flipper::Rules::Property.new("name")) + it "returns Flipper::Expressions::Property instance" do + expect(Flipper.property("name")).to eq(Flipper::Expressions::Property.new("name")) end end describe ".random" do - it "returns Flipper::Rules::Random instance" do - expect(Flipper.random(100)).to eq(Flipper::Rules::Random.new(100)) - end - end - - describe ".object" do - it "returns Flipper::Rules::Object instance" do - expect(Flipper.object("test")).to eq(Flipper::Rules::Object.new("test")) + it "returns Flipper::Expressions::Random instance" do + expect(Flipper.random(100)).to eq(Flipper::Expressions::Random.new(100)) end end @@ -416,8 +409,10 @@ let(:age_rule) { Flipper.property(:age).gte(21) } let(:plan_rule) { Flipper.property(:plan).eq("basic") } - it "returns Flipper::Rules::Any instance" do - expect(Flipper.any(age_rule, plan_rule)).to eq(Flipper::Rules::Any.new(age_rule, plan_rule)) + it "returns Flipper::Expressions::Any instance" do + expect(Flipper.any(age_rule, plan_rule)).to eq( + Flipper::Expressions::Any.new([age_rule, plan_rule]) + ) end end @@ -425,8 +420,10 @@ let(:age_rule) { Flipper.property(:age).gte(21) } let(:plan_rule) { Flipper.property(:plan).eq("basic") } - it "returns Flipper::Rules::All instance" do - expect(Flipper.all(age_rule, plan_rule)).to eq(Flipper::Rules::All.new(age_rule, plan_rule)) + it "returns Flipper::Expressions::All instance" do + expect(Flipper.all(age_rule, plan_rule)).to eq( + Flipper::Expressions::All.new([age_rule, plan_rule]) + ) end end end diff --git a/spec/helper.rb b/spec/helper.rb index 29510b8bd..873fce9d6 100644 --- a/spec/helper.rb +++ b/spec/helper.rb @@ -24,6 +24,7 @@ Flipper.unregister_groups Flipper.configuration = nil end + # config.fail_fast = true config.disable_monkey_patching! From c64f020e3bcff724a91c1676784d7e4bdf8809ad Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 17 Nov 2021 20:59:55 -0500 Subject: [PATCH 093/176] Ensure value is correct --- lib/flipper/expression.rb | 2 +- spec/flipper/rules/expressions/equal_spec.rb | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/flipper/expression.rb b/lib/flipper/expression.rb index 93c9d53e8..29a86849e 100644 --- a/lib/flipper/expression.rb +++ b/lib/flipper/expression.rb @@ -30,7 +30,7 @@ def eql?(other) def value { - self.class.name => args.map { |arg| + self.class.name.split("::").last => args.map { |arg| arg.is_a?(Expression) ? arg.value : arg } } diff --git a/spec/flipper/rules/expressions/equal_spec.rb b/spec/flipper/rules/expressions/equal_spec.rb index a8df8e268..33ff1d2b5 100644 --- a/spec/flipper/rules/expressions/equal_spec.rb +++ b/spec/flipper/rules/expressions/equal_spec.rb @@ -35,4 +35,20 @@ expect(expression.evaluate).to be(false) end end + + describe "#value" do + it "returns Hash" do + expression = Flipper::Expressions::Equal.new([ + Flipper::Expressions::Property.new(["plan"]), + Flipper::Expressions::String.new(["basic"]), + ]) + + expect(expression.value).to eq({ + "Equal" => [ + {"Property" => ["plan"]}, + {"String" => ["basic"]}, + ], + }) + end + end end From 41e86f29a0a1a3bd46444fcbbf6c12853044bacd Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 17 Nov 2021 21:00:20 -0500 Subject: [PATCH 094/176] Add automatic typing of some primitives --- lib/flipper/expression.rb | 66 +++++++++++++++++++------ spec/flipper/adapters/read_only_spec.rb | 10 ++-- 2 files changed, 55 insertions(+), 21 deletions(-) diff --git a/lib/flipper/expression.rb b/lib/flipper/expression.rb index 29a86849e..8c27eabf7 100644 --- a/lib/flipper/expression.rb +++ b/lib/flipper/expression.rb @@ -1,5 +1,16 @@ module Flipper class Expression + SUPPORTED_TYPES_MAP = { + String => "String", + Numeric => "Number", + NilClass => "Null", + TrueClass => "Boolean", + FalseClass => "Boolean", + }.freeze + + SUPPORTED_TYPE_CLASSES = SUPPORTED_TYPES_MAP.keys.freeze + SUPPORTED_TYPE_NAMES = SUPPORTED_TYPES_MAP.values.freeze + def self.build(object) return object if object.is_a?(Flipper::Expression) @@ -52,31 +63,56 @@ def all Expressions::All.new([self]) end - def eq(*args) - Expressions::Equal.new([self].concat(args)) + def equal(object) + Expressions::Equal.new([self, Expression.build(typed(object))]) + end + alias eq equal + + def not_equal(object) + Expressions::NotEqual.new([self, build(object)]) + end + alias neq not_equal + + def greater_than(object) + Expressions::GreaterThan.new([self, build(object)]) end + alias gt greater_than - def neq(*args) - Expressions::NotEqual.new([self].concat(args)) + def greater_than_or_equal(object) + Expressions::GreaterThan.new([self, build(object)]) end + alias gte greater_than_or_equal - ##################################################################### - # TODO: convert naked primitive to Number, String, Boolean, etc. - ##################################################################### - def gt(*args) - Expressions::GreaterThan.new([self].concat(args)) + def less_than(object) + Expressions::GreaterThan.new([self, build(object)]) end + alias lt less_than - def gte(*args) - Expressions::GreaterThan.new([self].concat(args)) + def less_than_or_equal(object) + Expressions::GreaterThan.new([self, build(object)]) end + alias lte less_than_or_equal - def lt(*args) - Expressions::GreaterThan.new([self].concat(args)) + private + + def build(object) + Expression.build(typed(object)) + end + + def typed(object) + {type_of(object) => [object]} end - def lte(*args) - Expressions::GreaterThan.new([self].concat(args)) + def type_of(object) + type_class = SUPPORTED_TYPE_CLASSES.detect { |klass, type| object.is_a?(klass) } + + if type_class.nil? + raise ArgumentError, + "#{object.inspect} is not a supported primitive." + + " Object must be one of: #{SUPPORTED_TYPE_CLASSES.join(", ")}." + end + + SUPPORTED_TYPES_MAP[type_class] end end end diff --git a/spec/flipper/adapters/read_only_spec.rb b/spec/flipper/adapters/read_only_spec.rb index aa0770691..1274e2f06 100644 --- a/spec/flipper/adapters/read_only_spec.rb +++ b/spec/flipper/adapters/read_only_spec.rb @@ -57,12 +57,10 @@ groups: Set['admins'], actors: Set['22'], rule: { - "type" => "Condition", - "value" => { - "left" => {"type" => "Property", "value" => "plan"}, - "operator" => {"type" => "Operator", "value" => "eq"}, - "right" => {"type" => "String", "value" => "basic"}, - } + "Equal" => [ + {"Property" => ["plan"]}, + {"String" => ["basic"]}, + ] }, percentage_of_actors: '25', percentage_of_time: '45', From 4c2e1101ff3a19044f5bc65fad80e3a54d827622 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Thu, 18 Nov 2021 12:09:31 -0500 Subject: [PATCH 095/176] Get specs passing --- lib/flipper.rb | 4 + lib/flipper/api/error_response.rb | 6 +- lib/flipper/api/v1/actions/rule_gate.rb | 25 ++-- lib/flipper/expression.rb | 13 ++- lib/flipper/expressions/object.rb | 15 +++ spec/flipper/api/v1/actions/rule_gate_spec.rb | 109 +++--------------- spec/helper.rb | 1 - spec/support/spec_helpers.rb | 12 +- 8 files changed, 53 insertions(+), 132 deletions(-) create mode 100644 lib/flipper/expressions/object.rb diff --git a/lib/flipper.rb b/lib/flipper.rb index 32af1045f..0fecef2d5 100644 --- a/lib/flipper.rb +++ b/lib/flipper.rb @@ -77,6 +77,10 @@ def random(name) Flipper::Expressions::Random.new(name) end + def object(object) + Flipper::Expressions::Object.new(object) + end + def any(*args) Flipper::Expressions::Any.new(args) end diff --git a/lib/flipper/api/error_response.rb b/lib/flipper/api/error_response.rb index e4fc3e46c..0ce4ef3a8 100644 --- a/lib/flipper/api/error_response.rb +++ b/lib/flipper/api/error_response.rb @@ -24,12 +24,10 @@ def as_json ERRORS = { feature_not_found: Error.new(1, 'Feature not found.', 404), group_not_registered: Error.new(2, 'Group not registered.', 404), - percentage_invalid: - Error.new(3, 'Percentage must be a positive number less than or equal to 100.', 422), + percentage_invalid: Error.new(3, 'Percentage must be a positive number less than or equal to 100.', 422), flipper_id_invalid: Error.new(4, 'Required parameter flipper_id is missing.', 422), name_invalid: Error.new(5, 'Required parameter name is missing.', 422), - rule_type_invalid: Error.new(6, 'Required parameter rule type is missing.', 422), - rule_value_invalid: Error.new(7, 'Required parameter rule value is missing.', 422), + expression_invalid: Error.new(6, 'The provided expression was not valid.', 422), }.freeze end end diff --git a/lib/flipper/api/v1/actions/rule_gate.rb b/lib/flipper/api/v1/actions/rule_gate.rb index 9f79e38a5..7ae0212a7 100644 --- a/lib/flipper/api/v1/actions/rule_gate.rb +++ b/lib/flipper/api/v1/actions/rule_gate.rb @@ -11,12 +11,16 @@ class RuleGate < Api::Action route %r{\A/features/(?.*)/rule/?\Z} def post - ensure_valid_params feature = flipper[feature_name] - feature.enable_rule Flipper::Expression.build(rule_hash) - decorated_feature = Decorators::Feature.new(feature) - json_response(decorated_feature.as_json, 200) + begin + expression = Flipper::Expression.build(rule_hash) + feature.enable_rule expression + decorated_feature = Decorators::Feature.new(feature) + json_response(decorated_feature.as_json, 200) + rescue NameError => exception + json_error_response(:expression_invalid) + end end def delete @@ -29,22 +33,9 @@ def delete private - def ensure_valid_params - json_error_response(:rule_type_invalid) if rule_type.nil? - json_error_response(:rule_value_invalid) if rule_value.nil? - end - def rule_hash @rule_hash ||= request.env["parsed_request_body".freeze] || {}.freeze end - - def rule_type - @rule_type ||= rule_hash["type".freeze] - end - - def rule_value - @rule_value ||= rule_hash["value".freeze] - end end end end diff --git a/lib/flipper/expression.rb b/lib/flipper/expression.rb index 8c27eabf7..63cf0ea0a 100644 --- a/lib/flipper/expression.rb +++ b/lib/flipper/expression.rb @@ -79,20 +79,24 @@ def greater_than(object) alias gt greater_than def greater_than_or_equal(object) - Expressions::GreaterThan.new([self, build(object)]) + Expressions::GreaterThanOrEqualTo.new([self, build(object)]) end alias gte greater_than_or_equal def less_than(object) - Expressions::GreaterThan.new([self, build(object)]) + Expressions::LessThan.new([self, build(object)]) end alias lt less_than def less_than_or_equal(object) - Expressions::GreaterThan.new([self, build(object)]) + Expressions::LessThanOrEqualTo.new([self, build(object)]) end alias lte less_than_or_equal + def percentage(object) + Expressions::Percentage.new([self, build(object)]) + end + private def build(object) @@ -117,8 +121,8 @@ def type_of(object) end end -require "flipper/expressions/any" require "flipper/expressions/all" +require "flipper/expressions/any" require "flipper/expressions/boolean" require "flipper/expressions/equal" require "flipper/expressions/greater_than_or_equal_to" @@ -127,6 +131,7 @@ def type_of(object) require "flipper/expressions/less_than" require "flipper/expressions/not_equal" require "flipper/expressions/number" +require "flipper/expressions/object" require "flipper/expressions/percentage" require "flipper/expressions/property" require "flipper/expressions/random" diff --git a/lib/flipper/expressions/object.rb b/lib/flipper/expressions/object.rb new file mode 100644 index 000000000..34f05a597 --- /dev/null +++ b/lib/flipper/expressions/object.rb @@ -0,0 +1,15 @@ +require "flipper/expression" + +module Flipper + module Expressions + class Object < Expression + def initialize(args) + super Array(args) + end + + def evaluate(feature_name: "", properties: {}) + args[0] + end + end + end +end diff --git a/spec/flipper/api/v1/actions/rule_gate_spec.rb b/spec/flipper/api/v1/actions/rule_gate_spec.rb index 204eac5f4..4c0506450 100644 --- a/spec/flipper/api/v1/actions/rule_gate_spec.rb +++ b/spec/flipper/api/v1/actions/rule_gate_spec.rb @@ -13,7 +13,8 @@ describe 'enable' do before do flipper[:my_feature].disable_rule - post '/features/my_feature/rule', JSON.dump(rule.value), "CONTENT_TYPE" => "application/json" + post '/features/my_feature/rule', JSON.dump(rule.value), + "CONTENT_TYPE" => "application/json" end it 'enables feature for rule' do @@ -31,7 +32,8 @@ describe 'disable' do before do flipper[:my_feature].enable_rule(rule) - delete '/features/my_feature/rule', JSON.dump(rule.value), "CONTENT_TYPE" => "application/json" + delete '/features/my_feature/rule', JSON.dump({}), + "CONTENT_TYPE" => "application/json" end it 'disables rule for feature' do @@ -49,7 +51,8 @@ describe 'enable feature with slash in name' do before do flipper["my/feature"].disable_rule - post '/features/my/feature/rule', JSON.dump(rule.value), "CONTENT_TYPE" => "application/json" + post '/features/my/feature/rule', JSON.dump(rule.value), + "CONTENT_TYPE" => "application/json" end it 'enables feature for rule' do @@ -67,7 +70,8 @@ describe 'enable feature with space in name' do before do flipper["sp ace"].disable_rule - post '/features/sp%20ace/rule', JSON.dump(rule.value), "CONTENT_TYPE" => "application/json" + post '/features/sp%20ace/rule', JSON.dump(rule.value), + "CONTENT_TYPE" => "application/json" end it 'enables feature for rule' do @@ -82,103 +86,16 @@ end end - describe 'enable missing type parameter' do + describe 'enable with invalid data' do before do - data = rule.value - data.delete("type") - post '/features/my_feature/rule', JSON.dump(data), "CONTENT_TYPE" => "application/json" + data = {"blah" => "blah"} + post '/features/my_feature/rule', JSON.dump(data), + "CONTENT_TYPE" => "application/json" end it 'returns correct error response' do expect(last_response.status).to eq(422) - expect(json_response).to eq(api_rule_type_invalid_response) - end - end - - describe 'disable missing type parameter' do - before do - data = rule.value - data.delete("type") - delete '/features/my_feature/rule' - end - - it 'returns correct error response' do - expect(last_response.status).to eq(200) - end - end - - describe 'enable missing value parameter' do - before do - data = rule.value - data.delete("value") - post '/features/my_feature/rule', JSON.dump(data), "CONTENT_TYPE" => "application/json" - end - - it 'returns correct error response' do - expect(last_response.status).to eq(422) - expect(json_response).to eq(api_rule_value_invalid_response) - end - end - - describe 'disable missing value parameter' do - before do - data = rule.value - data.delete("value") - delete '/features/my_feature/rule', JSON.dump(data), "CONTENT_TYPE" => "application/json" - end - - it 'returns correct error response' do - expect(last_response.status).to eq(200) - end - end - - describe 'enable nil type parameter' do - before do - data = rule.value - data["type"] = nil - post '/features/my_feature/rule', JSON.dump(data), "CONTENT_TYPE" => "application/json" - end - - it 'returns correct error response' do - expect(last_response.status).to eq(422) - expect(json_response).to eq(api_rule_type_invalid_response) - end - end - - describe 'disable nil type parameter' do - before do - data = rule.value - data["type"] = nil - delete '/features/my_feature/rule', JSON.dump(data), "CONTENT_TYPE" => "application/json" - end - - it 'returns correct error response' do - expect(last_response.status).to eq(200) - end - end - - describe 'enable nil value parameter' do - before do - data = rule.value - data["value"] = nil - post '/features/my_feature/rule', JSON.dump(data), "CONTENT_TYPE" => "application/json" - end - - it 'returns correct error response' do - expect(last_response.status).to eq(422) - expect(json_response).to eq(api_rule_value_invalid_response) - end - end - - describe 'disable nil value parameter' do - before do - data = rule.value - data["value"] = nil - delete '/features/my_feature/rule', JSON.dump(data), "CONTENT_TYPE" => "application/json" - end - - it 'returns correct error response' do - expect(last_response.status).to eq(200) + expect(json_response).to eq(api_rule_invalid_response) end end diff --git a/spec/helper.rb b/spec/helper.rb index 873fce9d6..29510b8bd 100644 --- a/spec/helper.rb +++ b/spec/helper.rb @@ -24,7 +24,6 @@ Flipper.unregister_groups Flipper.configuration = nil end - # config.fail_fast = true config.disable_monkey_patching! diff --git a/spec/support/spec_helpers.rb b/spec/support/spec_helpers.rb index 050169918..7fa6c43e5 100644 --- a/spec/support/spec_helpers.rb +++ b/spec/support/spec_helpers.rb @@ -58,18 +58,10 @@ def api_flipper_id_is_missing_response } end - def api_rule_type_invalid_response + def api_rule_invalid_response { 'code' => 6, - 'message' => 'Required parameter rule type is missing.', - 'more_info' => api_error_code_reference_url, - } - end - - def api_rule_value_invalid_response - { - 'code' => 7, - 'message' => 'Required parameter rule value is missing.', + 'message' => 'The provided expression was not valid.', 'more_info' => api_error_code_reference_url, } end From 17954ff4d0797761c6fcc434ef7b3858ecb2e97f Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Thu, 18 Nov 2021 12:09:52 -0500 Subject: [PATCH 096/176] Tweak rules --- examples/rules.rb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/examples/rules.rb b/examples/rules.rb index 281c48791..eb367dfdb 100644 --- a/examples/rules.rb +++ b/examples/rules.rb @@ -2,18 +2,20 @@ require 'flipper' def assert(value) - p value - unless value - puts "#{value} expected to be true but was false. Please correct." + if value + p value + else + puts "Expected true but was #{value}. Please correct." exit 1 end end def refute(value) - p value if value - puts "#{value} expected to be false but was true. Please correct." + puts "Expected false but was #{value}. Please correct." exit 1 + else + p value end end @@ -61,7 +63,7 @@ class Org < Struct.new(:id, :flipper_properties) "plan" => "plus", "age" => 18, "org_admin" => true, - "now" => NOW + DAY, + "now" => NOW - DAY, }) age_rule = Flipper.property(:age).gte(21) From e37d27bcf87569992cc8c2e00c0dae54516bcbd0 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Thu, 18 Nov 2021 12:10:02 -0500 Subject: [PATCH 097/176] Remove errant puts --- spec/flipper/feature_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/flipper/feature_spec.rb b/spec/flipper/feature_spec.rb index 579bd5419..e12567a88 100644 --- a/spec/flipper/feature_spec.rb +++ b/spec/flipper/feature_spec.rb @@ -924,7 +924,6 @@ it "removes Any if it matches" do new_rule = Flipper.any(Flipper.property(:plan).eq("premium")) subject.add_rule new_rule - p subject.rule expect(subject.rule.args.size).to be(2) subject.remove_rule new_rule expect(subject.rule).to eq(rule) From bc868c56e64603eb19830bb87ef3547e3e31f8e6 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Thu, 18 Nov 2021 12:19:53 -0500 Subject: [PATCH 098/176] Rename object to value and auto require expressions --- examples/rules.rb | 2 +- lib/flipper.rb | 20 +++++++++---------- lib/flipper/expression.rb | 18 +++-------------- .../expressions/{object.rb => value.rb} | 2 +- 4 files changed, 15 insertions(+), 27 deletions(-) rename lib/flipper/expressions/{object.rb => value.rb} (88%) diff --git a/examples/rules.rb b/examples/rules.rb index eb367dfdb..2c80eb257 100644 --- a/examples/rules.rb +++ b/examples/rules.rb @@ -130,7 +130,7 @@ class Org < Struct.new(:id, :flipper_properties) refute Flipper.enabled?(:something, other_user) puts "\n\nBoolean Rule" -boolean_rule = Flipper.object(true).eq(true) +boolean_rule = Flipper.value(true).eq(true) Flipper.enable_rule :something, boolean_rule assert Flipper.enabled?(:something) assert Flipper.enabled?(:something, user) diff --git a/lib/flipper.rb b/lib/flipper.rb index 0fecef2d5..d2d8b8df4 100644 --- a/lib/flipper.rb +++ b/lib/flipper.rb @@ -69,24 +69,24 @@ def instance=(flipper) :memoize=, :memoizing?, :sync, :sync_secret # For Flipper::Cloud. Will error for OSS Flipper. - def property(name) - Flipper::Expressions::Property.new(name) + def any(*args) + Flipper::Expressions::Any.new(args) end - def random(name) - Flipper::Expressions::Random.new(name) + def all(*args) + Flipper::Expressions::All.new(args) end - def object(object) - Flipper::Expressions::Object.new(object) + def property(name) + Flipper::Expressions::Property.new(name) end - def any(*args) - Flipper::Expressions::Any.new(args) + def random(name) + Flipper::Expressions::Random.new(name) end - def all(*args) - Flipper::Expressions::All.new(args) + def value(value) + Flipper::Expressions::Value.new(value) end # Public: Use this to register a group by name. diff --git a/lib/flipper/expression.rb b/lib/flipper/expression.rb index 63cf0ea0a..537658289 100644 --- a/lib/flipper/expression.rb +++ b/lib/flipper/expression.rb @@ -121,18 +121,6 @@ def type_of(object) end end -require "flipper/expressions/all" -require "flipper/expressions/any" -require "flipper/expressions/boolean" -require "flipper/expressions/equal" -require "flipper/expressions/greater_than_or_equal_to" -require "flipper/expressions/greater_than" -require "flipper/expressions/less_than_or_equal_to" -require "flipper/expressions/less_than" -require "flipper/expressions/not_equal" -require "flipper/expressions/number" -require "flipper/expressions/object" -require "flipper/expressions/percentage" -require "flipper/expressions/property" -require "flipper/expressions/random" -require "flipper/expressions/string" +Dir[File.join(File.dirname(__FILE__), 'expressions', '*.rb')].sort.each do |file| + require "flipper/expressions/#{File.basename(file, '.rb')}" +end diff --git a/lib/flipper/expressions/object.rb b/lib/flipper/expressions/value.rb similarity index 88% rename from lib/flipper/expressions/object.rb rename to lib/flipper/expressions/value.rb index 34f05a597..edb73a5c9 100644 --- a/lib/flipper/expressions/object.rb +++ b/lib/flipper/expressions/value.rb @@ -2,7 +2,7 @@ module Flipper module Expressions - class Object < Expression + class Value < Expression def initialize(args) super Array(args) end From 0dc1c019b40c407245d5a89918f9561ab75ba0df Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Thu, 18 Nov 2021 14:00:49 -0500 Subject: [PATCH 099/176] Use constant for primitives --- lib/flipper/expression.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/flipper/expression.rb b/lib/flipper/expression.rb index 537658289..e396c482e 100644 --- a/lib/flipper/expression.rb +++ b/lib/flipper/expression.rb @@ -21,7 +21,7 @@ def self.build(object) type = object.keys.first args = object.values.first Expressions.const_get(type).new(args) - when String, Symbol, Numeric, TrueClass, FalseClass + when *SUPPORTED_TYPE_CLASSES object else raise ArgumentError, "#{object.inspect} cannot be converted into a rule expression" From 44fb8a8cfc1bdbf63e410cfa0f4ae55ea9dfa2c6 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Thu, 18 Nov 2021 14:01:17 -0500 Subject: [PATCH 100/176] Rename lte and gte to be shorter --- lib/flipper/expression.rb | 4 ++-- ..._than_or_equal_to.rb => greater_than_or_equal.rb} | 2 +- ...ess_than_or_equal_to.rb => less_than_or_equal.rb} | 2 +- spec/flipper/rules/expression_spec.rb | 12 ++++++------ .../expressions/greater_than_or_equal_to_spec.rb | 2 +- .../rules/expressions/less_than_or_equal_to_spec.rb | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) rename lib/flipper/expressions/{greater_than_or_equal_to.rb => greater_than_or_equal.rb} (90%) rename lib/flipper/expressions/{less_than_or_equal_to.rb => less_than_or_equal.rb} (90%) diff --git a/lib/flipper/expression.rb b/lib/flipper/expression.rb index e396c482e..da3242791 100644 --- a/lib/flipper/expression.rb +++ b/lib/flipper/expression.rb @@ -79,7 +79,7 @@ def greater_than(object) alias gt greater_than def greater_than_or_equal(object) - Expressions::GreaterThanOrEqualTo.new([self, build(object)]) + Expressions::GreaterThanOrEqual.new([self, build(object)]) end alias gte greater_than_or_equal @@ -89,7 +89,7 @@ def less_than(object) alias lt less_than def less_than_or_equal(object) - Expressions::LessThanOrEqualTo.new([self, build(object)]) + Expressions::LessThanOrEqual.new([self, build(object)]) end alias lte less_than_or_equal diff --git a/lib/flipper/expressions/greater_than_or_equal_to.rb b/lib/flipper/expressions/greater_than_or_equal.rb similarity index 90% rename from lib/flipper/expressions/greater_than_or_equal_to.rb rename to lib/flipper/expressions/greater_than_or_equal.rb index c0dc8b562..bc5e9190f 100644 --- a/lib/flipper/expressions/greater_than_or_equal_to.rb +++ b/lib/flipper/expressions/greater_than_or_equal.rb @@ -2,7 +2,7 @@ module Flipper module Expressions - class GreaterThanOrEqualTo < Expression + class GreaterThanOrEqual < Expression def evaluate(feature_name: "", properties: {}) return false unless args[0] && args[1] diff --git a/lib/flipper/expressions/less_than_or_equal_to.rb b/lib/flipper/expressions/less_than_or_equal.rb similarity index 90% rename from lib/flipper/expressions/less_than_or_equal_to.rb rename to lib/flipper/expressions/less_than_or_equal.rb index 8948dacd0..d9582bebf 100644 --- a/lib/flipper/expressions/less_than_or_equal_to.rb +++ b/lib/flipper/expressions/less_than_or_equal.rb @@ -2,7 +2,7 @@ module Flipper module Expressions - class LessThanOrEqualTo < Expression + class LessThanOrEqual < Expression def evaluate(feature_name: "", properties: {}) return false unless args[0] && args[1] diff --git a/spec/flipper/rules/expression_spec.rb b/spec/flipper/rules/expression_spec.rb index f20f0b9a6..2549af8e6 100644 --- a/spec/flipper/rules/expression_spec.rb +++ b/spec/flipper/rules/expression_spec.rb @@ -17,15 +17,15 @@ ]) end - it "can build GreaterThanOrEqualTo" do + it "can build GreaterThanOrEqual" do expression = Flipper::Expression.build({ - "GreaterThanOrEqualTo" => [ + "GreaterThanOrEqual" => [ {"Number" => [2]}, {"Number" => [1]}, ] }) - expect(expression).to be_instance_of(Flipper::Expressions::GreaterThanOrEqualTo) + expect(expression).to be_instance_of(Flipper::Expressions::GreaterThanOrEqual) expect(expression.args).to eq([ Flipper::Expressions::Number.new([2]), Flipper::Expressions::Number.new([1]), @@ -47,15 +47,15 @@ ]) end - it "can build LessThanOrEqualTo" do + it "can build LessThanOrEqual" do expression = Flipper::Expression.build({ - "LessThanOrEqualTo" => [ + "LessThanOrEqual" => [ {"Number" => [2]}, {"Number" => [1]}, ] }) - expect(expression).to be_instance_of(Flipper::Expressions::LessThanOrEqualTo) + expect(expression).to be_instance_of(Flipper::Expressions::LessThanOrEqual) expect(expression.args).to eq([ Flipper::Expressions::Number.new([2]), Flipper::Expressions::Number.new([1]), diff --git a/spec/flipper/rules/expressions/greater_than_or_equal_to_spec.rb b/spec/flipper/rules/expressions/greater_than_or_equal_to_spec.rb index f8a55e858..45dbe25c4 100644 --- a/spec/flipper/rules/expressions/greater_than_or_equal_to_spec.rb +++ b/spec/flipper/rules/expressions/greater_than_or_equal_to_spec.rb @@ -1,6 +1,6 @@ require 'helper' -RSpec.describe Flipper::Expressions::GreaterThanOrEqualTo do +RSpec.describe Flipper::Expressions::GreaterThanOrEqual do describe "#evaluate" do it "returns true when equal" do expression = described_class.new([ diff --git a/spec/flipper/rules/expressions/less_than_or_equal_to_spec.rb b/spec/flipper/rules/expressions/less_than_or_equal_to_spec.rb index b5ff859c7..79ad6ec4d 100644 --- a/spec/flipper/rules/expressions/less_than_or_equal_to_spec.rb +++ b/spec/flipper/rules/expressions/less_than_or_equal_to_spec.rb @@ -1,6 +1,6 @@ require 'helper' -RSpec.describe Flipper::Expressions::LessThanOrEqualTo do +RSpec.describe Flipper::Expressions::LessThanOrEqual do describe "#evaluate" do it "returns true when equal" do expression = described_class.new([ From 63ea0eafa511bbf35e63e1c36f58dfb5c2168ae5 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Thu, 18 Nov 2021 14:29:24 -0500 Subject: [PATCH 101/176] Get rid of String, Number, and Boolean in favor of just Value --- Guardfile | 1 + lib/flipper.rb | 4 ++ lib/flipper/expression.rb | 40 ++++------- lib/flipper/expressions/boolean.rb | 15 ---- lib/flipper/expressions/number.rb | 15 ---- lib/flipper/expressions/string.rb | 15 ---- spec/flipper/adapters/read_only_spec.rb | 2 +- spec/flipper/{rules => }/expression_spec.rb | 68 +++++++++---------- .../{rules => }/expressions/equal_spec.rb | 20 +++--- .../greater_than_or_equal_to_spec.rb | 12 ++-- .../expressions/greater_than_spec.rb | 12 ++-- .../expressions/less_than_or_equal_to_spec.rb | 12 ++-- .../{rules => }/expressions/less_than_spec.rb | 12 ++-- .../{rules => }/expressions/not_equal_spec.rb | 8 +-- .../expressions/percentage_spec.rb | 12 ++-- .../{rules => }/expressions/property_spec.rb | 0 .../{rules => }/expressions/random_spec.rb | 0 .../value_spec.rb} | 7 +- spec/flipper/rules/expressions/number_spec.rb | 20 ------ spec/flipper_integration_spec.rb | 56 +++++---------- spec/flipper_spec.rb | 5 +- 21 files changed, 121 insertions(+), 215 deletions(-) delete mode 100644 lib/flipper/expressions/boolean.rb delete mode 100644 lib/flipper/expressions/number.rb delete mode 100644 lib/flipper/expressions/string.rb rename spec/flipper/{rules => }/expression_spec.rb (64%) rename spec/flipper/{rules => }/expressions/equal_spec.rb (66%) rename spec/flipper/{rules => }/expressions/greater_than_or_equal_to_spec.rb (65%) rename spec/flipper/{rules => }/expressions/greater_than_spec.rb (65%) rename spec/flipper/{rules => }/expressions/less_than_or_equal_to_spec.rb (65%) rename spec/flipper/{rules => }/expressions/less_than_spec.rb (65%) rename spec/flipper/{rules => }/expressions/not_equal_spec.rb (65%) rename spec/flipper/{rules => }/expressions/percentage_spec.rb (70%) rename spec/flipper/{rules => }/expressions/property_spec.rb (100%) rename spec/flipper/{rules => }/expressions/random_spec.rb (100%) rename spec/flipper/{rules/expressions/string_spec.rb => expressions/value_spec.rb} (66%) delete mode 100644 spec/flipper/rules/expressions/number_spec.rb diff --git a/Guardfile b/Guardfile index ad6c9b856..b7d688396 100644 --- a/Guardfile +++ b/Guardfile @@ -21,4 +21,5 @@ guard 'rspec', rspec_options do watch('lib/flipper/api/middleware.rb') { 'spec/flipper/api_spec.rb' } watch(/shared_adapter_specs\.rb$/) { 'spec' } watch('spec/helper.rb') { 'spec' } + watch(%r{.*}) { 'spec' } end diff --git a/lib/flipper.rb b/lib/flipper.rb index d2d8b8df4..df772f361 100644 --- a/lib/flipper.rb +++ b/lib/flipper.rb @@ -81,6 +81,10 @@ def property(name) Flipper::Expressions::Property.new(name) end + def value(value) + Flipper::Expressions::Value.new(value) + end + def random(name) Flipper::Expressions::Random.new(name) end diff --git a/lib/flipper/expression.rb b/lib/flipper/expression.rb index da3242791..590e68a89 100644 --- a/lib/flipper/expression.rb +++ b/lib/flipper/expression.rb @@ -1,15 +1,12 @@ module Flipper class Expression - SUPPORTED_TYPES_MAP = { - String => "String", - Numeric => "Number", - NilClass => "Null", - TrueClass => "Boolean", - FalseClass => "Boolean", - }.freeze - - SUPPORTED_TYPE_CLASSES = SUPPORTED_TYPES_MAP.keys.freeze - SUPPORTED_TYPE_NAMES = SUPPORTED_TYPES_MAP.values.freeze + SUPPORTED_TYPE_CLASSES = [ + String, + Numeric, + NilClass, + TrueClass, + FalseClass, + ].freeze def self.build(object) return object if object.is_a?(Flipper::Expression) @@ -64,7 +61,7 @@ def all end def equal(object) - Expressions::Equal.new([self, Expression.build(typed(object))]) + Expressions::Equal.new([self, build(object)]) end alias eq equal @@ -100,23 +97,14 @@ def percentage(object) private def build(object) - Expression.build(typed(object)) - end - - def typed(object) - {type_of(object) => [object]} - end - - def type_of(object) - type_class = SUPPORTED_TYPE_CLASSES.detect { |klass, type| object.is_a?(klass) } + return object if object.is_a?(Flipper::Expression) - if type_class.nil? - raise ArgumentError, - "#{object.inspect} is not a supported primitive." + - " Object must be one of: #{SUPPORTED_TYPE_CLASSES.join(", ")}." + case object + when *SUPPORTED_TYPE_CLASSES + Expression.build({"Value" => [object]}) + else + raise ArgumentError, "#{object} is not a supported type" end - - SUPPORTED_TYPES_MAP[type_class] end end end diff --git a/lib/flipper/expressions/boolean.rb b/lib/flipper/expressions/boolean.rb deleted file mode 100644 index 66c857182..000000000 --- a/lib/flipper/expressions/boolean.rb +++ /dev/null @@ -1,15 +0,0 @@ -require "flipper/expression" - -module Flipper - module Expressions - class Boolean < Expression - def initialize(args) - super Array(args) - end - - def evaluate(feature_name: "", properties: {}) - args[0] - end - end - end -end diff --git a/lib/flipper/expressions/number.rb b/lib/flipper/expressions/number.rb deleted file mode 100644 index ba22abd92..000000000 --- a/lib/flipper/expressions/number.rb +++ /dev/null @@ -1,15 +0,0 @@ -require "flipper/expression" - -module Flipper - module Expressions - class Number < Expression - def initialize(args) - super Array(args) - end - - def evaluate(feature_name: "", properties: {}) - args[0] - end - end - end -end diff --git a/lib/flipper/expressions/string.rb b/lib/flipper/expressions/string.rb deleted file mode 100644 index 42d77a2e5..000000000 --- a/lib/flipper/expressions/string.rb +++ /dev/null @@ -1,15 +0,0 @@ -require "flipper/expression" - -module Flipper - module Expressions - class String < Expression - def initialize(args) - super Array(args) - end - - def evaluate(feature_name: "", properties: {}) - args[0] - end - end - end -end diff --git a/spec/flipper/adapters/read_only_spec.rb b/spec/flipper/adapters/read_only_spec.rb index 1274e2f06..64729e953 100644 --- a/spec/flipper/adapters/read_only_spec.rb +++ b/spec/flipper/adapters/read_only_spec.rb @@ -59,7 +59,7 @@ rule: { "Equal" => [ {"Property" => ["plan"]}, - {"String" => ["basic"]}, + {"Value" => ["basic"]}, ] }, percentage_of_actors: '25', diff --git a/spec/flipper/rules/expression_spec.rb b/spec/flipper/expression_spec.rb similarity index 64% rename from spec/flipper/rules/expression_spec.rb rename to spec/flipper/expression_spec.rb index 2549af8e6..7e1df41d4 100644 --- a/spec/flipper/rules/expression_spec.rb +++ b/spec/flipper/expression_spec.rb @@ -5,123 +5,123 @@ it "can build Equal" do expression = Flipper::Expression.build({ "Equal" => [ - {"String" => ["basic"]}, - {"String" => ["basic"]}, + {"Value" => ["basic"]}, + {"Value" => ["basic"]}, ] }) expect(expression).to be_instance_of(Flipper::Expressions::Equal) expect(expression.args).to eq([ - Flipper::Expressions::String.new(["basic"]), - Flipper::Expressions::String.new(["basic"]), + Flipper.value("basic"), + Flipper.value("basic"), ]) end it "can build GreaterThanOrEqual" do expression = Flipper::Expression.build({ "GreaterThanOrEqual" => [ - {"Number" => [2]}, - {"Number" => [1]}, + {"Value" => [2]}, + {"Value" => [1]}, ] }) expect(expression).to be_instance_of(Flipper::Expressions::GreaterThanOrEqual) expect(expression.args).to eq([ - Flipper::Expressions::Number.new([2]), - Flipper::Expressions::Number.new([1]), + Flipper.value(2), + Flipper.value(1), ]) end it "can build GreaterThan" do expression = Flipper::Expression.build({ "GreaterThan" => [ - {"Number" => [2]}, - {"Number" => [1]}, + {"Value" => [2]}, + {"Value" => [1]}, ] }) expect(expression).to be_instance_of(Flipper::Expressions::GreaterThan) expect(expression.args).to eq([ - Flipper::Expressions::Number.new([2]), - Flipper::Expressions::Number.new([1]), + Flipper.value(2), + Flipper.value(1), ]) end it "can build LessThanOrEqual" do expression = Flipper::Expression.build({ "LessThanOrEqual" => [ - {"Number" => [2]}, - {"Number" => [1]}, + {"Value" => [2]}, + {"Value" => [1]}, ] }) expect(expression).to be_instance_of(Flipper::Expressions::LessThanOrEqual) expect(expression.args).to eq([ - Flipper::Expressions::Number.new([2]), - Flipper::Expressions::Number.new([1]), + Flipper.value(2), + Flipper.value(1), ]) end it "can build LessThan" do expression = Flipper::Expression.build({ "LessThan" => [ - {"Number" => [2]}, - {"Number" => [1]}, + {"Value" => [2]}, + {"Value" => [1]}, ] }) expect(expression).to be_instance_of(Flipper::Expressions::LessThan) expect(expression.args).to eq([ - Flipper::Expressions::Number.new([2]), - Flipper::Expressions::Number.new([1]), + Flipper.value(2), + Flipper.value(1), ]) end it "can build NotEqual" do expression = Flipper::Expression.build({ "NotEqual" => [ - {"String" => ["basic"]}, - {"String" => ["plus"]}, + {"Value" => ["basic"]}, + {"Value" => ["plus"]}, ] }) expect(expression).to be_instance_of(Flipper::Expressions::NotEqual) expect(expression.args).to eq([ - Flipper::Expressions::String.new(["basic"]), - Flipper::Expressions::String.new(["plus"]), + Flipper.value("basic"), + Flipper.value("plus"), ]) end - it "can build Number" do + it "can build Value" do expression = Flipper::Expression.build({ - "Number" => [1] + "Value" => [1] }) - expect(expression).to be_instance_of(Flipper::Expressions::Number) + expect(expression).to be_instance_of(Flipper::Expressions::Value) expect(expression.args).to eq([1]) end it "can build Percentage" do expression = Flipper::Expression.build({ "Percentage" => [ - {"String" => ["User;1"]}, - {"Number" => [40]}, + {"Value" => ["User;1"]}, + {"Value" => [40]}, ] }) expect(expression).to be_instance_of(Flipper::Expressions::Percentage) expect(expression.args).to eq([ - Flipper::Expressions::String.new(["User;1"]), - Flipper::Expressions::Number.new([40]), + Flipper.value("User;1"), + Flipper.value(40), ]) end - it "can build String" do + it "can build Value" do expression = Flipper::Expression.build({ - "String" => ["basic"] + "Value" => ["basic"] }) - expect(expression).to be_instance_of(Flipper::Expressions::String) + expect(expression).to be_instance_of(Flipper::Expressions::Value) expect(expression.args).to eq(["basic"]) end end diff --git a/spec/flipper/rules/expressions/equal_spec.rb b/spec/flipper/expressions/equal_spec.rb similarity index 66% rename from spec/flipper/rules/expressions/equal_spec.rb rename to spec/flipper/expressions/equal_spec.rb index 33ff1d2b5..94b5209bc 100644 --- a/spec/flipper/rules/expressions/equal_spec.rb +++ b/spec/flipper/expressions/equal_spec.rb @@ -4,23 +4,23 @@ it "can be built" do expression = described_class.build({ "Equal" => [ - {"String" => ["basic"]}, - {"String" => ["basic"]}, + {"Value" => ["basic"]}, + {"Value" => ["basic"]}, ] }) expect(expression).to be_instance_of(Flipper::Expressions::Equal) expect(expression.args).to eq([ - Flipper::Expressions::String.new(["basic"]), - Flipper::Expressions::String.new(["basic"]), + Flipper::Expressions::Value.new(["basic"]), + Flipper::Expressions::Value.new(["basic"]), ]) end describe "#evaluate" do it "returns true when equal" do expression = Flipper::Expressions::Equal.new([ - Flipper::Expressions::String.new(["basic"]), - Flipper::Expressions::String.new(["basic"]), + Flipper::Expressions::Value.new(["basic"]), + Flipper::Expressions::Value.new(["basic"]), ]) expect(expression.evaluate).to be(true) @@ -28,8 +28,8 @@ it "returns false when not equal" do expression = Flipper::Expressions::Equal.new([ - Flipper::Expressions::String.new(["basic"]), - Flipper::Expressions::String.new(["plus"]), + Flipper::Expressions::Value.new(["basic"]), + Flipper::Expressions::Value.new(["plus"]), ]) expect(expression.evaluate).to be(false) @@ -40,13 +40,13 @@ it "returns Hash" do expression = Flipper::Expressions::Equal.new([ Flipper::Expressions::Property.new(["plan"]), - Flipper::Expressions::String.new(["basic"]), + Flipper::Expressions::Value.new(["basic"]), ]) expect(expression.value).to eq({ "Equal" => [ {"Property" => ["plan"]}, - {"String" => ["basic"]}, + {"Value" => ["basic"]}, ], }) end diff --git a/spec/flipper/rules/expressions/greater_than_or_equal_to_spec.rb b/spec/flipper/expressions/greater_than_or_equal_to_spec.rb similarity index 65% rename from spec/flipper/rules/expressions/greater_than_or_equal_to_spec.rb rename to spec/flipper/expressions/greater_than_or_equal_to_spec.rb index 45dbe25c4..d9f134acd 100644 --- a/spec/flipper/rules/expressions/greater_than_or_equal_to_spec.rb +++ b/spec/flipper/expressions/greater_than_or_equal_to_spec.rb @@ -4,8 +4,8 @@ describe "#evaluate" do it "returns true when equal" do expression = described_class.new([ - Flipper::Expressions::Number.new([2]), - Flipper::Expressions::Number.new([2]), + Flipper::Expressions::Value.new([2]), + Flipper::Expressions::Value.new([2]), ]) expect(expression.evaluate).to be(true) @@ -13,8 +13,8 @@ it "returns true when greater" do expression = described_class.new([ - Flipper::Expressions::Number.new([2]), - Flipper::Expressions::Number.new([1]), + Flipper::Expressions::Value.new([2]), + Flipper::Expressions::Value.new([1]), ]) expect(expression.evaluate).to be(true) @@ -22,8 +22,8 @@ it "returns false when less" do expression = described_class.new([ - Flipper::Expressions::Number.new([1]), - Flipper::Expressions::Number.new([2]), + Flipper::Expressions::Value.new([1]), + Flipper::Expressions::Value.new([2]), ]) expect(expression.evaluate).to be(false) diff --git a/spec/flipper/rules/expressions/greater_than_spec.rb b/spec/flipper/expressions/greater_than_spec.rb similarity index 65% rename from spec/flipper/rules/expressions/greater_than_spec.rb rename to spec/flipper/expressions/greater_than_spec.rb index f5c648b16..eb1642cbb 100644 --- a/spec/flipper/rules/expressions/greater_than_spec.rb +++ b/spec/flipper/expressions/greater_than_spec.rb @@ -4,8 +4,8 @@ describe "#evaluate" do it "returns false when equal" do expression = described_class.new([ - Flipper::Expressions::Number.new([2]), - Flipper::Expressions::Number.new([2]), + Flipper::Expressions::Value.new([2]), + Flipper::Expressions::Value.new([2]), ]) expect(expression.evaluate).to be(false) @@ -13,8 +13,8 @@ it "returns true when greater" do expression = described_class.new([ - Flipper::Expressions::Number.new([2]), - Flipper::Expressions::Number.new([1]), + Flipper::Expressions::Value.new([2]), + Flipper::Expressions::Value.new([1]), ]) expect(expression.evaluate).to be(true) @@ -22,8 +22,8 @@ it "returns false when less" do expression = described_class.new([ - Flipper::Expressions::Number.new([1]), - Flipper::Expressions::Number.new([2]), + Flipper::Expressions::Value.new([1]), + Flipper::Expressions::Value.new([2]), ]) expect(expression.evaluate).to be(false) diff --git a/spec/flipper/rules/expressions/less_than_or_equal_to_spec.rb b/spec/flipper/expressions/less_than_or_equal_to_spec.rb similarity index 65% rename from spec/flipper/rules/expressions/less_than_or_equal_to_spec.rb rename to spec/flipper/expressions/less_than_or_equal_to_spec.rb index 79ad6ec4d..fae52b537 100644 --- a/spec/flipper/rules/expressions/less_than_or_equal_to_spec.rb +++ b/spec/flipper/expressions/less_than_or_equal_to_spec.rb @@ -4,8 +4,8 @@ describe "#evaluate" do it "returns true when equal" do expression = described_class.new([ - Flipper::Expressions::Number.new([2]), - Flipper::Expressions::Number.new([2]), + Flipper::Expressions::Value.new([2]), + Flipper::Expressions::Value.new([2]), ]) expect(expression.evaluate).to be(true) @@ -13,8 +13,8 @@ it "returns true when less" do expression = described_class.new([ - Flipper::Expressions::Number.new([1]), - Flipper::Expressions::Number.new([2]), + Flipper::Expressions::Value.new([1]), + Flipper::Expressions::Value.new([2]), ]) expect(expression.evaluate).to be(true) @@ -22,8 +22,8 @@ it "returns false when greater" do expression = described_class.new([ - Flipper::Expressions::Number.new([2]), - Flipper::Expressions::Number.new([1]), + Flipper::Expressions::Value.new([2]), + Flipper::Expressions::Value.new([1]), ]) expect(expression.evaluate).to be(false) diff --git a/spec/flipper/rules/expressions/less_than_spec.rb b/spec/flipper/expressions/less_than_spec.rb similarity index 65% rename from spec/flipper/rules/expressions/less_than_spec.rb rename to spec/flipper/expressions/less_than_spec.rb index baff7a08d..712236413 100644 --- a/spec/flipper/rules/expressions/less_than_spec.rb +++ b/spec/flipper/expressions/less_than_spec.rb @@ -4,8 +4,8 @@ describe "#evaluate" do it "returns false when equal" do expression = described_class.new([ - Flipper::Expressions::Number.new(2), - Flipper::Expressions::Number.new(2), + Flipper::Expressions::Value.new(2), + Flipper::Expressions::Value.new(2), ]) expect(expression.evaluate).to be(false) @@ -13,8 +13,8 @@ it "returns true when less" do expression = described_class.new([ - Flipper::Expressions::Number.new([1]), - Flipper::Expressions::Number.new([2]), + Flipper::Expressions::Value.new([1]), + Flipper::Expressions::Value.new([2]), ]) expect(expression.evaluate).to be(true) @@ -22,8 +22,8 @@ it "returns false when greater" do expression = described_class.new([ - Flipper::Expressions::Number.new([2]), - Flipper::Expressions::Number.new([1]), + Flipper::Expressions::Value.new([2]), + Flipper::Expressions::Value.new([1]), ]) expect(expression.evaluate).to be(false) diff --git a/spec/flipper/rules/expressions/not_equal_spec.rb b/spec/flipper/expressions/not_equal_spec.rb similarity index 65% rename from spec/flipper/rules/expressions/not_equal_spec.rb rename to spec/flipper/expressions/not_equal_spec.rb index 220bbc967..b85376062 100644 --- a/spec/flipper/rules/expressions/not_equal_spec.rb +++ b/spec/flipper/expressions/not_equal_spec.rb @@ -4,8 +4,8 @@ describe "#evaluate" do it "returns true when not equal" do expression = described_class.new([ - Flipper::Expressions::String.new("basic"), - Flipper::Expressions::String.new("plus"), + Flipper::Expressions::Value.new("basic"), + Flipper::Expressions::Value.new("plus"), ]) expect(expression.evaluate).to be(true) @@ -13,8 +13,8 @@ it "returns false when equal" do expression = described_class.new([ - Flipper::Expressions::String.new("basic"), - Flipper::Expressions::String.new("basic"), + Flipper::Expressions::Value.new("basic"), + Flipper::Expressions::Value.new("basic"), ]) expect(expression.evaluate).to be(false) diff --git a/spec/flipper/rules/expressions/percentage_spec.rb b/spec/flipper/expressions/percentage_spec.rb similarity index 70% rename from spec/flipper/rules/expressions/percentage_spec.rb rename to spec/flipper/expressions/percentage_spec.rb index 23d03b815..932eacd6d 100644 --- a/spec/flipper/rules/expressions/percentage_spec.rb +++ b/spec/flipper/expressions/percentage_spec.rb @@ -4,8 +4,8 @@ describe "#evaluate" do it "returns true when string in percentage enabled" do expression = described_class.new([ - Flipper::Expressions::String.new("User;1"), - Flipper::Expressions::Number.new(100), + Flipper::Expressions::Value.new("User;1"), + Flipper::Expressions::Value.new(100), ]) expect(expression.evaluate).to be(true) @@ -13,8 +13,8 @@ it "returns false when string in percentage enabled" do expression = described_class.new([ - Flipper::Expressions::String.new("User;1"), - Flipper::Expressions::Number.new(0), + Flipper::Expressions::Value.new("User;1"), + Flipper::Expressions::Value.new(0), ]) expect(expression.evaluate).to be(false) @@ -22,8 +22,8 @@ it "changes value based on feature_name so not all actors get all features first" do expression = described_class.new([ - Flipper::Expressions::String.new("User;1"), - Flipper::Expressions::Number.new(70), + Flipper::Expressions::Value.new("User;1"), + Flipper::Expressions::Value.new(70), ]) expect(expression.evaluate(feature_name: "a")).to be(true) diff --git a/spec/flipper/rules/expressions/property_spec.rb b/spec/flipper/expressions/property_spec.rb similarity index 100% rename from spec/flipper/rules/expressions/property_spec.rb rename to spec/flipper/expressions/property_spec.rb diff --git a/spec/flipper/rules/expressions/random_spec.rb b/spec/flipper/expressions/random_spec.rb similarity index 100% rename from spec/flipper/rules/expressions/random_spec.rb rename to spec/flipper/expressions/random_spec.rb diff --git a/spec/flipper/rules/expressions/string_spec.rb b/spec/flipper/expressions/value_spec.rb similarity index 66% rename from spec/flipper/rules/expressions/string_spec.rb rename to spec/flipper/expressions/value_spec.rb index 20a25ddba..bf9d9d61d 100644 --- a/spec/flipper/rules/expressions/string_spec.rb +++ b/spec/flipper/expressions/value_spec.rb @@ -1,12 +1,17 @@ require 'helper' -RSpec.describe Flipper::Expressions::String do +RSpec.describe Flipper::Expressions::Value do it "can initialize with string" do expect(described_class.new("basic").args).to eq(["basic"]) end + it "can initialize with number" do + expect(described_class.new(1).args).to eq([1]) + end + it "can initialize with array" do expect(described_class.new(["basic"]).args).to eq(["basic"]) + expect(described_class.new(["basic"]).args).to eq(["basic"]) end describe "#evaluate" do diff --git a/spec/flipper/rules/expressions/number_spec.rb b/spec/flipper/rules/expressions/number_spec.rb deleted file mode 100644 index 16970e451..000000000 --- a/spec/flipper/rules/expressions/number_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'helper' - -RSpec.describe Flipper::Expressions::Number do - it "can initialize with number" do - expect(described_class.new(1).args).to eq([1]) - end - - it "can initialize with array" do - expect(described_class.new([1]).args).to eq([1]) - end - - describe "#evaluate" do - [1, 1.1, 1_000].each do |value| - it "returns first arg for #{value}" do - expression = described_class.new([value]) - expect(expression.evaluate).to eq(value) - end - end - end -end diff --git a/spec/flipper_integration_spec.rb b/spec/flipper_integration_spec.rb index 87906ee5d..40e77f2ed 100644 --- a/spec/flipper_integration_spec.rb +++ b/spec/flipper_integration_spec.rb @@ -552,10 +552,7 @@ context "for rule" do it "works" do - rule = Flipper::Expressions::Equal.new([ - Flipper::Expressions::Property.new("plan"), - Flipper::Expressions::String.new("basic"), - ]) + rule = Flipper.property(:plan).eq("basic") feature.enable rule expect(feature.enabled?(basic_plan_thing)).to be(true) @@ -565,16 +562,10 @@ context "for Any" do it "works" do - rule = Flipper::Expressions::Any.new([ - Flipper::Expressions::Equal.new([ - Flipper::Expressions::Property.new("plan"), - Flipper::Expressions::String.new("basic"), - ]), - Flipper::Expressions::Equal.new([ - Flipper::Expressions::Property.new("plan"), - Flipper::Expressions::String.new("plus"), - ]) - ]) + rule = Flipper.any( + Flipper.property(:plan).eq("basic"), + Flipper.property(:plan).eq("plus"), + ) feature.enable rule expect(feature.enabled?(basic_plan_thing)).to be(true) @@ -592,16 +583,10 @@ "plan" => "basic", "age" => 20, }) - rule = Flipper::Expressions::All.new([ - Flipper::Expressions::Equal.new([ - Flipper::Expressions::Property.new("plan"), - Flipper::Expressions::String.new("basic"), - ]), - Flipper::Expressions::Equal.new([ - Flipper::Expressions::Property.new("age"), - Flipper::Expressions::Number.new(21), - ]) - ]) + rule = Flipper.all( + Flipper.property(:plan).eq("basic"), + Flipper.property(:age).eq(21) + ) feature.enable rule expect(feature.enabled?(true_actor)).to be(true) @@ -620,22 +605,13 @@ "plan" => "basic", "age" => 20, }) - rule = Flipper::Expressions::Any.new([ - Flipper::Expressions::Equal.new([ - Flipper::Expressions::Property.new("admin"), - Flipper::Expressions::Boolean.new(true), - ]), - Flipper::Expressions::All.new([ - Flipper::Expressions::Equal.new([ - Flipper::Expressions::Property.new("plan"), - Flipper::Expressions::String.new("basic"), - ]), - Flipper::Expressions::Equal.new([ - Flipper::Expressions::Property.new("age"), - Flipper::Expressions::Number.new(21), - ]) - ]) - ]) + rule = Flipper.any( + Flipper.property(:admin).eq(true), + Flipper.all( + Flipper.property(:plan).eq("basic"), + Flipper.property(:age).eq(21) + ) + ) feature.enable rule diff --git a/spec/flipper_spec.rb b/spec/flipper_spec.rb index 494013504..8062aae53 100644 --- a/spec/flipper_spec.rb +++ b/spec/flipper_spec.rb @@ -71,10 +71,7 @@ }) } let(:rule) { - Flipper::Expressions::Equal.new([ - Flipper::Expressions::Property.new("plan"), - Flipper::Expressions::String.new("basic"), - ]) + Flipper.property(:plan).eq("basic") } before do From 3e528e80c0a2bcdb2b5aa0a5c14a5d32ff859320 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sat, 20 Nov 2021 14:27:56 -0500 Subject: [PATCH 102/176] Remove line to run all specs for every change --- Guardfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Guardfile b/Guardfile index b7d688396..c5db74ea9 100644 --- a/Guardfile +++ b/Guardfile @@ -21,5 +21,7 @@ guard 'rspec', rspec_options do watch('lib/flipper/api/middleware.rb') { 'spec/flipper/api_spec.rb' } watch(/shared_adapter_specs\.rb$/) { 'spec' } watch('spec/helper.rb') { 'spec' } - watch(%r{.*}) { 'spec' } + + # To run all specs on every change... (useful with focus and fit) + # watch(%r{.*}) { 'spec' } end From b86bfb52070fba4929571f126e2dfcfa3407192e Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sat, 20 Nov 2021 14:28:04 -0500 Subject: [PATCH 103/176] Add expression equality specs --- spec/flipper/expression_spec.rb | 40 +++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/spec/flipper/expression_spec.rb b/spec/flipper/expression_spec.rb index 7e1df41d4..7faca70fe 100644 --- a/spec/flipper/expression_spec.rb +++ b/spec/flipper/expression_spec.rb @@ -125,4 +125,44 @@ expect(expression.args).to eq(["basic"]) end end + + describe "#eql?" do + it "returns true for same class and args" do + expression = Flipper::Expressions::Value.new("foo") + other = Flipper::Expressions::Value.new("foo") + expect(expression.eql?(other)).to be(true) + end + + it "returns false for different class" do + expression = Flipper::Expressions::Value.new("foo") + other = Object.new + expect(expression.eql?(other)).to be(false) + end + + it "returns false for different args" do + expression = Flipper::Expressions::Value.new("foo") + other = Flipper::Expressions::Value.new("bar") + expect(expression.eql?(other)).to be(false) + end + end + + describe "#==" do + it "returns true for same class and args" do + expression = Flipper::Expressions::Value.new("foo") + other = Flipper::Expressions::Value.new("foo") + expect(expression == other).to be(true) + end + + it "returns false for different class" do + expression = Flipper::Expressions::Value.new("foo") + other = Object.new + expect(expression == other).to be(false) + end + + it "returns false for different args" do + expression = Flipper::Expressions::Value.new("foo") + other = Flipper::Expressions::Value.new("bar") + expect(expression == other).to be(false) + end + end end From 06177e7d8f2d35da2d8eb95b3814380e9b1abaee Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sat, 20 Nov 2021 14:28:52 -0500 Subject: [PATCH 104/176] Change expression specs to use expression instead of subclass --- spec/flipper/expression_spec.rb | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/flipper/expression_spec.rb b/spec/flipper/expression_spec.rb index 7faca70fe..04c0efd42 100644 --- a/spec/flipper/expression_spec.rb +++ b/spec/flipper/expression_spec.rb @@ -128,40 +128,40 @@ describe "#eql?" do it "returns true for same class and args" do - expression = Flipper::Expressions::Value.new("foo") - other = Flipper::Expressions::Value.new("foo") + expression = Flipper::Expression.new("foo") + other = Flipper::Expression.new("foo") expect(expression.eql?(other)).to be(true) end it "returns false for different class" do - expression = Flipper::Expressions::Value.new("foo") + expression = Flipper::Expression.new("foo") other = Object.new expect(expression.eql?(other)).to be(false) end it "returns false for different args" do - expression = Flipper::Expressions::Value.new("foo") - other = Flipper::Expressions::Value.new("bar") + expression = Flipper::Expression.new("foo") + other = Flipper::Expression.new("bar") expect(expression.eql?(other)).to be(false) end end describe "#==" do it "returns true for same class and args" do - expression = Flipper::Expressions::Value.new("foo") - other = Flipper::Expressions::Value.new("foo") + expression = Flipper::Expression.new("foo") + other = Flipper::Expression.new("foo") expect(expression == other).to be(true) end it "returns false for different class" do - expression = Flipper::Expressions::Value.new("foo") + expression = Flipper::Expression.new("foo") other = Object.new expect(expression == other).to be(false) end it "returns false for different args" do - expression = Flipper::Expressions::Value.new("foo") - other = Flipper::Expressions::Value.new("bar") + expression = Flipper::Expression.new("foo") + other = Flipper::Expression.new("bar") expect(expression == other).to be(false) end end From 6e3754fb322730688ee78066b87af5f9e1d807cd Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sat, 20 Nov 2021 17:05:56 -0500 Subject: [PATCH 105/176] Make expressions always take an array for args --- lib/flipper/expression.rb | 3 +++ spec/flipper/expression_spec.rb | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/flipper/expression.rb b/lib/flipper/expression.rb index 590e68a89..9260555aa 100644 --- a/lib/flipper/expression.rb +++ b/lib/flipper/expression.rb @@ -28,6 +28,9 @@ def self.build(object) attr_reader :args def initialize(args) + unless args.is_a?(Array) + raise ArgumentError, "args must always be an Array but was #{args.inspect}" + end @args = self.class.build(args) end diff --git a/spec/flipper/expression_spec.rb b/spec/flipper/expression_spec.rb index 04c0efd42..ed2f14493 100644 --- a/spec/flipper/expression_spec.rb +++ b/spec/flipper/expression_spec.rb @@ -128,40 +128,40 @@ describe "#eql?" do it "returns true for same class and args" do - expression = Flipper::Expression.new("foo") - other = Flipper::Expression.new("foo") + expression = Flipper::Expression.new(["foo"]) + other = Flipper::Expression.new(["foo"]) expect(expression.eql?(other)).to be(true) end it "returns false for different class" do - expression = Flipper::Expression.new("foo") + expression = Flipper::Expression.new(["foo"]) other = Object.new expect(expression.eql?(other)).to be(false) end it "returns false for different args" do - expression = Flipper::Expression.new("foo") - other = Flipper::Expression.new("bar") + expression = Flipper::Expression.new(["foo"]) + other = Flipper::Expression.new(["bar"]) expect(expression.eql?(other)).to be(false) end end describe "#==" do it "returns true for same class and args" do - expression = Flipper::Expression.new("foo") - other = Flipper::Expression.new("foo") + expression = Flipper::Expression.new(["foo"]) + other = Flipper::Expression.new(["foo"]) expect(expression == other).to be(true) end it "returns false for different class" do - expression = Flipper::Expression.new("foo") + expression = Flipper::Expression.new(["foo"]) other = Object.new expect(expression == other).to be(false) end it "returns false for different args" do - expression = Flipper::Expression.new("foo") - other = Flipper::Expression.new("bar") + expression = Flipper::Expression.new(["foo"]) + other = Flipper::Expression.new(["bar"]) expect(expression == other).to be(false) end end From 4df6f08460ff222406225c41c9064e682f0f57fe Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sat, 20 Nov 2021 17:19:44 -0500 Subject: [PATCH 106/176] Ensure expression always has array args --- lib/flipper/expression.rb | 2 +- spec/flipper/expression_spec.rb | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/flipper/expression.rb b/lib/flipper/expression.rb index 9260555aa..926437896 100644 --- a/lib/flipper/expression.rb +++ b/lib/flipper/expression.rb @@ -29,7 +29,7 @@ def self.build(object) def initialize(args) unless args.is_a?(Array) - raise ArgumentError, "args must always be an Array but was #{args.inspect}" + raise ArgumentError, "args must be an Array but was #{args.inspect}" end @args = self.class.build(args) end diff --git a/spec/flipper/expression_spec.rb b/spec/flipper/expression_spec.rb index ed2f14493..83dfc5d6d 100644 --- a/spec/flipper/expression_spec.rb +++ b/spec/flipper/expression_spec.rb @@ -126,6 +126,24 @@ end end + describe "#initialize" do + it "works with Array" do + expect(described_class.new([1]).args).to eq([1]) + end + + it "raises ArgumentError if not Array" do + [ + "asdf", + 1, + {"foo" => "bar"}, + ].each do |value| + expect { + described_class.new(value) + }.to raise_error(ArgumentError, /args must be an Array but was #{value.inspect}/) + end + end + end + describe "#eql?" do it "returns true for same class and args" do expression = Flipper::Expression.new(["foo"]) From 67d6ab01a13afb86750594b44dffc96695f2ca3b Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sat, 20 Nov 2021 17:52:53 -0500 Subject: [PATCH 107/176] Add conversion specs of expressions to other expressions --- spec/flipper/expression_spec.rb | 75 +++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/spec/flipper/expression_spec.rb b/spec/flipper/expression_spec.rb index 83dfc5d6d..cb625f3a5 100644 --- a/spec/flipper/expression_spec.rb +++ b/spec/flipper/expression_spec.rb @@ -183,4 +183,79 @@ expect(expression == other).to be(false) end end + + describe "#add" do + it "converts to Any and adds new expressions" do + expression = described_class.new(["something"]) + first = Flipper.value(true).eq(true) + second = Flipper.value(false).eq(false) + new_expression = expression.add(first, second) + expect(new_expression).to be_instance_of(Flipper::Expressions::Any) + expect(new_expression.args).to eq([ + expression, + first, + second, + ]) + end + end + + describe "#remove" do + it "converts to Any and removes any expressions that match" do + expression = described_class.new(["something"]) + first = Flipper.value(true).eq(true) + second = Flipper.value(false).eq(false) + new_expression = expression.remove(described_class.new(["something"]), first, second) + expect(new_expression).to be_instance_of(Flipper::Expressions::Any) + expect(new_expression.args).to eq([]) + end + end + + it "can convert to Any" do + expression = described_class.new(["something"]) + converted = expression.any + expect(converted).to be_instance_of(Flipper::Expressions::Any) + expect(converted.args).to eq([expression]) + end + + it "can convert to All" do + expression = described_class.new(["something"]) + converted = expression.all + expect(converted).to be_instance_of(Flipper::Expressions::All) + expect(converted.args).to eq([expression]) + end + + [ + [[2], [3], "equal", "eq", Flipper::Expressions::Equal], + [[2], [3], "not_equal", "neq", Flipper::Expressions::NotEqual], + [[2], [3], "greater_than", "gt", Flipper::Expressions::GreaterThan], + [[2], [3], "greater_than_or_equal", "gte", Flipper::Expressions::GreaterThanOrEqual], + [[2], [3], "less_than", "lt", Flipper::Expressions::LessThan], + [[2], [3], "less_than_or_equal", "lte", Flipper::Expressions::LessThanOrEqual], + ].each do |(args, other_args, method_name, shortcut_name, klass)| + it "can convert to #{klass}" do + expression = described_class.new(args) + other = described_class.new(other_args) + converted = expression.send(method_name, other) + expect(converted).to be_instance_of(klass) + expect(converted.args).to eq([expression, other]) + end + + it "can convert to #{klass} using #{shortcut_name}" do + expression = described_class.new(args) + other = described_class.new(other_args) + converted = expression.send(shortcut_name, other) + expect(converted).to be_instance_of(klass) + expect(converted.args).to eq([expression, other]) + end + end + + it "can convert to Percentage" do + expression = Flipper.value("User;1") + converted = expression.percentage(40) + expect(converted).to be_instance_of(Flipper::Expressions::Percentage) + expect(converted.args).to eq([ + expression, + Flipper.value(40) + ]) + end end From 399f7fbb9384a3f43afcc2127f65573c46698f42 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sat, 20 Nov 2021 20:29:30 -0500 Subject: [PATCH 108/176] Add any and all specs --- lib/flipper/expression.rb | 31 +++----- lib/flipper/expressions/all.rb | 4 - lib/flipper/expressions/any.rb | 4 - spec/flipper/expression_spec.rb | 30 +++++++- spec/flipper/expressions/all_spec.rb | 105 ++++++++++++++++++++++++++ spec/flipper/expressions/any_spec.rb | 106 +++++++++++++++++++++++++++ 6 files changed, 246 insertions(+), 34 deletions(-) create mode 100644 spec/flipper/expressions/all_spec.rb create mode 100644 spec/flipper/expressions/any_spec.rb diff --git a/lib/flipper/expression.rb b/lib/flipper/expression.rb index 926437896..65b9f7892 100644 --- a/lib/flipper/expression.rb +++ b/lib/flipper/expression.rb @@ -8,7 +8,7 @@ class Expression FalseClass, ].freeze - def self.build(object) + def self.build(object, convert_to_values: false) return object if object.is_a?(Flipper::Expression) case object @@ -19,7 +19,7 @@ def self.build(object) args = object.values.first Expressions.const_get(type).new(args) when *SUPPORTED_TYPE_CLASSES - object + convert_to_values ? Expressions::Value.new(object) : object else raise ArgumentError, "#{object.inspect} cannot be converted into a rule expression" end @@ -64,50 +64,37 @@ def all end def equal(object) - Expressions::Equal.new([self, build(object)]) + Expressions::Equal.new([self, self.class.build(object, convert_to_values: true)]) end alias eq equal def not_equal(object) - Expressions::NotEqual.new([self, build(object)]) + Expressions::NotEqual.new([self, self.class.build(object, convert_to_values: true)]) end alias neq not_equal def greater_than(object) - Expressions::GreaterThan.new([self, build(object)]) + Expressions::GreaterThan.new([self, self.class.build(object, convert_to_values: true)]) end alias gt greater_than def greater_than_or_equal(object) - Expressions::GreaterThanOrEqual.new([self, build(object)]) + Expressions::GreaterThanOrEqual.new([self, self.class.build(object, convert_to_values: true)]) end alias gte greater_than_or_equal def less_than(object) - Expressions::LessThan.new([self, build(object)]) + Expressions::LessThan.new([self, self.class.build(object, convert_to_values: true)]) end alias lt less_than def less_than_or_equal(object) - Expressions::LessThanOrEqual.new([self, build(object)]) + Expressions::LessThanOrEqual.new([self, self.class.build(object, convert_to_values: true)]) end alias lte less_than_or_equal def percentage(object) - Expressions::Percentage.new([self, build(object)]) - end - - private - - def build(object) - return object if object.is_a?(Flipper::Expression) - - case object - when *SUPPORTED_TYPE_CLASSES - Expression.build({"Value" => [object]}) - else - raise ArgumentError, "#{object} is not a supported type" - end + Expressions::Percentage.new([self, self.class.build(object, convert_to_values: true)]) end end end diff --git a/lib/flipper/expressions/all.rb b/lib/flipper/expressions/all.rb index bac1c9340..1be57a655 100644 --- a/lib/flipper/expressions/all.rb +++ b/lib/flipper/expressions/all.rb @@ -7,10 +7,6 @@ def evaluate(feature_name: "", properties: {}) args.all? { |arg| arg.evaluate(feature_name: feature_name, properties: properties) == true } end - def any - Expressions::Any.new([self]) - end - def all self end diff --git a/lib/flipper/expressions/any.rb b/lib/flipper/expressions/any.rb index a7fb6e2b3..b7fe7767a 100644 --- a/lib/flipper/expressions/any.rb +++ b/lib/flipper/expressions/any.rb @@ -11,10 +11,6 @@ def any self end - def all - Expressions::All.new([self]) - end - def add(*expressions) self.class.new(args + expressions.flatten) end diff --git a/spec/flipper/expression_spec.rb b/spec/flipper/expression_spec.rb index cb625f3a5..848f34051 100644 --- a/spec/flipper/expression_spec.rb +++ b/spec/flipper/expression_spec.rb @@ -124,6 +124,15 @@ expect(expression).to be_instance_of(Flipper::Expressions::Value) expect(expression.args).to eq(["basic"]) end + + it "can build Property" do + expression = Flipper::Expression.build({ + "Property" => ["flipper_id"] + }) + + expect(expression).to be_instance_of(Flipper::Expressions::Property) + expect(expression.args).to eq(["flipper_id"]) + end end describe "#initialize" do @@ -240,6 +249,22 @@ expect(converted.args).to eq([expression, other]) end + it "builds args into expressions when converting to #{klass}" do + expression = described_class.new(args) + other = Flipper.property(:age) + converted = expression.send(method_name, other.value) + expect(converted).to be_instance_of(klass) + expect(converted.args).to eq([expression, other]) + end + + it "builds array args into expressions when converting to #{klass}" do + expression = described_class.new(args) + other = Flipper.random(100) + converted = expression.send(method_name, [other.value]) + expect(converted).to be_instance_of(klass) + expect(converted.args).to eq([expression, [other]]) + end + it "can convert to #{klass} using #{shortcut_name}" do expression = described_class.new(args) other = described_class.new(other_args) @@ -253,9 +278,6 @@ expression = Flipper.value("User;1") converted = expression.percentage(40) expect(converted).to be_instance_of(Flipper::Expressions::Percentage) - expect(converted.args).to eq([ - expression, - Flipper.value(40) - ]) + expect(converted.args).to eq([expression, Flipper.value(40)]) end end diff --git a/spec/flipper/expressions/all_spec.rb b/spec/flipper/expressions/all_spec.rb new file mode 100644 index 000000000..72c65d567 --- /dev/null +++ b/spec/flipper/expressions/all_spec.rb @@ -0,0 +1,105 @@ +require 'helper' + +RSpec.describe Flipper::Expressions::All do + describe "#evaluate" do + it "returns true if all args evaluate as true" do + expression = described_class.new([ + Flipper.value(true), + Flipper.value("yep").eq("yep"), + ]) + expect(expression.evaluate).to be(true) + end + + it "returns false if any args evaluate as false" do + expression = described_class.new([ + Flipper.value(false), + Flipper.value("yep").eq("yep"), + ]) + expect(expression.evaluate).to be(false) + end + end + + describe "#all" do + it "returns self" do + expression = described_class.new([ + Flipper.value(true), + Flipper.value("yep").eq("yep"), + ]) + expect(expression.all).to be(expression) + end + end + + describe "#add" do + it "returns new instance with expression added" do + expression = described_class.new([Flipper.value(true)]) + other = Flipper.value("yep").eq("yep") + + result = expression.add(other) + expect(result.args).to eq([ + Flipper.value(true), + Flipper.value("yep").eq("yep"), + ]) + end + + it "returns new instance with many expressions added" do + expression = described_class.new([Flipper.value(true)]) + second = Flipper.value("yep").eq("yep") + third = Flipper.value(1).lte(20) + + result = expression.add(second, third) + expect(result.args).to eq([ + Flipper.value(true), + Flipper.value("yep").eq("yep"), + Flipper.value(1).lte(20), + ]) + end + + it "returns new instance with array of expressions added" do + expression = described_class.new([Flipper.value(true)]) + second = Flipper.value("yep").eq("yep") + third = Flipper.value(1).lte(20) + + result = expression.add([second, third]) + expect(result.args).to eq([ + Flipper.value(true), + Flipper.value("yep").eq("yep"), + Flipper.value(1).lte(20), + ]) + end + end + + describe "#remove" do + it "returns new instance with expression removed" do + first = Flipper.value(true) + second = Flipper.value("yep").eq("yep") + third = Flipper.value(1).lte(20) + expression = described_class.new([first, second, third]) + + result = expression.remove(second) + expect(expression.args).to eq([first, second, third]) + expect(result.args).to eq([first, third]) + end + + it "returns new instance with many expressions removed" do + first = Flipper.value(true) + second = Flipper.value("yep").eq("yep") + third = Flipper.value(1).lte(20) + expression = described_class.new([first, second, third]) + + result = expression.remove(second, third) + expect(expression.args).to eq([first, second, third]) + expect(result.args).to eq([first]) + end + + it "returns new instance with array of expressions removed" do + first = Flipper.value(true) + second = Flipper.value("yep").eq("yep") + third = Flipper.value(1).lte(20) + expression = described_class.new([first, second, third]) + + result = expression.remove([second, third]) + expect(expression.args).to eq([first, second, third]) + expect(result.args).to eq([first]) + end + end +end diff --git a/spec/flipper/expressions/any_spec.rb b/spec/flipper/expressions/any_spec.rb new file mode 100644 index 000000000..7dbb14181 --- /dev/null +++ b/spec/flipper/expressions/any_spec.rb @@ -0,0 +1,106 @@ +require 'helper' + +RSpec.describe Flipper::Expressions::Any do + describe "#evaluate" do + it "returns true if any args evaluate as true" do + expression = described_class.new([ + Flipper.value(true), + Flipper.value("yep").eq("nope"), + Flipper.value(1).gte(10), + ]) + expect(expression.evaluate).to be(true) + end + + it "returns false if all args evaluate as false" do + expression = described_class.new([ + Flipper.value(false), + Flipper.value("yep").eq("nope"), + ]) + expect(expression.evaluate).to be(false) + end + end + + describe "#any" do + it "returns self" do + expression = described_class.new([ + Flipper.value(true), + Flipper.value("yep").eq("yep"), + ]) + expect(expression.any).to be(expression) + end + end + + describe "#add" do + it "returns new instance with expression added" do + expression = described_class.new([Flipper.value(true)]) + other = Flipper.value("yep").eq("yep") + + result = expression.add(other) + expect(result.args).to eq([ + Flipper.value(true), + Flipper.value("yep").eq("yep"), + ]) + end + + it "returns new instance with many expressions added" do + expression = described_class.new([Flipper.value(true)]) + second = Flipper.value("yep").eq("yep") + third = Flipper.value(1).lte(20) + + result = expression.add(second, third) + expect(result.args).to eq([ + Flipper.value(true), + Flipper.value("yep").eq("yep"), + Flipper.value(1).lte(20), + ]) + end + + it "returns new instance with array of expressions added" do + expression = described_class.new([Flipper.value(true)]) + second = Flipper.value("yep").eq("yep") + third = Flipper.value(1).lte(20) + + result = expression.add([second, third]) + expect(result.args).to eq([ + Flipper.value(true), + Flipper.value("yep").eq("yep"), + Flipper.value(1).lte(20), + ]) + end + end + + describe "#remove" do + it "returns new instance with expression removed" do + first = Flipper.value(true) + second = Flipper.value("yep").eq("yep") + third = Flipper.value(1).lte(20) + expression = described_class.new([first, second, third]) + + result = expression.remove(second) + expect(expression.args).to eq([first, second, third]) + expect(result.args).to eq([first, third]) + end + + it "returns new instance with many expressions removed" do + first = Flipper.value(true) + second = Flipper.value("yep").eq("yep") + third = Flipper.value(1).lte(20) + expression = described_class.new([first, second, third]) + + result = expression.remove(second, third) + expect(expression.args).to eq([first, second, third]) + expect(result.args).to eq([first]) + end + + it "returns new instance with array of expressions removed" do + first = Flipper.value(true) + second = Flipper.value("yep").eq("yep") + third = Flipper.value(1).lte(20) + expression = described_class.new([first, second, third]) + + result = expression.remove([second, third]) + expect(expression.args).to eq([first, second, third]) + expect(result.args).to eq([first]) + end + end +end From 6255252464aa123536e8d2953a2be27661104715 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sat, 20 Nov 2021 20:37:32 -0500 Subject: [PATCH 109/176] Add #value specs to expressions --- spec/flipper/expressions/equal_spec.rb | 6 ++--- .../greater_than_or_equal_to_spec.rb | 16 +++++++++++++ spec/flipper/expressions/greater_than_spec.rb | 16 +++++++++++++ .../expressions/less_than_or_equal_to_spec.rb | 16 +++++++++++++ spec/flipper/expressions/less_than_spec.rb | 16 +++++++++++++ spec/flipper/expressions/not_equal_spec.rb | 24 +++++++++++++++---- spec/flipper/expressions/percentage_spec.rb | 16 +++++++++++++ spec/flipper/expressions/property_spec.rb | 12 ++++++++++ spec/flipper/expressions/random_spec.rb | 12 ++++++++++ spec/flipper/expressions/value_spec.rb | 10 ++++++++ 10 files changed, 137 insertions(+), 7 deletions(-) diff --git a/spec/flipper/expressions/equal_spec.rb b/spec/flipper/expressions/equal_spec.rb index 94b5209bc..1272a9c59 100644 --- a/spec/flipper/expressions/equal_spec.rb +++ b/spec/flipper/expressions/equal_spec.rb @@ -18,7 +18,7 @@ describe "#evaluate" do it "returns true when equal" do - expression = Flipper::Expressions::Equal.new([ + expression = described_class.new([ Flipper::Expressions::Value.new(["basic"]), Flipper::Expressions::Value.new(["basic"]), ]) @@ -27,7 +27,7 @@ end it "returns false when not equal" do - expression = Flipper::Expressions::Equal.new([ + expression = described_class.new([ Flipper::Expressions::Value.new(["basic"]), Flipper::Expressions::Value.new(["plus"]), ]) @@ -38,7 +38,7 @@ describe "#value" do it "returns Hash" do - expression = Flipper::Expressions::Equal.new([ + expression = described_class.new([ Flipper::Expressions::Property.new(["plan"]), Flipper::Expressions::Value.new(["basic"]), ]) diff --git a/spec/flipper/expressions/greater_than_or_equal_to_spec.rb b/spec/flipper/expressions/greater_than_or_equal_to_spec.rb index d9f134acd..5878937eb 100644 --- a/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +++ b/spec/flipper/expressions/greater_than_or_equal_to_spec.rb @@ -29,4 +29,20 @@ expect(expression.evaluate).to be(false) end end + + describe "#value" do + it "returns Hash" do + expression = described_class.new([ + Flipper.value(20), + Flipper.value(10), + ]) + + expect(expression.value).to eq({ + "GreaterThanOrEqual" => [ + {"Value" => [20]}, + {"Value" => [10]}, + ], + }) + end + end end diff --git a/spec/flipper/expressions/greater_than_spec.rb b/spec/flipper/expressions/greater_than_spec.rb index eb1642cbb..22abd0a1c 100644 --- a/spec/flipper/expressions/greater_than_spec.rb +++ b/spec/flipper/expressions/greater_than_spec.rb @@ -29,4 +29,20 @@ expect(expression.evaluate).to be(false) end end + + describe "#value" do + it "returns Hash" do + expression = described_class.new([ + Flipper.value(20), + Flipper.value(10), + ]) + + expect(expression.value).to eq({ + "GreaterThan" => [ + {"Value" => [20]}, + {"Value" => [10]}, + ], + }) + end + end end diff --git a/spec/flipper/expressions/less_than_or_equal_to_spec.rb b/spec/flipper/expressions/less_than_or_equal_to_spec.rb index fae52b537..784b81978 100644 --- a/spec/flipper/expressions/less_than_or_equal_to_spec.rb +++ b/spec/flipper/expressions/less_than_or_equal_to_spec.rb @@ -29,4 +29,20 @@ expect(expression.evaluate).to be(false) end end + + describe "#value" do + it "returns Hash" do + expression = described_class.new([ + Flipper.value(20), + Flipper.value(10), + ]) + + expect(expression.value).to eq({ + "LessThanOrEqual" => [ + {"Value" => [20]}, + {"Value" => [10]}, + ], + }) + end + end end diff --git a/spec/flipper/expressions/less_than_spec.rb b/spec/flipper/expressions/less_than_spec.rb index 712236413..500996bf8 100644 --- a/spec/flipper/expressions/less_than_spec.rb +++ b/spec/flipper/expressions/less_than_spec.rb @@ -29,4 +29,20 @@ expect(expression.evaluate).to be(false) end end + + describe "#value" do + it "returns Hash" do + expression = described_class.new([ + Flipper.value(20), + Flipper.value(10), + ]) + + expect(expression.value).to eq({ + "LessThan" => [ + {"Value" => [20]}, + {"Value" => [10]}, + ], + }) + end + end end diff --git a/spec/flipper/expressions/not_equal_spec.rb b/spec/flipper/expressions/not_equal_spec.rb index b85376062..2ac5588e9 100644 --- a/spec/flipper/expressions/not_equal_spec.rb +++ b/spec/flipper/expressions/not_equal_spec.rb @@ -4,8 +4,8 @@ describe "#evaluate" do it "returns true when not equal" do expression = described_class.new([ - Flipper::Expressions::Value.new("basic"), - Flipper::Expressions::Value.new("plus"), + Flipper.value("basic"), + Flipper.value("plus"), ]) expect(expression.evaluate).to be(true) @@ -13,11 +13,27 @@ it "returns false when equal" do expression = described_class.new([ - Flipper::Expressions::Value.new("basic"), - Flipper::Expressions::Value.new("basic"), + Flipper.value("basic"), + Flipper.value("basic"), ]) expect(expression.evaluate).to be(false) end end + + describe "#value" do + it "returns Hash" do + expression = described_class.new([ + Flipper.value(20), + Flipper.value(10), + ]) + + expect(expression.value).to eq({ + "NotEqual" => [ + {"Value" => [20]}, + {"Value" => [10]}, + ], + }) + end + end end diff --git a/spec/flipper/expressions/percentage_spec.rb b/spec/flipper/expressions/percentage_spec.rb index 932eacd6d..4764813dd 100644 --- a/spec/flipper/expressions/percentage_spec.rb +++ b/spec/flipper/expressions/percentage_spec.rb @@ -30,4 +30,20 @@ expect(expression.evaluate(feature_name: "b")).to be(false) end end + + describe "#value" do + it "returns Hash" do + expression = described_class.new([ + Flipper.value("User;1"), + Flipper.value(10), + ]) + + expect(expression.value).to eq({ + "Percentage" => [ + {"Value" => ["User;1"]}, + {"Value" => [10]}, + ], + }) + end + end end diff --git a/spec/flipper/expressions/property_spec.rb b/spec/flipper/expressions/property_spec.rb index a33994757..c698ea794 100644 --- a/spec/flipper/expressions/property_spec.rb +++ b/spec/flipper/expressions/property_spec.rb @@ -31,4 +31,16 @@ expect(expression.evaluate).to be(nil) end end + + describe "#value" do + it "returns Hash" do + expression = described_class.new([ + "flipper_id", + ]) + + expect(expression.value).to eq({ + "Property" => ["flipper_id"], + }) + end + end end diff --git a/spec/flipper/expressions/random_spec.rb b/spec/flipper/expressions/random_spec.rb index a570449a4..aeb8677ca 100644 --- a/spec/flipper/expressions/random_spec.rb +++ b/spec/flipper/expressions/random_spec.rb @@ -17,4 +17,16 @@ expect(result).to be <= 10 end end + + describe "#value" do + it "returns Hash" do + expression = described_class.new([ + 100, + ]) + + expect(expression.value).to eq({ + "Random" => [100], + }) + end + end end diff --git a/spec/flipper/expressions/value_spec.rb b/spec/flipper/expressions/value_spec.rb index bf9d9d61d..2591707b0 100644 --- a/spec/flipper/expressions/value_spec.rb +++ b/spec/flipper/expressions/value_spec.rb @@ -20,4 +20,14 @@ expect(expression.evaluate).to eq("basic") end end + + describe "#value" do + it "returns Hash" do + expression = described_class.new([20]) + + expect(expression.value).to eq({ + "Value" => [20], + }) + end + end end From ea499ee4e8fc41bea80d00d410e5a5f00c709dfe Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sat, 20 Nov 2021 20:44:35 -0500 Subject: [PATCH 110/176] Add property evaluation specs to equal --- spec/flipper/expressions/equal_spec.rb | 40 +++++++++++++++++++++----- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/spec/flipper/expressions/equal_spec.rb b/spec/flipper/expressions/equal_spec.rb index 1272a9c59..281b607ea 100644 --- a/spec/flipper/expressions/equal_spec.rb +++ b/spec/flipper/expressions/equal_spec.rb @@ -11,36 +11,62 @@ expect(expression).to be_instance_of(Flipper::Expressions::Equal) expect(expression.args).to eq([ - Flipper::Expressions::Value.new(["basic"]), - Flipper::Expressions::Value.new(["basic"]), + Flipper.value("basic"), + Flipper.value("basic"), ]) end describe "#evaluate" do it "returns true when equal" do expression = described_class.new([ - Flipper::Expressions::Value.new(["basic"]), - Flipper::Expressions::Value.new(["basic"]), + Flipper.value("basic"), + Flipper.value("basic"), ]) expect(expression.evaluate).to be(true) end + it "returns true when properties equal" do + expression = described_class.new([ + Flipper.property(:first), + Flipper.property(:second), + ]) + + properties = { + "first" => "foo", + "second" => "foo", + } + expect(expression.evaluate(properties: properties)).to be(true) + end + it "returns false when not equal" do expression = described_class.new([ - Flipper::Expressions::Value.new(["basic"]), - Flipper::Expressions::Value.new(["plus"]), + Flipper.value("basic"), + Flipper.value("plus"), ]) expect(expression.evaluate).to be(false) end + + it "returns false when properties not equal" do + expression = described_class.new([ + Flipper.property(:first), + Flipper.property(:second), + ]) + + properties = { + "first" => "foo", + "second" => "bar", + } + expect(expression.evaluate(properties: properties)).to be(false) + end end describe "#value" do it "returns Hash" do expression = described_class.new([ Flipper::Expressions::Property.new(["plan"]), - Flipper::Expressions::Value.new(["basic"]), + Flipper.value("basic"), ]) expect(expression.value).to eq({ From 0979df2b9b612a0394f786d0b02c8c288de50c17 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sat, 20 Nov 2021 20:44:49 -0500 Subject: [PATCH 111/176] Minor spec re-arrangement --- spec/flipper/expressions/random_spec.rb | 16 ++++++++-------- spec/flipper/expressions/value_spec.rb | 20 +++++++++++--------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/spec/flipper/expressions/random_spec.rb b/spec/flipper/expressions/random_spec.rb index aeb8677ca..29dfb692c 100644 --- a/spec/flipper/expressions/random_spec.rb +++ b/spec/flipper/expressions/random_spec.rb @@ -1,12 +1,14 @@ require 'helper' RSpec.describe Flipper::Expressions::Random do - it "can initialize with number" do - expect(described_class.new(1).args).to eq([1]) - end + describe "#initialize" do + it "works with number" do + expect(described_class.new(1).args).to eq([1]) + end - it "can initialize with array" do - expect(described_class.new([1]).args).to eq([1]) + it "works with array" do + expect(described_class.new([1]).args).to eq([1]) + end end describe "#evaluate" do @@ -20,9 +22,7 @@ describe "#value" do it "returns Hash" do - expression = described_class.new([ - 100, - ]) + expression = described_class.new([100]) expect(expression.value).to eq({ "Random" => [100], diff --git a/spec/flipper/expressions/value_spec.rb b/spec/flipper/expressions/value_spec.rb index 2591707b0..b4a3e9ab2 100644 --- a/spec/flipper/expressions/value_spec.rb +++ b/spec/flipper/expressions/value_spec.rb @@ -1,17 +1,19 @@ require 'helper' RSpec.describe Flipper::Expressions::Value do - it "can initialize with string" do - expect(described_class.new("basic").args).to eq(["basic"]) - end + describe "#initialize" do + it "works with string" do + expect(described_class.new("basic").args).to eq(["basic"]) + end - it "can initialize with number" do - expect(described_class.new(1).args).to eq([1]) - end + it "works with number" do + expect(described_class.new(1).args).to eq([1]) + end - it "can initialize with array" do - expect(described_class.new(["basic"]).args).to eq(["basic"]) - expect(described_class.new(["basic"]).args).to eq(["basic"]) + it "works with array" do + expect(described_class.new(["basic"]).args).to eq(["basic"]) + expect(described_class.new(["basic"]).args).to eq(["basic"]) + end end describe "#evaluate" do From a53a30be8f935e021b016109811154820def0bca Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sat, 20 Nov 2021 20:54:07 -0500 Subject: [PATCH 112/176] Add some specs to equal and not equal --- spec/flipper/expressions/equal_spec.rb | 16 +++++++++++++ spec/flipper/expressions/not_equal_spec.rb | 26 ++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/spec/flipper/expressions/equal_spec.rb b/spec/flipper/expressions/equal_spec.rb index 281b607ea..ec7fb32be 100644 --- a/spec/flipper/expressions/equal_spec.rb +++ b/spec/flipper/expressions/equal_spec.rb @@ -39,6 +39,22 @@ expect(expression.evaluate(properties: properties)).to be(true) end + it "works when nested" do + expression = described_class.new([ + Flipper.value(true), + Flipper.all( + Flipper.property(:stinky).eq(true), + Flipper.value("admin").eq(Flipper.property(:role)), + ), + ]) + + properties = { + "stinky" => true, + "role" => "admin", + } + expect(expression.evaluate(properties: properties)).to be(true) + end + it "returns false when not equal" do expression = described_class.new([ Flipper.value("basic"), diff --git a/spec/flipper/expressions/not_equal_spec.rb b/spec/flipper/expressions/not_equal_spec.rb index 2ac5588e9..35c445cc3 100644 --- a/spec/flipper/expressions/not_equal_spec.rb +++ b/spec/flipper/expressions/not_equal_spec.rb @@ -11,6 +11,19 @@ expect(expression.evaluate).to be(true) end + it "returns true when properties not equal" do + expression = described_class.new([ + Flipper.property(:first), + Flipper.property(:second), + ]) + + properties = { + "first" => "foo", + "second" => "bar", + } + expect(expression.evaluate(properties: properties)).to be(true) + end + it "returns false when equal" do expression = described_class.new([ Flipper.value("basic"), @@ -19,6 +32,19 @@ expect(expression.evaluate).to be(false) end + + it "returns false when properties are equal" do + expression = described_class.new([ + Flipper.property(:first), + Flipper.property(:second), + ]) + + properties = { + "first" => "foo", + "second" => "foo", + } + expect(expression.evaluate(properties: properties)).to be(false) + end end describe "#value" do From 8db412a9363ef922e7aa3d7f4efb369ca0540ba4 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sun, 21 Nov 2021 11:15:01 -0500 Subject: [PATCH 113/176] Change kwargs to just be a context hash Seems more flexible for now. --- lib/flipper/expression.rb | 6 ++++ lib/flipper/expressions/all.rb | 4 +-- lib/flipper/expressions/any.rb | 4 +-- lib/flipper/expressions/equal.rb | 6 ++-- lib/flipper/expressions/greater_than.rb | 6 ++-- .../expressions/greater_than_or_equal.rb | 6 ++-- lib/flipper/expressions/less_than.rb | 6 ++-- lib/flipper/expressions/less_than_or_equal.rb | 6 ++-- lib/flipper/expressions/not_equal.rb | 6 ++-- lib/flipper/expressions/percentage.rb | 11 ++++--- lib/flipper/expressions/property.rb | 9 +++-- lib/flipper/expressions/random.rb | 2 +- lib/flipper/expressions/value.rb | 2 +- spec/flipper/expressions/percentage_spec.rb | 33 +++++++++++++++---- 14 files changed, 70 insertions(+), 37 deletions(-) diff --git a/lib/flipper/expression.rb b/lib/flipper/expression.rb index 65b9f7892..4b0c4209b 100644 --- a/lib/flipper/expression.rb +++ b/lib/flipper/expression.rb @@ -96,6 +96,12 @@ def less_than_or_equal(object) def percentage(object) Expressions::Percentage.new([self, self.class.build(object, convert_to_values: true)]) end + + private + + def evaluate_arg(arg, context = {}) + arg.evaluate(context) + end end end diff --git a/lib/flipper/expressions/all.rb b/lib/flipper/expressions/all.rb index 1be57a655..a32c6dc8c 100644 --- a/lib/flipper/expressions/all.rb +++ b/lib/flipper/expressions/all.rb @@ -3,8 +3,8 @@ module Flipper module Expressions class All < Expression - def evaluate(feature_name: "", properties: {}) - args.all? { |arg| arg.evaluate(feature_name: feature_name, properties: properties) == true } + def evaluate(context = {}) + args.all? { |arg| arg.evaluate(context) == true } end def all diff --git a/lib/flipper/expressions/any.rb b/lib/flipper/expressions/any.rb index b7fe7767a..b5fdcfb0b 100644 --- a/lib/flipper/expressions/any.rb +++ b/lib/flipper/expressions/any.rb @@ -3,8 +3,8 @@ module Flipper module Expressions class Any < Expression - def evaluate(feature_name: "", properties: {}) - args.any? { |arg| arg.evaluate(feature_name: feature_name, properties: properties) == true } + def evaluate(context = {}) + args.any? { |arg| arg.evaluate(context) == true } end def any diff --git a/lib/flipper/expressions/equal.rb b/lib/flipper/expressions/equal.rb index a4f0ade8b..49de9a12d 100644 --- a/lib/flipper/expressions/equal.rb +++ b/lib/flipper/expressions/equal.rb @@ -3,11 +3,11 @@ module Flipper module Expressions class Equal < Expression - def evaluate(feature_name: "", properties: {}) + def evaluate(context = {}) return false unless args[0] && args[1] - left = args[0].evaluate(feature_name: feature_name, properties: properties) - right = args[1].evaluate(feature_name: feature_name, properties: properties) + left = args[0].evaluate(context) + right = args[1].evaluate(context) left == right end diff --git a/lib/flipper/expressions/greater_than.rb b/lib/flipper/expressions/greater_than.rb index 86eb2fa65..cffd74252 100644 --- a/lib/flipper/expressions/greater_than.rb +++ b/lib/flipper/expressions/greater_than.rb @@ -3,11 +3,11 @@ module Flipper module Expressions class GreaterThan < Expression - def evaluate(feature_name: "", properties: {}) + def evaluate(context = {}) return false unless args[0] && args[1] - left = args[0].evaluate(feature_name: feature_name, properties: properties) - right = args[1].evaluate(feature_name: feature_name, properties: properties) + left = args[0].evaluate(context) + right = args[1].evaluate(context) left && right && left > right end diff --git a/lib/flipper/expressions/greater_than_or_equal.rb b/lib/flipper/expressions/greater_than_or_equal.rb index bc5e9190f..477f3dbb8 100644 --- a/lib/flipper/expressions/greater_than_or_equal.rb +++ b/lib/flipper/expressions/greater_than_or_equal.rb @@ -3,11 +3,11 @@ module Flipper module Expressions class GreaterThanOrEqual < Expression - def evaluate(feature_name: "", properties: {}) + def evaluate(context = {}) return false unless args[0] && args[1] - left = args[0].evaluate(feature_name: feature_name, properties: properties) - right = args[1].evaluate(feature_name: feature_name, properties: properties) + left = args[0].evaluate(context) + right = args[1].evaluate(context) left && right && left >= right end diff --git a/lib/flipper/expressions/less_than.rb b/lib/flipper/expressions/less_than.rb index a786dec84..12a40c684 100644 --- a/lib/flipper/expressions/less_than.rb +++ b/lib/flipper/expressions/less_than.rb @@ -3,11 +3,11 @@ module Flipper module Expressions class LessThan < Expression - def evaluate(feature_name: "", properties: {}) + def evaluate(context = {}) return false unless args[0] && args[1] - left = args[0].evaluate(feature_name: feature_name, properties: properties) - right = args[1].evaluate(feature_name: feature_name, properties: properties) + left = args[0].evaluate(context) + right = args[1].evaluate(context) left && right && left < right end diff --git a/lib/flipper/expressions/less_than_or_equal.rb b/lib/flipper/expressions/less_than_or_equal.rb index d9582bebf..61092c59d 100644 --- a/lib/flipper/expressions/less_than_or_equal.rb +++ b/lib/flipper/expressions/less_than_or_equal.rb @@ -3,11 +3,11 @@ module Flipper module Expressions class LessThanOrEqual < Expression - def evaluate(feature_name: "", properties: {}) + def evaluate(context = {}) return false unless args[0] && args[1] - left = args[0].evaluate(feature_name: feature_name, properties: properties) - right = args[1].evaluate(feature_name: feature_name, properties: properties) + left = args[0].evaluate(context) + right = args[1].evaluate(context) left && right && left <= right end diff --git a/lib/flipper/expressions/not_equal.rb b/lib/flipper/expressions/not_equal.rb index 39977d0dd..777599126 100644 --- a/lib/flipper/expressions/not_equal.rb +++ b/lib/flipper/expressions/not_equal.rb @@ -3,11 +3,11 @@ module Flipper module Expressions class NotEqual < Expression - def evaluate(feature_name: "", properties: {}) + def evaluate(context = {}) return false unless args[0] && args[1] - left = args[0].evaluate(feature_name: feature_name, properties: properties) - right = args[1].evaluate(feature_name: feature_name, properties: properties) + left = args[0].evaluate(context) + right = args[1].evaluate(context) left != right end diff --git a/lib/flipper/expressions/percentage.rb b/lib/flipper/expressions/percentage.rb index 398a894b9..a903f641e 100644 --- a/lib/flipper/expressions/percentage.rb +++ b/lib/flipper/expressions/percentage.rb @@ -5,15 +5,16 @@ module Expressions class Percentage < Expression SCALING_FACTOR = 1_000 - def evaluate(feature_name: "", properties: {}) + def evaluate(context = {}) return false unless args[0] && args[1] - left = args[0].evaluate(feature_name: feature_name, properties: properties) - right = args[1].evaluate(feature_name: feature_name, properties: properties) + text = evaluate_arg(args[0], context) + percentage = evaluate_arg(args[1], context) - return false unless left && right + return false unless text && percentage - Zlib.crc32("#{feature_name}#{left}") % (100 * SCALING_FACTOR) < right * SCALING_FACTOR + prefix = context[:feature_name] || "" + Zlib.crc32("#{prefix}#{text}") % (100 * SCALING_FACTOR) < percentage * SCALING_FACTOR end end end diff --git a/lib/flipper/expressions/property.rb b/lib/flipper/expressions/property.rb index 3c03eda41..f2247cb02 100644 --- a/lib/flipper/expressions/property.rb +++ b/lib/flipper/expressions/property.rb @@ -7,9 +7,14 @@ def initialize(args) super Array(args).map(&:to_s) end - def evaluate(feature_name: "", properties: {}) + def evaluate(context = {}) key = args[0] - properties[key] + + if properties = context[:properties] + properties[key] + else + nil + end end end end diff --git a/lib/flipper/expressions/random.rb b/lib/flipper/expressions/random.rb index 0effdbc9d..097561e56 100644 --- a/lib/flipper/expressions/random.rb +++ b/lib/flipper/expressions/random.rb @@ -7,7 +7,7 @@ def initialize(args) super Array(args) end - def evaluate(feature_name: "", properties: {}) + def evaluate(context = {}) rand args[0] end end diff --git a/lib/flipper/expressions/value.rb b/lib/flipper/expressions/value.rb index edb73a5c9..e58bc98cd 100644 --- a/lib/flipper/expressions/value.rb +++ b/lib/flipper/expressions/value.rb @@ -7,7 +7,7 @@ def initialize(args) super Array(args) end - def evaluate(feature_name: "", properties: {}) + def evaluate(context = {}) args[0] end end diff --git a/spec/flipper/expressions/percentage_spec.rb b/spec/flipper/expressions/percentage_spec.rb index 4764813dd..17dc92e73 100644 --- a/spec/flipper/expressions/percentage_spec.rb +++ b/spec/flipper/expressions/percentage_spec.rb @@ -4,17 +4,38 @@ describe "#evaluate" do it "returns true when string in percentage enabled" do expression = described_class.new([ - Flipper::Expressions::Value.new("User;1"), - Flipper::Expressions::Value.new(100), + Flipper.value("User;1"), + Flipper.value(42), + ]) + + expect(expression.evaluate).to be(true) + end + + it "returns true when string in fractional percentage enabled" do + expression = described_class.new([ + Flipper.value("User;1"), + Flipper.value(41.687), ]) expect(expression.evaluate).to be(true) end + it "returns true when property evalutes to string that is percentage enabled" do + expression = described_class.new([ + Flipper.property(:flipper_id), + Flipper.value(42), + ]) + + properties = { + "flipper_id" => "User;1", + } + expect(expression.evaluate(properties: properties)).to be(true) + end + it "returns false when string in percentage enabled" do expression = described_class.new([ - Flipper::Expressions::Value.new("User;1"), - Flipper::Expressions::Value.new(0), + Flipper.value("User;1"), + Flipper.value(0), ]) expect(expression.evaluate).to be(false) @@ -22,8 +43,8 @@ it "changes value based on feature_name so not all actors get all features first" do expression = described_class.new([ - Flipper::Expressions::Value.new("User;1"), - Flipper::Expressions::Value.new(70), + Flipper.value("User;1"), + Flipper.value(70), ]) expect(expression.evaluate(feature_name: "a")).to be(true) From e88b4c69c9f8596fac87fbb642f1b9e94809a9ad Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sun, 21 Nov 2021 11:30:05 -0500 Subject: [PATCH 114/176] Allow evaluation for property arg --- lib/flipper/expression.rb | 12 ++++++++++-- lib/flipper/expressions/percentage.rb | 4 ++-- lib/flipper/expressions/property.rb | 4 ++-- spec/flipper/expressions/property_spec.rb | 9 +++++++++ 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/lib/flipper/expression.rb b/lib/flipper/expression.rb index 4b0c4209b..20c465f4c 100644 --- a/lib/flipper/expression.rb +++ b/lib/flipper/expression.rb @@ -20,6 +20,8 @@ def self.build(object, convert_to_values: false) Expressions.const_get(type).new(args) when *SUPPORTED_TYPE_CLASSES convert_to_values ? Expressions::Value.new(object) : object + when Symbol + convert_to_values ? Expressions::Value.new(object.to_s) : object.to_s else raise ArgumentError, "#{object.inspect} cannot be converted into a rule expression" end @@ -99,8 +101,14 @@ def percentage(object) private - def evaluate_arg(arg, context = {}) - arg.evaluate(context) + def evaluate_arg(index, context = {}) + object = args[index] + + if object.is_a?(Flipper::Expression) + object.evaluate(context) + else + object + end end end end diff --git a/lib/flipper/expressions/percentage.rb b/lib/flipper/expressions/percentage.rb index a903f641e..dc7113496 100644 --- a/lib/flipper/expressions/percentage.rb +++ b/lib/flipper/expressions/percentage.rb @@ -8,8 +8,8 @@ class Percentage < Expression def evaluate(context = {}) return false unless args[0] && args[1] - text = evaluate_arg(args[0], context) - percentage = evaluate_arg(args[1], context) + text = evaluate_arg(0, context) + percentage = evaluate_arg(1, context) return false unless text && percentage diff --git a/lib/flipper/expressions/property.rb b/lib/flipper/expressions/property.rb index f2247cb02..f07652361 100644 --- a/lib/flipper/expressions/property.rb +++ b/lib/flipper/expressions/property.rb @@ -4,11 +4,11 @@ module Flipper module Expressions class Property < Expression def initialize(args) - super Array(args).map(&:to_s) + super Array(args) end def evaluate(context = {}) - key = args[0] + key = evaluate_arg(0, context) if properties = context[:properties] properties[key] diff --git a/spec/flipper/expressions/property_spec.rb b/spec/flipper/expressions/property_spec.rb index c698ea794..c27988a21 100644 --- a/spec/flipper/expressions/property_spec.rb +++ b/spec/flipper/expressions/property_spec.rb @@ -26,6 +26,15 @@ expect(expression.evaluate(properties: properties)).to eq("User;1") end + it "can evalute arg and use result for property name" do + expression = described_class.new(Flipper.property(:rollout_key)) + properties = { + "rollout_key" => "flipper_id", + "flipper_id" => "User;1", + } + expect(expression.evaluate(properties: properties)).to eq("User;1") + end + it "returns nil if key not found in properties" do expression = described_class.new("flipper_id") expect(expression.evaluate).to be(nil) From f5c8e9a5b91671f92002364ab8f5bf0dba44a6fb Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sun, 21 Nov 2021 20:04:31 -0500 Subject: [PATCH 115/176] Add a primitive spec for equal --- spec/flipper/expressions/equal_spec.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spec/flipper/expressions/equal_spec.rb b/spec/flipper/expressions/equal_spec.rb index ec7fb32be..3aa837986 100644 --- a/spec/flipper/expressions/equal_spec.rb +++ b/spec/flipper/expressions/equal_spec.rb @@ -16,6 +16,15 @@ ]) end + it "can be built with primitives" do + expression = described_class.build({ + "Equal" => ["basic", "basic"], + }) + + expect(expression).to be_instance_of(Flipper::Expressions::Equal) + expect(expression.args).to eq(["basic", "basic"]) + end + describe "#evaluate" do it "returns true when equal" do expression = described_class.new([ From 538b6367468a82fd9927f371ff53dfe88f11cf33 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sun, 21 Nov 2021 20:23:06 -0500 Subject: [PATCH 116/176] Rename rule to expression --- Guardfile | 2 +- docs/Gates.md | 16 +- docs/api/README.md | 80 ++--- examples/{rules.rb => expressions.rb} | 94 ++--- lib/flipper.rb | 3 +- lib/flipper/adapter.rb | 2 +- lib/flipper/adapters/http.rb | 2 +- lib/flipper/adapters/rollout.rb | 2 +- .../adapters/sync/feature_synchronizer.rb | 12 +- lib/flipper/api/middleware.rb | 2 +- .../{rule_gate.rb => expression_gate.rb} | 14 +- lib/flipper/dsl.rb | 36 +- lib/flipper/expression.rb | 2 +- lib/flipper/feature.rb | 54 +-- lib/flipper/gate.rb | 2 +- lib/flipper/gate_values.rb | 8 +- lib/flipper/gates/{rule.rb => expression.rb} | 6 +- lib/flipper/model/active_record.rb | 2 +- lib/flipper/model/sequel.rb | 2 +- lib/flipper/spec/shared_adapter_specs.rb | 32 +- lib/flipper/test/shared_adapter_test.rb | 22 +- spec/flipper/adapter_spec.rb | 2 +- spec/flipper/adapters/read_only_spec.rb | 18 +- spec/flipper/adapters/rollout_spec.rb | 6 +- .../sync/feature_synchronizer_spec.rb | 26 +- spec/flipper/api/v1/actions/feature_spec.rb | 16 +- spec/flipper/api/v1/actions/features_spec.rb | 8 +- spec/flipper/api/v1/actions/rule_gate_spec.rb | 80 ++--- spec/flipper/cloud_spec.rb | 24 +- spec/flipper/dsl_spec.rb | 46 +-- spec/flipper/feature_spec.rb | 338 +++++++++--------- spec/flipper_integration_spec.rb | 18 +- spec/flipper_spec.rb | 48 ++- spec/support/spec_helpers.rb | 2 +- 34 files changed, 513 insertions(+), 514 deletions(-) rename examples/{rules.rb => expressions.rb} (64%) rename lib/flipper/api/v1/actions/{rule_gate.rb => expression_gate.rb} (67%) rename lib/flipper/gates/{rule.rb => expression.rb} (95%) diff --git a/Guardfile b/Guardfile index c5db74ea9..4777b30ee 100644 --- a/Guardfile +++ b/Guardfile @@ -16,7 +16,7 @@ rspec_options = { guard 'rspec', rspec_options do watch(%r{^spec/.+_spec\.rb$}) watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } - watch('lib/flipper/rule.rb') { 'spec/flipper_integration_spec.rb' } + watch('lib/flipper/expression.rb') { 'spec/flipper_integration_spec.rb' } watch('lib/flipper/ui/middleware.rb') { 'spec/flipper/ui_spec.rb' } watch('lib/flipper/api/middleware.rb') { 'spec/flipper/api_spec.rb' } watch(/shared_adapter_specs\.rb$/) { 'spec' } diff --git a/docs/Gates.md b/docs/Gates.md index ffda21791..cfbbe9c1b 100644 --- a/docs/Gates.md +++ b/docs/Gates.md @@ -12,15 +12,15 @@ Flipper.disable :stats # turn off Flipper.enabled? :stats # check ``` -## 2. Rule +## 2. Expression -Turn feature on for one or more rules. Rules have the same power and flexibility as groups. But the benefit is that they can be changed at runtime (because they are stored in adapter), whereas groups cannot (because they are defined in code). +Turn feature on for one or more expressions. Expressions have the same power and flexibility as groups. But the benefit is that they can be changed at runtime (because they are stored in adapter), whereas groups cannot (because they are defined in code). -A rule is made up of a left value, operator and right value and can be combined with other rules to define pretty complex logic (and, or, eq, neq, gt, gte, lt, lte, in, nin, and percentage). +An expression is made up of a left value, operator and right value and can be combined with other expressions to define pretty complex logic (and, or, eq, neq, gt, gte, lt, lte, in, nin, and percentage). -**Note**: Eventually all other gates will be deprecated in favor of rules since rules can easily power all other gates. +**Note**: Eventually all other gates will be deprecated in favor of expressions since expressions can easily power all other gates. -To make rules useful, you'll need to ensure that actors respond to `flipper_properties`. +To make expressions useful, you'll need to ensure that actors respond to `flipper_properties`. ```ruby class User < Struct.new(:id, :flipper_properties) @@ -31,12 +31,12 @@ basic_user = User.new(1, {"plan" => "basic", "age" => 30}) premium_user = User.new(2, {"plan" => "premium", "age" => 40}) # enable stats feature for anything where property == "basic" -Flipper.enable_rule :stats, Flipper.property(:plan).eq("basic") +Flipper.enable :stats, Flipper.property(:plan).eq("basic") Flipper.enabled? :stats, basic_user # true Flipper.enabled? :stats, premium_user # false # enable stats for anyone on basic plan or age >= 40 -Flipper.enable_rule :stats, Flipper.any( +Flipper.enable :stats, Flipper.any( Flipper.property(:plan).eq("basic"), Flipper.property(:age).gte(40), ) @@ -44,7 +44,7 @@ Flipper.enabled? :stats, basic_user # true because plan == "basic" Flipper.enabled? :stats, premium_user # true because age >= 40 ``` -To learn more, check out the plethora of code samples in [examples/rules.rb](https://github.com/jnunemaker/flipper/blob/master/examples/rules.rb). +To learn more, check out the plethora of code samples in [examples/expressions.rb](https://github.com/jnunemaker/flipper/blob/master/examples/expressions.rb). ## 3. Individual Actor diff --git a/docs/api/README.md b/docs/api/README.md index 5debfc4d7..ca3d2145c 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -93,8 +93,8 @@ Returns an array of feature objects: "value": [] }, { - "key": "rule", - "name": "rule", + "key": "expression", + "name": "expression", "value": nil }, { @@ -129,8 +129,8 @@ Returns an array of feature objects: "value": [] }, { - "key": "rule", - "name": "rule", + "key": "expression", + "name": "expression", "value": nil }, { @@ -210,8 +210,8 @@ Returns an individual feature object: "value": [] }, { - "key": "rule", - "name": "rule", + "key": "expression", + "name": "expression", "value": nil }, { @@ -323,8 +323,8 @@ Successful enabling of the boolean gate will return a 200 HTTP status and the fe "value": [] }, { - "key": "rule", - "name": "rule", + "key": "expression", + "name": "expression", "value": nil }, { @@ -383,8 +383,8 @@ Successful disabling of the boolean gate will return a 200 HTTP status and the f "value": [] }, { - "key": "rule", - "name": "rule", + "key": "expression", + "name": "expression", "value": nil }, { @@ -401,24 +401,24 @@ Successful disabling of the boolean gate will return a 200 HTTP status and the f } ``` -### Enable Rule +### Enable Expression **URL** -`POST /features/{feature_name}/rule` +`POST /features/{feature_name}/expression` **Parameters** * `feature_name` - The name of the feature -* `type` - The type of rule being enabled +* `type` - The type of expression being enabled -* `value` - The JSON representation of the rule. +* `value` - The JSON representation of the expression. **Request** ``` -curl -X POST -H "Content-Type: application/json" -d '{"type":"Condition","value":{"left":{"type":"Property","value":"plan"},"operator":{"type":"Operator","value":"eq"},"right":{"type":"String","value":"basic"}}}' http://example.com/flipper/api/features/reports/rule +curl -X POST -H "Content-Type: application/json" -d '{"type":"Condition","value":{"left":{"type":"Property","value":"plan"},"operator":{"type":"Operator","value":"eq"},"right":{"type":"String","value":"basic"}}}' http://example.com/flipper/api/features/reports/expression ``` **Response** @@ -441,8 +441,8 @@ Successful enabling of the group will return a 200 HTTP status and the feature o "value": [] }, { - "key": "rule", - "name": "rule", + "key": "expression", + "name": "expression", "value": { "type": "Condition", "value": { @@ -480,24 +480,24 @@ Successful enabling of the group will return a 200 HTTP status and the feature o } ``` -### Disable Rule +### Disable Expression **URL** -`DELETE /features/{feature_name}/rule` +`DELETE /features/{feature_name}/expression` **Parameters** * `feature_name` - The name of the feature -* `type` - The type of rule being enabled +* `type` - The type of expression being enabled -* `value` - The JSON representation of the rule. +* `value` - The JSON representation of the expression. **Request** ``` -curl -X DELETE -H "Content-Type: application/json" -d '{"type":"Condition","value":{"left":{"type":"Property","value":"plan"},"operator":{"type":"Operator","value":"eq"},"right":{"type":"String","value":"basic"}}}' http://example.com/flipper/api/features/reports/rule +curl -X DELETE -H "Content-Type: application/json" -d '{"type":"Condition","value":{"left":{"type":"Property","value":"plan"},"operator":{"type":"Operator","value":"eq"},"right":{"type":"String","value":"basic"}}}' http://example.com/flipper/api/features/reports/expression ``` **Response** @@ -520,8 +520,8 @@ Successful disabling of the group will return a 200 HTTP status and the feature "value": [] }, { - "key": "rule", - "name": "rule", + "key": "expression", + "name": "expression", "value": nil }, { @@ -586,8 +586,8 @@ Successful enabling of the group will return a 200 HTTP status and the feature o "value": [] }, { - "key": "rule", - "name": "rule", + "key": "expression", + "name": "expression", "value": nil }, { @@ -647,8 +647,8 @@ Successful disabling of the group will return a 200 HTTP status and the feature "value": [] }, { - "key": "rule", - "name": "rule", + "key": "expression", + "name": "expression", "value": nil }, { @@ -708,8 +708,8 @@ Successful enabling of the actor will return a 200 HTTP status and the feature o "value": ["User;1"] }, { - "key": "rule", - "name": "rule", + "key": "expression", + "name": "expression", "value": nil }, { @@ -769,8 +769,8 @@ Successful disabling of the actor will return a 200 HTTP status and the feature "value": [] }, { - "key": "rule", - "name": "rule", + "key": "expression", + "name": "expression", "value": nil }, { @@ -830,8 +830,8 @@ Successful enabling of a percentage of actors will return a 200 HTTP status and "value": [] }, { - "key": "rule", - "name": "rule", + "key": "expression", + "name": "expression", "value": nil }, { @@ -889,8 +889,8 @@ Successful disabling of a percentage of actors will set the percentage to 0 and "value": [] }, { - "key": "rule", - "name": "rule", + "key": "expression", + "name": "expression", "value": nil }, { @@ -950,8 +950,8 @@ Successful enabling of a percentage of time will return a 200 HTTP status and th "value": [] }, { - "key": "rule", - "name": "rule", + "key": "expression", + "name": "expression", "value": nil }, { @@ -1009,8 +1009,8 @@ Successful disabling of a percentage of time will set the percentage to 0 and re "value": [] }, { - "key": "rule", - "name": "rule", + "key": "expression", + "name": "expression", "value": nil }, { diff --git a/examples/rules.rb b/examples/expressions.rb similarity index 64% rename from examples/rules.rb rename to examples/expressions.rb index 2c80eb257..a3e9be469 100644 --- a/examples/rules.rb +++ b/examples/expressions.rb @@ -20,7 +20,7 @@ def refute(value) end def reset - Flipper.disable_rule :something + Flipper.disable_expression :something end class User < Struct.new(:id, :flipper_properties) @@ -66,96 +66,96 @@ class Org < Struct.new(:id, :flipper_properties) "now" => NOW - DAY, }) -age_rule = Flipper.property(:age).gte(21) -plan_rule = Flipper.property(:plan).eq("basic") -admin_rule = Flipper.property(:admin).eq(true) +age_expression = Flipper.property(:age).gte(21) +plan_expression = Flipper.property(:plan).eq("basic") +admin_expression = Flipper.property(:admin).eq(true) -puts "Single Rule" +puts "Single Expression" refute Flipper.enabled?(:something, user) -puts "Enabling single rule" -Flipper.enable_rule :something, plan_rule +puts "Enabling single expression" +Flipper.enable :something, plan_expression assert Flipper.enabled?(:something, user) refute Flipper.enabled?(:something, admin_user) refute Flipper.enabled?(:something, other_user) -puts "Disabling single rule" +puts "Disabling single expression" reset refute Flipper.enabled?(:something, user) -puts "\n\nAny Rule" -any_rule = Flipper.any(plan_rule, age_rule) +puts "\n\nAny Expression" +any_expression = Flipper.any(plan_expression, age_expression) refute Flipper.enabled?(:something, user) -puts "Enabling any rule" -Flipper.enable_rule :something, any_rule +puts "Enabling any expression" +Flipper.enable :something, any_expression assert Flipper.enabled?(:something, user) refute Flipper.enabled?(:something, admin_user) refute Flipper.enabled?(:something, other_user) -puts "Disabling any rule" +puts "Disabling any expression" reset refute Flipper.enabled?(:something, user) -puts "\n\nAll Rule" -all_rule = Flipper.all(plan_rule, age_rule) +puts "\n\nAll Expression" +all_expression = Flipper.all(plan_expression, age_expression) refute Flipper.enabled?(:something, user) -puts "Enabling all rule" -Flipper.enable_rule :something, all_rule +puts "Enabling all expression" +Flipper.enable :something, all_expression assert Flipper.enabled?(:something, user) refute Flipper.enabled?(:something, admin_user) refute Flipper.enabled?(:something, other_user) -puts "Disabling all rule" +puts "Disabling all expression" reset refute Flipper.enabled?(:something, user) -puts "\n\nNested Rule" -nested_rule = Flipper.any(admin_rule, all_rule) +puts "\n\nNested Expression" +nested_expression = Flipper.any(admin_expression, all_expression) refute Flipper.enabled?(:something, user) refute Flipper.enabled?(:something, admin_user) refute Flipper.enabled?(:something, other_user) -puts "Enabling nested rule" -Flipper.enable_rule :something, nested_rule +puts "Enabling nested expression" +Flipper.enable :something, nested_expression assert Flipper.enabled?(:something, user) assert Flipper.enabled?(:something, admin_user) refute Flipper.enabled?(:something, other_user) -puts "Disabling nested rule" +puts "Disabling nested expression" reset refute Flipper.enabled?(:something, user) refute Flipper.enabled?(:something, admin_user) refute Flipper.enabled?(:something, other_user) -puts "\n\nBoolean Rule" -boolean_rule = Flipper.value(true).eq(true) -Flipper.enable_rule :something, boolean_rule +puts "\n\nBoolean Expression" +boolean_expression = Flipper.value(true).eq(true) +Flipper.enable :something, boolean_expression assert Flipper.enabled?(:something) assert Flipper.enabled?(:something, user) reset -puts "\n\nSet of Actors Rule" -set_of_actors_rule = Flipper.any( +puts "\n\nSet of Actors Expression" +set_of_actors_expression = Flipper.any( Flipper.property(:flipper_id).eq("User;1"), Flipper.property(:flipper_id).eq("User;3"), ) -Flipper.enable_rule :something, set_of_actors_rule +Flipper.enable :something, set_of_actors_expression assert Flipper.enabled?(:something, user) assert Flipper.enabled?(:something, other_user) refute Flipper.enabled?(:something, admin_user) reset -puts "\n\n% of Actors Rule" +puts "\n\n% of Actors Expression" percentage_of_actors = Flipper.property(:flipper_id).percentage(30) -Flipper.enable_rule :something, percentage_of_actors +Flipper.enable :something, percentage_of_actors refute Flipper.enabled?(:something, user) refute Flipper.enabled?(:something, other_user) assert Flipper.enabled?(:something, admin_user) reset -puts "\n\n% of Actors Per Type Rule" +puts "\n\n% of Actors Per Type Expression" percentage_of_actors_per_type = Flipper.any( Flipper.all( Flipper.property(:type).eq("User"), @@ -166,16 +166,16 @@ class Org < Struct.new(:id, :flipper_properties) Flipper.property(:flipper_id).percentage(10), ) ) -Flipper.enable_rule :something, percentage_of_actors_per_type +Flipper.enable :something, percentage_of_actors_per_type refute Flipper.enabled?(:something, user) # not in the 40% enabled for Users assert Flipper.enabled?(:something, other_user) assert Flipper.enabled?(:something, admin_user) refute Flipper.enabled?(:something, org) # not in the 10% of enabled for Orgs reset -puts "\n\nPercentage of Time Rule" -percentage_of_time_rule = Flipper.random(100).lt(50) -Flipper.enable_rule :something, percentage_of_time_rule +puts "\n\nPercentage of Time Expression" +percentage_of_time_expression = Flipper.random(100).lt(50) +Flipper.enable :something, percentage_of_time_expression results = (1..10000).map { |n| Flipper.enabled?(:something, user) } enabled, disabled = results.partition { |r| r } p enabled: enabled.size @@ -184,30 +184,30 @@ class Org < Struct.new(:id, :flipper_properties) assert (4_700..5_200).include?(disabled.size) reset -puts "\n\nChanging single rule to all rule" -Flipper.enable_rule :something, plan_rule -Flipper.enable_rule :something, Flipper.rule(:something).all.add(age_rule) +puts "\n\nChanging single expression to all expression" +Flipper.enable :something, plan_expression +Flipper.enable :something, Flipper.expression(:something).all.add(age_expression) assert Flipper.enabled?(:something, user) refute Flipper.enabled?(:something, admin_user) refute Flipper.enabled?(:something, other_user) -puts "\n\nChanging single rule to any rule" -Flipper.enable_rule :something, plan_rule -Flipper.enable_rule :something, Flipper.rule(:something).any.add(age_rule, admin_rule) +puts "\n\nChanging single expression to any expression" +Flipper.enable :something, plan_expression +Flipper.enable :something, Flipper.expression(:something).any.add(age_expression, admin_expression) assert Flipper.enabled?(:something, user) assert Flipper.enabled?(:something, admin_user) refute Flipper.enabled?(:something, other_user) -puts "\n\nChanging single rule to any rule by adding to condition" -Flipper.enable_rule :something, plan_rule -Flipper.enable_rule :something, Flipper.rule(:something).add(admin_rule) +puts "\n\nChanging single expression to any expression by adding to condition" +Flipper.enable :something, plan_expression +Flipper.enable :something, Flipper.expression(:something).add(admin_expression) assert Flipper.enabled?(:something, user) assert Flipper.enabled?(:something, admin_user) refute Flipper.enabled?(:something, other_user) puts "\n\nEnabling based on time" -scheduled_time_rule = Flipper.property(:now).gte(NOW) -Flipper.enable_rule :something, scheduled_time_rule +scheduled_time_expression = Flipper.property(:now).gte(NOW) +Flipper.enable :something, scheduled_time_expression assert Flipper.enabled?(:something, user) assert Flipper.enabled?(:something, admin_user) refute Flipper.enabled?(:something, other_user) diff --git a/lib/flipper.rb b/lib/flipper.rb index df772f361..4c8642351 100644 --- a/lib/flipper.rb +++ b/lib/flipper.rb @@ -57,7 +57,8 @@ def instance=(flipper) # interface of Flipper::DSL. def_delegators :instance, :enabled?, :enable, :disable, :bool, :boolean, - :enable_rule, :disable_rule, :rule, :add_rule, :remove_rule, + :enable_expression, :disable_expression, + :expression, :add_expression, :remove_expression, :enable_actor, :disable_actor, :actor, :enable_group, :disable_group, :enable_percentage_of_actors, :disable_percentage_of_actors, diff --git a/lib/flipper/adapter.rb b/lib/flipper/adapter.rb index f56857fed..7ee3d9cd4 100644 --- a/lib/flipper/adapter.rb +++ b/lib/flipper/adapter.rb @@ -16,7 +16,7 @@ def default_config boolean: nil, groups: Set.new, actors: Set.new, - rule: nil, + expression: nil, percentage_of_actors: nil, percentage_of_time: nil, } diff --git a/lib/flipper/adapters/http.rb b/lib/flipper/adapters/http.rb index f9e6a20b1..52efbf0b4 100644 --- a/lib/flipper/adapters/http.rb +++ b/lib/flipper/adapters/http.rb @@ -133,7 +133,7 @@ def request_body_for_gate(gate, value) { flipper_id: value.to_s } when :percentage_of_actors, :percentage_of_time { percentage: value.to_s } - when :rule + when :expression value else raise "#{gate.key} is not a valid flipper gate key" diff --git a/lib/flipper/adapters/rollout.rb b/lib/flipper/adapters/rollout.rb index e56d702ea..370ce1889 100644 --- a/lib/flipper/adapters/rollout.rb +++ b/lib/flipper/adapters/rollout.rb @@ -53,7 +53,7 @@ def get(feature) actors: actors, percentage_of_actors: percentage_of_actors, percentage_of_time: nil, - rule: nil, + expression: nil, } end diff --git a/lib/flipper/adapters/sync/feature_synchronizer.rb b/lib/flipper/adapters/sync/feature_synchronizer.rb index 60cc213e8..29fddf9a8 100644 --- a/lib/flipper/adapters/sync/feature_synchronizer.rb +++ b/lib/flipper/adapters/sync/feature_synchronizer.rb @@ -9,7 +9,7 @@ class Sync class FeatureSynchronizer extend Forwardable - def_delegator :@local_gate_values, :rule, :local_rule + def_delegator :@local_gate_values, :expression, :local_expression def_delegator :@local_gate_values, :boolean, :local_boolean def_delegator :@local_gate_values, :actors, :local_actors def_delegator :@local_gate_values, :groups, :local_groups @@ -18,7 +18,7 @@ class FeatureSynchronizer def_delegator :@local_gate_values, :percentage_of_time, :local_percentage_of_time - def_delegator :@remote_gate_values, :rule, :remote_rule + def_delegator :@remote_gate_values, :expression, :remote_expression def_delegator :@remote_gate_values, :boolean, :remote_boolean def_delegator :@remote_gate_values, :actors, :remote_actors def_delegator :@remote_gate_values, :groups, :remote_groups @@ -44,7 +44,7 @@ def call @feature.disable if local_boolean_enabled? sync_groups sync_actors - sync_rule + sync_expression sync_percentage_of_actors sync_percentage_of_time end @@ -52,10 +52,10 @@ def call private - def sync_rule - return if local_rule == remote_rule + def sync_expression + return if local_expression == remote_expression - @feature.enable_rule remote_rule + @feature.enable_expression remote_expression end def sync_actors diff --git a/lib/flipper/api/middleware.rb b/lib/flipper/api/middleware.rb index 3dfc7eb13..dba00a7cc 100644 --- a/lib/flipper/api/middleware.rb +++ b/lib/flipper/api/middleware.rb @@ -14,7 +14,7 @@ def initialize(app, options = {}) @env_key = options.fetch(:env_key, 'flipper') @action_collection = ActionCollection.new - @action_collection.add Api::V1::Actions::RuleGate + @action_collection.add Api::V1::Actions::ExpressionGate @action_collection.add Api::V1::Actions::PercentageOfTimeGate @action_collection.add Api::V1::Actions::PercentageOfActorsGate @action_collection.add Api::V1::Actions::ActorsGate diff --git a/lib/flipper/api/v1/actions/rule_gate.rb b/lib/flipper/api/v1/actions/expression_gate.rb similarity index 67% rename from lib/flipper/api/v1/actions/rule_gate.rb rename to lib/flipper/api/v1/actions/expression_gate.rb index 7ae0212a7..db93c013e 100644 --- a/lib/flipper/api/v1/actions/rule_gate.rb +++ b/lib/flipper/api/v1/actions/expression_gate.rb @@ -5,17 +5,17 @@ module Flipper module Api module V1 module Actions - class RuleGate < Api::Action + class ExpressionGate < Api::Action include FeatureNameFromRoute - route %r{\A/features/(?.*)/rule/?\Z} + route %r{\A/features/(?.*)/expression/?\Z} def post feature = flipper[feature_name] begin - expression = Flipper::Expression.build(rule_hash) - feature.enable_rule expression + expression = Flipper::Expression.build(expression_hash) + feature.enable_expression expression decorated_feature = Decorators::Feature.new(feature) json_response(decorated_feature.as_json, 200) rescue NameError => exception @@ -25,7 +25,7 @@ def post def delete feature = flipper[feature_name] - feature.disable_rule + feature.disable_expression decorated_feature = Decorators::Feature.new(feature) json_response(decorated_feature.as_json, 200) @@ -33,8 +33,8 @@ def delete private - def rule_hash - @rule_hash ||= request.env["parsed_request_body".freeze] || {}.freeze + def expression_hash + @expression_hash ||= request.env["parsed_request_body".freeze] || {}.freeze end end end diff --git a/lib/flipper/dsl.rb b/lib/flipper/dsl.rb index c627a3f73..02cbe4d07 100644 --- a/lib/flipper/dsl.rb +++ b/lib/flipper/dsl.rb @@ -44,23 +44,23 @@ def enable(name, *args) feature(name).enable(*args) end - # Public: Enable a feature for a rule. + # Public: Enable a feature for an expression. # # name - The String or Symbol name of the feature. - # rule - a Flipper::Expression instance or a Hash. + # expression - a Flipper::Expression instance or a Hash. # # Returns result of Feature#enable. - def enable_rule(name, rule) - feature(name).enable_rule(rule) + def enable_expression(name, expression) + feature(name).enable_expression(expression) end - # Public: Add a rule to a feature. + # Public: Add an expression to a feature. # - # rule - a rule or Hash that can be converted to a rule. + # expression - an expression or Hash that can be converted to an expression. # # Returns result of enable. - def add_rule(name, rule) - feature(name).add_rule(rule) + def add_expression(name, expression) + feature(name).add_expression(expression) end # Public: Enable a feature for an actor. @@ -117,22 +117,22 @@ def disable(name, *args) feature(name).disable(*args) end - # Public: Disable rule for feature. + # Public: Disable expression for feature. # # name - The String or Symbol name of the feature. # # Returns result of Feature#disable. - def disable_rule(name) - feature(name).disable_rule + def disable_expression(name) + feature(name).disable_expression end - # Public: Remove a rule from a feature. + # Public: Remove an expression from a feature. # - # rule - a rule or Hash that can be converted to a rule. + # expression - an Expression or Hash that can be converted to an expression. # # Returns result of enable. - def remove_rule(name, rule) - feature(name).remove_rule(rule) + def remove_expression(name, expression) + feature(name).remove_expression(expression) end # Public: Disable a feature for an actor. @@ -280,13 +280,13 @@ def actor(thing) Types::Actor.new(thing) end - # Public: Gets the rule for the feature. + # Public: Gets the expression for the feature. # # name - The String or Symbol name of the feature. # # Returns an instance of Flipper::Expression. - def rule(name) - feature(name).rule + def expression(name) + feature(name).expression end # Public: Shortcut for getting a percentage of time instance. diff --git a/lib/flipper/expression.rb b/lib/flipper/expression.rb index 20c465f4c..8ae54b700 100644 --- a/lib/flipper/expression.rb +++ b/lib/flipper/expression.rb @@ -23,7 +23,7 @@ def self.build(object, convert_to_values: false) when Symbol convert_to_values ? Expressions::Value.new(object.to_s) : object.to_s else - raise ArgumentError, "#{object.inspect} cannot be converted into a rule expression" + raise ArgumentError, "#{object.inspect} cannot be converted into an expression" end end diff --git a/lib/flipper/feature.rb b/lib/flipper/feature.rb index d1174b634..ae62b5153 100644 --- a/lib/flipper/feature.rb +++ b/lib/flipper/feature.rb @@ -119,25 +119,25 @@ def enabled?(thing = nil) end end - # Public: Enables a rule for a feature. + # Public: Enables an expression_to_add for a feature. # - # rule - a rule or Hash that can be converted to a rule. + # expression - an Expression or Hash that can be converted to an expression. # # Returns result of enable. - def enable_rule(rule) - enable Expression.build(rule) + def enable_expression(expression) + enable Expression.build(expression) end - # Public: Add a rule for a feature. + # Public: Add an expression for a feature. # - # rule_to_add - a rule or Hash that can be converted to a rule. + # expression_to_add - an expression or Hash that can be converted to an expression. # # Returns result of enable. - def add_rule(rule_to_add) - if (current_rule = rule) - enable current_rule.add(rule_to_add) + def add_expression(expression_to_add) + if (current_expression = expression) + enable current_expression.add(expression_to_add) else - enable rule_to_add + enable expression_to_add end end @@ -181,24 +181,24 @@ def enable_percentage_of_actors(percentage) enable Types::PercentageOfActors.wrap(percentage) end - # Public: Disables a rule for a feature. + # Public: Disables an expression for a feature. # - # rule - a rule or Hash that can be converted to a rule. + # expression - an expression or Hash that can be converted to an expression. # # Returns result of disable. - def disable_rule - disable Flipper.all # just need a rule to clear + def disable_expression + disable Flipper.all # just need an expression to clear end - # Public: Remove a rule from a feature. Does nothing if no rule is + # Public: Remove an expression from a feature. Does nothing if no expression is # currently enabled. # - # rule_to_remove - a rule or Hash that can be converted to a rule. + # expression - an Expression or Hash that can be converted to an expression. # - # Returns result of enable or nil (if no rule enabled). - def remove_rule(rule_to_remove) - if (current_rule = rule) - enable current_rule.remove(rule_to_remove) + # Returns result of enable or nil (if no expression enabled). + def remove_expression(expression_to_remove) + if (current_expression = expression) + enable current_expression.remove(expression_to_remove) end end @@ -293,8 +293,8 @@ def disabled_groups Flipper.groups - enabled_groups end - def rule - Flipper::Expression.build(rule_value) if rule_value + def expression + Flipper::Expression.build(expression_value) if expression_value end # Public: Get the adapter value for the groups gate. @@ -304,11 +304,11 @@ def groups_value gate_values.groups end - # Public: Get the adapter value for the rule gate. + # Public: Get the adapter value for the expression gate. # - # Returns rule. - def rule_value - gate_values.rule + # Returns expression. + def expression_value + gate_values.expression end # Public: Get the adapter value for the actors gate. @@ -395,7 +395,7 @@ def inspect def gates @gates ||= [ Gates::Boolean.new, - Gates::Rule.new, + Gates::Expression.new, Gates::Actor.new, Gates::PercentageOfActors.new, Gates::PercentageOfTime.new, diff --git a/lib/flipper/gate.rb b/lib/flipper/gate.rb index 6997f13fa..1230ef588 100644 --- a/lib/flipper/gate.rb +++ b/lib/flipper/gate.rb @@ -59,4 +59,4 @@ def inspect require 'flipper/gates/group' require 'flipper/gates/percentage_of_actors' require 'flipper/gates/percentage_of_time' -require 'flipper/gates/rule' +require 'flipper/gates/expression' diff --git a/lib/flipper/gate_values.rb b/lib/flipper/gate_values.rb index 4c65d1cc4..96966bd83 100644 --- a/lib/flipper/gate_values.rb +++ b/lib/flipper/gate_values.rb @@ -9,7 +9,7 @@ class GateValues 'boolean' => '@boolean', 'actors' => '@actors', 'groups' => '@groups', - 'rule' => '@rule', + 'expression' => '@expression', 'percentage_of_time' => '@percentage_of_time', 'percentage_of_actors' => '@percentage_of_actors', }.freeze @@ -17,7 +17,7 @@ class GateValues attr_reader :boolean attr_reader :actors attr_reader :groups - attr_reader :rule + attr_reader :expression attr_reader :percentage_of_actors attr_reader :percentage_of_time @@ -25,7 +25,7 @@ def initialize(adapter_values) @boolean = Typecast.to_boolean(adapter_values[:boolean]) @actors = Typecast.to_set(adapter_values[:actors]) @groups = Typecast.to_set(adapter_values[:groups]) - @rule = adapter_values[:rule] + @expression = adapter_values[:expression] @percentage_of_actors = Typecast.to_percentage(adapter_values[:percentage_of_actors]) @percentage_of_time = Typecast.to_percentage(adapter_values[:percentage_of_time]) end @@ -41,7 +41,7 @@ def eql?(other) boolean == other.boolean && actors == other.actors && groups == other.groups && - rule == other.rule && + expression == other.expression && percentage_of_actors == other.percentage_of_actors && percentage_of_time == other.percentage_of_time end diff --git a/lib/flipper/gates/rule.rb b/lib/flipper/gates/expression.rb similarity index 95% rename from lib/flipper/gates/rule.rb rename to lib/flipper/gates/expression.rb index 5ffdb2a12..e8ac40b4b 100644 --- a/lib/flipper/gates/rule.rb +++ b/lib/flipper/gates/expression.rb @@ -2,15 +2,15 @@ module Flipper module Gates - class Rule < Gate + class Expression < Gate # Internal: The name of the gate. Used for instrumentation, etc. def name - :rule + :expression end # Internal: Name converted to value safe for adapter. def key - :rule + :expression end def data_type diff --git a/lib/flipper/model/active_record.rb b/lib/flipper/model/active_record.rb index 76eee215f..b334ec11f 100644 --- a/lib/flipper/model/active_record.rb +++ b/lib/flipper/model/active_record.rb @@ -3,7 +3,7 @@ module Model module ActiveRecord include Flipper::Identifier - # Properties used to evaluate rules + # Properties used to evaluate expressions def flipper_properties {"type" => self.class.name}.merge(attributes) end diff --git a/lib/flipper/model/sequel.rb b/lib/flipper/model/sequel.rb index 344cc15af..7630f0ba0 100644 --- a/lib/flipper/model/sequel.rb +++ b/lib/flipper/model/sequel.rb @@ -3,7 +3,7 @@ module Model module Sequel include Flipper::Identifier - # Properties used to evaluate rules + # Properties used to evaluate expressions def flipper_properties {"type" => self.class.name}.update(to_hash.transform_keys(&:to_s)) end diff --git a/lib/flipper/spec/shared_adapter_specs.rb b/lib/flipper/spec/shared_adapter_specs.rb index 5c46e6572..0a1bc73d3 100644 --- a/lib/flipper/spec/shared_adapter_specs.rb +++ b/lib/flipper/spec/shared_adapter_specs.rb @@ -4,12 +4,12 @@ let(:flipper) { Flipper.new(subject) } let(:feature) { flipper[:stats] } - let(:boolean_gate) { feature.gate(:boolean) } - let(:rule_gate) { feature.gate(:rule) } - let(:group_gate) { feature.gate(:group) } - let(:actor_gate) { feature.gate(:actor) } - let(:actors_gate) { feature.gate(:percentage_of_actors) } - let(:time_gate) { feature.gate(:percentage_of_time) } + let(:boolean_gate) { feature.gate(:boolean) } + let(:expression_gate) { feature.gate(:expression) } + let(:group_gate) { feature.gate(:group) } + let(:actor_gate) { feature.gate(:actor) } + let(:actors_gate) { feature.gate(:percentage_of_actors) } + let(:time_gate) { feature.gate(:percentage_of_time) } before do Flipper.register(:admins) do |actor| @@ -62,22 +62,22 @@ expect(subject.get(feature)).to eq(subject.default_config) end - it 'can enable, disable and get value for rule gate' do - basic_rule = Flipper.property(:plan).eq("basic") - age_rule = Flipper.property(:age).gte(21) - any_rule = Flipper.any(basic_rule, age_rule) + it 'can enable, disable and get value for expression gate' do + basic_expression = Flipper.property(:plan).eq("basic") + age_expression = Flipper.property(:age).gte(21) + any_expression = Flipper.any(basic_expression, age_expression) - expect(subject.enable(feature, rule_gate, any_rule)).to eq(true) + expect(subject.enable(feature, expression_gate, any_expression)).to eq(true) result = subject.get(feature) - expect(result[:rule]).to eq(any_rule.value) + expect(result[:expression]).to eq(any_expression.value) - expect(subject.enable(feature, rule_gate, basic_rule)).to eq(true) + expect(subject.enable(feature, expression_gate, basic_expression)).to eq(true) result = subject.get(feature) - expect(result[:rule]).to eq(basic_rule.value) + expect(result[:expression]).to eq(basic_expression.value) - expect(subject.disable(feature, rule_gate, basic_rule)).to eq(true) + expect(subject.disable(feature, expression_gate, basic_expression)).to eq(true) result = subject.get(feature) - expect(result[:rule]).to be(nil) + expect(result[:expression]).to be(nil) end it 'can enable, disable and get value for group gate' do diff --git a/lib/flipper/test/shared_adapter_test.rb b/lib/flipper/test/shared_adapter_test.rb index ef2fd065b..12375fb24 100644 --- a/lib/flipper/test/shared_adapter_test.rb +++ b/lib/flipper/test/shared_adapter_test.rb @@ -7,7 +7,7 @@ def setup @feature = @flipper[:stats] @boolean_gate = @feature.gate(:boolean) @group_gate = @feature.gate(:group) - @rule_gate = @feature.gate(:rule) + @expression_gate = @feature.gate(:expression) @actor_gate = @feature.gate(:actor) @actors_gate = @feature.gate(:percentage_of_actors) @time_gate = @feature.gate(:percentage_of_time) @@ -57,22 +57,22 @@ def test_fully_disables_all_enabled_things_when_boolean_gate_disabled assert_equal @adapter.default_config, @adapter.get(@feature) end - def test_can_enable_disable_and_get_value_for_rule_gate - basic_rule = Flipper.property(:plan).eq("basic") - age_rule = Flipper.property(:age).gte(21) - any_rule = Flipper.any(basic_rule, age_rule) + def test_can_enable_disable_and_get_value_for_expression_gate + basic_expression = Flipper.property(:plan).eq("basic") + age_expression = Flipper.property(:age).gte(21) + any_expression = Flipper.any(basic_expression, age_expression) - assert_equal true, @adapter.enable(@feature, @rule_gate, any_rule) + assert_equal true, @adapter.enable(@feature, @expression_gate, any_expression) result = @adapter.get(@feature) - assert_equal any_rule.value, result[:rule] + assert_equal any_expression.value, result[:expression] - assert_equal true, @adapter.enable(@feature, @rule_gate, basic_rule) + assert_equal true, @adapter.enable(@feature, @expression_gate, basic_expression) result = @adapter.get(@feature) - assert_equal basic_rule.value, result[:rule] + assert_equal basic_expression.value, result[:expression] - assert_equal true, @adapter.disable(@feature, @rule_gate, basic_rule) + assert_equal true, @adapter.disable(@feature, @expression_gate, basic_expression) result = @adapter.get(@feature) - assert_nil result[:rule] + assert_nil result[:expression] end def test_can_enable_disable_get_value_for_group_gate diff --git a/spec/flipper/adapter_spec.rb b/spec/flipper/adapter_spec.rb index 12888b5d3..629b55ca5 100644 --- a/spec/flipper/adapter_spec.rb +++ b/spec/flipper/adapter_spec.rb @@ -8,7 +8,7 @@ boolean: nil, groups: Set.new, actors: Set.new, - rule: nil, + expression: nil, percentage_of_actors: nil, percentage_of_time: nil, } diff --git a/spec/flipper/adapters/read_only_spec.rb b/spec/flipper/adapters/read_only_spec.rb index 64729e953..55b5dffed 100644 --- a/spec/flipper/adapters/read_only_spec.rb +++ b/spec/flipper/adapters/read_only_spec.rb @@ -6,12 +6,12 @@ let(:flipper) { Flipper.new(subject) } let(:feature) { flipper[:stats] } - let(:boolean_gate) { feature.gate(:boolean) } - let(:group_gate) { feature.gate(:group) } - let(:actor_gate) { feature.gate(:actor) } - let(:rule_gate) { feature.gate(:rule) } - let(:actors_gate) { feature.gate(:percentage_of_actors) } - let(:time_gate) { feature.gate(:percentage_of_time) } + let(:boolean_gate) { feature.gate(:boolean) } + let(:group_gate) { feature.gate(:group) } + let(:actor_gate) { feature.gate(:actor) } + let(:expression_gate) { feature.gate(:expression) } + let(:actors_gate) { feature.gate(:percentage_of_actors) } + let(:time_gate) { feature.gate(:percentage_of_time) } subject { described_class.new(adapter) } @@ -43,20 +43,20 @@ end it 'can get feature' do - rule = Flipper.property(:plan).eq("basic") + expression = Flipper.property(:plan).eq("basic") actor22 = Flipper::Actor.new('22') adapter.enable(feature, boolean_gate, flipper.boolean) adapter.enable(feature, group_gate, flipper.group(:admins)) adapter.enable(feature, actor_gate, flipper.actor(actor22)) adapter.enable(feature, actors_gate, flipper.actors(25)) adapter.enable(feature, time_gate, flipper.time(45)) - adapter.enable(feature, rule_gate, rule) + adapter.enable(feature, expression_gate, expression) expect(subject.get(feature)).to eq({ boolean: 'true', groups: Set['admins'], actors: Set['22'], - rule: { + expression: { "Equal" => [ {"Property" => ["plan"]}, {"Value" => ["basic"]}, diff --git a/spec/flipper/adapters/rollout_spec.rb b/spec/flipper/adapters/rollout_spec.rb index 0c729ab0b..cdfcd54f5 100644 --- a/spec/flipper/adapters/rollout_spec.rb +++ b/spec/flipper/adapters/rollout_spec.rb @@ -37,7 +37,7 @@ boolean: nil, groups: Set.new([:admins]), actors: Set.new(["1"]), - rule: nil, + expression: nil, percentage_of_actors: 20.0, percentage_of_time: nil, } @@ -51,7 +51,7 @@ boolean: true, groups: Set.new, actors: Set.new, - rule: nil, + expression: nil, percentage_of_actors: nil, percentage_of_time: nil, } @@ -67,7 +67,7 @@ boolean: true, groups: Set.new, actors: Set.new, - rule: nil, + expression: nil, percentage_of_actors: nil, percentage_of_time: nil, } diff --git a/spec/flipper/adapters/sync/feature_synchronizer_spec.rb b/spec/flipper/adapters/sync/feature_synchronizer_spec.rb index 331b4c0af..1220af7c8 100644 --- a/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +++ b/spec/flipper/adapters/sync/feature_synchronizer_spec.rb @@ -9,8 +9,8 @@ end let(:flipper) { Flipper.new(adapter) } let(:feature) { flipper[:search] } - let(:plan_rule) { Flipper.property(:plan).eq("basic") } - let(:age_rule) { Flipper.property(:age).gte(21) } + let(:plan_expression) { Flipper.property(:plan).eq("basic") } + let(:age_expression) { Flipper.property(:age).gte(21) } context "when remote disabled" do let(:remote) { Flipper::GateValues.new({}) } @@ -66,7 +66,7 @@ boolean: nil, actors: Set["1"], groups: Set["staff"], - rule: plan_rule.value, + expression: plan_expression.value, percentage_of_time: 10, percentage_of_actors: 15, } @@ -78,31 +78,31 @@ expect(local_gate_values_hash.fetch(:boolean)).to be(nil) expect(local_gate_values_hash.fetch(:actors)).to eq(Set["1"]) expect(local_gate_values_hash.fetch(:groups)).to eq(Set["staff"]) - expect(local_gate_values_hash.fetch(:rule)).to eq(plan_rule.value) + expect(local_gate_values_hash.fetch(:expression)).to eq(plan_expression.value) expect(local_gate_values_hash.fetch(:percentage_of_time)).to eq("10") expect(local_gate_values_hash.fetch(:percentage_of_actors)).to eq("15") end - it "updates rule when remote is updated" do - any_rule = Flipper.any(plan_rule, age_rule) - remote = Flipper::GateValues.new(rule: any_rule.value) - feature.enable_rule(age_rule) + it "updates expression when remote is updated" do + any_expression = Flipper.any(plan_expression, age_expression) + remote = Flipper::GateValues.new(expression: any_expression.value) + feature.enable_expression(age_expression) adapter.reset described_class.new(feature, feature.gate_values, remote).call - expect(feature.rule_value).to eq(any_rule.value) + expect(feature.expression_value).to eq(any_expression.value) expect_only_enable end - it "does nothing to rule if in sync" do - remote = Flipper::GateValues.new(rule: plan_rule.value) - feature.enable_rule(plan_rule) + it "does nothing to expression if in sync" do + remote = Flipper::GateValues.new(expression: plan_expression.value) + feature.enable_expression(plan_expression) adapter.reset described_class.new(feature, feature.gate_values, remote).call - expect(feature.rule_value).to eq(plan_rule.value) + expect(feature.expression_value).to eq(plan_expression.value) expect_no_enable_or_disable end diff --git a/spec/flipper/api/v1/actions/feature_spec.rb b/spec/flipper/api/v1/actions/feature_spec.rb index 0bd2435df..00f33f7ef 100644 --- a/spec/flipper/api/v1/actions/feature_spec.rb +++ b/spec/flipper/api/v1/actions/feature_spec.rb @@ -23,8 +23,8 @@ 'value' => 'true', }, { - 'key' => 'rule', - 'name' => 'rule', + 'key' => 'expression', + 'name' => 'expression', 'value' => nil, }, { @@ -72,8 +72,8 @@ 'value' => nil, }, { - 'key' => 'rule', - 'name' => 'rule', + 'key' => 'expression', + 'name' => 'expression', 'value' => nil, }, { @@ -137,8 +137,8 @@ 'value' => 'true', }, { - 'key' => 'rule', - 'name' => 'rule', + 'key' => 'expression', + 'name' => 'expression', 'value' => nil, }, { @@ -186,8 +186,8 @@ 'value' => 'true', }, { - 'key' => 'rule', - 'name' => 'rule', + 'key' => 'expression', + 'name' => 'expression', 'value' => nil, }, { diff --git a/spec/flipper/api/v1/actions/features_spec.rb b/spec/flipper/api/v1/actions/features_spec.rb index 43be9c50e..ea7943bfa 100644 --- a/spec/flipper/api/v1/actions/features_spec.rb +++ b/spec/flipper/api/v1/actions/features_spec.rb @@ -26,8 +26,8 @@ 'value' => 'true', }, { - 'key' => 'rule', - 'name' => 'rule', + 'key' => 'expression', + 'name' => 'expression', 'value' => nil, }, { @@ -126,8 +126,8 @@ 'value' => nil, }, { - 'key' => 'rule', - 'name' => 'rule', + 'key' => 'expression', + 'name' => 'expression', 'value' => nil, }, { diff --git a/spec/flipper/api/v1/actions/rule_gate_spec.rb b/spec/flipper/api/v1/actions/rule_gate_spec.rb index 4c0506450..22093d95c 100644 --- a/spec/flipper/api/v1/actions/rule_gate_spec.rb +++ b/spec/flipper/api/v1/actions/rule_gate_spec.rb @@ -1,6 +1,6 @@ require 'helper' -RSpec.describe Flipper::Api::V1::Actions::RuleGate do +RSpec.describe Flipper::Api::V1::Actions::ExpressionGate do let(:app) { build_api(flipper) } let(:actor) { Flipper::Actor.new('1', { @@ -8,127 +8,127 @@ "age" => 21, }) } - let(:rule) { Flipper.property(:plan).eq("basic") } + let(:expression) { Flipper.property(:plan).eq("basic") } describe 'enable' do before do - flipper[:my_feature].disable_rule - post '/features/my_feature/rule', JSON.dump(rule.value), + flipper[:my_feature].disable_expression + post '/features/my_feature/expression', JSON.dump(expression.value), "CONTENT_TYPE" => "application/json" end - it 'enables feature for rule' do + it 'enables feature for expression' do expect(last_response.status).to eq(200) expect(flipper[:my_feature].enabled?(actor)).to be_truthy - expect(flipper[:my_feature].enabled_gate_names).to eq([:rule]) + expect(flipper[:my_feature].enabled_gate_names).to eq([:expression]) end - it 'returns decorated feature with rule enabled' do - gate = json_response['gates'].find { |gate| gate['key'] == 'rule' } - expect(gate['value']).to eq(rule.value) + it 'returns decorated feature with expression enabled' do + gate = json_response['gates'].find { |gate| gate['key'] == 'expression' } + expect(gate['value']).to eq(expression.value) end end describe 'disable' do before do - flipper[:my_feature].enable_rule(rule) - delete '/features/my_feature/rule', JSON.dump({}), + flipper[:my_feature].enable_expression(expression) + delete '/features/my_feature/expression', JSON.dump({}), "CONTENT_TYPE" => "application/json" end - it 'disables rule for feature' do + it 'disables expression for feature' do expect(last_response.status).to eq(200) expect(flipper[:my_feature].enabled?(actor)).to be_falsy expect(flipper[:my_feature].enabled_gate_names).to be_empty end - it 'returns decorated feature with rule gate disabled' do - gate = json_response['gates'].find { |gate| gate['key'] == 'rule' } + it 'returns decorated feature with expression gate disabled' do + gate = json_response['gates'].find { |gate| gate['key'] == 'expression' } expect(gate['value']).to be(nil) end end describe 'enable feature with slash in name' do before do - flipper["my/feature"].disable_rule - post '/features/my/feature/rule', JSON.dump(rule.value), + flipper["my/feature"].disable_expression + post '/features/my/feature/expression', JSON.dump(expression.value), "CONTENT_TYPE" => "application/json" end - it 'enables feature for rule' do + it 'enables feature for expression' do expect(last_response.status).to eq(200) expect(flipper["my/feature"].enabled?(actor)).to be_truthy - expect(flipper["my/feature"].enabled_gate_names).to eq([:rule]) + expect(flipper["my/feature"].enabled_gate_names).to eq([:expression]) end - it 'returns decorated feature with rule enabled' do - gate = json_response['gates'].find { |gate| gate['key'] == 'rule' } - expect(gate['value']).to eq(rule.value) + it 'returns decorated feature with expression enabled' do + gate = json_response['gates'].find { |gate| gate['key'] == 'expression' } + expect(gate['value']).to eq(expression.value) end end describe 'enable feature with space in name' do before do - flipper["sp ace"].disable_rule - post '/features/sp%20ace/rule', JSON.dump(rule.value), + flipper["sp ace"].disable_expression + post '/features/sp%20ace/expression', JSON.dump(expression.value), "CONTENT_TYPE" => "application/json" end - it 'enables feature for rule' do + it 'enables feature for expression' do expect(last_response.status).to eq(200) expect(flipper["sp ace"].enabled?(actor)).to be_truthy - expect(flipper["sp ace"].enabled_gate_names).to eq([:rule]) + expect(flipper["sp ace"].enabled_gate_names).to eq([:expression]) end - it 'returns decorated feature with rule enabled' do - gate = json_response['gates'].find { |gate| gate['key'] == 'rule' } - expect(gate['value']).to eq(rule.value) + it 'returns decorated feature with expression enabled' do + gate = json_response['gates'].find { |gate| gate['key'] == 'expression' } + expect(gate['value']).to eq(expression.value) end end describe 'enable with invalid data' do before do data = {"blah" => "blah"} - post '/features/my_feature/rule', JSON.dump(data), + post '/features/my_feature/expression', JSON.dump(data), "CONTENT_TYPE" => "application/json" end it 'returns correct error response' do expect(last_response.status).to eq(422) - expect(json_response).to eq(api_rule_invalid_response) + expect(json_response).to eq(api_expression_invalid_response) end end describe 'enable missing feature' do before do - post '/features/my_feature/rule', JSON.dump(rule.value), "CONTENT_TYPE" => "application/json" + post '/features/my_feature/expression', JSON.dump(expression.value), "CONTENT_TYPE" => "application/json" end - it 'enables rule for feature' do + it 'enables expression for feature' do expect(last_response.status).to eq(200) expect(flipper[:my_feature].enabled?(actor)).to be_truthy - expect(flipper[:my_feature].enabled_gate_names).to eq([:rule]) + expect(flipper[:my_feature].enabled_gate_names).to eq([:expression]) end - it 'returns decorated feature with rule enabled' do - gate = json_response['gates'].find { |gate| gate['key'] == 'rule' } - expect(gate['value']).to eq(rule.value) + it 'returns decorated feature with expression enabled' do + gate = json_response['gates'].find { |gate| gate['key'] == 'expression' } + expect(gate['value']).to eq(expression.value) end end describe 'disable missing feature' do before do - delete '/features/my_feature/rule', "CONTENT_TYPE" => "application/json" + delete '/features/my_feature/expression', "CONTENT_TYPE" => "application/json" end - it 'disables rule for feature' do + it 'disables expression for feature' do expect(last_response.status).to eq(200) expect(flipper[:my_feature].enabled?(actor)).to be_falsy expect(flipper[:my_feature].enabled_gate_names).to be_empty end - it 'returns decorated feature with rule gate disabled' do - gate = json_response['gates'].find { |gate| gate['key'] == 'rule' } + it 'returns decorated feature with expression gate disabled' do + gate = json_response['gates'].find { |gate| gate['key'] == 'expression' } expect(gate['value']).to be(nil) end end diff --git a/spec/flipper/cloud_spec.rb b/spec/flipper/cloud_spec.rb index cec7f56ef..4ee64e029 100644 --- a/spec/flipper/cloud_spec.rb +++ b/spec/flipper/cloud_spec.rb @@ -129,10 +129,10 @@ cloud_flipper = Flipper::Cloud.new(token: "asdf") get_all = { - "logging" => {actors: Set.new, boolean: nil, groups: Set.new, rule: nil, percentage_of_actors: nil, percentage_of_time: "5"}, - "search" => {actors: Set.new, boolean: "true", groups: Set.new, rule: nil, percentage_of_actors: nil, percentage_of_time: nil}, - "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, rule: nil, percentage_of_actors: nil, percentage_of_time: nil}, - "test" => {actors: Set.new, boolean: "true", groups: Set.new, rule: nil, percentage_of_actors: nil, percentage_of_time: nil}, + "logging" => {actors: Set.new, boolean: nil, groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: "5"}, + "search" => {actors: Set.new, boolean: "true", groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil}, + "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil}, + "test" => {actors: Set.new, boolean: "true", groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil}, } expect(flipper.adapter.get_all).to eq(get_all) @@ -155,10 +155,10 @@ cloud_flipper = Flipper::Cloud.new(token: "asdf") get_all = { - "logging" => {actors: Set.new, boolean: nil, groups: Set.new, rule: nil, percentage_of_actors: nil, percentage_of_time: "5"}, - "search" => {actors: Set.new, boolean: "true", groups: Set.new, rule: nil, percentage_of_actors: nil, percentage_of_time: nil}, - "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, rule: nil, percentage_of_actors: nil, percentage_of_time: nil}, - "test" => {actors: Set.new, boolean: "true", groups: Set.new, rule: nil, percentage_of_actors: nil, percentage_of_time: nil}, + "logging" => {actors: Set.new, boolean: nil, groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: "5"}, + "search" => {actors: Set.new, boolean: "true", groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil}, + "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil}, + "test" => {actors: Set.new, boolean: "true", groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil}, } expect(flipper.adapter.get_all).to eq(get_all) @@ -180,10 +180,10 @@ cloud_flipper = Flipper::Cloud.new(token: "asdf") get_all = { - "logging" => {actors: Set.new, boolean: nil, groups: Set.new, rule: nil, percentage_of_actors: nil, percentage_of_time: "5"}, - "search" => {actors: Set.new, boolean: "true", groups: Set.new, rule: nil, percentage_of_actors: nil, percentage_of_time: nil}, - "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, rule: nil, percentage_of_actors: nil, percentage_of_time: nil}, - "test" => {actors: Set.new, boolean: "true", groups: Set.new, rule: nil, percentage_of_actors: nil, percentage_of_time: nil}, + "logging" => {actors: Set.new, boolean: nil, groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: "5"}, + "search" => {actors: Set.new, boolean: "true", groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil}, + "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil}, + "test" => {actors: Set.new, boolean: "true", groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil}, } expect(flipper.adapter.get_all).to eq(get_all) diff --git a/spec/flipper/dsl_spec.rb b/spec/flipper/dsl_spec.rb index 637af58b7..e4cf91d39 100644 --- a/spec/flipper/dsl_spec.rb +++ b/spec/flipper/dsl_spec.rb @@ -139,15 +139,15 @@ end end - describe '#rule' do - it "returns nil if feature has no rule" do - expect(subject.rule(:stats)).to be(nil) + describe '#expression' do + it "returns nil if feature has no expression" do + expect(subject.expression(:stats)).to be(nil) end - it "returns rule if feature has rule" do - rule = Flipper.property(:plan).eq("basic") - subject[:stats].enable_rule rule - expect(subject.rule(:stats)).to eq(rule) + it "returns expression if feature has expression" do + expression = Flipper.property(:plan).eq("basic") + subject[:stats].enable_expression expression + expect(subject.expression(:stats)).to eq(expression) end end @@ -249,30 +249,30 @@ end end - describe '#enable_rule/disable_rule' do - it 'enables and disables the feature for the rule' do - rule = Flipper.property(:plan).eq("basic") + describe '#enable_expression/disable_expression' do + it 'enables and disables the feature for the expression' do + expression = Flipper.property(:plan).eq("basic") - expect(subject[:stats].rule).to be(nil) - subject.enable_rule(:stats, rule) - expect(subject[:stats].rule).to eq(rule) + expect(subject[:stats].expression).to be(nil) + subject.enable_expression(:stats, expression) + expect(subject[:stats].expression).to eq(expression) - subject.disable_rule(:stats) - expect(subject[:stats].rule).to be(nil) + subject.disable_expression(:stats) + expect(subject[:stats].expression).to be(nil) end end - describe '#add_rule/remove_rule' do - it 'enables and disables the feature for the rule' do + describe '#add_expression/remove_expression' do + it 'enables and disables the feature for the expression' do expression = Flipper.property(:plan).eq("basic") - rule = Flipper.any(expression) + any_expression = Flipper.any(expression) - expect(subject[:stats].rule).to be(nil) - subject.add_rule(:stats, rule) - expect(subject[:stats].rule).to eq(rule) + expect(subject[:stats].expression).to be(nil) + subject.add_expression(:stats, any_expression) + expect(subject[:stats].expression).to eq(any_expression) - subject.remove_rule(:stats, expression) - expect(subject[:stats].rule).to eq(Flipper.any) + subject.remove_expression(:stats, expression) + expect(subject[:stats].expression).to eq(Flipper.any) end end diff --git a/spec/flipper/feature_spec.rb b/spec/flipper/feature_spec.rb index e12567a88..3bfd26671 100644 --- a/spec/flipper/feature_spec.rb +++ b/spec/flipper/feature_spec.rb @@ -650,358 +650,358 @@ end end - describe '#rule' do - it "returns nil if feature has no rule" do - expect(subject.rule).to be(nil) + describe '#expression' do + it "returns nil if feature has no expression" do + expect(subject.expression).to be(nil) end - it "returns rule if feature has rule" do - rule = Flipper.property(:plan).eq("basic") - subject.enable_rule rule - expect(subject.rule).to eq(rule) + it "returns expression if feature has expression" do + expression = Flipper.property(:plan).eq("basic") + subject.enable_expression expression + expect(subject.expression).to eq(expression) end end - describe '#enable_rule/disable_rule' do - context "with rule instance" do - it "updates gate values to equal rule or clears rule" do - rule = Flipper.property(:plan).eq("basic") - other_rule = Flipper.property(:age).gte(21) - expect(subject.gate_values.rule).to be(nil) - subject.enable_rule(rule) - expect(subject.gate_values.rule).to eq(rule.value) - subject.disable_rule - expect(subject.gate_values.rule).to be(nil) + describe '#enable_expression/disable_expression' do + context "with expression instance" do + it "updates gate values to equal expression or clears expression" do + expression = Flipper.property(:plan).eq("basic") + other_expression = Flipper.property(:age).gte(21) + expect(subject.gate_values.expression).to be(nil) + subject.enable_expression(expression) + expect(subject.gate_values.expression).to eq(expression.value) + subject.disable_expression + expect(subject.gate_values.expression).to be(nil) end end context "with Hash" do - it "updates gate values to equal rule or clears rule" do - rule = Flipper.property(:plan).eq("basic") - other_rule = Flipper.property(:age).gte(21) - expect(subject.gate_values.rule).to be(nil) - subject.enable_rule(rule.value) - expect(subject.gate_values.rule).to eq(rule.value) - subject.disable_rule - expect(subject.gate_values.rule).to be(nil) + it "updates gate values to equal expression or clears expression" do + expression = Flipper.property(:plan).eq("basic") + other_expression = Flipper.property(:age).gte(21) + expect(subject.gate_values.expression).to be(nil) + subject.enable_expression(expression.value) + expect(subject.gate_values.expression).to eq(expression.value) + subject.disable_expression + expect(subject.gate_values.expression).to be(nil) end end end - describe "#add_rule" do + describe "#add_expression" do context "when nothing enabled" do context "with Expression instance" do - it "sets rule to Expression" do - rule = Flipper.property(:plan).eq("basic") - subject.add_rule(rule) - expect(subject.rule).to be_instance_of(Flipper::Expressions::Equal) - expect(subject.rule).to eq(rule) + it "sets expression to Expression" do + expression = Flipper.property(:plan).eq("basic") + subject.add_expression(expression) + expect(subject.expression).to be_instance_of(Flipper::Expressions::Equal) + expect(subject.expression).to eq(expression) end end context "with Any instance" do - it "sets rule to Any" do - rule = Flipper.any(Flipper.property(:plan).eq("basic")) - subject.add_rule(rule) - expect(subject.rule).to be_instance_of(Flipper::Expressions::Any) - expect(subject.rule).to eq(rule) + it "sets expression to Any" do + expression = Flipper.any(Flipper.property(:plan).eq("basic")) + subject.add_expression(expression) + expect(subject.expression).to be_instance_of(Flipper::Expressions::Any) + expect(subject.expression).to eq(expression) end end context "with All instance" do - it "sets rule to All" do - rule = Flipper.all(Flipper.property(:plan).eq("basic")) - subject.add_rule(rule) - expect(subject.rule).to be_instance_of(Flipper::Expressions::All) - expect(subject.rule).to eq(rule) + it "sets expression to All" do + expression = Flipper.all(Flipper.property(:plan).eq("basic")) + subject.add_expression(expression) + expect(subject.expression).to be_instance_of(Flipper::Expressions::All) + expect(subject.expression).to eq(expression) end end end context "when Expression enabled" do - let(:rule) { Flipper.property(:plan).eq("basic") } + let(:expression) { Flipper.property(:plan).eq("basic") } before do - subject.enable_rule rule + subject.enable_expression expression end context "with Expression instance" do - it "changes rule to Any and adds new Expression" do - new_rule = Flipper.property(:age).gte(21) - subject.add_rule(new_rule) - expect(subject.rule).to be_instance_of(Flipper::Expressions::Any) - expect(subject.rule.args).to include(rule) - expect(subject.rule.args).to include(new_rule) + it "changes expression to Any and adds new Expression" do + new_expression = Flipper.property(:age).gte(21) + subject.add_expression(new_expression) + expect(subject.expression).to be_instance_of(Flipper::Expressions::Any) + expect(subject.expression.args).to include(expression) + expect(subject.expression.args).to include(new_expression) end end context "with Any instance" do - it "changes rule to Any and adds new Any" do - new_rule = Flipper.any(Flipper.property(:age).eq(21)) - subject.add_rule new_rule - expect(subject.rule).to be_instance_of(Flipper::Expressions::Any) - expect(subject.rule.args).to include(rule) - expect(subject.rule.args).to include(new_rule) + it "changes expression to Any and adds new Any" do + new_expression = Flipper.any(Flipper.property(:age).eq(21)) + subject.add_expression new_expression + expect(subject.expression).to be_instance_of(Flipper::Expressions::Any) + expect(subject.expression.args).to include(expression) + expect(subject.expression.args).to include(new_expression) end end context "with All instance" do - it "changes rule to Any and adds new All" do - new_rule = Flipper.all(Flipper.property(:plan).eq("basic")) - subject.add_rule new_rule - expect(subject.rule).to be_instance_of(Flipper::Expressions::Any) - expect(subject.rule.args).to include(rule) - expect(subject.rule.args).to include(new_rule) + it "changes expression to Any and adds new All" do + new_expression = Flipper.all(Flipper.property(:plan).eq("basic")) + subject.add_expression new_expression + expect(subject.expression).to be_instance_of(Flipper::Expressions::Any) + expect(subject.expression.args).to include(expression) + expect(subject.expression.args).to include(new_expression) end end end context "when Any enabled" do let(:condition) { Flipper.property(:plan).eq("basic") } - let(:rule) { Flipper.any(condition) } + let(:expression) { Flipper.any(condition) } before do - subject.enable_rule rule + subject.enable_expression expression end context "with Expression instance" do it "adds Expression to Any" do - new_rule = Flipper.property(:age).gte(21) - subject.add_rule(new_rule) - expect(subject.rule).to be_instance_of(Flipper::Expressions::Any) - expect(subject.rule.args).to include(condition) - expect(subject.rule.args).to include(new_rule) + new_expression = Flipper.property(:age).gte(21) + subject.add_expression(new_expression) + expect(subject.expression).to be_instance_of(Flipper::Expressions::Any) + expect(subject.expression.args).to include(condition) + expect(subject.expression.args).to include(new_expression) end end context "with Any instance" do it "adds Any to Any" do - new_rule = Flipper.any(Flipper.property(:age).gte(21)) - subject.add_rule(new_rule) - expect(subject.rule).to be_instance_of(Flipper::Expressions::Any) - expect(subject.rule.args).to include(condition) - expect(subject.rule.args).to include(new_rule) + new_expression = Flipper.any(Flipper.property(:age).gte(21)) + subject.add_expression(new_expression) + expect(subject.expression).to be_instance_of(Flipper::Expressions::Any) + expect(subject.expression.args).to include(condition) + expect(subject.expression.args).to include(new_expression) end end context "with All instance" do it "adds All to Any" do - new_rule = Flipper.all(Flipper.property(:age).gte(21)) - subject.add_rule(new_rule) - expect(subject.rule).to be_instance_of(Flipper::Expressions::Any) - expect(subject.rule.args).to include(condition) - expect(subject.rule.args).to include(new_rule) + new_expression = Flipper.all(Flipper.property(:age).gte(21)) + subject.add_expression(new_expression) + expect(subject.expression).to be_instance_of(Flipper::Expressions::Any) + expect(subject.expression.args).to include(condition) + expect(subject.expression.args).to include(new_expression) end end end context "when All enabled" do let(:condition) { Flipper.property(:plan).eq("basic") } - let(:rule) { Flipper.all(condition) } + let(:expression) { Flipper.all(condition) } before do - subject.enable_rule rule + subject.enable_expression expression end context "with Expression instance" do it "adds Expression to All" do - new_rule = Flipper.property(:age).gte(21) - subject.add_rule(new_rule) - expect(subject.rule).to be_instance_of(Flipper::Expressions::All) - expect(subject.rule.args).to include(condition) - expect(subject.rule.args).to include(new_rule) + new_expression = Flipper.property(:age).gte(21) + subject.add_expression(new_expression) + expect(subject.expression).to be_instance_of(Flipper::Expressions::All) + expect(subject.expression.args).to include(condition) + expect(subject.expression.args).to include(new_expression) end end context "with Any instance" do it "adds Any to All" do - new_rule = Flipper.any(Flipper.property(:age).gte(21)) - subject.add_rule(new_rule) - expect(subject.rule).to be_instance_of(Flipper::Expressions::All) - expect(subject.rule.args).to include(condition) - expect(subject.rule.args).to include(new_rule) + new_expression = Flipper.any(Flipper.property(:age).gte(21)) + subject.add_expression(new_expression) + expect(subject.expression).to be_instance_of(Flipper::Expressions::All) + expect(subject.expression.args).to include(condition) + expect(subject.expression.args).to include(new_expression) end end context "with All instance" do it "adds All to All" do - new_rule = Flipper.all(Flipper.property(:age).gte(21)) - subject.add_rule(new_rule) - expect(subject.rule).to be_instance_of(Flipper::Expressions::All) - expect(subject.rule.args).to include(condition) - expect(subject.rule.args).to include(new_rule) + new_expression = Flipper.all(Flipper.property(:age).gte(21)) + subject.add_expression(new_expression) + expect(subject.expression).to be_instance_of(Flipper::Expressions::All) + expect(subject.expression.args).to include(condition) + expect(subject.expression.args).to include(new_expression) end end end end - describe '#remove_rule' do + describe '#remove_expression' do context "when nothing enabled" do context "with Expression instance" do it "does nothing" do - rule = Flipper.property(:plan).eq("basic") - subject.remove_rule(rule) - expect(subject.rule).to be(nil) + expression = Flipper.property(:plan).eq("basic") + subject.remove_expression(expression) + expect(subject.expression).to be(nil) end end context "with Any instance" do it "does nothing" do - rule = Flipper.any(Flipper.property(:plan).eq("basic")) - subject.remove_rule rule - expect(subject.rule).to be(nil) + expression = Flipper.any(Flipper.property(:plan).eq("basic")) + subject.remove_expression expression + expect(subject.expression).to be(nil) end end context "with All instance" do it "does nothing" do - rule = Flipper.all(Flipper.property(:plan).eq("basic")) - subject.remove_rule rule - expect(subject.rule).to be(nil) + expression = Flipper.all(Flipper.property(:plan).eq("basic")) + subject.remove_expression expression + expect(subject.expression).to be(nil) end end end context "when Expression enabled" do - let(:rule) { Flipper.property(:plan).eq("basic") } + let(:expression) { Flipper.property(:plan).eq("basic") } before do - subject.enable_rule rule + subject.enable_expression expression end context "with Expression instance" do - it "changes rule to Any and removes Expression if it matches" do - new_rule = Flipper.property(:plan).eq("basic") - subject.remove_rule new_rule - expect(subject.rule).to eq(Flipper.any) + it "changes expression to Any and removes Expression if it matches" do + new_expression = Flipper.property(:plan).eq("basic") + subject.remove_expression new_expression + expect(subject.expression).to eq(Flipper.any) end - it "changes rule to Any if Expression doesn't match" do - new_rule = Flipper.property(:plan).eq("premium") - subject.remove_rule new_rule - expect(subject.rule).to eq(Flipper.any(rule)) + it "changes expression to Any if Expression doesn't match" do + new_expression = Flipper.property(:plan).eq("premium") + subject.remove_expression new_expression + expect(subject.expression).to eq(Flipper.any(expression)) end end context "with Any instance" do - it "changes rule to Any and does nothing" do - new_rule = Flipper.any(Flipper.property(:plan).eq("basic")) - subject.remove_rule new_rule - expect(subject.rule).to eq(Flipper.any(rule)) + it "changes expression to Any and does nothing" do + new_expression = Flipper.any(Flipper.property(:plan).eq("basic")) + subject.remove_expression new_expression + expect(subject.expression).to eq(Flipper.any(expression)) end end context "with All instance" do - it "changes rule to Any and does nothing" do - new_rule = Flipper.all(Flipper.property(:plan).eq("basic")) - subject.remove_rule new_rule - expect(subject.rule).to eq(Flipper.any(rule)) + it "changes expression to Any and does nothing" do + new_expression = Flipper.all(Flipper.property(:plan).eq("basic")) + subject.remove_expression new_expression + expect(subject.expression).to eq(Flipper.any(expression)) end end end context "when Any enabled" do let(:condition) { Flipper.property(:plan).eq("basic") } - let(:rule) { Flipper.any condition } + let(:expression) { Flipper.any condition } before do - subject.enable_rule rule + subject.enable_expression expression end context "with Expression instance" do it "removes Expression if it matches" do - subject.remove_rule condition - expect(subject.rule).to eq(Flipper.any) + subject.remove_expression condition + expect(subject.expression).to eq(Flipper.any) end it "does nothing if Expression does not match" do - subject.remove_rule Flipper.property(:plan).eq("premium") - expect(subject.rule).to eq(rule) + subject.remove_expression Flipper.property(:plan).eq("premium") + expect(subject.expression).to eq(expression) end end context "with Any instance" do it "removes Any if it matches" do - new_rule = Flipper.any(Flipper.property(:plan).eq("premium")) - subject.add_rule new_rule - expect(subject.rule.args.size).to be(2) - subject.remove_rule new_rule - expect(subject.rule).to eq(rule) + new_expression = Flipper.any(Flipper.property(:plan).eq("premium")) + subject.add_expression new_expression + expect(subject.expression.args.size).to be(2) + subject.remove_expression new_expression + expect(subject.expression).to eq(expression) end it "does nothing if Any does not match" do - new_rule = Flipper.any(Flipper.property(:plan).eq("premium")) - subject.remove_rule new_rule - expect(subject.rule).to eq(rule) + new_expression = Flipper.any(Flipper.property(:plan).eq("premium")) + subject.remove_expression new_expression + expect(subject.expression).to eq(expression) end end context "with All instance" do it "removes All if it matches" do - new_rule = Flipper.all(Flipper.property(:plan).eq("premium")) - subject.add_rule new_rule - expect(subject.rule.args.size).to be(2) - subject.remove_rule new_rule - expect(subject.rule).to eq(rule) + new_expression = Flipper.all(Flipper.property(:plan).eq("premium")) + subject.add_expression new_expression + expect(subject.expression.args.size).to be(2) + subject.remove_expression new_expression + expect(subject.expression).to eq(expression) end it "does nothing if All does not match" do - new_rule = Flipper.all(Flipper.property(:plan).eq("premium")) - subject.remove_rule new_rule - expect(subject.rule).to eq(rule) + new_expression = Flipper.all(Flipper.property(:plan).eq("premium")) + subject.remove_expression new_expression + expect(subject.expression).to eq(expression) end end end context "when All enabled" do let(:condition) { Flipper.property(:plan).eq("basic") } - let(:rule) { Flipper.all condition } + let(:expression) { Flipper.all condition } before do - subject.enable_rule rule + subject.enable_expression expression end context "with Expression instance" do it "removes Expression if it matches" do - subject.remove_rule condition - expect(subject.rule).to eq(Flipper.all) + subject.remove_expression condition + expect(subject.expression).to eq(Flipper.all) end it "does nothing if Expression does not match" do - subject.remove_rule Flipper.property(:plan).eq("premium") - expect(subject.rule).to eq(rule) + subject.remove_expression Flipper.property(:plan).eq("premium") + expect(subject.expression).to eq(expression) end end context "with Any instance" do it "removes Any if it matches" do - new_rule = Flipper.any(Flipper.property(:plan).eq("premium")) - subject.add_rule new_rule - expect(subject.rule.args.size).to be(2) - subject.remove_rule new_rule - expect(subject.rule).to eq(rule) + new_expression = Flipper.any(Flipper.property(:plan).eq("premium")) + subject.add_expression new_expression + expect(subject.expression.args.size).to be(2) + subject.remove_expression new_expression + expect(subject.expression).to eq(expression) end it "does nothing if Any does not match" do - new_rule = Flipper.any(Flipper.property(:plan).eq("premium")) - subject.remove_rule new_rule - expect(subject.rule).to eq(rule) + new_expression = Flipper.any(Flipper.property(:plan).eq("premium")) + subject.remove_expression new_expression + expect(subject.expression).to eq(expression) end end context "with All instance" do it "removes All if it matches" do - new_rule = Flipper.all(Flipper.property(:plan).eq("premium")) - subject.add_rule new_rule - expect(subject.rule.args.size).to be(2) - subject.remove_rule new_rule - expect(subject.rule).to eq(rule) + new_expression = Flipper.all(Flipper.property(:plan).eq("premium")) + subject.add_expression new_expression + expect(subject.expression.args.size).to be(2) + subject.remove_expression new_expression + expect(subject.expression).to eq(expression) end it "does nothing if All does not match" do - new_rule = Flipper.all(Flipper.property(:plan).eq("premium")) - subject.remove_rule new_rule - expect(subject.rule).to eq(rule) + new_expression = Flipper.all(Flipper.property(:plan).eq("premium")) + subject.remove_expression new_expression + expect(subject.expression).to eq(expression) end end end @@ -1159,14 +1159,14 @@ :actor, :boolean, :group, - :rule, + :expression, ]) expect(subject.disabled_gate_names.to_set).to eq(Set[ :actor, :boolean, :group, - :rule, + :expression, ]) end end diff --git a/spec/flipper_integration_spec.rb b/spec/flipper_integration_spec.rb index 40e77f2ed..fc60d1352 100644 --- a/spec/flipper_integration_spec.rb +++ b/spec/flipper_integration_spec.rb @@ -550,10 +550,10 @@ end end - context "for rule" do + context "for expression" do it "works" do - rule = Flipper.property(:plan).eq("basic") - feature.enable rule + expression = Flipper.property(:plan).eq("basic") + feature.enable expression expect(feature.enabled?(basic_plan_thing)).to be(true) expect(feature.enabled?(premium_plan_thing)).to be(false) @@ -562,11 +562,11 @@ context "for Any" do it "works" do - rule = Flipper.any( + expression = Flipper.any( Flipper.property(:plan).eq("basic"), Flipper.property(:plan).eq("plus"), ) - feature.enable rule + feature.enable expression expect(feature.enabled?(basic_plan_thing)).to be(true) expect(feature.enabled?(premium_plan_thing)).to be(false) @@ -583,11 +583,11 @@ "plan" => "basic", "age" => 20, }) - rule = Flipper.all( + expression = Flipper.all( Flipper.property(:plan).eq("basic"), Flipper.property(:age).eq(21) ) - feature.enable rule + feature.enable expression expect(feature.enabled?(true_actor)).to be(true) expect(feature.enabled?(false_actor)).to be(false) @@ -605,7 +605,7 @@ "plan" => "basic", "age" => 20, }) - rule = Flipper.any( + expression = Flipper.any( Flipper.property(:admin).eq(true), Flipper.all( Flipper.property(:plan).eq("basic"), @@ -613,7 +613,7 @@ ) ) - feature.enable rule + feature.enable expression expect(feature.enabled?(admin_actor)).to be(true) expect(feature.enabled?(true_actor)).to be(true) diff --git a/spec/flipper_spec.rb b/spec/flipper_spec.rb index 8062aae53..05313a2fb 100644 --- a/spec/flipper_spec.rb +++ b/spec/flipper_spec.rb @@ -70,9 +70,7 @@ "plan" => "basic", }) } - let(:rule) { - Flipper.property(:plan).eq("basic") - } + let(:expression) { Flipper.property(:plan).eq("basic") } before do described_class.configure do |config| @@ -104,34 +102,34 @@ expect(described_class.boolean).to eq(described_class.instance.boolean) end - it 'delegates rule to instance' do - expect(described_class.rule(:search)).to be(nil) + it 'delegates expression to instance' do + expect(described_class.expression(:search)).to be(nil) - rule = Flipper.property(:plan).eq("basic") - Flipper.instance.enable_rule :search, rule + expression = Flipper.property(:plan).eq("basic") + Flipper.instance.enable_expression :search, expression - expect(described_class.rule(:search)).to eq(rule) + expect(described_class.expression(:search)).to eq(expression) end - it 'delegates enable_rule to instance' do - described_class.enable_rule(:search, rule) + it 'delegates enable_expression to instance' do + described_class.enable_expression(:search, expression) expect(described_class.instance.enabled?(:search, actor)).to be(true) end - it 'delegates disable_rule to instance' do - described_class.disable_rule(:search) + it 'delegates disable_expression to instance' do + described_class.disable_expression(:search) expect(described_class.instance.enabled?(:search, actor)).to be(false) end - it 'delegates add_rule to instance' do - described_class.add_rule(:search, rule) + it 'delegates add_expression to instance' do + described_class.add_expression(:search, expression) expect(described_class.instance.enabled?(:search, actor)).to be(true) end - it 'delegates remove_rule to instance' do - described_class.enable_rule(:search, Flipper.any(rule)) + it 'delegates remove_expression to instance' do + described_class.enable_expression(:search, Flipper.any(expression)) expect(described_class.instance.enabled?(:search, actor)).to be(true) - described_class.remove_rule(:search, rule) + described_class.remove_expression(:search, expression) expect(described_class.instance.enabled?(:search, actor)).to be(false) end @@ -403,23 +401,23 @@ end describe ".any" do - let(:age_rule) { Flipper.property(:age).gte(21) } - let(:plan_rule) { Flipper.property(:plan).eq("basic") } + let(:age_expression) { Flipper.property(:age).gte(21) } + let(:plan_expression) { Flipper.property(:plan).eq("basic") } it "returns Flipper::Expressions::Any instance" do - expect(Flipper.any(age_rule, plan_rule)).to eq( - Flipper::Expressions::Any.new([age_rule, plan_rule]) + expect(Flipper.any(age_expression, plan_expression)).to eq( + Flipper::Expressions::Any.new([age_expression, plan_expression]) ) end end describe ".all" do - let(:age_rule) { Flipper.property(:age).gte(21) } - let(:plan_rule) { Flipper.property(:plan).eq("basic") } + let(:age_expression) { Flipper.property(:age).gte(21) } + let(:plan_expression) { Flipper.property(:plan).eq("basic") } it "returns Flipper::Expressions::All instance" do - expect(Flipper.all(age_rule, plan_rule)).to eq( - Flipper::Expressions::All.new([age_rule, plan_rule]) + expect(Flipper.all(age_expression, plan_expression)).to eq( + Flipper::Expressions::All.new([age_expression, plan_expression]) ) end end diff --git a/spec/support/spec_helpers.rb b/spec/support/spec_helpers.rb index 7fa6c43e5..a6d57374f 100644 --- a/spec/support/spec_helpers.rb +++ b/spec/support/spec_helpers.rb @@ -58,7 +58,7 @@ def api_flipper_id_is_missing_response } end - def api_rule_invalid_response + def api_expression_invalid_response { 'code' => 6, 'message' => 'The provided expression was not valid.', From 804309ea1583ac14961e8a2173af73046425f873 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sun, 21 Nov 2021 20:34:56 -0500 Subject: [PATCH 117/176] Add early return to the number comparisons --- lib/flipper/expressions/greater_than.rb | 4 +++- lib/flipper/expressions/greater_than_or_equal.rb | 4 +++- lib/flipper/expressions/less_than.rb | 4 +++- lib/flipper/expressions/less_than_or_equal.rb | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/flipper/expressions/greater_than.rb b/lib/flipper/expressions/greater_than.rb index cffd74252..7ccba7a6b 100644 --- a/lib/flipper/expressions/greater_than.rb +++ b/lib/flipper/expressions/greater_than.rb @@ -9,7 +9,9 @@ def evaluate(context = {}) left = args[0].evaluate(context) right = args[1].evaluate(context) - left && right && left > right + return false unless left && right + + left > right end end end diff --git a/lib/flipper/expressions/greater_than_or_equal.rb b/lib/flipper/expressions/greater_than_or_equal.rb index 477f3dbb8..528e05b6e 100644 --- a/lib/flipper/expressions/greater_than_or_equal.rb +++ b/lib/flipper/expressions/greater_than_or_equal.rb @@ -9,7 +9,9 @@ def evaluate(context = {}) left = args[0].evaluate(context) right = args[1].evaluate(context) - left && right && left >= right + return false unless left && right + + left >= right end end end diff --git a/lib/flipper/expressions/less_than.rb b/lib/flipper/expressions/less_than.rb index 12a40c684..b6bf44ce2 100644 --- a/lib/flipper/expressions/less_than.rb +++ b/lib/flipper/expressions/less_than.rb @@ -9,7 +9,9 @@ def evaluate(context = {}) left = args[0].evaluate(context) right = args[1].evaluate(context) - left && right && left < right + return false unless left && right + + left < right end end end diff --git a/lib/flipper/expressions/less_than_or_equal.rb b/lib/flipper/expressions/less_than_or_equal.rb index 61092c59d..41fef393b 100644 --- a/lib/flipper/expressions/less_than_or_equal.rb +++ b/lib/flipper/expressions/less_than_or_equal.rb @@ -9,7 +9,9 @@ def evaluate(context = {}) left = args[0].evaluate(context) right = args[1].evaluate(context) - left && right && left <= right + return false unless left && right + + left <= right end end end From 94c4824efdfca15987a96670b67e45ea1b5f9de9 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sun, 21 Nov 2021 20:36:14 -0500 Subject: [PATCH 118/176] Support evaluations for rand --- lib/flipper/expressions/random.rb | 2 +- spec/flipper/expressions/random_spec.rb | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/flipper/expressions/random.rb b/lib/flipper/expressions/random.rb index 097561e56..22182f35f 100644 --- a/lib/flipper/expressions/random.rb +++ b/lib/flipper/expressions/random.rb @@ -8,7 +8,7 @@ def initialize(args) end def evaluate(context = {}) - rand args[0] + rand evaluate_arg(0, context) end end end diff --git a/spec/flipper/expressions/random_spec.rb b/spec/flipper/expressions/random_spec.rb index 29dfb692c..a7f7b7238 100644 --- a/spec/flipper/expressions/random_spec.rb +++ b/spec/flipper/expressions/random_spec.rb @@ -18,6 +18,13 @@ expect(result).to be >= 0 expect(result).to be <= 10 end + + it "returns random number based on seed that is Value" do + expression = described_class.new([Flipper.value(10)]) + result = expression.evaluate + expect(result).to be >= 0 + expect(result).to be <= 10 + end end describe "#value" do From 637a58ee0a95ab1541cc57ff9c9751b98c702f47 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sun, 21 Nov 2021 20:38:45 -0500 Subject: [PATCH 119/176] Use arg evaluation everywhere --- lib/flipper/expressions/equal.rb | 4 ++-- lib/flipper/expressions/greater_than.rb | 4 ++-- lib/flipper/expressions/greater_than_or_equal.rb | 4 ++-- lib/flipper/expressions/less_than.rb | 4 ++-- lib/flipper/expressions/less_than_or_equal.rb | 4 ++-- lib/flipper/expressions/not_equal.rb | 4 ++-- lib/flipper/expressions/value.rb | 2 +- spec/flipper/expressions/value_spec.rb | 5 +++++ 8 files changed, 18 insertions(+), 13 deletions(-) diff --git a/lib/flipper/expressions/equal.rb b/lib/flipper/expressions/equal.rb index 49de9a12d..0f968e4b7 100644 --- a/lib/flipper/expressions/equal.rb +++ b/lib/flipper/expressions/equal.rb @@ -6,8 +6,8 @@ class Equal < Expression def evaluate(context = {}) return false unless args[0] && args[1] - left = args[0].evaluate(context) - right = args[1].evaluate(context) + left = evaluate_arg(0, context) + right = evaluate_arg(1, context) left == right end diff --git a/lib/flipper/expressions/greater_than.rb b/lib/flipper/expressions/greater_than.rb index 7ccba7a6b..292f073c5 100644 --- a/lib/flipper/expressions/greater_than.rb +++ b/lib/flipper/expressions/greater_than.rb @@ -6,8 +6,8 @@ class GreaterThan < Expression def evaluate(context = {}) return false unless args[0] && args[1] - left = args[0].evaluate(context) - right = args[1].evaluate(context) + left = evaluate_arg(0, context) + right = evaluate_arg(1, context) return false unless left && right diff --git a/lib/flipper/expressions/greater_than_or_equal.rb b/lib/flipper/expressions/greater_than_or_equal.rb index 528e05b6e..5975e2ccd 100644 --- a/lib/flipper/expressions/greater_than_or_equal.rb +++ b/lib/flipper/expressions/greater_than_or_equal.rb @@ -6,8 +6,8 @@ class GreaterThanOrEqual < Expression def evaluate(context = {}) return false unless args[0] && args[1] - left = args[0].evaluate(context) - right = args[1].evaluate(context) + left = evaluate_arg(0, context) + right = evaluate_arg(1, context) return false unless left && right diff --git a/lib/flipper/expressions/less_than.rb b/lib/flipper/expressions/less_than.rb index b6bf44ce2..dffb6e563 100644 --- a/lib/flipper/expressions/less_than.rb +++ b/lib/flipper/expressions/less_than.rb @@ -6,8 +6,8 @@ class LessThan < Expression def evaluate(context = {}) return false unless args[0] && args[1] - left = args[0].evaluate(context) - right = args[1].evaluate(context) + left = evaluate_arg(0, context) + right = evaluate_arg(1, context) return false unless left && right diff --git a/lib/flipper/expressions/less_than_or_equal.rb b/lib/flipper/expressions/less_than_or_equal.rb index 41fef393b..4d1348e0b 100644 --- a/lib/flipper/expressions/less_than_or_equal.rb +++ b/lib/flipper/expressions/less_than_or_equal.rb @@ -6,8 +6,8 @@ class LessThanOrEqual < Expression def evaluate(context = {}) return false unless args[0] && args[1] - left = args[0].evaluate(context) - right = args[1].evaluate(context) + left = evaluate_arg(0, context) + right = evaluate_arg(1, context) return false unless left && right diff --git a/lib/flipper/expressions/not_equal.rb b/lib/flipper/expressions/not_equal.rb index 777599126..95e913d0c 100644 --- a/lib/flipper/expressions/not_equal.rb +++ b/lib/flipper/expressions/not_equal.rb @@ -6,8 +6,8 @@ class NotEqual < Expression def evaluate(context = {}) return false unless args[0] && args[1] - left = args[0].evaluate(context) - right = args[1].evaluate(context) + left = evaluate_arg(0, context) + right = evaluate_arg(1, context) left != right end diff --git a/lib/flipper/expressions/value.rb b/lib/flipper/expressions/value.rb index e58bc98cd..072df07fb 100644 --- a/lib/flipper/expressions/value.rb +++ b/lib/flipper/expressions/value.rb @@ -8,7 +8,7 @@ def initialize(args) end def evaluate(context = {}) - args[0] + evaluate_arg(0, context) end end end diff --git a/spec/flipper/expressions/value_spec.rb b/spec/flipper/expressions/value_spec.rb index b4a3e9ab2..e245d0400 100644 --- a/spec/flipper/expressions/value_spec.rb +++ b/spec/flipper/expressions/value_spec.rb @@ -21,6 +21,11 @@ expression = described_class.new(["basic"]) expect(expression.evaluate).to eq("basic") end + + it "returns arg when it needs evaluation" do + expression = described_class.new([Flipper.value("basic")]) + expect(expression.evaluate).to eq("basic") + end end describe "#value" do From c53a32d7b0e5666e7c53b710a538e7ab55c83955 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sun, 21 Nov 2021 20:42:57 -0500 Subject: [PATCH 120/176] Add some equal specs for wrong number of arguments --- lib/flipper/expressions/equal.rb | 1 + spec/flipper/expressions/equal_spec.rb | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/lib/flipper/expressions/equal.rb b/lib/flipper/expressions/equal.rb index 0f968e4b7..1646e4528 100644 --- a/lib/flipper/expressions/equal.rb +++ b/lib/flipper/expressions/equal.rb @@ -4,6 +4,7 @@ module Flipper module Expressions class Equal < Expression def evaluate(context = {}) + return false if args.size > 2 return false unless args[0] && args[1] left = evaluate_arg(0, context) diff --git a/spec/flipper/expressions/equal_spec.rb b/spec/flipper/expressions/equal_spec.rb index 3aa837986..f6b200125 100644 --- a/spec/flipper/expressions/equal_spec.rb +++ b/spec/flipper/expressions/equal_spec.rb @@ -85,6 +85,25 @@ } expect(expression.evaluate(properties: properties)).to be(false) end + + it "returns false when no args" do + expression = described_class.new([]) + expect(expression.evaluate).to be(false) + end + + it "returns false when one arg" do + expression = described_class.new([Flipper.value(10)]) + expect(expression.evaluate).to be(false) + end + + it "returns false when three args" do + expression = described_class.new([ + Flipper.value(10), + Flipper.value(20), + Flipper.value(30), + ]) + expect(expression.evaluate).to be(false) + end end describe "#value" do From 344df5fecb137e3ffef93d858ca548cf9fa32da5 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sun, 21 Nov 2021 20:48:03 -0500 Subject: [PATCH 121/176] Suffix greater/less than or equal with "to" Still alias the old. --- lib/flipper/expression.rb | 14 ++++++++------ ...or_equal.rb => greater_than_or_equal_to.rb} | 2 +- ...an_or_equal.rb => less_than_or_equal_to.rb} | 2 +- spec/flipper/expression_spec.rb | 18 ++++++++++-------- .../greater_than_or_equal_to_spec.rb | 4 ++-- .../expressions/less_than_or_equal_to_spec.rb | 4 ++-- 6 files changed, 24 insertions(+), 20 deletions(-) rename lib/flipper/expressions/{greater_than_or_equal.rb => greater_than_or_equal_to.rb} (87%) rename lib/flipper/expressions/{less_than_or_equal.rb => less_than_or_equal_to.rb} (88%) diff --git a/lib/flipper/expression.rb b/lib/flipper/expression.rb index 8ae54b700..807a51637 100644 --- a/lib/flipper/expression.rb +++ b/lib/flipper/expression.rb @@ -80,20 +80,22 @@ def greater_than(object) end alias gt greater_than - def greater_than_or_equal(object) - Expressions::GreaterThanOrEqual.new([self, self.class.build(object, convert_to_values: true)]) + def greater_than_or_equal_to(object) + Expressions::GreaterThanOrEqualTo.new([self, self.class.build(object, convert_to_values: true)]) end - alias gte greater_than_or_equal + alias gte greater_than_or_equal_to + alias greater_than_or_equal greater_than_or_equal_to def less_than(object) Expressions::LessThan.new([self, self.class.build(object, convert_to_values: true)]) end alias lt less_than - def less_than_or_equal(object) - Expressions::LessThanOrEqual.new([self, self.class.build(object, convert_to_values: true)]) + def less_than_or_equal_to(object) + Expressions::LessThanOrEqualTo.new([self, self.class.build(object, convert_to_values: true)]) end - alias lte less_than_or_equal + alias lte less_than_or_equal_to + alias less_than_or_equal less_than_or_equal_to def percentage(object) Expressions::Percentage.new([self, self.class.build(object, convert_to_values: true)]) diff --git a/lib/flipper/expressions/greater_than_or_equal.rb b/lib/flipper/expressions/greater_than_or_equal_to.rb similarity index 87% rename from lib/flipper/expressions/greater_than_or_equal.rb rename to lib/flipper/expressions/greater_than_or_equal_to.rb index 5975e2ccd..9a76282cb 100644 --- a/lib/flipper/expressions/greater_than_or_equal.rb +++ b/lib/flipper/expressions/greater_than_or_equal_to.rb @@ -2,7 +2,7 @@ module Flipper module Expressions - class GreaterThanOrEqual < Expression + class GreaterThanOrEqualTo < Expression def evaluate(context = {}) return false unless args[0] && args[1] diff --git a/lib/flipper/expressions/less_than_or_equal.rb b/lib/flipper/expressions/less_than_or_equal_to.rb similarity index 88% rename from lib/flipper/expressions/less_than_or_equal.rb rename to lib/flipper/expressions/less_than_or_equal_to.rb index 4d1348e0b..83685391a 100644 --- a/lib/flipper/expressions/less_than_or_equal.rb +++ b/lib/flipper/expressions/less_than_or_equal_to.rb @@ -2,7 +2,7 @@ module Flipper module Expressions - class LessThanOrEqual < Expression + class LessThanOrEqualTo < Expression def evaluate(context = {}) return false unless args[0] && args[1] diff --git a/spec/flipper/expression_spec.rb b/spec/flipper/expression_spec.rb index 848f34051..8be29dcd0 100644 --- a/spec/flipper/expression_spec.rb +++ b/spec/flipper/expression_spec.rb @@ -17,15 +17,15 @@ ]) end - it "can build GreaterThanOrEqual" do + it "can build GreaterThanOrEqualTo" do expression = Flipper::Expression.build({ - "GreaterThanOrEqual" => [ + "GreaterThanOrEqualTo" => [ {"Value" => [2]}, {"Value" => [1]}, ] }) - expect(expression).to be_instance_of(Flipper::Expressions::GreaterThanOrEqual) + expect(expression).to be_instance_of(Flipper::Expressions::GreaterThanOrEqualTo) expect(expression.args).to eq([ Flipper.value(2), Flipper.value(1), @@ -47,15 +47,15 @@ ]) end - it "can build LessThanOrEqual" do + it "can build LessThanOrEqualTo" do expression = Flipper::Expression.build({ - "LessThanOrEqual" => [ + "LessThanOrEqualTo" => [ {"Value" => [2]}, {"Value" => [1]}, ] }) - expect(expression).to be_instance_of(Flipper::Expressions::LessThanOrEqual) + expect(expression).to be_instance_of(Flipper::Expressions::LessThanOrEqualTo) expect(expression.args).to eq([ Flipper.value(2), Flipper.value(1), @@ -237,9 +237,11 @@ [[2], [3], "equal", "eq", Flipper::Expressions::Equal], [[2], [3], "not_equal", "neq", Flipper::Expressions::NotEqual], [[2], [3], "greater_than", "gt", Flipper::Expressions::GreaterThan], - [[2], [3], "greater_than_or_equal", "gte", Flipper::Expressions::GreaterThanOrEqual], + [[2], [3], "greater_than_or_equal_to", "gte", Flipper::Expressions::GreaterThanOrEqualTo], + [[2], [3], "greater_than_or_equal_to", "greater_than_or_equal", Flipper::Expressions::GreaterThanOrEqualTo], [[2], [3], "less_than", "lt", Flipper::Expressions::LessThan], - [[2], [3], "less_than_or_equal", "lte", Flipper::Expressions::LessThanOrEqual], + [[2], [3], "less_than_or_equal_to", "lte", Flipper::Expressions::LessThanOrEqualTo], + [[2], [3], "less_than_or_equal_to", "less_than_or_equal", Flipper::Expressions::LessThanOrEqualTo], ].each do |(args, other_args, method_name, shortcut_name, klass)| it "can convert to #{klass}" do expression = described_class.new(args) diff --git a/spec/flipper/expressions/greater_than_or_equal_to_spec.rb b/spec/flipper/expressions/greater_than_or_equal_to_spec.rb index 5878937eb..0e21ef278 100644 --- a/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +++ b/spec/flipper/expressions/greater_than_or_equal_to_spec.rb @@ -1,6 +1,6 @@ require 'helper' -RSpec.describe Flipper::Expressions::GreaterThanOrEqual do +RSpec.describe Flipper::Expressions::GreaterThanOrEqualTo do describe "#evaluate" do it "returns true when equal" do expression = described_class.new([ @@ -38,7 +38,7 @@ ]) expect(expression.value).to eq({ - "GreaterThanOrEqual" => [ + "GreaterThanOrEqualTo" => [ {"Value" => [20]}, {"Value" => [10]}, ], diff --git a/spec/flipper/expressions/less_than_or_equal_to_spec.rb b/spec/flipper/expressions/less_than_or_equal_to_spec.rb index 784b81978..664cf78fe 100644 --- a/spec/flipper/expressions/less_than_or_equal_to_spec.rb +++ b/spec/flipper/expressions/less_than_or_equal_to_spec.rb @@ -1,6 +1,6 @@ require 'helper' -RSpec.describe Flipper::Expressions::LessThanOrEqual do +RSpec.describe Flipper::Expressions::LessThanOrEqualTo do describe "#evaluate" do it "returns true when equal" do expression = described_class.new([ @@ -38,7 +38,7 @@ ]) expect(expression.value).to eq({ - "LessThanOrEqual" => [ + "LessThanOrEqualTo" => [ {"Value" => [20]}, {"Value" => [10]}, ], From 3ba6c0f67386701ab9e3b009bd60425847332b04 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sun, 21 Nov 2021 20:51:47 -0500 Subject: [PATCH 122/176] Change equal spec to only evaluate first two --- lib/flipper/expressions/equal.rb | 1 - spec/flipper/expressions/equal_spec.rb | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/flipper/expressions/equal.rb b/lib/flipper/expressions/equal.rb index 1646e4528..0f968e4b7 100644 --- a/lib/flipper/expressions/equal.rb +++ b/lib/flipper/expressions/equal.rb @@ -4,7 +4,6 @@ module Flipper module Expressions class Equal < Expression def evaluate(context = {}) - return false if args.size > 2 return false unless args[0] && args[1] left = evaluate_arg(0, context) diff --git a/spec/flipper/expressions/equal_spec.rb b/spec/flipper/expressions/equal_spec.rb index f6b200125..20db4d37f 100644 --- a/spec/flipper/expressions/equal_spec.rb +++ b/spec/flipper/expressions/equal_spec.rb @@ -96,13 +96,13 @@ expect(expression.evaluate).to be(false) end - it "returns false when three args" do + it "only evaluates first two arguments equality" do expression = described_class.new([ - Flipper.value(10), + Flipper.value(20), Flipper.value(20), Flipper.value(30), ]) - expect(expression.evaluate).to be(false) + expect(expression.evaluate).to be(true) end end From 3a74bc44a35ee800e7af4f0dc8fabcfc6b23874c Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sun, 21 Nov 2021 20:53:24 -0500 Subject: [PATCH 123/176] Add not equal extra arg spec --- spec/flipper/expressions/not_equal_spec.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spec/flipper/expressions/not_equal_spec.rb b/spec/flipper/expressions/not_equal_spec.rb index 35c445cc3..bc6b7e0fa 100644 --- a/spec/flipper/expressions/not_equal_spec.rb +++ b/spec/flipper/expressions/not_equal_spec.rb @@ -45,6 +45,15 @@ } expect(expression.evaluate(properties: properties)).to be(false) end + + it "only evaluates first two arguments equality" do + expression = described_class.new([ + Flipper.value(20), + Flipper.value(10), + Flipper.value(20), + ]) + expect(expression.evaluate).to be(true) + end end describe "#value" do From 22a563243cceb8b8ffaefd30c010e007ab62195e Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sun, 21 Nov 2021 21:05:18 -0500 Subject: [PATCH 124/176] A bunch of spec tweaks --- .../greater_than_or_equal_to_spec.rb | 29 +++++++++++------- spec/flipper/expressions/greater_than_spec.rb | 30 ++++++++++++------- .../expressions/less_than_or_equal_to_spec.rb | 29 +++++++++++------- spec/flipper/expressions/less_than_spec.rb | 29 ++++++++++-------- spec/flipper/expressions/not_equal_spec.rb | 18 ++--------- 5 files changed, 75 insertions(+), 60 deletions(-) diff --git a/spec/flipper/expressions/greater_than_or_equal_to_spec.rb b/spec/flipper/expressions/greater_than_or_equal_to_spec.rb index 0e21ef278..a643d3112 100644 --- a/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +++ b/spec/flipper/expressions/greater_than_or_equal_to_spec.rb @@ -3,29 +3,36 @@ RSpec.describe Flipper::Expressions::GreaterThanOrEqualTo do describe "#evaluate" do it "returns true when equal" do + expression = described_class.new([2, 2]) + expect(expression.evaluate).to be(true) + end + + it "returns true when equal with args that need evaluation" do expression = described_class.new([ - Flipper::Expressions::Value.new([2]), - Flipper::Expressions::Value.new([2]), + Flipper.value(2), + Flipper.value(2), ]) expect(expression.evaluate).to be(true) end it "returns true when greater" do - expression = described_class.new([ - Flipper::Expressions::Value.new([2]), - Flipper::Expressions::Value.new([1]), - ]) - + expression = described_class.new([2, 1]) expect(expression.evaluate).to be(true) end it "returns false when less" do - expression = described_class.new([ - Flipper::Expressions::Value.new([1]), - Flipper::Expressions::Value.new([2]), - ]) + expression = described_class.new([1, 2]) + expect(expression.evaluate).to be(false) + end + + it "returns false with no arguments" do + expression = described_class.new([]) + expect(expression.evaluate).to be(false) + end + it "returns false with one argument" do + expression = described_class.new([10]) expect(expression.evaluate).to be(false) end end diff --git a/spec/flipper/expressions/greater_than_spec.rb b/spec/flipper/expressions/greater_than_spec.rb index 22abd0a1c..30ceedd50 100644 --- a/spec/flipper/expressions/greater_than_spec.rb +++ b/spec/flipper/expressions/greater_than_spec.rb @@ -3,29 +3,37 @@ RSpec.describe Flipper::Expressions::GreaterThan do describe "#evaluate" do it "returns false when equal" do - expression = described_class.new([ - Flipper::Expressions::Value.new([2]), - Flipper::Expressions::Value.new([2]), - ]) - + expression = described_class.new([2, 2]) expect(expression.evaluate).to be(false) end it "returns true when greater" do + expression = described_class.new([2, 1]) + expect(expression.evaluate).to be(true) + end + + it "returns true when greater with args that need evaluation" do expression = described_class.new([ - Flipper::Expressions::Value.new([2]), - Flipper::Expressions::Value.new([1]), + Flipper.value(2), + Flipper.value(1), ]) - expect(expression.evaluate).to be(true) end it "returns false when less" do + expression = described_class.new([1, 2]) + expect(expression.evaluate).to be(false) + end + + it "returns false with no arguments" do + expression = described_class.new([]) + expect(expression.evaluate).to be(false) + end + + it "returns false with one argument" do expression = described_class.new([ - Flipper::Expressions::Value.new([1]), - Flipper::Expressions::Value.new([2]), + Flipper.value(10), ]) - expect(expression.evaluate).to be(false) end end diff --git a/spec/flipper/expressions/less_than_or_equal_to_spec.rb b/spec/flipper/expressions/less_than_or_equal_to_spec.rb index 664cf78fe..09c1bed33 100644 --- a/spec/flipper/expressions/less_than_or_equal_to_spec.rb +++ b/spec/flipper/expressions/less_than_or_equal_to_spec.rb @@ -3,29 +3,38 @@ RSpec.describe Flipper::Expressions::LessThanOrEqualTo do describe "#evaluate" do it "returns true when equal" do + expression = described_class.new([2, 2]) + + expect(expression.evaluate).to be(true) + end + + it "returns true when equal with args that need evaluation" do expression = described_class.new([ - Flipper::Expressions::Value.new([2]), - Flipper::Expressions::Value.new([2]), + Flipper.value(2), + Flipper.value(2), ]) expect(expression.evaluate).to be(true) end it "returns true when less" do - expression = described_class.new([ - Flipper::Expressions::Value.new([1]), - Flipper::Expressions::Value.new([2]), - ]) + expression = described_class.new([1, 2]) expect(expression.evaluate).to be(true) end it "returns false when greater" do - expression = described_class.new([ - Flipper::Expressions::Value.new([2]), - Flipper::Expressions::Value.new([1]), - ]) + expression = described_class.new([2, 1]) + expect(expression.evaluate).to be(false) + end + + it "returns false with no arguments" do + expression = described_class.new([]) + expect(expression.evaluate).to be(false) + end + it "returns false with one argument" do + expression = described_class.new([10]) expect(expression.evaluate).to be(false) end end diff --git a/spec/flipper/expressions/less_than_spec.rb b/spec/flipper/expressions/less_than_spec.rb index 500996bf8..a8c01dd91 100644 --- a/spec/flipper/expressions/less_than_spec.rb +++ b/spec/flipper/expressions/less_than_spec.rb @@ -3,29 +3,32 @@ RSpec.describe Flipper::Expressions::LessThan do describe "#evaluate" do it "returns false when equal" do - expression = described_class.new([ - Flipper::Expressions::Value.new(2), - Flipper::Expressions::Value.new(2), - ]) - + expression = described_class.new([2, 2]) expect(expression.evaluate).to be(false) end it "returns true when less" do - expression = described_class.new([ - Flipper::Expressions::Value.new([1]), - Flipper::Expressions::Value.new([2]), - ]) + expression = described_class.new([1, 2]) + expect(expression.evaluate).to be(true) + end + it "returns true when less with args that need evaluation" do + expression = described_class.new([1, 2]) expect(expression.evaluate).to be(true) end it "returns false when greater" do - expression = described_class.new([ - Flipper::Expressions::Value.new([2]), - Flipper::Expressions::Value.new([1]), - ]) + expression = described_class.new([2, 1]) + expect(expression.evaluate).to be(false) + end + + it "returns false with no arguments" do + expression = described_class.new([]) + expect(expression.evaluate).to be(false) + end + it "returns false with one argument" do + expression = described_class.new([10]) expect(expression.evaluate).to be(false) end end diff --git a/spec/flipper/expressions/not_equal_spec.rb b/spec/flipper/expressions/not_equal_spec.rb index bc6b7e0fa..1a19165db 100644 --- a/spec/flipper/expressions/not_equal_spec.rb +++ b/spec/flipper/expressions/not_equal_spec.rb @@ -3,11 +3,7 @@ RSpec.describe Flipper::Expressions::NotEqual do describe "#evaluate" do it "returns true when not equal" do - expression = described_class.new([ - Flipper.value("basic"), - Flipper.value("plus"), - ]) - + expression = described_class.new(["basic", "plus"]) expect(expression.evaluate).to be(true) end @@ -25,11 +21,7 @@ end it "returns false when equal" do - expression = described_class.new([ - Flipper.value("basic"), - Flipper.value("basic"), - ]) - + expression = described_class.new(["basic", "basic"]) expect(expression.evaluate).to be(false) end @@ -47,11 +39,7 @@ end it "only evaluates first two arguments equality" do - expression = described_class.new([ - Flipper.value(20), - Flipper.value(10), - Flipper.value(20), - ]) + expression = described_class.new([20, 10, 20]) expect(expression.evaluate).to be(true) end end From 141be3c40eab072b19cc4dd5e8e795f155db40e4 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Mon, 22 Nov 2021 12:53:21 -0500 Subject: [PATCH 125/176] Add specs for expression gate Fixes a few things like adding protection for hashes and wrapping of them. --- lib/flipper/gates/expression.rb | 10 ++- spec/flipper/gates/expression_spec.rb | 109 ++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 spec/flipper/gates/expression_spec.rb diff --git a/lib/flipper/gates/expression.rb b/lib/flipper/gates/expression.rb index e8ac40b4b..321307e0b 100644 --- a/lib/flipper/gates/expression.rb +++ b/lib/flipper/gates/expression.rb @@ -18,7 +18,7 @@ def data_type end def enabled?(value) - value && !value.empty? + !value.nil? && !value.empty? end # Internal: Checks if the gate is open for a thing. @@ -36,7 +36,11 @@ def open?(context) end def protects?(thing) - thing.is_a?(Flipper::Expression) + thing.is_a?(Flipper::Expression) || thing.is_a?(Hash) + end + + def wrap(thing) + Flipper::Expression.build(thing) end private @@ -55,6 +59,8 @@ def properties(actor) warn "#{actor.inspect} does not respond to `flipper_properties` but should." end + properties.transform_keys!(&:to_s) + if actor.respond_to?(:flipper_id) properties["flipper_id".freeze] = actor.flipper_id end diff --git a/spec/flipper/gates/expression_spec.rb b/spec/flipper/gates/expression_spec.rb new file mode 100644 index 000000000..05d4e03e1 --- /dev/null +++ b/spec/flipper/gates/expression_spec.rb @@ -0,0 +1,109 @@ +require 'helper' + +RSpec.describe Flipper::Gates::Expression do + let(:feature_name) { :search } + + subject do + described_class.new + end + + def context(expression, properties: {}) + Flipper::FeatureCheckContext.new( + feature_name: feature_name, + values: Flipper::GateValues.new(expression: expression), + thing: Flipper::Types::Actor.new(Flipper::Actor.new(1, properties)) + ) + end + + describe '#enabled?' do + context 'for nil value' do + it 'returns false' do + expect(subject.enabled?(nil)).to eq(false) + end + end + + context 'for empty value' do + it 'returns false' do + expect(subject.enabled?({})).to eq(false) + end + end + + context "for not empty value" do + it 'returns true' do + expect(subject.enabled?({"Value" => [true]})).to eq(true) + end + end + end + + describe '#open?' do + context 'for expression that evaluates to true' do + it 'returns true' do + expression = Flipper.value(true).eq(true) + expect(subject.open?(context(expression.value))).to be(true) + end + end + + context 'for expression that evaluates to false' do + it 'returns false' do + expression = Flipper.value(true).eq(false) + expect(subject.open?(context(expression.value))).to be(false) + end + end + + context 'for properties that have string keys' do + it 'returns true when expression evalutes to true' do + expression = Flipper.property(:type).eq("User") + context = context(expression.value, properties: {"type" => "User"}) + expect(subject.open?(context)).to be(true) + end + + it 'returns false when expression evaluates to false' do + expression = Flipper.property(:type).eq("User") + context = context(expression.value, properties: {"type" => "Org"}) + expect(subject.open?(context)).to be(false) + end + end + + context 'for properties that have symbol keys' do + it 'returns true when expression evalutes to true' do + expression = Flipper.property(:type).eq("User") + context = context(expression.value, properties: {type: "User"}) + expect(subject.open?(context)).to be(true) + end + + it 'returns false when expression evaluates to false' do + expression = Flipper.property(:type).eq("User") + context = context(expression.value, properties: {type: "Org"}) + expect(subject.open?(context)).to be(false) + end + end + end + + describe '#protects?' do + it 'returns true for Flipper::Expression' do + expression = Flipper.value(20).eq(20) + expect(subject.protects?(expression)).to be(true) + end + + it 'returns true for Hash' do + expression = Flipper.value(20).eq(20) + expect(subject.protects?(expression.value)).to be(true) + end + + it 'returns false for other things' do + expect(subject.protects?(false)).to be(false) + end + end + + describe '#wrap' do + it 'returns self for Flipper::Expression' do + expression = Flipper.value(20).eq(20) + expect(subject.wrap(expression)).to be_instance_of(Flipper::Expressions::Equal) + end + + it 'returns Flipper::Expression for Hash' do + expression = Flipper.value(20).eq(20) + expect(subject.wrap(expression.value)).to be_instance_of(Flipper::Expressions::Equal) + end + end +end From 157052049d2b873d4f8afa9c586d5f9ec570094d Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Mon, 24 Jan 2022 09:08:47 -0500 Subject: [PATCH 126/176] Remove helper require --- spec/flipper/adapters/sequel_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/flipper/adapters/sequel_spec.rb b/spec/flipper/adapters/sequel_spec.rb index f522041bb..875941963 100644 --- a/spec/flipper/adapters/sequel_spec.rb +++ b/spec/flipper/adapters/sequel_spec.rb @@ -1,4 +1,3 @@ -require 'helper' require 'sequel' Sequel::Model.db = Sequel.sqlite(':memory:') From eb4e68e9da3e9a2bf5a4f1ecb8833683e2f1bcd3 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Mon, 24 Jan 2022 10:42:10 -0500 Subject: [PATCH 127/176] Remove all helper requires in specs --- spec/flipper/api/v1/actions/rule_gate_spec.rb | 2 -- spec/flipper/expression_spec.rb | 2 +- spec/flipper/expressions/all_spec.rb | 2 -- spec/flipper/expressions/any_spec.rb | 2 -- spec/flipper/expressions/equal_spec.rb | 2 -- spec/flipper/expressions/greater_than_or_equal_to_spec.rb | 2 -- spec/flipper/expressions/greater_than_spec.rb | 2 -- spec/flipper/expressions/less_than_or_equal_to_spec.rb | 2 -- spec/flipper/expressions/less_than_spec.rb | 2 -- spec/flipper/expressions/not_equal_spec.rb | 2 -- spec/flipper/expressions/percentage_spec.rb | 2 -- spec/flipper/expressions/property_spec.rb | 2 -- spec/flipper/expressions/random_spec.rb | 2 -- spec/flipper/expressions/value_spec.rb | 2 -- spec/flipper/gates/expression_spec.rb | 2 -- spec/flipper/model/active_record_spec.rb | 1 - spec/flipper/model/sequel_spec.rb | 1 - test_rails/generators/flipper/active_record_generator_test.rb | 1 - 18 files changed, 1 insertion(+), 32 deletions(-) diff --git a/spec/flipper/api/v1/actions/rule_gate_spec.rb b/spec/flipper/api/v1/actions/rule_gate_spec.rb index 22093d95c..7aecb9346 100644 --- a/spec/flipper/api/v1/actions/rule_gate_spec.rb +++ b/spec/flipper/api/v1/actions/rule_gate_spec.rb @@ -1,5 +1,3 @@ -require 'helper' - RSpec.describe Flipper::Api::V1::Actions::ExpressionGate do let(:app) { build_api(flipper) } let(:actor) { diff --git a/spec/flipper/expression_spec.rb b/spec/flipper/expression_spec.rb index 8be29dcd0..ee2e771c6 100644 --- a/spec/flipper/expression_spec.rb +++ b/spec/flipper/expression_spec.rb @@ -1,4 +1,4 @@ -require 'helper' +require 'flipper/expression' RSpec.describe Flipper::Expression do describe "#build" do diff --git a/spec/flipper/expressions/all_spec.rb b/spec/flipper/expressions/all_spec.rb index 72c65d567..1876bbcb8 100644 --- a/spec/flipper/expressions/all_spec.rb +++ b/spec/flipper/expressions/all_spec.rb @@ -1,5 +1,3 @@ -require 'helper' - RSpec.describe Flipper::Expressions::All do describe "#evaluate" do it "returns true if all args evaluate as true" do diff --git a/spec/flipper/expressions/any_spec.rb b/spec/flipper/expressions/any_spec.rb index 7dbb14181..75203996e 100644 --- a/spec/flipper/expressions/any_spec.rb +++ b/spec/flipper/expressions/any_spec.rb @@ -1,5 +1,3 @@ -require 'helper' - RSpec.describe Flipper::Expressions::Any do describe "#evaluate" do it "returns true if any args evaluate as true" do diff --git a/spec/flipper/expressions/equal_spec.rb b/spec/flipper/expressions/equal_spec.rb index 20db4d37f..c3c08e365 100644 --- a/spec/flipper/expressions/equal_spec.rb +++ b/spec/flipper/expressions/equal_spec.rb @@ -1,5 +1,3 @@ -require 'helper' - RSpec.describe Flipper::Expressions::Equal do it "can be built" do expression = described_class.build({ diff --git a/spec/flipper/expressions/greater_than_or_equal_to_spec.rb b/spec/flipper/expressions/greater_than_or_equal_to_spec.rb index a643d3112..eae4718d2 100644 --- a/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +++ b/spec/flipper/expressions/greater_than_or_equal_to_spec.rb @@ -1,5 +1,3 @@ -require 'helper' - RSpec.describe Flipper::Expressions::GreaterThanOrEqualTo do describe "#evaluate" do it "returns true when equal" do diff --git a/spec/flipper/expressions/greater_than_spec.rb b/spec/flipper/expressions/greater_than_spec.rb index 30ceedd50..86083d4b4 100644 --- a/spec/flipper/expressions/greater_than_spec.rb +++ b/spec/flipper/expressions/greater_than_spec.rb @@ -1,5 +1,3 @@ -require 'helper' - RSpec.describe Flipper::Expressions::GreaterThan do describe "#evaluate" do it "returns false when equal" do diff --git a/spec/flipper/expressions/less_than_or_equal_to_spec.rb b/spec/flipper/expressions/less_than_or_equal_to_spec.rb index 09c1bed33..689c167a3 100644 --- a/spec/flipper/expressions/less_than_or_equal_to_spec.rb +++ b/spec/flipper/expressions/less_than_or_equal_to_spec.rb @@ -1,5 +1,3 @@ -require 'helper' - RSpec.describe Flipper::Expressions::LessThanOrEqualTo do describe "#evaluate" do it "returns true when equal" do diff --git a/spec/flipper/expressions/less_than_spec.rb b/spec/flipper/expressions/less_than_spec.rb index a8c01dd91..f89ec9ff1 100644 --- a/spec/flipper/expressions/less_than_spec.rb +++ b/spec/flipper/expressions/less_than_spec.rb @@ -1,5 +1,3 @@ -require 'helper' - RSpec.describe Flipper::Expressions::LessThan do describe "#evaluate" do it "returns false when equal" do diff --git a/spec/flipper/expressions/not_equal_spec.rb b/spec/flipper/expressions/not_equal_spec.rb index 1a19165db..ee1ac564c 100644 --- a/spec/flipper/expressions/not_equal_spec.rb +++ b/spec/flipper/expressions/not_equal_spec.rb @@ -1,5 +1,3 @@ -require 'helper' - RSpec.describe Flipper::Expressions::NotEqual do describe "#evaluate" do it "returns true when not equal" do diff --git a/spec/flipper/expressions/percentage_spec.rb b/spec/flipper/expressions/percentage_spec.rb index 17dc92e73..eadc27e72 100644 --- a/spec/flipper/expressions/percentage_spec.rb +++ b/spec/flipper/expressions/percentage_spec.rb @@ -1,5 +1,3 @@ -require 'helper' - RSpec.describe Flipper::Expressions::Percentage do describe "#evaluate" do it "returns true when string in percentage enabled" do diff --git a/spec/flipper/expressions/property_spec.rb b/spec/flipper/expressions/property_spec.rb index c27988a21..c97b19a47 100644 --- a/spec/flipper/expressions/property_spec.rb +++ b/spec/flipper/expressions/property_spec.rb @@ -1,5 +1,3 @@ -require 'helper' - RSpec.describe Flipper::Expressions::Property do it "can initialize with string" do expect(described_class.new("flipper_id").args).to eq(["flipper_id"]) diff --git a/spec/flipper/expressions/random_spec.rb b/spec/flipper/expressions/random_spec.rb index a7f7b7238..1da09e340 100644 --- a/spec/flipper/expressions/random_spec.rb +++ b/spec/flipper/expressions/random_spec.rb @@ -1,5 +1,3 @@ -require 'helper' - RSpec.describe Flipper::Expressions::Random do describe "#initialize" do it "works with number" do diff --git a/spec/flipper/expressions/value_spec.rb b/spec/flipper/expressions/value_spec.rb index e245d0400..81c279ed4 100644 --- a/spec/flipper/expressions/value_spec.rb +++ b/spec/flipper/expressions/value_spec.rb @@ -1,5 +1,3 @@ -require 'helper' - RSpec.describe Flipper::Expressions::Value do describe "#initialize" do it "works with string" do diff --git a/spec/flipper/gates/expression_spec.rb b/spec/flipper/gates/expression_spec.rb index 05d4e03e1..2b33f0136 100644 --- a/spec/flipper/gates/expression_spec.rb +++ b/spec/flipper/gates/expression_spec.rb @@ -1,5 +1,3 @@ -require 'helper' - RSpec.describe Flipper::Gates::Expression do let(:feature_name) { :search } diff --git a/spec/flipper/model/active_record_spec.rb b/spec/flipper/model/active_record_spec.rb index b2f86c652..00433acad 100644 --- a/spec/flipper/model/active_record_spec.rb +++ b/spec/flipper/model/active_record_spec.rb @@ -1,4 +1,3 @@ -require 'helper' require 'active_record' require 'flipper/model/active_record' diff --git a/spec/flipper/model/sequel_spec.rb b/spec/flipper/model/sequel_spec.rb index 9a453e3cf..01defeee9 100644 --- a/spec/flipper/model/sequel_spec.rb +++ b/spec/flipper/model/sequel_spec.rb @@ -1,4 +1,3 @@ -require 'helper' require 'flipper/model/sequel' RSpec.describe Flipper::Model::Sequel do diff --git a/test_rails/generators/flipper/active_record_generator_test.rb b/test_rails/generators/flipper/active_record_generator_test.rb index fd2dc8569..aca969ec5 100644 --- a/test_rails/generators/flipper/active_record_generator_test.rb +++ b/test_rails/generators/flipper/active_record_generator_test.rb @@ -1,4 +1,3 @@ -require 'helper' require 'active_record' require 'rails/generators/test_case' require 'generators/flipper/active_record_generator' From bb26dc89da80e307b261ecfaf9fe024a230248f7 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Mon, 24 Jan 2022 10:43:33 -0500 Subject: [PATCH 128/176] Use example instead of test in examples.yml github action --- .github/workflows/examples.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index b65b96699..69a2f24ec 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -3,7 +3,7 @@ on: [push, pull_request] jobs: test: if: github.repository_owner == 'jnunemaker' - name: Test on ruby ${{ matrix.ruby }} and rails ${{ matrix.rails }} + name: Example on ruby ${{ matrix.ruby }} and rails ${{ matrix.rails }} runs-on: ubuntu-latest services: redis: From c6b57a41b02a2a5aea06ba36b87a6a38fa97deb6 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Mon, 24 Jan 2022 11:12:58 -0500 Subject: [PATCH 129/176] Get flipper properties loading from railtie --- lib/flipper/railtie.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/flipper/railtie.rb b/lib/flipper/railtie.rb index 9a020248b..bd429d921 100644 --- a/lib/flipper/railtie.rb +++ b/lib/flipper/railtie.rb @@ -16,6 +16,14 @@ class Railtie < Rails::Railtie end end + initializer "flipper.properties" do + require "flipper/model/active_record" + + ActiveSupport.on_load(:active_record) do + ActiveRecord::Base.include Flipper::Model::ActiveRecord + end + end + initializer "flipper.default", before: :load_config_initializers do |app| Flipper.configure do |config| config.default do From 1562674a51fb00fa7eaff2dd43cd62d2a2cec829 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Mon, 24 Jan 2022 11:34:36 -0500 Subject: [PATCH 130/176] Try using environment per matrix I'm wondering if they are clobbering each other and causing errors. --- examples/cloud/cloud_setup.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/examples/cloud/cloud_setup.rb b/examples/cloud/cloud_setup.rb index 9825445c4..28d7f6883 100644 --- a/examples/cloud/cloud_setup.rb +++ b/examples/cloud/cloud_setup.rb @@ -2,3 +2,15 @@ warn "FLIPPER_CLOUD_TOKEN missing so skipping cloud example." exit end + +suffix_rails = ENV["RAILS_VERSION"].split(".").take(2).join +suffix_ruby = RUBY_VERSION.split(".").take(2).join +matrix_key = "FLIPPER_CLOUD_TOKEN_#{suffix_ruby}_#{suffix_rails}" + +if matrix_token = ENV[matrix_key] + puts "Using #{matrix_key} for FLIPPER_CLOUD_TOKEN" + ENV["FLIPPER_CLOUD_TOKEN"] = matrix_token +else + warn "Missing #{matrix_key}. Go create an environment in flipper cloud and set #{matrix_key} to the adapter token for that environment in github actions secrets." + exit 1 +end From 9f46a7d74678160351fc628ce92313a2e9852423 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Mon, 24 Jan 2022 11:48:00 -0500 Subject: [PATCH 131/176] Export all matrix envs Hate doing this. Need to find a way to export them all automatically. --- .github/workflows/examples.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 69a2f24ec..21006bb55 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -63,5 +63,17 @@ jobs: - name: Run Examples with Rails ${{ matrix.rails }} env: FLIPPER_CLOUD_TOKEN: ${{ secrets.FLIPPER_CLOUD_TOKEN }} + FLIPPER_CLOUD_TOKEN_26_52: ${{ secrets.FLIPPER_CLOUD_TOKEN_26_52 }} + FLIPPER_CLOUD_TOKEN_26_60: ${{ secrets.FLIPPER_CLOUD_TOKEN_26_60 }} + FLIPPER_CLOUD_TOKEN_26_61: ${{ secrets.FLIPPER_CLOUD_TOKEN_26_61 }} + FLIPPER_CLOUD_TOKEN_27_52: ${{ secrets.FLIPPER_CLOUD_TOKEN_27_52 }} + FLIPPER_CLOUD_TOKEN_27_60: ${{ secrets.FLIPPER_CLOUD_TOKEN_27_60 }} + FLIPPER_CLOUD_TOKEN_27_61: ${{ secrets.FLIPPER_CLOUD_TOKEN_27_61 }} + FLIPPER_CLOUD_TOKEN_27_70: ${{ secrets.FLIPPER_CLOUD_TOKEN_27_70 }} + FLIPPER_CLOUD_TOKEN_30_60: ${{ secrets.FLIPPER_CLOUD_TOKEN_30_60 }} + FLIPPER_CLOUD_TOKEN_30_61: ${{ secrets.FLIPPER_CLOUD_TOKEN_30_61 }} + FLIPPER_CLOUD_TOKEN_30_70: ${{ secrets.FLIPPER_CLOUD_TOKEN_30_70 }} + FLIPPER_CLOUD_TOKEN_31_61: ${{ secrets.FLIPPER_CLOUD_TOKEN_31_61 }} + FLIPPER_CLOUD_TOKEN_31_70: ${{ secrets.FLIPPER_CLOUD_TOKEN_31_70 }} RAILS_VERSION: ${{ matrix.rails }} run: script/examples From 2562e60ca01f925a8be29dea7faf6171a0403f8c Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Mon, 7 Feb 2022 16:25:33 -0500 Subject: [PATCH 132/176] Rename percentage expression to percentage of actors One to one mapping for previous functionality of the gate of the same name so why not make expression name match. --- examples/expressions.rb | 6 +++--- lib/flipper/expression.rb | 4 ++-- .../{percentage.rb => percentage_of_actors.rb} | 2 +- spec/flipper/expression_spec.rb | 12 ++++++------ ...rcentage_spec.rb => percentage_of_actors_spec.rb} | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) rename lib/flipper/expressions/{percentage.rb => percentage_of_actors.rb} (91%) rename spec/flipper/expressions/{percentage_spec.rb => percentage_of_actors_spec.rb} (94%) diff --git a/examples/expressions.rb b/examples/expressions.rb index a3e9be469..b300c51c5 100644 --- a/examples/expressions.rb +++ b/examples/expressions.rb @@ -148,7 +148,7 @@ class Org < Struct.new(:id, :flipper_properties) reset puts "\n\n% of Actors Expression" -percentage_of_actors = Flipper.property(:flipper_id).percentage(30) +percentage_of_actors = Flipper.property(:flipper_id).percentage_of_actors(30) Flipper.enable :something, percentage_of_actors refute Flipper.enabled?(:something, user) refute Flipper.enabled?(:something, other_user) @@ -159,11 +159,11 @@ class Org < Struct.new(:id, :flipper_properties) percentage_of_actors_per_type = Flipper.any( Flipper.all( Flipper.property(:type).eq("User"), - Flipper.property(:flipper_id).percentage(40), + Flipper.property(:flipper_id).percentage_of_actors(40), ), Flipper.all( Flipper.property(:type).eq("Org"), - Flipper.property(:flipper_id).percentage(10), + Flipper.property(:flipper_id).percentage_of_actors(10), ) ) Flipper.enable :something, percentage_of_actors_per_type diff --git a/lib/flipper/expression.rb b/lib/flipper/expression.rb index 807a51637..64709711c 100644 --- a/lib/flipper/expression.rb +++ b/lib/flipper/expression.rb @@ -97,8 +97,8 @@ def less_than_or_equal_to(object) alias lte less_than_or_equal_to alias less_than_or_equal less_than_or_equal_to - def percentage(object) - Expressions::Percentage.new([self, self.class.build(object, convert_to_values: true)]) + def percentage_of_actors(object) + Expressions::PercentageOfActors.new([self, self.class.build(object, convert_to_values: true)]) end private diff --git a/lib/flipper/expressions/percentage.rb b/lib/flipper/expressions/percentage_of_actors.rb similarity index 91% rename from lib/flipper/expressions/percentage.rb rename to lib/flipper/expressions/percentage_of_actors.rb index dc7113496..c11727b15 100644 --- a/lib/flipper/expressions/percentage.rb +++ b/lib/flipper/expressions/percentage_of_actors.rb @@ -2,7 +2,7 @@ module Flipper module Expressions - class Percentage < Expression + class PercentageOfActors < Expression SCALING_FACTOR = 1_000 def evaluate(context = {}) diff --git a/spec/flipper/expression_spec.rb b/spec/flipper/expression_spec.rb index ee2e771c6..544dda0ad 100644 --- a/spec/flipper/expression_spec.rb +++ b/spec/flipper/expression_spec.rb @@ -101,15 +101,15 @@ expect(expression.args).to eq([1]) end - it "can build Percentage" do + it "can build PercentageOfActors" do expression = Flipper::Expression.build({ - "Percentage" => [ + "PercentageOfActors" => [ {"Value" => ["User;1"]}, {"Value" => [40]}, ] }) - expect(expression).to be_instance_of(Flipper::Expressions::Percentage) + expect(expression).to be_instance_of(Flipper::Expressions::PercentageOfActors) expect(expression.args).to eq([ Flipper.value("User;1"), Flipper.value(40), @@ -276,10 +276,10 @@ end end - it "can convert to Percentage" do + it "can convert to PercentageOfActors" do expression = Flipper.value("User;1") - converted = expression.percentage(40) - expect(converted).to be_instance_of(Flipper::Expressions::Percentage) + converted = expression.percentage_of_actors(40) + expect(converted).to be_instance_of(Flipper::Expressions::PercentageOfActors) expect(converted.args).to eq([expression, Flipper.value(40)]) end end diff --git a/spec/flipper/expressions/percentage_spec.rb b/spec/flipper/expressions/percentage_of_actors_spec.rb similarity index 94% rename from spec/flipper/expressions/percentage_spec.rb rename to spec/flipper/expressions/percentage_of_actors_spec.rb index eadc27e72..d4980cb49 100644 --- a/spec/flipper/expressions/percentage_spec.rb +++ b/spec/flipper/expressions/percentage_of_actors_spec.rb @@ -1,4 +1,4 @@ -RSpec.describe Flipper::Expressions::Percentage do +RSpec.describe Flipper::Expressions::PercentageOfActors do describe "#evaluate" do it "returns true when string in percentage enabled" do expression = described_class.new([ @@ -58,7 +58,7 @@ ]) expect(expression.value).to eq({ - "Percentage" => [ + "PercentageOfActors" => [ {"Value" => ["User;1"]}, {"Value" => [10]}, ], From 7b5a2ddd3de243980030e9df674bdf970661934b Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Tue, 8 Feb 2022 08:31:51 -0500 Subject: [PATCH 133/176] Add boolean, number, percentage and string to expressions --- lib/flipper/expressions/boolean.rb | 15 +++++++ lib/flipper/expressions/number.rb | 15 +++++++ lib/flipper/expressions/percentage.rb | 20 +++++++++ lib/flipper/expressions/string.rb | 15 +++++++ spec/flipper/expression_spec.rb | 18 ++++++++ spec/flipper/expressions/boolean_spec.rb | 48 +++++++++++++++++++++ spec/flipper/expressions/number_spec.rb | 32 ++++++++++++++ spec/flipper/expressions/percentage_spec.rb | 39 +++++++++++++++++ spec/flipper/expressions/string_spec.rb | 32 ++++++++++++++ 9 files changed, 234 insertions(+) create mode 100644 lib/flipper/expressions/boolean.rb create mode 100644 lib/flipper/expressions/number.rb create mode 100644 lib/flipper/expressions/percentage.rb create mode 100644 lib/flipper/expressions/string.rb create mode 100644 spec/flipper/expressions/boolean_spec.rb create mode 100644 spec/flipper/expressions/number_spec.rb create mode 100644 spec/flipper/expressions/percentage_spec.rb create mode 100644 spec/flipper/expressions/string_spec.rb diff --git a/lib/flipper/expressions/boolean.rb b/lib/flipper/expressions/boolean.rb new file mode 100644 index 000000000..c01cac864 --- /dev/null +++ b/lib/flipper/expressions/boolean.rb @@ -0,0 +1,15 @@ +require "flipper/expression" + +module Flipper + module Expressions + class Boolean < Expression + def initialize(args) + super Array(args) + end + + def evaluate(context = {}) + !!evaluate_arg(0, context) + end + end + end +end diff --git a/lib/flipper/expressions/number.rb b/lib/flipper/expressions/number.rb new file mode 100644 index 000000000..a5e6e8b3a --- /dev/null +++ b/lib/flipper/expressions/number.rb @@ -0,0 +1,15 @@ +require "flipper/expression" + +module Flipper + module Expressions + class Number < Expression + def initialize(args) + super Array(args) + end + + def evaluate(context = {}) + evaluate_arg(0, context).to_f + end + end + end +end diff --git a/lib/flipper/expressions/percentage.rb b/lib/flipper/expressions/percentage.rb new file mode 100644 index 000000000..1767543ae --- /dev/null +++ b/lib/flipper/expressions/percentage.rb @@ -0,0 +1,20 @@ +require "flipper/expression" + +module Flipper + module Expressions + class Percentage < Number + def initialize(args) + super Array(args) + end + + def evaluate(context = {}) + value = evaluate_arg(0, context) + + value = 0 if value < 0 + value = 100 if value > 100 + + value + end + end + end +end diff --git a/lib/flipper/expressions/string.rb b/lib/flipper/expressions/string.rb new file mode 100644 index 000000000..8f6abd864 --- /dev/null +++ b/lib/flipper/expressions/string.rb @@ -0,0 +1,15 @@ +require "flipper/expression" + +module Flipper + module Expressions + class String < Expression + def initialize(args) + super Array(args) + end + + def evaluate(context = {}) + evaluate_arg(0, context).to_s + end + end + end +end diff --git a/spec/flipper/expression_spec.rb b/spec/flipper/expression_spec.rb index 544dda0ad..67ca3c244 100644 --- a/spec/flipper/expression_spec.rb +++ b/spec/flipper/expression_spec.rb @@ -101,6 +101,24 @@ expect(expression.args).to eq([1]) end + it "can build Number" do + expression = Flipper::Expression.build({ + "Number" => [1] + }) + + expect(expression).to be_instance_of(Flipper::Expressions::Number) + expect(expression.args).to eq([1]) + end + + it "can build Percentage" do + expression = Flipper::Expression.build({ + "Percentage" => [1] + }) + + expect(expression).to be_instance_of(Flipper::Expressions::Percentage) + expect(expression.args).to eq([1]) + end + it "can build PercentageOfActors" do expression = Flipper::Expression.build({ "PercentageOfActors" => [ diff --git a/spec/flipper/expressions/boolean_spec.rb b/spec/flipper/expressions/boolean_spec.rb new file mode 100644 index 000000000..6ef245f5d --- /dev/null +++ b/spec/flipper/expressions/boolean_spec.rb @@ -0,0 +1,48 @@ +RSpec.describe Flipper::Expressions::Boolean do + describe "#initialize" do + it "works with TrueClass" do + expect(described_class.new(true).args).to eq([true]) + end + + it "works with FalseClass" do + expect(described_class.new(false).args).to eq([false]) + end + + it "works with array" do + expect(described_class.new([true]).args).to eq([true]) + end + end + + describe "#evaluate" do + it "returns a true" do + expression = described_class.new([true]) + result = expression.evaluate + expect(result).to be(true) + end + + it "returns a false" do + expression = described_class.new([false]) + result = expression.evaluate + expect(result).to be(false) + end + + it "returns true for String" do + expression = described_class.new([""]) + result = expression.evaluate + expect(result).to be(true) + end + + it "returns true for Numeric" do + expression = described_class.new([0]) + result = expression.evaluate + expect(result).to be(true) + end + end + + describe "#value" do + it "returns Hash" do + expression = described_class.new([true]) + expect(expression.value).to eq({"Boolean" => [true]}) + end + end +end diff --git a/spec/flipper/expressions/number_spec.rb b/spec/flipper/expressions/number_spec.rb new file mode 100644 index 000000000..434520a75 --- /dev/null +++ b/spec/flipper/expressions/number_spec.rb @@ -0,0 +1,32 @@ +RSpec.describe Flipper::Expressions::Number do + describe "#initialize" do + it "works with number" do + expect(described_class.new(1).args).to eq([1]) + end + + it "works with array" do + expect(described_class.new([1]).args).to eq([1]) + end + end + + describe "#evaluate" do + it "returns Numeric" do + expression = described_class.new([10]) + result = expression.evaluate + expect(result).to be(10.0) + end + + it "returns Numeric for String" do + expression = described_class.new(['10']) + result = expression.evaluate + expect(result).to be(10.0) + end + end + + describe "#value" do + it "returns Hash" do + expression = described_class.new([99]) + expect(expression.value).to eq({"Number" => [99]}) + end + end +end diff --git a/spec/flipper/expressions/percentage_spec.rb b/spec/flipper/expressions/percentage_spec.rb new file mode 100644 index 000000000..92842e32b --- /dev/null +++ b/spec/flipper/expressions/percentage_spec.rb @@ -0,0 +1,39 @@ +RSpec.describe Flipper::Expressions::Percentage do + describe "#initialize" do + it "works with number" do + expect(described_class.new(1).args).to eq([1]) + end + + it "works with array" do + expect(described_class.new([1]).args).to eq([1]) + end + end + + describe "#evaluate" do + it "returns numeric" do + expression = described_class.new([10]) + result = expression.evaluate + expect(result).to be(10) + end + + it "returns 0 if less than 0" do + expression = described_class.new([-1]) + result = expression.evaluate + expect(result).to be(0) + end + + it "returns 100 if greater than 100" do + expression = described_class.new([101]) + result = expression.evaluate + expect(result).to be(100) + end + end + + describe "#value" do + it "returns Hash" do + expression = described_class.new([99]) + + expect(expression.value).to eq({"Percentage" => [99]}) + end + end +end diff --git a/spec/flipper/expressions/string_spec.rb b/spec/flipper/expressions/string_spec.rb new file mode 100644 index 000000000..bc5610b0e --- /dev/null +++ b/spec/flipper/expressions/string_spec.rb @@ -0,0 +1,32 @@ +RSpec.describe Flipper::Expressions::String do + describe "#initialize" do + it "works with string" do + expect(described_class.new("test").args).to eq(["test"]) + end + + it "works with array" do + expect(described_class.new(["test"]).args).to eq(["test"]) + end + end + + describe "#evaluate" do + it "returns String for Numeric" do + expression = described_class.new([10]) + result = expression.evaluate + expect(result).to eq("10") + end + + it "returns String" do + expression = described_class.new(["test"]) + result = expression.evaluate + expect(result).to eq("test") + end + end + + describe "#value" do + it "returns Hash" do + expression = described_class.new(["test"]) + expect(expression.value).to eq({"String" => ["test"]}) + end + end +end From 6447d4ea08c6b7be4a4900f3b480170f4c125d07 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 9 Feb 2022 15:08:54 -0500 Subject: [PATCH 134/176] Get rid of already defined constant warnings Probably better as ivars anyway so they can be tweaked easily later. --- .../adapters/active_support_cache_store.rb | 27 ++++++------- lib/flipper/adapters/dalli.rb | 27 ++++++------- lib/flipper/adapters/memoizable.rb | 24 +++++------- lib/flipper/adapters/memory.rb | 2 - lib/flipper/adapters/mongo.rb | 10 ++--- lib/flipper/adapters/pstore.rb | 9 ++--- lib/flipper/adapters/redis.rb | 10 ++--- lib/flipper/adapters/redis_cache.rb | 29 ++++++-------- .../active_support_cache_store_spec.rb | 38 +++++++++---------- spec/flipper/adapters/dalli_spec.rb | 16 ++++---- spec/flipper/adapters/memoizable_spec.rb | 30 +++++++-------- spec/flipper/adapters/read_only_spec.rb | 2 +- spec/flipper/adapters/redis_cache_spec.rb | 16 ++++---- 13 files changed, 106 insertions(+), 134 deletions(-) diff --git a/lib/flipper/adapters/active_support_cache_store.rb b/lib/flipper/adapters/active_support_cache_store.rb index 4474bdd15..62fb31308 100644 --- a/lib/flipper/adapters/active_support_cache_store.rb +++ b/lib/flipper/adapters/active_support_cache_store.rb @@ -8,16 +8,6 @@ module Adapters class ActiveSupportCacheStore include ::Flipper::Adapter - Version = 'v1'.freeze - Namespace = "flipper/#{Version}".freeze - FeaturesKey = "#{Namespace}/features".freeze - GetAllKey = "#{Namespace}/get_all".freeze - - # Private - def self.key_for(key) - "#{Namespace}/feature/#{key}" - end - # Internal attr_reader :cache @@ -32,6 +22,11 @@ def initialize(adapter, cache, expires_in: nil, write_through: false) @write_options = {} @write_options[:expires_in] = expires_in if expires_in @write_through = write_through + + @cache_version = 'v1'.freeze + @namespace = "flipper/#{@cache_version}".freeze + @features_key = "#{@namespace}/features".freeze + @get_all_key = "#{@namespace}/get_all".freeze end # Public @@ -42,14 +37,14 @@ def features # Public def add(feature) result = @adapter.add(feature) - @cache.delete(FeaturesKey) + @cache.delete(@features_key) result end ## Public def remove(feature) result = @adapter.remove(feature) - @cache.delete(FeaturesKey) + @cache.delete(@features_key) if @write_through @cache.write(key_for(feature.key), default_config, @write_options) @@ -79,12 +74,12 @@ def get_multi(features) end def get_all - if @cache.write(GetAllKey, Time.now.to_i, @write_options.merge(unless_exist: true)) + if @cache.write(@get_all_key, Time.now.to_i, @write_options.merge(unless_exist: true)) response = @adapter.get_all response.each do |key, value| @cache.write(key_for(key), value, @write_options) end - @cache.write(FeaturesKey, response.keys.to_set, @write_options) + @cache.write(@features_key, response.keys.to_set, @write_options) response else features = read_feature_keys.map { |key| Flipper::Feature.new(key, self) } @@ -121,12 +116,12 @@ def disable(feature, gate, thing) private def key_for(key) - self.class.key_for(key) + "#{@namespace}/feature/#{key}" end # Internal: Returns an array of the known feature keys. def read_feature_keys - @cache.fetch(FeaturesKey, @write_options) { @adapter.features } + @cache.fetch(@features_key, @write_options) { @adapter.features } end # Internal: Given an array of features, attempts to read through cache in diff --git a/lib/flipper/adapters/dalli.rb b/lib/flipper/adapters/dalli.rb index d98f2e685..31f65d2e1 100644 --- a/lib/flipper/adapters/dalli.rb +++ b/lib/flipper/adapters/dalli.rb @@ -8,16 +8,6 @@ module Adapters class Dalli include ::Flipper::Adapter - Version = 'v1'.freeze - Namespace = "flipper/#{Version}".freeze - FeaturesKey = "#{Namespace}/features".freeze - GetAllKey = "#{Namespace}/get_all".freeze - - # Private - def self.key_for(key) - "#{Namespace}/feature/#{key}" - end - # Internal attr_reader :cache @@ -33,6 +23,11 @@ def initialize(adapter, cache, ttl = 0) @name = :dalli @cache = cache @ttl = ttl + + @cache_version = 'v1'.freeze + @namespace = "flipper/#{@cache_version}".freeze + @features_key = "#{@namespace}/features".freeze + @get_all_key = "#{@namespace}/get_all".freeze end # Public @@ -43,14 +38,14 @@ def features # Public def add(feature) result = @adapter.add(feature) - @cache.delete(FeaturesKey) + @cache.delete(@features_key) result end # Public def remove(feature) result = @adapter.remove(feature) - @cache.delete(FeaturesKey) + @cache.delete(@features_key) @cache.delete(key_for(feature.key)) result end @@ -74,12 +69,12 @@ def get_multi(features) end def get_all - if @cache.add(GetAllKey, Time.now.to_i, @ttl) + if @cache.add(@get_all_key, Time.now.to_i, @ttl) response = @adapter.get_all response.each do |key, value| @cache.set(key_for(key), value, @ttl) end - @cache.set(FeaturesKey, response.keys.to_set, @ttl) + @cache.set(@features_key, response.keys.to_set, @ttl) response else features = read_feature_keys.map { |key| Flipper::Feature.new(key, self) } @@ -104,11 +99,11 @@ def disable(feature, gate, thing) private def key_for(key) - self.class.key_for(key) + "#{@namespace}/feature/#{key}" end def read_feature_keys - @cache.fetch(FeaturesKey, @ttl) { @adapter.features } + @cache.fetch(@features_key, @ttl) { @adapter.features } end # Internal: Given an array of features, attempts to read through cache in diff --git a/lib/flipper/adapters/memoizable.rb b/lib/flipper/adapters/memoizable.rb index 0cd4fcd58..a425b0e13 100644 --- a/lib/flipper/adapters/memoizable.rb +++ b/lib/flipper/adapters/memoizable.rb @@ -8,9 +8,6 @@ module Adapters class Memoizable < SimpleDelegator include ::Flipper::Adapter - FeaturesKey = :flipper_features - GetAllKey = :all_memoized - # Internal attr_reader :cache @@ -20,11 +17,6 @@ class Memoizable < SimpleDelegator # Internal: The adapter this adapter is wrapping. attr_reader :adapter - # Private - def self.key_for(key) - "feature/#{key}" - end - # Public def initialize(adapter, cache = nil) super(adapter) @@ -32,12 +24,14 @@ def initialize(adapter, cache = nil) @name = :memoizable @cache = cache || {} @memoize = false + @features_key = :flipper_features + @get_all_key = :all_memoized end # Public def features if memoizing? - cache.fetch(FeaturesKey) { cache[FeaturesKey] = @adapter.features } + cache.fetch(@features_key) { cache[@features_key] = @adapter.features } else @adapter.features end @@ -95,9 +89,9 @@ def get_multi(features) def get_all if memoizing? response = nil - if cache[GetAllKey] + if cache[@get_all_key] response = {} - cache[FeaturesKey].each do |key| + cache[@features_key].each do |key| response[key] = cache[key_for(key)] end else @@ -105,8 +99,8 @@ def get_all response.each do |key, value| cache[key_for(key)] = value end - cache[FeaturesKey] = response.keys.to_set - cache[GetAllKey] = true + cache[@features_key] = response.keys.to_set + cache[@get_all_key] = true end # Ensures that looking up other features that do not exist doesn't @@ -144,7 +138,7 @@ def memoizing? private def key_for(key) - self.class.key_for(key) + "feature/#{key}" end def expire_feature(feature) @@ -152,7 +146,7 @@ def expire_feature(feature) end def expire_features_set - cache.delete(FeaturesKey) if memoizing? + cache.delete(@features_key) if memoizing? end end end diff --git a/lib/flipper/adapters/memory.rb b/lib/flipper/adapters/memory.rb index ac4b55596..47dbe680b 100644 --- a/lib/flipper/adapters/memory.rb +++ b/lib/flipper/adapters/memory.rb @@ -7,8 +7,6 @@ module Adapters class Memory include ::Flipper::Adapter - FeaturesKey = :features - # Public: The name of the adapter. attr_reader :name diff --git a/lib/flipper/adapters/mongo.rb b/lib/flipper/adapters/mongo.rb index 1f8e61362..819076aa5 100644 --- a/lib/flipper/adapters/mongo.rb +++ b/lib/flipper/adapters/mongo.rb @@ -7,9 +7,6 @@ module Adapters class Mongo include ::Flipper::Adapter - # Private: The key that stores the set of known features. - FeaturesKey = :flipper_features - # Public: The name of the adapter. attr_reader :name @@ -19,6 +16,7 @@ class Mongo def initialize(collection) @collection = collection @name = :mongo + @features_key = :flipper_features end # Public: The set of known features. @@ -28,13 +26,13 @@ def features # Public: Adds a feature to the set of known features. def add(feature) - update FeaturesKey, '$addToSet' => { 'features' => feature.key } + update @features_key, '$addToSet' => { 'features' => feature.key } true end # Public: Removes a feature from the set of known features. def remove(feature) - update FeaturesKey, '$pull' => { 'features' => feature.key } + update @features_key, '$pull' => { 'features' => feature.key } clear feature true end @@ -128,7 +126,7 @@ def disable(feature, gate, thing) private def read_feature_keys - find(FeaturesKey).fetch('features') { Set.new }.to_set + find(@features_key).fetch('features') { Set.new }.to_set end def read_many_features(features) diff --git a/lib/flipper/adapters/pstore.rb b/lib/flipper/adapters/pstore.rb index a3e20ff0b..e1681c570 100644 --- a/lib/flipper/adapters/pstore.rb +++ b/lib/flipper/adapters/pstore.rb @@ -10,8 +10,6 @@ module Adapters class PStore include ::Flipper::Adapter - FeaturesKey = :flipper_features - # Public: The name of the adapter. attr_reader :name @@ -23,6 +21,7 @@ def initialize(path = 'flipper.pstore', thread_safe = true) @name = :pstore @path = path @store = ::PStore.new(path, thread_safe) + @features_key = :flipper_features end # Public: The set of known features. @@ -35,7 +34,7 @@ def features # Public: Adds a feature to the set of known features. def add(feature) @store.transaction do - set_add FeaturesKey, feature.key + set_add @features_key, feature.key end true end @@ -44,7 +43,7 @@ def add(feature) # all the values for the feature. def remove(feature) @store.transaction do - set_delete FeaturesKey, feature.key + set_delete @features_key, feature.key clear_gates(feature) end true @@ -142,7 +141,7 @@ def clear_gates(feature) end def read_feature_keys - set_members FeaturesKey + set_members @features_key end def read_many_features(features) diff --git a/lib/flipper/adapters/redis.rb b/lib/flipper/adapters/redis.rb index 38b6638a7..989cd47ee 100644 --- a/lib/flipper/adapters/redis.rb +++ b/lib/flipper/adapters/redis.rb @@ -7,9 +7,6 @@ module Adapters class Redis include ::Flipper::Adapter - # Private: The key that stores the set of known features. - FeaturesKey = :flipper_features - # Public: The name of the adapter. attr_reader :name @@ -19,6 +16,7 @@ class Redis def initialize(client) @client = client @name = :redis + @features_key = :flipper_features end # Public: The set of known features. @@ -28,13 +26,13 @@ def features # Public: Adds a feature to the set of known features. def add(feature) - @client.sadd FeaturesKey, feature.key + @client.sadd @features_key, feature.key true end # Public: Removes a feature from the set of known features. def remove(feature) - @client.srem FeaturesKey, feature.key + @client.srem @features_key, feature.key @client.del feature.key true end @@ -123,7 +121,7 @@ def read_many_features(features) end def read_feature_keys - @client.smembers(FeaturesKey).to_set + @client.smembers(@features_key).to_set end # Private: Gets a hash of fields => values for the given feature. diff --git a/lib/flipper/adapters/redis_cache.rb b/lib/flipper/adapters/redis_cache.rb index 168cfd328..0c359dbd1 100644 --- a/lib/flipper/adapters/redis_cache.rb +++ b/lib/flipper/adapters/redis_cache.rb @@ -8,16 +8,6 @@ module Adapters class RedisCache include ::Flipper::Adapter - Version = 'v1'.freeze - Namespace = "flipper/#{Version}".freeze - FeaturesKey = "#{Namespace}/features".freeze - GetAllKey = "#{Namespace}/get_all".freeze - - # Private - def self.key_for(key) - "#{Namespace}/feature/#{key}" - end - # Internal attr_reader :cache @@ -30,6 +20,11 @@ def initialize(adapter, cache, ttl = 3600) @name = :redis_cache @cache = cache @ttl = ttl + + @version = 'v1'.freeze + @namespace = "flipper/#{@version}".freeze + @features_key = "#{@namespace}/features".freeze + @get_all_key = "#{@namespace}/get_all".freeze end # Public @@ -40,14 +35,14 @@ def features # Public def add(feature) result = @adapter.add(feature) - @cache.del(FeaturesKey) + @cache.del(@features_key) result end # Public def remove(feature) result = @adapter.remove(feature) - @cache.del(FeaturesKey) + @cache.del(@features_key) @cache.del(key_for(feature.key)) result end @@ -71,13 +66,13 @@ def get_multi(features) end def get_all - if @cache.setnx(GetAllKey, Time.now.to_i) - @cache.expire(GetAllKey, @ttl) + if @cache.setnx(@get_all_key, Time.now.to_i) + @cache.expire(@get_all_key, @ttl) response = @adapter.get_all response.each do |key, value| set_with_ttl key_for(key), value end - set_with_ttl FeaturesKey, response.keys.to_set + set_with_ttl @features_key, response.keys.to_set response else features = read_feature_keys.map { |key| Flipper::Feature.new(key, self) } @@ -102,11 +97,11 @@ def disable(feature, gate, thing) private def key_for(key) - self.class.key_for(key) + "#{@namespace}/feature/#{key}" end def read_feature_keys - fetch(FeaturesKey) { @adapter.features } + fetch(@features_key) { @adapter.features } end def read_many_features(features) diff --git a/spec/flipper/adapters/active_support_cache_store_spec.rb b/spec/flipper/adapters/active_support_cache_store_spec.rb index f94a99fcb..69ae58910 100644 --- a/spec/flipper/adapters/active_support_cache_store_spec.rb +++ b/spec/flipper/adapters/active_support_cache_store_spec.rb @@ -28,8 +28,8 @@ end it 'expires feature and deletes the cache' do - expect(cache.read(described_class.key_for(feature))).to be_nil - expect(cache.exist?(described_class.key_for(feature))).to be(false) + expect(cache.read("flipper/v1/feature/#{feature.key}")).to be_nil + expect(cache.exist?("flipper/v1/feature/#{feature.key}")).to be(false) expect(feature).not_to be_enabled end @@ -37,8 +37,8 @@ let(:write_through) { true } it 'expires feature and writes an empty value to the cache' do - expect(cache.read(described_class.key_for(feature))).to eq(adapter.default_config) - expect(cache.exist?(described_class.key_for(feature))).to be(true) + expect(cache.read("flipper/v1/feature/#{feature.key}")).to eq(adapter.default_config) + expect(cache.exist?("flipper/v1/feature/#{feature.key}")).to be(true) expect(feature).not_to be_enabled end end @@ -52,8 +52,8 @@ end it 'enables feature and deletes the cache' do - expect(cache.read(described_class.key_for(feature))).to be_nil - expect(cache.exist?(described_class.key_for(feature))).to be(false) + expect(cache.read("flipper/v1/feature/#{feature.key}")).to be_nil + expect(cache.exist?("flipper/v1/feature/#{feature.key}")).to be(false) expect(feature).to be_enabled end @@ -61,8 +61,8 @@ let(:write_through) { true } it 'expires feature and writes to the cache' do - expect(cache.exist?(described_class.key_for(feature))).to be(true) - expect(cache.read(described_class.key_for(feature))).to include(boolean: 'true') + expect(cache.exist?("flipper/v1/feature/#{feature.key}")).to be(true) + expect(cache.read("flipper/v1/feature/#{feature.key}")).to include(boolean: 'true') expect(feature).to be_enabled end end @@ -76,8 +76,8 @@ end it 'disables feature and deletes the cache' do - expect(cache.read(described_class.key_for(feature))).to be_nil - expect(cache.exist?(described_class.key_for(feature))).to be(false) + expect(cache.read("flipper/v1/feature/#{feature.key}")).to be_nil + expect(cache.exist?("flipper/v1/feature/#{feature.key}")).to be(false) expect(feature).not_to be_enabled end @@ -85,8 +85,8 @@ let(:write_through) { true } it 'expires feature and writes to the cache' do - expect(cache.exist?(described_class.key_for(feature))).to be(true) - expect(cache.read(described_class.key_for(feature))).to include(boolean: nil) + expect(cache.exist?("flipper/v1/feature/#{feature.key}")).to be(true) + expect(cache.read("flipper/v1/feature/#{feature.key}")).to include(boolean: nil) expect(feature).not_to be_enabled end end @@ -103,13 +103,13 @@ memory_adapter.reset adapter.get(stats) - expect(cache.read(described_class.key_for(search))).to be(nil) - expect(cache.read(described_class.key_for(other))).to be(nil) + expect(cache.read("flipper/v1/feature/#{search.key}")).to be(nil) + expect(cache.read("flipper/v1/feature/#{other.key}")).to be(nil) adapter.get_multi([stats, search, other]) - expect(cache.read(described_class.key_for(search))[:boolean]).to eq('true') - expect(cache.read(described_class.key_for(other))[:boolean]).to be(nil) + expect(cache.read("flipper/v1/feature/#{search.key}")[:boolean]).to eq('true') + expect(cache.read("flipper/v1/feature/#{other.key}")[:boolean]).to be(nil) adapter.get_multi([stats, search, other]) adapter.get_multi([stats, search, other]) @@ -128,9 +128,9 @@ it 'warms all features' do adapter.get_all - expect(cache.read(described_class.key_for(stats))[:boolean]).to eq('true') - expect(cache.read(described_class.key_for(search))[:boolean]).to be(nil) - expect(cache.read(described_class::GetAllKey)).to be_within(2).of(Time.now.to_i) + expect(cache.read("flipper/v1/feature/#{stats.key}")[:boolean]).to eq('true') + expect(cache.read("flipper/v1/feature/#{search.key}")[:boolean]).to be(nil) + expect(cache.read("flipper/v1/get_all")).to be_within(2).of(Time.now.to_i) end it 'returns same result when already cached' do diff --git a/spec/flipper/adapters/dalli_spec.rb b/spec/flipper/adapters/dalli_spec.rb index 4abfb149b..ba1a0e589 100644 --- a/spec/flipper/adapters/dalli_spec.rb +++ b/spec/flipper/adapters/dalli_spec.rb @@ -28,7 +28,7 @@ feature = flipper[:stats] adapter.get(feature) adapter.remove(feature) - expect(cache.get(described_class.key_for(feature))).to be(nil) + expect(cache.get("flipper/v1/#{feature.key}")).to be(nil) end end @@ -43,13 +43,13 @@ memory_adapter.reset adapter.get(stats) - expect(cache.get(described_class.key_for(search))).to be(nil) - expect(cache.get(described_class.key_for(other))).to be(nil) + expect(cache.get("flipper/v1/feature/#{search.key}")).to be(nil) + expect(cache.get("flipper/v1/feature/#{other.key}")).to be(nil) adapter.get_multi([stats, search, other]) - expect(cache.get(described_class.key_for(search))[:boolean]).to eq('true') - expect(cache.get(described_class.key_for(other))[:boolean]).to be(nil) + expect(cache.get("flipper/v1/feature/#{search.key}")[:boolean]).to eq('true') + expect(cache.get("flipper/v1/feature/#{other.key}")[:boolean]).to be(nil) adapter.get_multi([stats, search, other]) adapter.get_multi([stats, search, other]) @@ -68,9 +68,9 @@ it 'warms all features' do adapter.get_all - expect(cache.get(described_class.key_for(stats))[:boolean]).to eq('true') - expect(cache.get(described_class.key_for(search))[:boolean]).to be(nil) - expect(cache.get(described_class::GetAllKey)).to be_within(2).of(Time.now.to_i) + expect(cache.get("flipper/v1/feature/#{stats.key}")[:boolean]).to eq('true') + expect(cache.get("flipper/v1/feature/#{search.key}")[:boolean]).to be(nil) + expect(cache.get("flipper/v1/get_all")).to be_within(2).of(Time.now.to_i) end it 'returns same result when already cached' do diff --git a/spec/flipper/adapters/memoizable_spec.rb b/spec/flipper/adapters/memoizable_spec.rb index 18e29f9c5..7d41c036f 100644 --- a/spec/flipper/adapters/memoizable_spec.rb +++ b/spec/flipper/adapters/memoizable_spec.rb @@ -2,7 +2,7 @@ require 'flipper/adapters/operation_logger' RSpec.describe Flipper::Adapters::Memoizable do - let(:features_key) { described_class::FeaturesKey } + let(:features_key) { :flipper_features } let(:adapter) { Flipper::Adapters::Memory.new } let(:flipper) { Flipper.new(adapter) } let(:cache) { {} } @@ -64,7 +64,7 @@ def foo it 'memoizes feature' do feature = flipper[:stats] result = subject.get(feature) - expect(cache[described_class.key_for(feature.key)]).to be(result) + expect(cache["feature/#{feature.key}"]).to be(result) end end @@ -93,8 +93,8 @@ def foo features = names.map { |name| flipper[name] } results = subject.get_multi(features) features.each do |feature| - expect(cache[described_class.key_for(feature.key)]).not_to be(nil) - expect(cache[described_class.key_for(feature.key)]).to be(results[feature.key]) + expect(cache["feature/#{feature.key}"]).not_to be(nil) + expect(cache["feature/#{feature.key}"]).to be(results[feature.key]) end end end @@ -125,10 +125,10 @@ def foo features = names.map { |name| flipper[name].tap(&:enable) } results = subject.get_all features.each do |feature| - expect(cache[described_class.key_for(feature.key)]).not_to be(nil) - expect(cache[described_class.key_for(feature.key)]).to be(results[feature.key]) + expect(cache["feature/#{feature.key}"]).not_to be(nil) + expect(cache["feature/#{feature.key}"]).to be(results[feature.key]) end - expect(cache[subject.class::FeaturesKey]).to eq(names.map(&:to_s).to_set) + expect(cache[:flipper_features]).to eq(names.map(&:to_s).to_set) end it 'only calls get_all once for memoized adapter' do @@ -198,9 +198,9 @@ def foo it 'unmemoizes feature' do feature = flipper[:stats] gate = feature.gate(:boolean) - cache[described_class.key_for(feature.key)] = { some: 'thing' } + cache["feature/#{feature.key}"] = { some: 'thing' } subject.enable(feature, gate, flipper.bool) - expect(cache[described_class.key_for(feature.key)]).to be_nil + expect(cache["feature/#{feature.key}"]).to be_nil end end @@ -228,9 +228,9 @@ def foo it 'unmemoizes feature' do feature = flipper[:stats] gate = feature.gate(:boolean) - cache[described_class.key_for(feature.key)] = { some: 'thing' } + cache["feature/#{feature.key}"] = { some: 'thing' } subject.disable(feature, gate, flipper.bool) - expect(cache[described_class.key_for(feature.key)]).to be_nil + expect(cache["feature/#{feature.key}"]).to be_nil end end @@ -312,9 +312,9 @@ def foo it 'unmemoizes the feature' do feature = flipper[:stats] - cache[described_class.key_for(feature.key)] = { some: 'thing' } + cache["feature/#{feature.key}"] = { some: 'thing' } subject.remove(feature) - expect(cache[described_class.key_for(feature.key)]).to be_nil + expect(cache["feature/#{feature.key}"]).to be_nil end end @@ -337,9 +337,9 @@ def foo it 'unmemoizes feature' do feature = flipper[:stats] - cache[described_class.key_for(feature.key)] = { some: 'thing' } + cache["feature/#{feature.key}"] = { some: 'thing' } subject.clear(feature) - expect(cache[described_class.key_for(feature.key)]).to be_nil + expect(cache["feature/#{feature.key}"]).to be_nil end end diff --git a/spec/flipper/adapters/read_only_spec.rb b/spec/flipper/adapters/read_only_spec.rb index b45c5f8f2..c317df4f5 100644 --- a/spec/flipper/adapters/read_only_spec.rb +++ b/spec/flipper/adapters/read_only_spec.rb @@ -58,7 +58,7 @@ expression: { "Equal" => [ {"Property" => ["plan"]}, - {"Value" => ["basic"]}, + {"String" => ["basic"]}, ] }, percentage_of_actors: '25', diff --git a/spec/flipper/adapters/redis_cache_spec.rb b/spec/flipper/adapters/redis_cache_spec.rb index 76fef3a7a..a7656e9ea 100644 --- a/spec/flipper/adapters/redis_cache_spec.rb +++ b/spec/flipper/adapters/redis_cache_spec.rb @@ -31,7 +31,7 @@ feature = flipper[:stats] adapter.get(feature) adapter.remove(feature) - expect(client.get(described_class.key_for(feature))).to be(nil) + expect(client.get("flipper/v1/feature/#{feature.key}")).to be(nil) end end @@ -39,7 +39,7 @@ it 'uses correct cache key' do stats = flipper[:stats] adapter.get(stats) - expect(client.get(described_class.key_for(stats))).not_to be_nil + expect(client.get("flipper/v1/feature/#{stats.key}")).not_to be_nil end end @@ -54,13 +54,13 @@ memory_adapter.reset adapter.get(stats) - expect(client.get(described_class.key_for(search))).to be(nil) - expect(client.get(described_class.key_for(other))).to be(nil) + expect(client.get("flipper/v1/feature/#{search.key}")).to be(nil) + expect(client.get("flipper/v1/feature/#{other.key}")).to be(nil) adapter.get_multi([stats, search, other]) search_cache_value, other_cache_value = [search, other].map do |f| - Marshal.load(client.get(described_class.key_for(f))) + Marshal.load(client.get("flipper/v1/feature/#{f.key}")) end expect(search_cache_value[:boolean]).to eq('true') expect(other_cache_value[:boolean]).to be(nil) @@ -82,9 +82,9 @@ it 'warms all features' do adapter.get_all - expect(Marshal.load(client.get(described_class.key_for(stats.key)))[:boolean]).to eq('true') - expect(Marshal.load(client.get(described_class.key_for(search.key)))[:boolean]).to be(nil) - expect(client.get(described_class::GetAllKey).to_i).to be_within(2).of(Time.now.to_i) + expect(Marshal.load(client.get("flipper/v1/feature/#{stats.key}"))[:boolean]).to eq('true') + expect(Marshal.load(client.get("flipper/v1/feature/#{search.key}"))[:boolean]).to be(nil) + expect(client.get("flipper/v1/get_all").to_i).to be_within(2).of(Time.now.to_i) end it 'returns same result when already cached' do From a377f980eeb8918ebf6f664a4191b7a9af7540f9 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 9 Feb 2022 15:41:27 -0500 Subject: [PATCH 135/176] Remove shortcuts for types from dsl also --- lib/flipper.rb | 22 ++--- lib/flipper/dsl.rb | 46 ---------- lib/flipper/spec/shared_adapter_specs.rb | 86 +++++++++---------- lib/flipper/test/shared_adapter_test.rb | 86 +++++++++---------- .../active_support_cache_store_spec.rb | 4 +- spec/flipper/adapters/dual_write_spec.rb | 4 +- spec/flipper/adapters/instrumented_spec.rb | 2 +- spec/flipper/adapters/memoizable_spec.rb | 12 +-- .../flipper/adapters/operation_logger_spec.rb | 4 +- spec/flipper/adapters/read_only_spec.rb | 12 +-- .../api/v1/actions/clear_feature_spec.rb | 16 ++-- spec/flipper/dsl_spec.rb | 75 ---------------- spec/flipper_integration_spec.rb | 24 +++--- spec/flipper_spec.rb | 37 ++------ 14 files changed, 144 insertions(+), 286 deletions(-) diff --git a/lib/flipper.rb b/lib/flipper.rb index 4c8642351..6268d16d8 100644 --- a/lib/flipper.rb +++ b/lib/flipper.rb @@ -56,15 +56,13 @@ def instance=(flipper) # Public: All the methods delegated to instance. These should match the # interface of Flipper::DSL. def_delegators :instance, - :enabled?, :enable, :disable, :bool, :boolean, + :enabled?, :enable, :disable, :enable_expression, :disable_expression, :expression, :add_expression, :remove_expression, - :enable_actor, :disable_actor, :actor, + :enable_actor, :disable_actor, :enable_group, :disable_group, :enable_percentage_of_actors, :disable_percentage_of_actors, - :actors, :percentage_of_actors, :enable_percentage_of_time, :disable_percentage_of_time, - :time, :percentage_of_time, :features, :feature, :[], :preload, :preload_all, :adapter, :add, :exist?, :remove, :import, :memoize=, :memoizing?, @@ -82,16 +80,20 @@ def property(name) Flipper::Expressions::Property.new(name) end - def value(value) - Flipper::Expressions::Value.new(value) + def string(value) + Flipper::Expressions::String.new(value) end - def random(name) - Flipper::Expressions::Random.new(name) + def number(value) + Flipper::Expressions::Number.new(value) + end + + def boolean(value) + Flipper::Expressions::Boolean.new(value) end - def value(value) - Flipper::Expressions::Value.new(value) + def random(name) + Flipper::Expressions::Random.new(name) end # Public: Use this to register a group by name. diff --git a/lib/flipper/dsl.rb b/lib/flipper/dsl.rb index 02cbe4d07..2d7ae2a60 100644 --- a/lib/flipper/dsl.rb +++ b/lib/flipper/dsl.rb @@ -245,22 +245,6 @@ def preload_all # Returns an instance of Flipper::Feature. alias_method :[], :feature - # Public: Shortcut for getting a boolean type instance. - # - # value - The true or false value for the boolean. - # - # Returns a Flipper::Types::Boolean instance. - def boolean(value = true) - Types::Boolean.new(value) - end - - # Public: Even shorter shortcut for getting a boolean type instance. - # - # value - The true or false value for the boolean. - # - # Returns a Flipper::Types::Boolean instance. - alias_method :bool, :boolean - # Public: Access a flipper group by name. # # name - The String or Symbol name of the feature. @@ -270,16 +254,6 @@ def group(name) Flipper.group(name) end - # Public: Wraps an object as a flipper actor. - # - # thing - The object that you would like to wrap. - # - # Returns an instance of Flipper::Types::Actor. - # Raises ArgumentError if thing does not respond to `flipper_id`. - def actor(thing) - Types::Actor.new(thing) - end - # Public: Gets the expression for the feature. # # name - The String or Symbol name of the feature. @@ -289,26 +263,6 @@ def expression(name) feature(name).expression end - # Public: Shortcut for getting a percentage of time instance. - # - # number - The percentage of time that should be enabled. - # - # Returns Flipper::Types::PercentageOfTime. - def time(number) - Types::PercentageOfTime.new(number) - end - alias_method :percentage_of_time, :time - - # Public: Shortcut for getting a percentage of actors instance. - # - # number - The percentage of actors that should be enabled. - # - # Returns Flipper::Types::PercentageOfActors. - def actors(number) - Types::PercentageOfActors.new(number) - end - alias_method :percentage_of_actors, :actors - # Public: Returns a Set of the known features for this adapter. # # Returns Set of Flipper::Feature instances. diff --git a/lib/flipper/spec/shared_adapter_specs.rb b/lib/flipper/spec/shared_adapter_specs.rb index 0a1bc73d3..e2a8c54b3 100644 --- a/lib/flipper/spec/shared_adapter_specs.rb +++ b/lib/flipper/spec/shared_adapter_specs.rb @@ -39,12 +39,12 @@ end it 'can enable, disable and get value for boolean gate' do - expect(subject.enable(feature, boolean_gate, flipper.boolean)).to eq(true) + expect(subject.enable(feature, boolean_gate, Flipper::Types::Boolean.new)).to eq(true) result = subject.get(feature) expect(result[:boolean]).to eq('true') - expect(subject.disable(feature, boolean_gate, flipper.boolean(false))).to eq(true) + expect(subject.disable(feature, boolean_gate, Flipper::Types::Boolean.new(false))).to eq(true) result = subject.get(feature) expect(result[:boolean]).to eq(nil) @@ -52,13 +52,13 @@ it 'fully disables all enabled things when boolean gate disabled' do actor22 = Flipper::Actor.new('22') - expect(subject.enable(feature, boolean_gate, flipper.boolean)).to eq(true) + expect(subject.enable(feature, boolean_gate, Flipper::Types::Boolean.new)).to eq(true) expect(subject.enable(feature, group_gate, flipper.group(:admins))).to eq(true) - expect(subject.enable(feature, actor_gate, flipper.actor(actor22))).to eq(true) - expect(subject.enable(feature, actors_gate, flipper.actors(25))).to eq(true) - expect(subject.enable(feature, time_gate, flipper.time(45))).to eq(true) + expect(subject.enable(feature, actor_gate, Flipper::Types::Actor.new(actor22))).to eq(true) + expect(subject.enable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(25))).to eq(true) + expect(subject.enable(feature, time_gate, Flipper::Types::PercentageOfTime.new(45))).to eq(true) - expect(subject.disable(feature, boolean_gate, flipper.boolean(false))).to eq(true) + expect(subject.disable(feature, boolean_gate, Flipper::Types::Boolean.new(false))).to eq(true) expect(subject.get(feature)).to eq(subject.default_config) end @@ -100,34 +100,34 @@ actor22 = Flipper::Actor.new('22') actor_asdf = Flipper::Actor.new('asdf') - expect(subject.enable(feature, actor_gate, flipper.actor(actor22))).to eq(true) - expect(subject.enable(feature, actor_gate, flipper.actor(actor_asdf))).to eq(true) + expect(subject.enable(feature, actor_gate, Flipper::Types::Actor.new(actor22))).to eq(true) + expect(subject.enable(feature, actor_gate, Flipper::Types::Actor.new(actor_asdf))).to eq(true) result = subject.get(feature) expect(result[:actors]).to eq(Set['22', 'asdf']) - expect(subject.disable(feature, actor_gate, flipper.actor(actor22))).to eq(true) + expect(subject.disable(feature, actor_gate, Flipper::Types::Actor.new(actor22))).to eq(true) result = subject.get(feature) expect(result[:actors]).to eq(Set['asdf']) - expect(subject.disable(feature, actor_gate, flipper.actor(actor_asdf))).to eq(true) + expect(subject.disable(feature, actor_gate, Flipper::Types::Actor.new(actor_asdf))).to eq(true) result = subject.get(feature) expect(result[:actors]).to eq(Set.new) end it 'can enable, disable and get value for percentage of actors gate' do - expect(subject.enable(feature, actors_gate, flipper.actors(15))).to eq(true) + expect(subject.enable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(15))).to eq(true) result = subject.get(feature) expect(result[:percentage_of_actors]).to eq('15') - expect(subject.disable(feature, actors_gate, flipper.actors(0))).to eq(true) + expect(subject.disable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(0))).to eq(true) result = subject.get(feature) expect(result[:percentage_of_actors]).to eq('0') end it 'can enable percentage of actors gate many times and consistently return values' do (1..100).each do |percentage| - expect(subject.enable(feature, actors_gate, flipper.actors(percentage))).to eq(true) + expect(subject.enable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(percentage))).to eq(true) result = subject.get(feature) expect(result[:percentage_of_actors]).to eq(percentage.to_s) end @@ -135,25 +135,25 @@ it 'can disable percentage of actors gate many times and consistently return values' do (1..100).each do |percentage| - expect(subject.disable(feature, actors_gate, flipper.actors(percentage))).to eq(true) + expect(subject.disable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(percentage))).to eq(true) result = subject.get(feature) expect(result[:percentage_of_actors]).to eq(percentage.to_s) end end it 'can enable, disable and get value for percentage of time gate' do - expect(subject.enable(feature, time_gate, flipper.time(10))).to eq(true) + expect(subject.enable(feature, time_gate, Flipper::Types::PercentageOfTime.new(10))).to eq(true) result = subject.get(feature) expect(result[:percentage_of_time]).to eq('10') - expect(subject.disable(feature, time_gate, flipper.time(0))).to eq(true) + expect(subject.disable(feature, time_gate, Flipper::Types::PercentageOfTime.new(0))).to eq(true) result = subject.get(feature) expect(result[:percentage_of_time]).to eq('0') end it 'can enable percentage of time gate many times and consistently return values' do (1..100).each do |percentage| - expect(subject.enable(feature, time_gate, flipper.time(percentage))).to eq(true) + expect(subject.enable(feature, time_gate, Flipper::Types::PercentageOfTime.new(percentage))).to eq(true) result = subject.get(feature) expect(result[:percentage_of_time]).to eq(percentage.to_s) end @@ -161,20 +161,20 @@ it 'can disable percentage of time gate many times and consistently return values' do (1..100).each do |percentage| - expect(subject.disable(feature, time_gate, flipper.time(percentage))).to eq(true) + expect(subject.disable(feature, time_gate, Flipper::Types::PercentageOfTime.new(percentage))).to eq(true) result = subject.get(feature) expect(result[:percentage_of_time]).to eq(percentage.to_s) end end it 'converts boolean value to a string' do - expect(subject.enable(feature, boolean_gate, flipper.boolean)).to eq(true) + expect(subject.enable(feature, boolean_gate, Flipper::Types::Boolean.new)).to eq(true) result = subject.get(feature) expect(result[:boolean]).to eq('true') end it 'converts the actor value to a string' do - expect(subject.enable(feature, actor_gate, flipper.actor(Flipper::Actor.new(22)))).to eq(true) + expect(subject.enable(feature, actor_gate, Flipper::Types::Actor.new(Flipper::Actor.new(22)))).to eq(true) result = subject.get(feature) expect(result[:actors]).to eq(Set['22']) end @@ -186,13 +186,13 @@ end it 'converts percentage of time integer value to a string' do - expect(subject.enable(feature, time_gate, flipper.time(10))).to eq(true) + expect(subject.enable(feature, time_gate, Flipper::Types::PercentageOfTime.new(10))).to eq(true) result = subject.get(feature) expect(result[:percentage_of_time]).to eq('10') end it 'converts percentage of actors integer value to a string' do - expect(subject.enable(feature, actors_gate, flipper.actors(10))).to eq(true) + expect(subject.enable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(10))).to eq(true) result = subject.get(feature) expect(result[:percentage_of_actors]).to eq('10') end @@ -215,11 +215,11 @@ it 'clears all the gate values for the feature on remove' do actor22 = Flipper::Actor.new('22') - expect(subject.enable(feature, boolean_gate, flipper.boolean)).to eq(true) + expect(subject.enable(feature, boolean_gate, Flipper::Types::Boolean.new)).to eq(true) expect(subject.enable(feature, group_gate, flipper.group(:admins))).to eq(true) - expect(subject.enable(feature, actor_gate, flipper.actor(actor22))).to eq(true) - expect(subject.enable(feature, actors_gate, flipper.actors(25))).to eq(true) - expect(subject.enable(feature, time_gate, flipper.time(45))).to eq(true) + expect(subject.enable(feature, actor_gate, Flipper::Types::Actor.new(actor22))).to eq(true) + expect(subject.enable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(25))).to eq(true) + expect(subject.enable(feature, time_gate, Flipper::Types::PercentageOfTime.new(45))).to eq(true) expect(subject.remove(feature)).to eq(true) @@ -231,11 +231,11 @@ subject.add(feature) expect(subject.features).to include(feature.key) - expect(subject.enable(feature, boolean_gate, flipper.boolean)).to eq(true) + expect(subject.enable(feature, boolean_gate, Flipper::Types::Boolean.new)).to eq(true) expect(subject.enable(feature, group_gate, flipper.group(:admins))).to eq(true) - expect(subject.enable(feature, actor_gate, flipper.actor(actor22))).to eq(true) - expect(subject.enable(feature, actors_gate, flipper.actors(25))).to eq(true) - expect(subject.enable(feature, time_gate, flipper.time(45))).to eq(true) + expect(subject.enable(feature, actor_gate, Flipper::Types::Actor.new(actor22))).to eq(true) + expect(subject.enable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(25))).to eq(true) + expect(subject.enable(feature, time_gate, Flipper::Types::PercentageOfTime.new(45))).to eq(true) expect(subject.clear(feature)).to eq(true) expect(subject.features).to include(feature.key) @@ -248,7 +248,7 @@ it 'can get multiple features' do expect(subject.add(flipper[:stats])).to eq(true) - expect(subject.enable(flipper[:stats], boolean_gate, flipper.boolean)).to eq(true) + expect(subject.enable(flipper[:stats], boolean_gate, Flipper::Types::Boolean.new)).to eq(true) expect(subject.add(flipper[:search])).to eq(true) result = subject.get_multi([flipper[:stats], flipper[:search], flipper[:other]]) @@ -264,7 +264,7 @@ it 'can get all features' do expect(subject.add(flipper[:stats])).to eq(true) - expect(subject.enable(flipper[:stats], boolean_gate, flipper.boolean)).to eq(true) + expect(subject.enable(flipper[:stats], boolean_gate, Flipper::Types::Boolean.new)).to eq(true) expect(subject.add(flipper[:search])).to eq(true) result = subject.get_all @@ -287,8 +287,8 @@ it 'can double enable an actor without error' do actor = Flipper::Actor.new('Flipper::Actor;22') - expect(subject.enable(feature, actor_gate, flipper.actor(actor))).to eq(true) - expect(subject.enable(feature, actor_gate, flipper.actor(actor))).to eq(true) + expect(subject.enable(feature, actor_gate, Flipper::Types::Actor.new(actor))).to eq(true) + expect(subject.enable(feature, actor_gate, Flipper::Types::Actor.new(actor))).to eq(true) expect(subject.get(feature).fetch(:actors)).to eq(Set['Flipper::Actor;22']) end @@ -299,13 +299,13 @@ end it 'can double enable percentage without error' do - expect(subject.enable(feature, actors_gate, flipper.actors(25))).to eq(true) - expect(subject.enable(feature, actors_gate, flipper.actors(25))).to eq(true) + expect(subject.enable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(25))).to eq(true) + expect(subject.enable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(25))).to eq(true) end it 'can double enable without error' do - expect(subject.enable(feature, boolean_gate, flipper.boolean)).to eq(true) - expect(subject.enable(feature, boolean_gate, flipper.boolean)).to eq(true) + expect(subject.enable(feature, boolean_gate, Flipper::Types::Boolean.new)).to eq(true) + expect(subject.enable(feature, boolean_gate, Flipper::Types::Boolean.new)).to eq(true) end it 'can get_all features when there are none' do @@ -315,11 +315,11 @@ it 'clears other gate values on enable' do actor = Flipper::Actor.new('Flipper::Actor;22') - subject.enable(feature, actors_gate, flipper.actors(25)) - subject.enable(feature, time_gate, flipper.time(25)) + subject.enable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(25)) + subject.enable(feature, time_gate, Flipper::Types::PercentageOfTime.new(25)) subject.enable(feature, group_gate, flipper.group(:admins)) - subject.enable(feature, actor_gate, flipper.actor(actor)) - subject.enable(feature, boolean_gate, flipper.boolean(true)) + subject.enable(feature, actor_gate, Flipper::Types::Actor.new(actor)) + subject.enable(feature, boolean_gate, Flipper::Types::Boolean.new(true)) expect(subject.get(feature)).to eq(subject.default_config.merge(boolean: "true")) end end diff --git a/lib/flipper/test/shared_adapter_test.rb b/lib/flipper/test/shared_adapter_test.rb index 12375fb24..407a7f5ca 100644 --- a/lib/flipper/test/shared_adapter_test.rb +++ b/lib/flipper/test/shared_adapter_test.rb @@ -40,20 +40,20 @@ def test_returns_correct_default_values_for_gates_if_none_are_enabled end def test_can_enable_disable_and_get_value_for_boolean_gate - assert_equal true, @adapter.enable(@feature, @boolean_gate, @flipper.boolean) + assert_equal true, @adapter.enable(@feature, @boolean_gate, @Flipper::Types::Boolean.new) assert_equal 'true', @adapter.get(@feature)[:boolean] - assert_equal true, @adapter.disable(@feature, @boolean_gate, @flipper.boolean(false)) + assert_equal true, @adapter.disable(@feature, @boolean_gate, @Flipper::Types::Boolean.new(false)) assert_nil @adapter.get(@feature)[:boolean] end def test_fully_disables_all_enabled_things_when_boolean_gate_disabled actor22 = Flipper::Actor.new('22') - assert_equal true, @adapter.enable(@feature, @boolean_gate, @flipper.boolean) + assert_equal true, @adapter.enable(@feature, @boolean_gate, @Flipper::Types::Boolean.new) assert_equal true, @adapter.enable(@feature, @group_gate, @flipper.group(:admins)) - assert_equal true, @adapter.enable(@feature, @actor_gate, @flipper.actor(actor22)) - assert_equal true, @adapter.enable(@feature, @actors_gate, @flipper.actors(25)) - assert_equal true, @adapter.enable(@feature, @time_gate, @flipper.time(45)) - assert_equal true, @adapter.disable(@feature, @boolean_gate, @flipper.boolean(false)) + assert_equal true, @adapter.enable(@feature, @actor_gate, Flipper::Types::Actor.new(actor22)) + assert_equal true, @adapter.enable(@feature, @actors_gate, Flipper::Types::PercentageOfActors.new(25)) + assert_equal true, @adapter.enable(@feature, @time_gate, Flipper::Types::PercentageOfTime.new(45)) + assert_equal true, @adapter.disable(@feature, @boolean_gate, @Flipper::Types::Boolean.new(false)) assert_equal @adapter.default_config, @adapter.get(@feature) end @@ -95,34 +95,34 @@ def test_can_enable_disable_and_get_value_for_an_actor_gate actor22 = Flipper::Actor.new('22') actor_asdf = Flipper::Actor.new('asdf') - assert_equal true, @adapter.enable(@feature, @actor_gate, @flipper.actor(actor22)) - assert_equal true, @adapter.enable(@feature, @actor_gate, @flipper.actor(actor_asdf)) + assert_equal true, @adapter.enable(@feature, @actor_gate, Flipper::Types::Actor.new(actor22)) + assert_equal true, @adapter.enable(@feature, @actor_gate, Flipper::Types::Actor.new(actor_asdf)) result = @adapter.get(@feature) assert_equal Set['22', 'asdf'], result[:actors] - assert true, @adapter.disable(@feature, @actor_gate, @flipper.actor(actor22)) + assert true, @adapter.disable(@feature, @actor_gate, Flipper::Types::Actor.new(actor22)) result = @adapter.get(@feature) assert_equal Set['asdf'], result[:actors] - assert_equal true, @adapter.disable(@feature, @actor_gate, @flipper.actor(actor_asdf)) + assert_equal true, @adapter.disable(@feature, @actor_gate, Flipper::Types::Actor.new(actor_asdf)) result = @adapter.get(@feature) assert_equal Set.new, result[:actors] end def test_can_enable_disable_get_value_for_percentage_of_actors_gate - assert_equal true, @adapter.enable(@feature, @actors_gate, @flipper.actors(15)) + assert_equal true, @adapter.enable(@feature, @actors_gate, Flipper::Types::PercentageOfActors.new(15)) result = @adapter.get(@feature) assert_equal '15', result[:percentage_of_actors] - assert_equal true, @adapter.disable(@feature, @actors_gate, @flipper.actors(0)) + assert_equal true, @adapter.disable(@feature, @actors_gate, Flipper::Types::PercentageOfActors.new(0)) result = @adapter.get(@feature) assert_equal '0', result[:percentage_of_actors] end def test_can_enable_percentage_of_actors_gate_many_times_and_consistently_return_values (1..100).each do |percentage| - assert_equal true, @adapter.enable(@feature, @actors_gate, @flipper.actors(percentage)) + assert_equal true, @adapter.enable(@feature, @actors_gate, Flipper::Types::PercentageOfActors.new(percentage)) result = @adapter.get(@feature) assert_equal percentage.to_s, result[:percentage_of_actors] end @@ -130,25 +130,25 @@ def test_can_enable_percentage_of_actors_gate_many_times_and_consistently_return def test_can_disable_percentage_of_actors_gate_many_times_and_consistently_return_values (1..100).each do |percentage| - assert_equal true, @adapter.disable(@feature, @actors_gate, @flipper.actors(percentage)) + assert_equal true, @adapter.disable(@feature, @actors_gate, Flipper::Types::PercentageOfActors.new(percentage)) result = @adapter.get(@feature) assert_equal percentage.to_s, result[:percentage_of_actors] end end def test_can_enable_disable_and_get_value_for_percentage_of_time_gate - assert_equal true, @adapter.enable(@feature, @time_gate, @flipper.time(10)) + assert_equal true, @adapter.enable(@feature, @time_gate, Flipper::Types::PercentageOfTime.new(10)) result = @adapter.get(@feature) assert_equal '10', result[:percentage_of_time] - assert_equal true, @adapter.disable(@feature, @time_gate, @flipper.time(0)) + assert_equal true, @adapter.disable(@feature, @time_gate, Flipper::Types::PercentageOfTime.new(0)) result = @adapter.get(@feature) assert_equal '0', result[:percentage_of_time] end def test_can_enable_percentage_of_time_gate_many_times_and_consistently_return_values (1..100).each do |percentage| - assert_equal true, @adapter.enable(@feature, @time_gate, @flipper.time(percentage)) + assert_equal true, @adapter.enable(@feature, @time_gate, Flipper::Types::PercentageOfTime.new(percentage)) result = @adapter.get(@feature) assert_equal percentage.to_s, result[:percentage_of_time] end @@ -156,21 +156,21 @@ def test_can_enable_percentage_of_time_gate_many_times_and_consistently_return_v def test_can_disable_percentage_of_time_gate_many_times_and_consistently_return_values (1..100).each do |percentage| - assert_equal true, @adapter.disable(@feature, @time_gate, @flipper.time(percentage)) + assert_equal true, @adapter.disable(@feature, @time_gate, Flipper::Types::PercentageOfTime.new(percentage)) result = @adapter.get(@feature) assert_equal percentage.to_s, result[:percentage_of_time] end end def test_converts_boolean_value_to_a_string - assert_equal true, @adapter.enable(@feature, @boolean_gate, @flipper.boolean) + assert_equal true, @adapter.enable(@feature, @boolean_gate, @Flipper::Types::Boolean.new) result = @adapter.get(@feature) assert_equal 'true', result[:boolean] end def test_converts_the_actor_value_to_a_string assert_equal true, - @adapter.enable(@feature, @actor_gate, @flipper.actor(Flipper::Actor.new(22))) + @adapter.enable(@feature, @actor_gate, Flipper::Types::Actor.new(Flipper::Actor.new(22))) result = @adapter.get(@feature) assert_equal Set['22'], result[:actors] end @@ -182,13 +182,13 @@ def test_converts_group_value_to_a_string end def test_converts_percentage_of_time_integer_value_to_a_string - assert_equal true, @adapter.enable(@feature, @time_gate, @flipper.time(10)) + assert_equal true, @adapter.enable(@feature, @time_gate, Flipper::Types::PercentageOfTime.new(10)) result = @adapter.get(@feature) assert_equal '10', result[:percentage_of_time] end def test_converts_percentage_of_actors_integer_value_to_a_string - assert_equal true, @adapter.enable(@feature, @actors_gate, @flipper.actors(10)) + assert_equal true, @adapter.enable(@feature, @actors_gate, Flipper::Types::PercentageOfActors.new(10)) result = @adapter.get(@feature) assert_equal '10', result[:percentage_of_actors] end @@ -211,11 +211,11 @@ def test_can_add_remove_and_list_known_features def test_clears_all_the_gate_values_for_the_feature_on_remove actor22 = Flipper::Actor.new('22') - assert_equal true, @adapter.enable(@feature, @boolean_gate, @flipper.boolean) + assert_equal true, @adapter.enable(@feature, @boolean_gate, @Flipper::Types::Boolean.new) assert_equal true, @adapter.enable(@feature, @group_gate, @flipper.group(:admins)) - assert_equal true, @adapter.enable(@feature, @actor_gate, @flipper.actor(actor22)) - assert_equal true, @adapter.enable(@feature, @actors_gate, @flipper.actors(25)) - assert_equal true, @adapter.enable(@feature, @time_gate, @flipper.time(45)) + assert_equal true, @adapter.enable(@feature, @actor_gate, Flipper::Types::Actor.new(actor22)) + assert_equal true, @adapter.enable(@feature, @actors_gate, Flipper::Types::PercentageOfActors.new(25)) + assert_equal true, @adapter.enable(@feature, @time_gate, Flipper::Types::PercentageOfTime.new(45)) assert_equal true, @adapter.remove(@feature) @@ -227,11 +227,11 @@ def test_can_clear_all_the_gate_values_for_a_feature @adapter.add(@feature) assert_includes @adapter.features, @feature.key - assert_equal true, @adapter.enable(@feature, @boolean_gate, @flipper.boolean) + assert_equal true, @adapter.enable(@feature, @boolean_gate, @Flipper::Types::Boolean.new) assert_equal true, @adapter.enable(@feature, @group_gate, @flipper.group(:admins)) - assert_equal true, @adapter.enable(@feature, @actor_gate, @flipper.actor(actor22)) - assert_equal true, @adapter.enable(@feature, @actors_gate, @flipper.actors(25)) - assert_equal true, @adapter.enable(@feature, @time_gate, @flipper.time(45)) + assert_equal true, @adapter.enable(@feature, @actor_gate, Flipper::Types::Actor.new(actor22)) + assert_equal true, @adapter.enable(@feature, @actors_gate, Flipper::Types::PercentageOfActors.new(25)) + assert_equal true, @adapter.enable(@feature, @time_gate, Flipper::Types::PercentageOfTime.new(45)) assert_equal true, @adapter.clear(@feature) assert_includes @adapter.features, @feature.key @@ -244,7 +244,7 @@ def test_does_not_complain_clearing_a_feature_that_does_not_exist_in_adapter def test_can_get_multiple_features assert @adapter.add(@flipper[:stats]) - assert @adapter.enable(@flipper[:stats], @boolean_gate, @flipper.boolean) + assert @adapter.enable(@flipper[:stats], @boolean_gate, @Flipper::Types::Boolean.new) assert @adapter.add(@flipper[:search]) result = @adapter.get_multi([@flipper[:stats], @flipper[:search], @flipper[:other]]) @@ -260,7 +260,7 @@ def test_can_get_multiple_features def test_can_get_all_features assert @adapter.add(@flipper[:stats]) - assert @adapter.enable(@flipper[:stats], @boolean_gate, @flipper.boolean) + assert @adapter.enable(@flipper[:stats], @boolean_gate, @Flipper::Types::Boolean.new) assert @adapter.add(@flipper[:search]) result = @adapter.get_all @@ -283,8 +283,8 @@ def test_includes_explicitly_disabled_features_when_getting_all_features def test_can_double_enable_an_actor_without_error actor = Flipper::Actor.new('Flipper::Actor;22') - assert_equal true, @adapter.enable(@feature, @actor_gate, @flipper.actor(actor)) - assert_equal true, @adapter.enable(@feature, @actor_gate, @flipper.actor(actor)) + assert_equal true, @adapter.enable(@feature, @actor_gate, Flipper::Types::Actor.new(actor)) + assert_equal true, @adapter.enable(@feature, @actor_gate, Flipper::Types::Actor.new(actor)) assert_equal Set['Flipper::Actor;22'], @adapter.get(@feature).fetch(:actors) end @@ -295,13 +295,13 @@ def test_can_double_enable_a_group_without_error end def test_can_double_enable_percentage_without_error - assert_equal true, @adapter.enable(@feature, @actors_gate, @flipper.actors(25)) - assert_equal true, @adapter.enable(@feature, @actors_gate, @flipper.actors(25)) + assert_equal true, @adapter.enable(@feature, @actors_gate, Flipper::Types::PercentageOfActors.new(25)) + assert_equal true, @adapter.enable(@feature, @actors_gate, Flipper::Types::PercentageOfActors.new(25)) end def test_can_double_enable_without_error - assert_equal true, @adapter.enable(@feature, @boolean_gate, @flipper.boolean) - assert_equal true, @adapter.enable(@feature, @boolean_gate, @flipper.boolean) + assert_equal true, @adapter.enable(@feature, @boolean_gate, @Flipper::Types::Boolean.new) + assert_equal true, @adapter.enable(@feature, @boolean_gate, @Flipper::Types::Boolean.new) end def test_can_get_all_features_when_there_are_none @@ -312,11 +312,11 @@ def test_can_get_all_features_when_there_are_none def test_clears_other_gate_values_on_enable actor = Flipper::Actor.new('Flipper::Actor;22') - assert_equal true, @adapter.enable(@feature, @actors_gate, @flipper.actors(25)) - assert_equal true, @adapter.enable(@feature, @time_gate, @flipper.time(25)) + assert_equal true, @adapter.enable(@feature, @actors_gate, Flipper::Types::PercentageOfActors.new(25)) + assert_equal true, @adapter.enable(@feature, @time_gate, Flipper::Types::PercentageOfTime.new(25)) assert_equal true, @adapter.enable(@feature, @group_gate, @flipper.group(:admins)) - assert_equal true, @adapter.enable(@feature, @actor_gate, @flipper.actor(actor)) - assert_equal true, @adapter.enable(@feature, @boolean_gate, @flipper.boolean(true)) + assert_equal true, @adapter.enable(@feature, @actor_gate, Flipper::Types::Actor.new(actor)) + assert_equal true, @adapter.enable(@feature, @boolean_gate, @Flipper::Types::Boolean.new(true)) assert_equal @adapter.default_config.merge(boolean: "true"), @adapter.get(@feature) end end diff --git a/spec/flipper/adapters/active_support_cache_store_spec.rb b/spec/flipper/adapters/active_support_cache_store_spec.rb index 69ae58910..21b94b938 100644 --- a/spec/flipper/adapters/active_support_cache_store_spec.rb +++ b/spec/flipper/adapters/active_support_cache_store_spec.rb @@ -48,7 +48,7 @@ let(:feature) { flipper[:stats] } before do - adapter.enable(feature, feature.gate(:boolean), flipper.boolean) + adapter.enable(feature, feature.gate(:boolean), Flipper::Types::Boolean.new) end it 'enables feature and deletes the cache' do @@ -72,7 +72,7 @@ let(:feature) { flipper[:stats] } before do - adapter.disable(feature, feature.gate(:boolean), flipper.boolean) + adapter.disable(feature, feature.gate(:boolean), Flipper::Types::Boolean.new) end it 'disables feature and deletes the cache' do diff --git a/spec/flipper/adapters/dual_write_spec.rb b/spec/flipper/adapters/dual_write_spec.rb index 56cb0c6ff..1d1fb52b5 100644 --- a/spec/flipper/adapters/dual_write_spec.rb +++ b/spec/flipper/adapters/dual_write_spec.rb @@ -55,14 +55,14 @@ it 'updates remote and local for #enable' do feature = sync[:search] - subject.enable feature, feature.gate(:boolean), local.boolean + subject.enable feature, feature.gate(:boolean), Flipper::Types::Boolean.new expect(remote_adapter.count(:enable)).to be(1) expect(local_adapter.count(:enable)).to be(1) end it 'updates remote and local for #disable' do feature = sync[:search] - subject.disable feature, feature.gate(:boolean), local.boolean(false) + subject.disable feature, feature.gate(:boolean), Flipper::Types::Boolean.new(false) expect(remote_adapter.count(:disable)).to be(1) expect(local_adapter.count(:disable)).to be(1) end diff --git a/spec/flipper/adapters/instrumented_spec.rb b/spec/flipper/adapters/instrumented_spec.rb index bfa71f534..c22eb4ed0 100644 --- a/spec/flipper/adapters/instrumented_spec.rb +++ b/spec/flipper/adapters/instrumented_spec.rb @@ -8,7 +8,7 @@ let(:feature) { flipper[:stats] } let(:gate) { feature.gate(:percentage_of_actors) } - let(:thing) { flipper.actors(22) } + let(:thing) { Flipper::Types::PercentageOfActors.new(22) } subject do described_class.new(adapter, instrumenter: instrumenter) diff --git a/spec/flipper/adapters/memoizable_spec.rb b/spec/flipper/adapters/memoizable_spec.rb index 7d41c036f..91804dcbf 100644 --- a/spec/flipper/adapters/memoizable_spec.rb +++ b/spec/flipper/adapters/memoizable_spec.rb @@ -199,7 +199,7 @@ def foo feature = flipper[:stats] gate = feature.gate(:boolean) cache["feature/#{feature.key}"] = { some: 'thing' } - subject.enable(feature, gate, flipper.bool) + subject.enable(feature, gate, Flipper::Types::Boolean.new) expect(cache["feature/#{feature.key}"]).to be_nil end end @@ -212,8 +212,8 @@ def foo it 'returns result' do feature = flipper[:stats] gate = feature.gate(:boolean) - result = subject.enable(feature, gate, flipper.bool) - adapter_result = adapter.enable(feature, gate, flipper.bool) + result = subject.enable(feature, gate, Flipper::Types::Boolean.new) + adapter_result = adapter.enable(feature, gate, Flipper::Types::Boolean.new) expect(result).to eq(adapter_result) end end @@ -229,7 +229,7 @@ def foo feature = flipper[:stats] gate = feature.gate(:boolean) cache["feature/#{feature.key}"] = { some: 'thing' } - subject.disable(feature, gate, flipper.bool) + subject.disable(feature, gate, Flipper::Types::Boolean.new) expect(cache["feature/#{feature.key}"]).to be_nil end end @@ -242,8 +242,8 @@ def foo it 'returns result' do feature = flipper[:stats] gate = feature.gate(:boolean) - result = subject.disable(feature, gate, flipper.bool) - adapter_result = adapter.disable(feature, gate, flipper.bool) + result = subject.disable(feature, gate, Flipper::Types::Boolean.new) + adapter_result = adapter.disable(feature, gate, Flipper::Types::Boolean.new) expect(result).to eq(adapter_result) end end diff --git a/spec/flipper/adapters/operation_logger_spec.rb b/spec/flipper/adapters/operation_logger_spec.rb index 8b3f12db1..38b275ebe 100644 --- a/spec/flipper/adapters/operation_logger_spec.rb +++ b/spec/flipper/adapters/operation_logger_spec.rb @@ -47,7 +47,7 @@ def foo before do @feature = flipper[:stats] @gate = @feature.gate(:boolean) - @thing = flipper.bool + @thing = Flipper::Types::Boolean.new @result = subject.enable(@feature, @gate, @thing) end @@ -64,7 +64,7 @@ def foo before do @feature = flipper[:stats] @gate = @feature.gate(:boolean) - @thing = flipper.bool + @thing = Flipper::Types::Boolean.new @result = subject.disable(@feature, @gate, @thing) end diff --git a/spec/flipper/adapters/read_only_spec.rb b/spec/flipper/adapters/read_only_spec.rb index c317df4f5..df8fa5e2f 100644 --- a/spec/flipper/adapters/read_only_spec.rb +++ b/spec/flipper/adapters/read_only_spec.rb @@ -44,11 +44,11 @@ it 'can get feature' do expression = Flipper.property(:plan).eq("basic") actor22 = Flipper::Actor.new('22') - adapter.enable(feature, boolean_gate, flipper.boolean) + adapter.enable(feature, boolean_gate, Flipper::Types::Boolean.new) adapter.enable(feature, group_gate, flipper.group(:admins)) - adapter.enable(feature, actor_gate, flipper.actor(actor22)) - adapter.enable(feature, actors_gate, flipper.actors(25)) - adapter.enable(feature, time_gate, flipper.time(45)) + adapter.enable(feature, actor_gate, Flipper::Types::Actor.new(actor22)) + adapter.enable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(25)) + adapter.enable(feature, time_gate, Flipper::Types::PercentageOfTime.new(45)) adapter.enable(feature, expression_gate, expression) expect(subject.get(feature)).to eq({ @@ -85,12 +85,12 @@ end it 'raises error on enable' do - expect { subject.enable(feature, boolean_gate, flipper.boolean) } + expect { subject.enable(feature, boolean_gate, Flipper::Types::Boolean.new) } .to raise_error(Flipper::Adapters::ReadOnly::WriteAttempted) end it 'raises error on disable' do - expect { subject.disable(feature, boolean_gate, flipper.boolean) } + expect { subject.disable(feature, boolean_gate, Flipper::Types::Boolean.new) } .to raise_error(Flipper::Adapters::ReadOnly::WriteAttempted) end end diff --git a/spec/flipper/api/v1/actions/clear_feature_spec.rb b/spec/flipper/api/v1/actions/clear_feature_spec.rb index 7a6373166..aa4ea7753 100644 --- a/spec/flipper/api/v1/actions/clear_feature_spec.rb +++ b/spec/flipper/api/v1/actions/clear_feature_spec.rb @@ -7,11 +7,11 @@ actor22 = Flipper::Actor.new('22') feature = flipper[:my_feature] - feature.enable flipper.boolean + feature.enable Flipper::Types::Boolean.new feature.enable flipper.group(:admins) - feature.enable flipper.actor(actor22) - feature.enable flipper.actors(25) - feature.enable flipper.time(45) + feature.enable Flipper::Types::Actor.new(actor22) + feature.enable Flipper::Types::PercentageOfActors.new(25) + feature.enable Flipper::Types::PercentageOfTime.new(45) delete '/features/my_feature/clear' end @@ -28,11 +28,11 @@ actor22 = Flipper::Actor.new('22') feature = flipper["my/feature"] - feature.enable flipper.boolean + feature.enable Flipper::Types::Boolean.new feature.enable flipper.group(:admins) - feature.enable flipper.actor(actor22) - feature.enable flipper.actors(25) - feature.enable flipper.time(45) + feature.enable Flipper::Types::Actor.new(actor22) + feature.enable Flipper::Types::PercentageOfActors.new(25) + feature.enable Flipper::Types::PercentageOfTime.new(45) delete '/features/my/feature/clear' end diff --git a/spec/flipper/dsl_spec.rb b/spec/flipper/dsl_spec.rb index 493e3a873..916282b9b 100644 --- a/spec/flipper/dsl_spec.rb +++ b/spec/flipper/dsl_spec.rb @@ -113,18 +113,6 @@ end end - describe '#boolean' do - it_should_behave_like 'a DSL boolean method' do - let(:method_name) { :boolean } - end - end - - describe '#bool' do - it_should_behave_like 'a DSL boolean method' do - let(:method_name) { :bool } - end - end - describe '#group' do context 'for registered group' do before do @@ -150,69 +138,6 @@ end end - describe '#actor' do - context 'for a thing' do - it 'returns actor instance' do - thing = Flipper::Actor.new(33) - actor = subject.actor(thing) - expect(actor).to be_instance_of(Flipper::Types::Actor) - expect(actor.value).to eq('33') - end - end - - context 'for nil' do - it 'raises argument error' do - expect do - subject.actor(nil) - end.to raise_error(ArgumentError) - end - end - - context 'for something that is not actor wrappable' do - it 'raises argument error' do - expect do - subject.actor(Object.new) - end.to raise_error(ArgumentError) - end - end - end - - describe '#time' do - before do - @result = subject.time(5) - end - - it 'returns percentage of time' do - expect(@result).to be_instance_of(Flipper::Types::PercentageOfTime) - end - - it 'sets value' do - expect(@result.value).to eq(5) - end - - it 'is aliased to percentage_of_time' do - expect(@result).to eq(subject.percentage_of_time(@result.value)) - end - end - - describe '#actors' do - before do - @result = subject.actors(17) - end - - it 'returns percentage of actors' do - expect(@result).to be_instance_of(Flipper::Types::PercentageOfActors) - end - - it 'sets value' do - expect(@result.value).to eq(17) - end - - it 'is aliased to percentage_of_actors' do - expect(@result).to eq(subject.percentage_of_actors(@result.value)) - end - end - describe '#features' do context 'with no features enabled/disabled' do it 'defaults to empty set' do diff --git a/spec/flipper_integration_spec.rb b/spec/flipper_integration_spec.rb index efebcd4a2..1796fa369 100644 --- a/spec/flipper_integration_spec.rb +++ b/spec/flipper_integration_spec.rb @@ -35,8 +35,8 @@ let(:pitt) { Flipper::Actor.new(1) } let(:clooney) { Flipper::Actor.new(10) } - let(:five_percent_of_actors) { flipper.actors(5) } - let(:five_percent_of_time) { flipper.time(5) } + let(:five_percent_of_actors) { Flipper::Types::PercentageOfActors.new(5) } + let(:five_percent_of_time) { Flipper::Types::PercentageOfTime.new(5) } before do described_class.register(:admins, &:admin?) @@ -80,11 +80,11 @@ end it 'enables feature for flipper actor in group' do - expect(feature.enabled?(flipper.actor(admin_thing))).to eq(true) + expect(feature.enabled?(Flipper::Types::Actor.new(admin_thing))).to eq(true) end it 'does not enable for flipper actor not in group' do - expect(feature.enabled?(flipper.actor(dev_thing))).to eq(false) + expect(feature.enabled?(Flipper::Types::Actor.new(dev_thing))).to eq(false) end it 'does not enable feature for all' do @@ -267,11 +267,11 @@ end it 'disables feature for flipper actor in group' do - expect(feature.enabled?(flipper.actor(admin_thing))).to eq(false) + expect(feature.enabled?(Flipper::Types::Actor.new(admin_thing))).to eq(false) end it 'does not disable feature for flipper actor in other groups' do - expect(feature.enabled?(flipper.actor(dev_thing))).to eq(true) + expect(feature.enabled?(Flipper::Types::Actor.new(dev_thing))).to eq(true) end it 'adds feature to set of features' do @@ -305,7 +305,7 @@ context 'with a percentage of actors' do before do - @result = feature.disable(flipper.actors(0)) + @result = feature.disable(Flipper::Types::PercentageOfActors.new(0)) end it 'returns true' do @@ -329,7 +329,7 @@ context 'with a percentage of time' do before do @gate = feature.gate(:percentage_of_time) - @result = feature.disable(flipper.time(0)) + @result = feature.disable(Flipper::Types::PercentageOfTime.new(0)) end it 'returns true' do @@ -384,23 +384,23 @@ end it 'returns true' do - expect(feature.enabled?(flipper.actor(admin_thing))).to eq(true) + expect(feature.enabled?(Flipper::Types::Actor.new(admin_thing))).to eq(true) expect(feature.enabled?(admin_thing)).to eq(true) end it 'returns true for truthy block values' do - expect(feature.enabled?(flipper.actor(admin_truthy_thing))).to eq(true) + expect(feature.enabled?(Flipper::Types::Actor.new(admin_truthy_thing))).to eq(true) end end context 'for actor in disabled group' do it 'returns false' do - expect(feature.enabled?(flipper.actor(dev_thing))).to eq(false) + expect(feature.enabled?(Flipper::Types::Actor.new(dev_thing))).to eq(false) expect(feature.enabled?(dev_thing)).to eq(false) end it 'returns false for falsey block values' do - expect(feature.enabled?(flipper.actor(admin_falsey_thing))).to eq(false) + expect(feature.enabled?(Flipper::Types::Actor.new(admin_falsey_thing))).to eq(false) end end diff --git a/spec/flipper_spec.rb b/spec/flipper_spec.rb index f7de5b9c5..7903d246c 100644 --- a/spec/flipper_spec.rb +++ b/spec/flipper_spec.rb @@ -93,14 +93,6 @@ expect(described_class.instance.enabled?(:search)).to be(false) end - it 'delegates bool to instance' do - expect(described_class.bool).to eq(described_class.instance.bool) - end - - it 'delegates boolean to instance' do - expect(described_class.boolean).to eq(described_class.instance.boolean) - end - it 'delegates expression to instance' do expect(described_class.expression(:search)).to be(nil) @@ -142,10 +134,6 @@ expect(described_class.instance.enabled?(:search, actor)).to be(false) end - it 'delegates actor to instance' do - expect(described_class.actor(actor)).to eq(described_class.instance.actor(actor)) - end - it 'delegates enable_group to instance' do described_class.enable_group(:search, group) expect(described_class.instance[:search].enabled_groups).to include(group) @@ -166,15 +154,6 @@ expect(described_class.instance[:search].percentage_of_actors_value).to be(0) end - it 'delegates actors to instance' do - expect(described_class.actors(5)).to eq(described_class.instance.actors(5)) - end - - it 'delegates percentage_of_actors to instance' do - expected = described_class.instance.percentage_of_actors(5) - expect(described_class.percentage_of_actors(5)).to eq(expected) - end - it 'delegates enable_percentage_of_time to instance' do described_class.enable_percentage_of_time(:search, 5) expect(described_class.instance[:search].percentage_of_time_value).to be(5) @@ -185,15 +164,6 @@ expect(described_class.instance[:search].percentage_of_time_value).to be(0) end - it 'delegates time to instance' do - expect(described_class.time(56)).to eq(described_class.instance.time(56)) - end - - it 'delegates percentage_of_time to instance' do - expected = described_class.instance.percentage_of_time(56) - expect(described_class.percentage_of_time(56)).to eq(expected) - end - it 'delegates features to instance' do described_class.instance.add(:search) expect(described_class.features).to eq(described_class.instance.features) @@ -393,6 +363,13 @@ end end + describe ".boolean" do + it "returns Flipper::Expressions::Boolean instance" do + expect(described_class.boolean(true)).to eq(Flipper::Expressions::Boolean.new(true)) + expect(described_class.boolean(false)).to eq(Flipper::Expressions::Boolean.new(false)) + end + end + describe ".random" do it "returns Flipper::Expressions::Random instance" do expect(Flipper.random(100)).to eq(Flipper::Expressions::Random.new(100)) From 7885a5a206c9b40eac0cfc321156a384e87110e0 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 9 Feb 2022 15:41:40 -0500 Subject: [PATCH 136/176] Add breaking changes to changelog --- Changelog.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Changelog.md b/Changelog.md index e2019022e..f2a7181ce 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,10 @@ ## Unreleased +### Breaking Changes + +* Removed top level `Flipper.bool`, `Flipper.actors`, `Flipper.time`, `Flipper.actor`, `Flipper.percentage_of_actors`, `Flipper.time`, and `Flipper.percentage_of_time`. Also removed correlated Flipper::DSL instance metho. They conflict with some new expression stuff and I can't think of when I've ever used them. I haven't seen others use them either. + + ### Additions/Changes * Add Ruby 3.0 and 3.1 to the CI matrix and fix groups block arity check for ruby 3 (https://github.com/jnunemaker/flipper/pull/601) From 73c7835c31570877b5d500602c427d87513a8173 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 9 Feb 2022 15:44:00 -0500 Subject: [PATCH 137/176] Add upgrade path for shortcut removal --- Changelog.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index f2a7181ce..5f1ce25fe 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,7 +2,14 @@ ### Breaking Changes -* Removed top level `Flipper.bool`, `Flipper.actors`, `Flipper.time`, `Flipper.actor`, `Flipper.percentage_of_actors`, `Flipper.time`, and `Flipper.percentage_of_time`. Also removed correlated Flipper::DSL instance metho. They conflict with some new expression stuff and I can't think of when I've ever used them. I haven't seen others use them either. +* Removed top level `Flipper.bool`, `Flipper.actors`, `Flipper.time`, `Flipper.actor`, `Flipper.percentage_of_actors`, `Flipper.time`, and `Flipper.percentage_of_time`. Also removed correlated Flipper::DSL instance metho. They conflict with some new expression stuff and I can't think of when I've ever used them. I haven't seen others use them either. If you are using them, you can migrate via a search and replace like so: + * Change Flipper.bool => Flipper::Types::Boolean.new + * Change Flipper.boolean => Flipper::Types::Boolean.new + * Change Flipper.actor => Flipper::Types::Actor.new + * Change Flipper.percentage_of_actors => Flipper::Types::PercentageOfActors.new + * Change Flipper.actors => Flipper::Types::PercentageOfActors.new + * Change Flipper.percentage_of_time => Flipper::Types::PercentageOfTime.new + * Change Flipper.time => Flipper::Types::PercentageOfTime.new ### Additions/Changes From 0ea98e928e3483ab6f70f49aeb5f6c18e2dae09a Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 9 Feb 2022 15:44:42 -0500 Subject: [PATCH 138/176] Remove the concept of value in favor of specific types --- examples/expressions.rb | 2 +- lib/flipper/expression.rb | 18 ++-- lib/flipper/expressions/percentage.rb | 2 +- lib/flipper/expressions/value.rb | 15 ---- spec/flipper/expression_spec.rb | 83 +++++++++---------- spec/flipper/expressions/all_spec.rb | 62 +++++++------- spec/flipper/expressions/any_spec.rb | 64 +++++++------- spec/flipper/expressions/equal_spec.rb | 32 +++---- .../greater_than_or_equal_to_spec.rb | 12 +-- spec/flipper/expressions/greater_than_spec.rb | 14 ++-- .../expressions/less_than_or_equal_to_spec.rb | 12 +-- spec/flipper/expressions/less_than_spec.rb | 8 +- spec/flipper/expressions/not_equal_spec.rb | 8 +- .../expressions/percentage_of_actors_spec.rb | 26 +++--- spec/flipper/expressions/percentage_spec.rb | 2 +- spec/flipper/expressions/random_spec.rb | 2 +- spec/flipper/expressions/value_spec.rb | 38 --------- spec/flipper/gates/expression_spec.rb | 14 ++-- 18 files changed, 174 insertions(+), 240 deletions(-) delete mode 100644 lib/flipper/expressions/value.rb delete mode 100644 spec/flipper/expressions/value_spec.rb diff --git a/examples/expressions.rb b/examples/expressions.rb index b300c51c5..41bb6ef94 100644 --- a/examples/expressions.rb +++ b/examples/expressions.rb @@ -130,7 +130,7 @@ class Org < Struct.new(:id, :flipper_properties) refute Flipper.enabled?(:something, other_user) puts "\n\nBoolean Expression" -boolean_expression = Flipper.value(true).eq(true) +boolean_expression = Flipper.boolean(true).eq(true) Flipper.enable :something, boolean_expression assert Flipper.enabled?(:something) assert Flipper.enabled?(:something, user) diff --git a/lib/flipper/expression.rb b/lib/flipper/expression.rb index 64709711c..c9ebbef82 100644 --- a/lib/flipper/expression.rb +++ b/lib/flipper/expression.rb @@ -1,13 +1,5 @@ module Flipper class Expression - SUPPORTED_TYPE_CLASSES = [ - String, - Numeric, - NilClass, - TrueClass, - FalseClass, - ].freeze - def self.build(object, convert_to_values: false) return object if object.is_a?(Flipper::Expression) @@ -18,10 +10,14 @@ def self.build(object, convert_to_values: false) type = object.keys.first args = object.values.first Expressions.const_get(type).new(args) - when *SUPPORTED_TYPE_CLASSES - convert_to_values ? Expressions::Value.new(object) : object + when String + convert_to_values ? Expressions::String.new(object.to_s) : object when Symbol - convert_to_values ? Expressions::Value.new(object.to_s) : object.to_s + convert_to_values ? Expressions::String.new(object.to_s) : object.to_s + when Numeric + convert_to_values ? Expressions::Number.new(object.to_f) : object + when TrueClass, FalseClass + convert_to_values ? Expressions::Boolean.new(object) : object else raise ArgumentError, "#{object.inspect} cannot be converted into an expression" end diff --git a/lib/flipper/expressions/percentage.rb b/lib/flipper/expressions/percentage.rb index 1767543ae..71b1fe935 100644 --- a/lib/flipper/expressions/percentage.rb +++ b/lib/flipper/expressions/percentage.rb @@ -8,7 +8,7 @@ def initialize(args) end def evaluate(context = {}) - value = evaluate_arg(0, context) + value = super value = 0 if value < 0 value = 100 if value > 100 diff --git a/lib/flipper/expressions/value.rb b/lib/flipper/expressions/value.rb deleted file mode 100644 index 072df07fb..000000000 --- a/lib/flipper/expressions/value.rb +++ /dev/null @@ -1,15 +0,0 @@ -require "flipper/expression" - -module Flipper - module Expressions - class Value < Expression - def initialize(args) - super Array(args) - end - - def evaluate(context = {}) - evaluate_arg(0, context) - end - end - end -end diff --git a/spec/flipper/expression_spec.rb b/spec/flipper/expression_spec.rb index 67ca3c244..811562dc1 100644 --- a/spec/flipper/expression_spec.rb +++ b/spec/flipper/expression_spec.rb @@ -5,102 +5,93 @@ it "can build Equal" do expression = Flipper::Expression.build({ "Equal" => [ - {"Value" => ["basic"]}, - {"Value" => ["basic"]}, + {"String" => ["basic"]}, + {"String" => ["basic"]}, ] }) expect(expression).to be_instance_of(Flipper::Expressions::Equal) expect(expression.args).to eq([ - Flipper.value("basic"), - Flipper.value("basic"), + Flipper.string("basic"), + Flipper.string("basic"), ]) end it "can build GreaterThanOrEqualTo" do expression = Flipper::Expression.build({ "GreaterThanOrEqualTo" => [ - {"Value" => [2]}, - {"Value" => [1]}, + {"Number" => [2]}, + {"Number" => [1]}, ] }) expect(expression).to be_instance_of(Flipper::Expressions::GreaterThanOrEqualTo) expect(expression.args).to eq([ - Flipper.value(2), - Flipper.value(1), + Flipper.number(2), + Flipper.number(1), ]) end it "can build GreaterThan" do expression = Flipper::Expression.build({ "GreaterThan" => [ - {"Value" => [2]}, - {"Value" => [1]}, + {"Number" => [2]}, + {"Number" => [1]}, ] }) expect(expression).to be_instance_of(Flipper::Expressions::GreaterThan) expect(expression.args).to eq([ - Flipper.value(2), - Flipper.value(1), + Flipper.number(2), + Flipper.number(1), ]) end it "can build LessThanOrEqualTo" do expression = Flipper::Expression.build({ "LessThanOrEqualTo" => [ - {"Value" => [2]}, - {"Value" => [1]}, + {"Number" => [2]}, + {"Number" => [1]}, ] }) expect(expression).to be_instance_of(Flipper::Expressions::LessThanOrEqualTo) expect(expression.args).to eq([ - Flipper.value(2), - Flipper.value(1), + Flipper.number(2), + Flipper.number(1), ]) end it "can build LessThan" do expression = Flipper::Expression.build({ "LessThan" => [ - {"Value" => [2]}, - {"Value" => [1]}, + {"Number" => [2]}, + {"Number" => [1]}, ] }) expect(expression).to be_instance_of(Flipper::Expressions::LessThan) expect(expression.args).to eq([ - Flipper.value(2), - Flipper.value(1), + Flipper.number(2), + Flipper.number(1), ]) end it "can build NotEqual" do expression = Flipper::Expression.build({ "NotEqual" => [ - {"Value" => ["basic"]}, - {"Value" => ["plus"]}, + {"String" => ["basic"]}, + {"String" => ["plus"]}, ] }) expect(expression).to be_instance_of(Flipper::Expressions::NotEqual) expect(expression.args).to eq([ - Flipper.value("basic"), - Flipper.value("plus"), + Flipper.string("basic"), + Flipper.string("plus"), ]) end - it "can build Value" do - expression = Flipper::Expression.build({ - "Value" => [1] - }) - - expect(expression).to be_instance_of(Flipper::Expressions::Value) - expect(expression.args).to eq([1]) - end - it "can build Number" do expression = Flipper::Expression.build({ "Number" => [1] @@ -122,24 +113,24 @@ it "can build PercentageOfActors" do expression = Flipper::Expression.build({ "PercentageOfActors" => [ - {"Value" => ["User;1"]}, - {"Value" => [40]}, + {"String" => ["User;1"]}, + {"Number" => [40]}, ] }) expect(expression).to be_instance_of(Flipper::Expressions::PercentageOfActors) expect(expression.args).to eq([ - Flipper.value("User;1"), - Flipper.value(40), + Flipper.string("User;1"), + Flipper.number(40), ]) end - it "can build Value" do + it "can build String" do expression = Flipper::Expression.build({ - "Value" => ["basic"] + "String" => ["basic"] }) - expect(expression).to be_instance_of(Flipper::Expressions::Value) + expect(expression).to be_instance_of(Flipper::Expressions::String) expect(expression.args).to eq(["basic"]) end @@ -214,8 +205,8 @@ describe "#add" do it "converts to Any and adds new expressions" do expression = described_class.new(["something"]) - first = Flipper.value(true).eq(true) - second = Flipper.value(false).eq(false) + first = Flipper.boolean(true).eq(true) + second = Flipper.boolean(false).eq(false) new_expression = expression.add(first, second) expect(new_expression).to be_instance_of(Flipper::Expressions::Any) expect(new_expression.args).to eq([ @@ -229,8 +220,8 @@ describe "#remove" do it "converts to Any and removes any expressions that match" do expression = described_class.new(["something"]) - first = Flipper.value(true).eq(true) - second = Flipper.value(false).eq(false) + first = Flipper.boolean(true).eq(true) + second = Flipper.boolean(false).eq(false) new_expression = expression.remove(described_class.new(["something"]), first, second) expect(new_expression).to be_instance_of(Flipper::Expressions::Any) expect(new_expression.args).to eq([]) @@ -295,9 +286,9 @@ end it "can convert to PercentageOfActors" do - expression = Flipper.value("User;1") + expression = Flipper.string("User;1") converted = expression.percentage_of_actors(40) expect(converted).to be_instance_of(Flipper::Expressions::PercentageOfActors) - expect(converted.args).to eq([expression, Flipper.value(40)]) + expect(converted.args).to eq([expression, Flipper.number(40)]) end end diff --git a/spec/flipper/expressions/all_spec.rb b/spec/flipper/expressions/all_spec.rb index 1876bbcb8..f3fd55c2f 100644 --- a/spec/flipper/expressions/all_spec.rb +++ b/spec/flipper/expressions/all_spec.rb @@ -2,16 +2,16 @@ describe "#evaluate" do it "returns true if all args evaluate as true" do expression = described_class.new([ - Flipper.value(true), - Flipper.value("yep").eq("yep"), + Flipper.boolean(true), + Flipper.string("yep").eq("yep"), ]) expect(expression.evaluate).to be(true) end it "returns false if any args evaluate as false" do expression = described_class.new([ - Flipper.value(false), - Flipper.value("yep").eq("yep"), + Flipper.boolean(false), + Flipper.string("yep").eq("yep"), ]) expect(expression.evaluate).to be(false) end @@ -20,8 +20,8 @@ describe "#all" do it "returns self" do expression = described_class.new([ - Flipper.value(true), - Flipper.value("yep").eq("yep"), + Flipper.boolean(true), + Flipper.string("yep").eq("yep"), ]) expect(expression.all).to be(expression) end @@ -29,48 +29,48 @@ describe "#add" do it "returns new instance with expression added" do - expression = described_class.new([Flipper.value(true)]) - other = Flipper.value("yep").eq("yep") + expression = described_class.new([Flipper.boolean(true)]) + other = Flipper.string("yep").eq("yep") result = expression.add(other) expect(result.args).to eq([ - Flipper.value(true), - Flipper.value("yep").eq("yep"), + Flipper.boolean(true), + Flipper.string("yep").eq("yep"), ]) end it "returns new instance with many expressions added" do - expression = described_class.new([Flipper.value(true)]) - second = Flipper.value("yep").eq("yep") - third = Flipper.value(1).lte(20) + expression = described_class.new([Flipper.boolean(true)]) + second = Flipper.string("yep").eq("yep") + third = Flipper.number(1).lte(20) result = expression.add(second, third) expect(result.args).to eq([ - Flipper.value(true), - Flipper.value("yep").eq("yep"), - Flipper.value(1).lte(20), + Flipper.boolean(true), + Flipper.string("yep").eq("yep"), + Flipper.number(1).lte(20), ]) end it "returns new instance with array of expressions added" do - expression = described_class.new([Flipper.value(true)]) - second = Flipper.value("yep").eq("yep") - third = Flipper.value(1).lte(20) + expression = described_class.new([Flipper.boolean(true)]) + second = Flipper.string("yep").eq("yep") + third = Flipper.number(1).lte(20) result = expression.add([second, third]) expect(result.args).to eq([ - Flipper.value(true), - Flipper.value("yep").eq("yep"), - Flipper.value(1).lte(20), + Flipper.boolean(true), + Flipper.string("yep").eq("yep"), + Flipper.number(1).lte(20), ]) end end describe "#remove" do it "returns new instance with expression removed" do - first = Flipper.value(true) - second = Flipper.value("yep").eq("yep") - third = Flipper.value(1).lte(20) + first = Flipper.boolean(true) + second = Flipper.string("yep").eq("yep") + third = Flipper.number(1).lte(20) expression = described_class.new([first, second, third]) result = expression.remove(second) @@ -79,9 +79,9 @@ end it "returns new instance with many expressions removed" do - first = Flipper.value(true) - second = Flipper.value("yep").eq("yep") - third = Flipper.value(1).lte(20) + first = Flipper.boolean(true) + second = Flipper.string("yep").eq("yep") + third = Flipper.number(1).lte(20) expression = described_class.new([first, second, third]) result = expression.remove(second, third) @@ -90,9 +90,9 @@ end it "returns new instance with array of expressions removed" do - first = Flipper.value(true) - second = Flipper.value("yep").eq("yep") - third = Flipper.value(1).lte(20) + first = Flipper.boolean(true) + second = Flipper.string("yep").eq("yep") + third = Flipper.number(1).lte(20) expression = described_class.new([first, second, third]) result = expression.remove([second, third]) diff --git a/spec/flipper/expressions/any_spec.rb b/spec/flipper/expressions/any_spec.rb index 75203996e..d7af12165 100644 --- a/spec/flipper/expressions/any_spec.rb +++ b/spec/flipper/expressions/any_spec.rb @@ -2,17 +2,17 @@ describe "#evaluate" do it "returns true if any args evaluate as true" do expression = described_class.new([ - Flipper.value(true), - Flipper.value("yep").eq("nope"), - Flipper.value(1).gte(10), + Flipper.boolean(true), + Flipper.string("yep").eq("nope"), + Flipper.number(1).gte(10), ]) expect(expression.evaluate).to be(true) end it "returns false if all args evaluate as false" do expression = described_class.new([ - Flipper.value(false), - Flipper.value("yep").eq("nope"), + Flipper.boolean(false), + Flipper.string("yep").eq("nope"), ]) expect(expression.evaluate).to be(false) end @@ -21,8 +21,8 @@ describe "#any" do it "returns self" do expression = described_class.new([ - Flipper.value(true), - Flipper.value("yep").eq("yep"), + Flipper.boolean(true), + Flipper.string("yep").eq("yep"), ]) expect(expression.any).to be(expression) end @@ -30,48 +30,48 @@ describe "#add" do it "returns new instance with expression added" do - expression = described_class.new([Flipper.value(true)]) - other = Flipper.value("yep").eq("yep") + expression = described_class.new([Flipper.boolean(true)]) + other = Flipper.string("yep").eq("yep") result = expression.add(other) expect(result.args).to eq([ - Flipper.value(true), - Flipper.value("yep").eq("yep"), + Flipper.boolean(true), + Flipper.string("yep").eq("yep"), ]) end it "returns new instance with many expressions added" do - expression = described_class.new([Flipper.value(true)]) - second = Flipper.value("yep").eq("yep") - third = Flipper.value(1).lte(20) + expression = described_class.new([Flipper.boolean(true)]) + second = Flipper.string("yep").eq("yep") + third = Flipper.number(1).lte(20) result = expression.add(second, third) expect(result.args).to eq([ - Flipper.value(true), - Flipper.value("yep").eq("yep"), - Flipper.value(1).lte(20), + Flipper.boolean(true), + Flipper.string("yep").eq("yep"), + Flipper.number(1).lte(20), ]) end it "returns new instance with array of expressions added" do - expression = described_class.new([Flipper.value(true)]) - second = Flipper.value("yep").eq("yep") - third = Flipper.value(1).lte(20) + expression = described_class.new([Flipper.boolean(true)]) + second = Flipper.string("yep").eq("yep") + third = Flipper.number(1).lte(20) result = expression.add([second, third]) expect(result.args).to eq([ - Flipper.value(true), - Flipper.value("yep").eq("yep"), - Flipper.value(1).lte(20), + Flipper.boolean(true), + Flipper.string("yep").eq("yep"), + Flipper.number(1).lte(20), ]) end end describe "#remove" do it "returns new instance with expression removed" do - first = Flipper.value(true) - second = Flipper.value("yep").eq("yep") - third = Flipper.value(1).lte(20) + first = Flipper.boolean(true) + second = Flipper.string("yep").eq("yep") + third = Flipper.number(1).lte(20) expression = described_class.new([first, second, third]) result = expression.remove(second) @@ -80,9 +80,9 @@ end it "returns new instance with many expressions removed" do - first = Flipper.value(true) - second = Flipper.value("yep").eq("yep") - third = Flipper.value(1).lte(20) + first = Flipper.boolean(true) + second = Flipper.string("yep").eq("yep") + third = Flipper.number(1).lte(20) expression = described_class.new([first, second, third]) result = expression.remove(second, third) @@ -91,9 +91,9 @@ end it "returns new instance with array of expressions removed" do - first = Flipper.value(true) - second = Flipper.value("yep").eq("yep") - third = Flipper.value(1).lte(20) + first = Flipper.boolean(true) + second = Flipper.string("yep").eq("yep") + third = Flipper.number(1).lte(20) expression = described_class.new([first, second, third]) result = expression.remove([second, third]) diff --git a/spec/flipper/expressions/equal_spec.rb b/spec/flipper/expressions/equal_spec.rb index c3c08e365..eb886e4e9 100644 --- a/spec/flipper/expressions/equal_spec.rb +++ b/spec/flipper/expressions/equal_spec.rb @@ -2,15 +2,15 @@ it "can be built" do expression = described_class.build({ "Equal" => [ - {"Value" => ["basic"]}, - {"Value" => ["basic"]}, + {"String" => ["basic"]}, + {"String" => ["basic"]}, ] }) expect(expression).to be_instance_of(Flipper::Expressions::Equal) expect(expression.args).to eq([ - Flipper.value("basic"), - Flipper.value("basic"), + Flipper.string("basic"), + Flipper.string("basic"), ]) end @@ -26,8 +26,8 @@ describe "#evaluate" do it "returns true when equal" do expression = described_class.new([ - Flipper.value("basic"), - Flipper.value("basic"), + Flipper.string("basic"), + Flipper.string("basic"), ]) expect(expression.evaluate).to be(true) @@ -48,10 +48,10 @@ it "works when nested" do expression = described_class.new([ - Flipper.value(true), + Flipper.boolean(true), Flipper.all( Flipper.property(:stinky).eq(true), - Flipper.value("admin").eq(Flipper.property(:role)), + Flipper.string("admin").eq(Flipper.property(:role)), ), ]) @@ -64,8 +64,8 @@ it "returns false when not equal" do expression = described_class.new([ - Flipper.value("basic"), - Flipper.value("plus"), + Flipper.string("basic"), + Flipper.string("plus"), ]) expect(expression.evaluate).to be(false) @@ -90,15 +90,15 @@ end it "returns false when one arg" do - expression = described_class.new([Flipper.value(10)]) + expression = described_class.new([Flipper.number(10)]) expect(expression.evaluate).to be(false) end it "only evaluates first two arguments equality" do expression = described_class.new([ - Flipper.value(20), - Flipper.value(20), - Flipper.value(30), + Flipper.number(20), + Flipper.number(20), + Flipper.number(30), ]) expect(expression.evaluate).to be(true) end @@ -108,13 +108,13 @@ it "returns Hash" do expression = described_class.new([ Flipper::Expressions::Property.new(["plan"]), - Flipper.value("basic"), + Flipper.string("basic"), ]) expect(expression.value).to eq({ "Equal" => [ {"Property" => ["plan"]}, - {"Value" => ["basic"]}, + {"String" => ["basic"]}, ], }) end diff --git a/spec/flipper/expressions/greater_than_or_equal_to_spec.rb b/spec/flipper/expressions/greater_than_or_equal_to_spec.rb index eae4718d2..1d87eacc7 100644 --- a/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +++ b/spec/flipper/expressions/greater_than_or_equal_to_spec.rb @@ -7,8 +7,8 @@ it "returns true when equal with args that need evaluation" do expression = described_class.new([ - Flipper.value(2), - Flipper.value(2), + Flipper.number(2), + Flipper.number(2), ]) expect(expression.evaluate).to be(true) @@ -38,14 +38,14 @@ describe "#value" do it "returns Hash" do expression = described_class.new([ - Flipper.value(20), - Flipper.value(10), + Flipper.number(20), + Flipper.number(10), ]) expect(expression.value).to eq({ "GreaterThanOrEqualTo" => [ - {"Value" => [20]}, - {"Value" => [10]}, + {"Number" => [20]}, + {"Number" => [10]}, ], }) end diff --git a/spec/flipper/expressions/greater_than_spec.rb b/spec/flipper/expressions/greater_than_spec.rb index 86083d4b4..3cf04da76 100644 --- a/spec/flipper/expressions/greater_than_spec.rb +++ b/spec/flipper/expressions/greater_than_spec.rb @@ -12,8 +12,8 @@ it "returns true when greater with args that need evaluation" do expression = described_class.new([ - Flipper.value(2), - Flipper.value(1), + Flipper.number(2), + Flipper.number(1), ]) expect(expression.evaluate).to be(true) end @@ -30,7 +30,7 @@ it "returns false with one argument" do expression = described_class.new([ - Flipper.value(10), + Flipper.number(10), ]) expect(expression.evaluate).to be(false) end @@ -39,14 +39,14 @@ describe "#value" do it "returns Hash" do expression = described_class.new([ - Flipper.value(20), - Flipper.value(10), + Flipper.number(20), + Flipper.number(10), ]) expect(expression.value).to eq({ "GreaterThan" => [ - {"Value" => [20]}, - {"Value" => [10]}, + {"Number" => [20]}, + {"Number" => [10]}, ], }) end diff --git a/spec/flipper/expressions/less_than_or_equal_to_spec.rb b/spec/flipper/expressions/less_than_or_equal_to_spec.rb index 689c167a3..53adbcae7 100644 --- a/spec/flipper/expressions/less_than_or_equal_to_spec.rb +++ b/spec/flipper/expressions/less_than_or_equal_to_spec.rb @@ -8,8 +8,8 @@ it "returns true when equal with args that need evaluation" do expression = described_class.new([ - Flipper.value(2), - Flipper.value(2), + Flipper.number(2), + Flipper.number(2), ]) expect(expression.evaluate).to be(true) @@ -40,14 +40,14 @@ describe "#value" do it "returns Hash" do expression = described_class.new([ - Flipper.value(20), - Flipper.value(10), + Flipper.number(20), + Flipper.number(10), ]) expect(expression.value).to eq({ "LessThanOrEqualTo" => [ - {"Value" => [20]}, - {"Value" => [10]}, + {"Number" => [20]}, + {"Number" => [10]}, ], }) end diff --git a/spec/flipper/expressions/less_than_spec.rb b/spec/flipper/expressions/less_than_spec.rb index f89ec9ff1..71da1678c 100644 --- a/spec/flipper/expressions/less_than_spec.rb +++ b/spec/flipper/expressions/less_than_spec.rb @@ -34,14 +34,14 @@ describe "#value" do it "returns Hash" do expression = described_class.new([ - Flipper.value(20), - Flipper.value(10), + Flipper.number(20), + Flipper.number(10), ]) expect(expression.value).to eq({ "LessThan" => [ - {"Value" => [20]}, - {"Value" => [10]}, + {"Number" => [20]}, + {"Number" => [10]}, ], }) end diff --git a/spec/flipper/expressions/not_equal_spec.rb b/spec/flipper/expressions/not_equal_spec.rb index ee1ac564c..df59511ef 100644 --- a/spec/flipper/expressions/not_equal_spec.rb +++ b/spec/flipper/expressions/not_equal_spec.rb @@ -45,14 +45,14 @@ describe "#value" do it "returns Hash" do expression = described_class.new([ - Flipper.value(20), - Flipper.value(10), + Flipper.number(20), + Flipper.number(10), ]) expect(expression.value).to eq({ "NotEqual" => [ - {"Value" => [20]}, - {"Value" => [10]}, + {"Number" => [20]}, + {"Number" => [10]}, ], }) end diff --git a/spec/flipper/expressions/percentage_of_actors_spec.rb b/spec/flipper/expressions/percentage_of_actors_spec.rb index d4980cb49..d91d966ef 100644 --- a/spec/flipper/expressions/percentage_of_actors_spec.rb +++ b/spec/flipper/expressions/percentage_of_actors_spec.rb @@ -2,8 +2,8 @@ describe "#evaluate" do it "returns true when string in percentage enabled" do expression = described_class.new([ - Flipper.value("User;1"), - Flipper.value(42), + Flipper.string("User;1"), + Flipper.number(42), ]) expect(expression.evaluate).to be(true) @@ -11,8 +11,8 @@ it "returns true when string in fractional percentage enabled" do expression = described_class.new([ - Flipper.value("User;1"), - Flipper.value(41.687), + Flipper.string("User;1"), + Flipper.number(41.687), ]) expect(expression.evaluate).to be(true) @@ -21,7 +21,7 @@ it "returns true when property evalutes to string that is percentage enabled" do expression = described_class.new([ Flipper.property(:flipper_id), - Flipper.value(42), + Flipper.number(42), ]) properties = { @@ -32,8 +32,8 @@ it "returns false when string in percentage enabled" do expression = described_class.new([ - Flipper.value("User;1"), - Flipper.value(0), + Flipper.string("User;1"), + Flipper.number(0), ]) expect(expression.evaluate).to be(false) @@ -41,8 +41,8 @@ it "changes value based on feature_name so not all actors get all features first" do expression = described_class.new([ - Flipper.value("User;1"), - Flipper.value(70), + Flipper.string("User;1"), + Flipper.number(70), ]) expect(expression.evaluate(feature_name: "a")).to be(true) @@ -53,14 +53,14 @@ describe "#value" do it "returns Hash" do expression = described_class.new([ - Flipper.value("User;1"), - Flipper.value(10), + Flipper.string("User;1"), + Flipper.number(10), ]) expect(expression.value).to eq({ "PercentageOfActors" => [ - {"Value" => ["User;1"]}, - {"Value" => [10]}, + {"String" => ["User;1"]}, + {"Number" => [10]}, ], }) end diff --git a/spec/flipper/expressions/percentage_spec.rb b/spec/flipper/expressions/percentage_spec.rb index 92842e32b..8aafde9b7 100644 --- a/spec/flipper/expressions/percentage_spec.rb +++ b/spec/flipper/expressions/percentage_spec.rb @@ -13,7 +13,7 @@ it "returns numeric" do expression = described_class.new([10]) result = expression.evaluate - expect(result).to be(10) + expect(result).to be(10.0) end it "returns 0 if less than 0" do diff --git a/spec/flipper/expressions/random_spec.rb b/spec/flipper/expressions/random_spec.rb index 1da09e340..94b24c927 100644 --- a/spec/flipper/expressions/random_spec.rb +++ b/spec/flipper/expressions/random_spec.rb @@ -18,7 +18,7 @@ end it "returns random number based on seed that is Value" do - expression = described_class.new([Flipper.value(10)]) + expression = described_class.new([Flipper.number(10)]) result = expression.evaluate expect(result).to be >= 0 expect(result).to be <= 10 diff --git a/spec/flipper/expressions/value_spec.rb b/spec/flipper/expressions/value_spec.rb deleted file mode 100644 index 81c279ed4..000000000 --- a/spec/flipper/expressions/value_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -RSpec.describe Flipper::Expressions::Value do - describe "#initialize" do - it "works with string" do - expect(described_class.new("basic").args).to eq(["basic"]) - end - - it "works with number" do - expect(described_class.new(1).args).to eq([1]) - end - - it "works with array" do - expect(described_class.new(["basic"]).args).to eq(["basic"]) - expect(described_class.new(["basic"]).args).to eq(["basic"]) - end - end - - describe "#evaluate" do - it "returns arg" do - expression = described_class.new(["basic"]) - expect(expression.evaluate).to eq("basic") - end - - it "returns arg when it needs evaluation" do - expression = described_class.new([Flipper.value("basic")]) - expect(expression.evaluate).to eq("basic") - end - end - - describe "#value" do - it "returns Hash" do - expression = described_class.new([20]) - - expect(expression.value).to eq({ - "Value" => [20], - }) - end - end -end diff --git a/spec/flipper/gates/expression_spec.rb b/spec/flipper/gates/expression_spec.rb index 2b33f0136..40f32bf81 100644 --- a/spec/flipper/gates/expression_spec.rb +++ b/spec/flipper/gates/expression_spec.rb @@ -28,7 +28,7 @@ def context(expression, properties: {}) context "for not empty value" do it 'returns true' do - expect(subject.enabled?({"Value" => [true]})).to eq(true) + expect(subject.enabled?({"Boolean" => [true]})).to eq(true) end end end @@ -36,14 +36,14 @@ def context(expression, properties: {}) describe '#open?' do context 'for expression that evaluates to true' do it 'returns true' do - expression = Flipper.value(true).eq(true) + expression = Flipper.boolean(true).eq(true) expect(subject.open?(context(expression.value))).to be(true) end end context 'for expression that evaluates to false' do it 'returns false' do - expression = Flipper.value(true).eq(false) + expression = Flipper.boolean(true).eq(false) expect(subject.open?(context(expression.value))).to be(false) end end @@ -79,12 +79,12 @@ def context(expression, properties: {}) describe '#protects?' do it 'returns true for Flipper::Expression' do - expression = Flipper.value(20).eq(20) + expression = Flipper.number(20).eq(20) expect(subject.protects?(expression)).to be(true) end it 'returns true for Hash' do - expression = Flipper.value(20).eq(20) + expression = Flipper.number(20).eq(20) expect(subject.protects?(expression.value)).to be(true) end @@ -95,12 +95,12 @@ def context(expression, properties: {}) describe '#wrap' do it 'returns self for Flipper::Expression' do - expression = Flipper.value(20).eq(20) + expression = Flipper.number(20).eq(20) expect(subject.wrap(expression)).to be_instance_of(Flipper::Expressions::Equal) end it 'returns Flipper::Expression for Hash' do - expression = Flipper.value(20).eq(20) + expression = Flipper.number(20).eq(20) expect(subject.wrap(expression.value)).to be_instance_of(Flipper::Expressions::Equal) end end From b3f49d1a05e1bd4b34ce3978b871527e0fec2eda Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Wed, 9 Feb 2022 15:44:51 -0500 Subject: [PATCH 139/176] Fix search/replace error in shared adapter tests --- lib/flipper/test/shared_adapter_test.rb | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/flipper/test/shared_adapter_test.rb b/lib/flipper/test/shared_adapter_test.rb index 407a7f5ca..260dfe2ea 100644 --- a/lib/flipper/test/shared_adapter_test.rb +++ b/lib/flipper/test/shared_adapter_test.rb @@ -40,20 +40,20 @@ def test_returns_correct_default_values_for_gates_if_none_are_enabled end def test_can_enable_disable_and_get_value_for_boolean_gate - assert_equal true, @adapter.enable(@feature, @boolean_gate, @Flipper::Types::Boolean.new) + assert_equal true, @adapter.enable(@feature, @boolean_gate, Flipper::Types::Boolean.new) assert_equal 'true', @adapter.get(@feature)[:boolean] - assert_equal true, @adapter.disable(@feature, @boolean_gate, @Flipper::Types::Boolean.new(false)) + assert_equal true, @adapter.disable(@feature, @boolean_gate, Flipper::Types::Boolean.new(false)) assert_nil @adapter.get(@feature)[:boolean] end def test_fully_disables_all_enabled_things_when_boolean_gate_disabled actor22 = Flipper::Actor.new('22') - assert_equal true, @adapter.enable(@feature, @boolean_gate, @Flipper::Types::Boolean.new) + assert_equal true, @adapter.enable(@feature, @boolean_gate, Flipper::Types::Boolean.new) assert_equal true, @adapter.enable(@feature, @group_gate, @flipper.group(:admins)) assert_equal true, @adapter.enable(@feature, @actor_gate, Flipper::Types::Actor.new(actor22)) assert_equal true, @adapter.enable(@feature, @actors_gate, Flipper::Types::PercentageOfActors.new(25)) assert_equal true, @adapter.enable(@feature, @time_gate, Flipper::Types::PercentageOfTime.new(45)) - assert_equal true, @adapter.disable(@feature, @boolean_gate, @Flipper::Types::Boolean.new(false)) + assert_equal true, @adapter.disable(@feature, @boolean_gate, Flipper::Types::Boolean.new(false)) assert_equal @adapter.default_config, @adapter.get(@feature) end @@ -163,7 +163,7 @@ def test_can_disable_percentage_of_time_gate_many_times_and_consistently_return_ end def test_converts_boolean_value_to_a_string - assert_equal true, @adapter.enable(@feature, @boolean_gate, @Flipper::Types::Boolean.new) + assert_equal true, @adapter.enable(@feature, @boolean_gate, Flipper::Types::Boolean.new) result = @adapter.get(@feature) assert_equal 'true', result[:boolean] end @@ -211,7 +211,7 @@ def test_can_add_remove_and_list_known_features def test_clears_all_the_gate_values_for_the_feature_on_remove actor22 = Flipper::Actor.new('22') - assert_equal true, @adapter.enable(@feature, @boolean_gate, @Flipper::Types::Boolean.new) + assert_equal true, @adapter.enable(@feature, @boolean_gate, Flipper::Types::Boolean.new) assert_equal true, @adapter.enable(@feature, @group_gate, @flipper.group(:admins)) assert_equal true, @adapter.enable(@feature, @actor_gate, Flipper::Types::Actor.new(actor22)) assert_equal true, @adapter.enable(@feature, @actors_gate, Flipper::Types::PercentageOfActors.new(25)) @@ -227,7 +227,7 @@ def test_can_clear_all_the_gate_values_for_a_feature @adapter.add(@feature) assert_includes @adapter.features, @feature.key - assert_equal true, @adapter.enable(@feature, @boolean_gate, @Flipper::Types::Boolean.new) + assert_equal true, @adapter.enable(@feature, @boolean_gate, Flipper::Types::Boolean.new) assert_equal true, @adapter.enable(@feature, @group_gate, @flipper.group(:admins)) assert_equal true, @adapter.enable(@feature, @actor_gate, Flipper::Types::Actor.new(actor22)) assert_equal true, @adapter.enable(@feature, @actors_gate, Flipper::Types::PercentageOfActors.new(25)) @@ -244,7 +244,7 @@ def test_does_not_complain_clearing_a_feature_that_does_not_exist_in_adapter def test_can_get_multiple_features assert @adapter.add(@flipper[:stats]) - assert @adapter.enable(@flipper[:stats], @boolean_gate, @Flipper::Types::Boolean.new) + assert @adapter.enable(@flipper[:stats], @boolean_gate, Flipper::Types::Boolean.new) assert @adapter.add(@flipper[:search]) result = @adapter.get_multi([@flipper[:stats], @flipper[:search], @flipper[:other]]) @@ -260,7 +260,7 @@ def test_can_get_multiple_features def test_can_get_all_features assert @adapter.add(@flipper[:stats]) - assert @adapter.enable(@flipper[:stats], @boolean_gate, @Flipper::Types::Boolean.new) + assert @adapter.enable(@flipper[:stats], @boolean_gate, Flipper::Types::Boolean.new) assert @adapter.add(@flipper[:search]) result = @adapter.get_all @@ -300,8 +300,8 @@ def test_can_double_enable_percentage_without_error end def test_can_double_enable_without_error - assert_equal true, @adapter.enable(@feature, @boolean_gate, @Flipper::Types::Boolean.new) - assert_equal true, @adapter.enable(@feature, @boolean_gate, @Flipper::Types::Boolean.new) + assert_equal true, @adapter.enable(@feature, @boolean_gate, Flipper::Types::Boolean.new) + assert_equal true, @adapter.enable(@feature, @boolean_gate, Flipper::Types::Boolean.new) end def test_can_get_all_features_when_there_are_none @@ -316,7 +316,7 @@ def test_clears_other_gate_values_on_enable assert_equal true, @adapter.enable(@feature, @time_gate, Flipper::Types::PercentageOfTime.new(25)) assert_equal true, @adapter.enable(@feature, @group_gate, @flipper.group(:admins)) assert_equal true, @adapter.enable(@feature, @actor_gate, Flipper::Types::Actor.new(actor)) - assert_equal true, @adapter.enable(@feature, @boolean_gate, @Flipper::Types::Boolean.new(true)) + assert_equal true, @adapter.enable(@feature, @boolean_gate, Flipper::Types::Boolean.new(true)) assert_equal @adapter.default_config.merge(boolean: "true"), @adapter.get(@feature) end end From ba0d962b771088fa97318e60ba5b69e5df848195 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Tue, 3 Jan 2023 15:21:24 -0500 Subject: [PATCH 140/176] Update Changelog --- Changelog.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/Changelog.md b/Changelog.md index c0d10b8ab..dbef7c159 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,19 @@ All notable changes to this project will be documented in this file. +## Unreleased + +### Breaking Changes + +* Removed top level `Flipper.bool`, `Flipper.actors`, `Flipper.time`, `Flipper.actor`, `Flipper.percentage_of_actors`, `Flipper.time`, and `Flipper.percentage_of_time`. Also removed correlated Flipper::DSL instance method. They conflict with some new expression stuff and are rarely if ever used. If you are using them, you can migrate via a search and replace like so: + * Change Flipper.bool => Flipper::Types::Boolean.new + * Change Flipper.boolean => Flipper::Types::Boolean.new + * Change Flipper.actor => Flipper::Types::Actor.new + * Change Flipper.percentage_of_actors => Flipper::Types::PercentageOfActors.new + * Change Flipper.actors => Flipper::Types::PercentageOfActors.new + * Change Flipper.percentage_of_time => Flipper::Types::PercentageOfTime.new + * Change Flipper.time => Flipper::Types::PercentageOfTime.new + ## 0.26.0 * Cloud Background Polling (https://github.com/jnunemaker/flipper/pull/682) @@ -54,18 +67,6 @@ All notable changes to this project will be documented in this file. ## 0.24.0 -### Breaking Changes - -* Removed top level `Flipper.bool`, `Flipper.actors`, `Flipper.time`, `Flipper.actor`, `Flipper.percentage_of_actors`, `Flipper.time`, and `Flipper.percentage_of_time`. Also removed correlated Flipper::DSL instance metho. They conflict with some new expression stuff and I can't think of when I've ever used them. I haven't seen others use them either. If you are using them, you can migrate via a search and replace like so: - * Change Flipper.bool => Flipper::Types::Boolean.new - * Change Flipper.boolean => Flipper::Types::Boolean.new - * Change Flipper.actor => Flipper::Types::Actor.new - * Change Flipper.percentage_of_actors => Flipper::Types::PercentageOfActors.new - * Change Flipper.actors => Flipper::Types::PercentageOfActors.new - * Change Flipper.percentage_of_time => Flipper::Types::PercentageOfTime.new - * Change Flipper.time => Flipper::Types::PercentageOfTime.new - - ### Additions/Changes * Add Ruby 3.0 and 3.1 to the CI matrix and fix groups block arity check for ruby 3 (https://github.com/jnunemaker/flipper/pull/601) From 76b17cbbe42b1010375c1e0ecb37a39cd9f6b8e8 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Tue, 3 Jan 2023 15:50:01 -0500 Subject: [PATCH 141/176] Update link to PR --- lib/flipper/adapters/active_record.rb | 2 +- lib/flipper/adapters/sequel.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/flipper/adapters/active_record.rb b/lib/flipper/adapters/active_record.rb index acf636e92..c18e9b171 100644 --- a/lib/flipper/adapters/active_record.rb +++ b/lib/flipper/adapters/active_record.rb @@ -244,7 +244,7 @@ def result_for_feature(feature, db_gates) end # Check if value column is text instead of string - # See TODO:link/to/PR + # See https://github.com/jnunemaker/flipper/pull/692 def value_not_text? @gate_class.column_for_attribute(:value).type != :text end diff --git a/lib/flipper/adapters/sequel.rb b/lib/flipper/adapters/sequel.rb index 738dd644e..e46435aea 100644 --- a/lib/flipper/adapters/sequel.rb +++ b/lib/flipper/adapters/sequel.rb @@ -239,7 +239,7 @@ def result_for_feature(feature, db_gates) end # Check if value column is text instead of string - # See TODO:link/to/PR + # See https://github.com/jnunemaker/flipper/pull/692 def value_not_text? @gate_class.db_schema[:value][:db_type] != "text" end From 449757ff3442feb304f905b4bec2e751d0669836 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Tue, 3 Jan 2023 15:56:04 -0500 Subject: [PATCH 142/176] Ignore case for db type --- lib/flipper/adapters/sequel.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/flipper/adapters/sequel.rb b/lib/flipper/adapters/sequel.rb index e46435aea..7dcf22bc8 100644 --- a/lib/flipper/adapters/sequel.rb +++ b/lib/flipper/adapters/sequel.rb @@ -241,7 +241,7 @@ def result_for_feature(feature, db_gates) # Check if value column is text instead of string # See https://github.com/jnunemaker/flipper/pull/692 def value_not_text? - @gate_class.db_schema[:value][:db_type] != "text" + "text".casecmp(@gate_class.db_schema[:value][:db_type]) != 0 end end end From 56db6a2f0adc4f94db8c186f16bb081b73ffe542 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Thu, 5 Jan 2023 07:55:55 -0500 Subject: [PATCH 143/176] Rename api/expression_gate_spec --- .../api/v1/actions/{rule_gate_spec.rb => expression_gate_spec.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename spec/flipper/api/v1/actions/{rule_gate_spec.rb => expression_gate_spec.rb} (100%) diff --git a/spec/flipper/api/v1/actions/rule_gate_spec.rb b/spec/flipper/api/v1/actions/expression_gate_spec.rb similarity index 100% rename from spec/flipper/api/v1/actions/rule_gate_spec.rb rename to spec/flipper/api/v1/actions/expression_gate_spec.rb From d3d766d7dfd23d2b9aed0d321245cf7626c317db Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Thu, 5 Jan 2023 08:38:23 -0500 Subject: [PATCH 144/176] Handle empty/blank value in expression gate API --- lib/flipper/api/v1/actions/expression_gate.rb | 2 +- lib/flipper/expression.rb | 3 +++ .../api/v1/actions/expression_gate_spec.rb | 24 +++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/lib/flipper/api/v1/actions/expression_gate.rb b/lib/flipper/api/v1/actions/expression_gate.rb index db93c013e..756714578 100644 --- a/lib/flipper/api/v1/actions/expression_gate.rb +++ b/lib/flipper/api/v1/actions/expression_gate.rb @@ -18,7 +18,7 @@ def post feature.enable_expression expression decorated_feature = Decorators::Feature.new(feature) json_response(decorated_feature.as_json, 200) - rescue NameError => exception + rescue NameError, ArgumentError => exception json_error_response(:expression_invalid) end end diff --git a/lib/flipper/expression.rb b/lib/flipper/expression.rb index c9ebbef82..195c1f3d4 100644 --- a/lib/flipper/expression.rb +++ b/lib/flipper/expression.rb @@ -9,6 +9,9 @@ def self.build(object, convert_to_values: false) when Hash type = object.keys.first args = object.values.first + unless type + raise ArgumentError, "#{object.inspect} cannot be converted into an expression" + end Expressions.const_get(type).new(args) when String convert_to_values ? Expressions::String.new(object.to_s) : object diff --git a/spec/flipper/api/v1/actions/expression_gate_spec.rb b/spec/flipper/api/v1/actions/expression_gate_spec.rb index 7aecb9346..dca7b34a0 100644 --- a/spec/flipper/api/v1/actions/expression_gate_spec.rb +++ b/spec/flipper/api/v1/actions/expression_gate_spec.rb @@ -84,6 +84,30 @@ end end + describe 'enable with no data' do + before do + post '/features/my_feature/expression', "CONTENT_TYPE" => "application/json" + end + + it 'returns correct error response' do + expect(last_response.status).to eq(422) + expect(json_response).to eq(api_expression_invalid_response) + end + end + + describe 'enable with empty object' do + before do + data = {} + post '/features/my_feature/expression', JSON.dump(data), + "CONTENT_TYPE" => "application/json" + end + + it 'returns correct error response' do + expect(last_response.status).to eq(422) + expect(json_response).to eq(api_expression_invalid_response) + end + end + describe 'enable with invalid data' do before do data = {"blah" => "blah"} From d8a8b3d304e32373bacd30e6236aa91ed06eaaf7 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Thu, 16 Mar 2023 14:11:47 -0400 Subject: [PATCH 145/176] Flipper::Identifier is already included in Flipper::Model --- lib/flipper/railtie.rb | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/flipper/railtie.rb b/lib/flipper/railtie.rb index 565dd52e1..d836ad719 100644 --- a/lib/flipper/railtie.rb +++ b/lib/flipper/railtie.rb @@ -10,12 +10,6 @@ class Railtie < Rails::Railtie ) end - initializer "flipper.identifier" do - ActiveSupport.on_load(:active_record) do - ActiveRecord::Base.include Flipper::Identifier - end - end - initializer "flipper.properties" do require "flipper/model/active_record" From 97f6aa44fd1d3714eea0d420476860d754a4ac7a Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sat, 18 Mar 2023 14:40:50 -0400 Subject: [PATCH 146/176] Get specs passing again --- spec/flipper/api/v1/actions/features_spec.rb | 1 + spec/flipper/exporters/json/v1_spec.rb | 6 +++--- spec/flipper/ui/actions/export_spec.rb | 6 +++--- spec/support/spec_helpers.rb | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/spec/flipper/api/v1/actions/features_spec.rb b/spec/flipper/api/v1/actions/features_spec.rb index 9bab82d7a..f1cbd0d55 100644 --- a/spec/flipper/api/v1/actions/features_spec.rb +++ b/spec/flipper/api/v1/actions/features_spec.rb @@ -65,6 +65,7 @@ 'state' => 'on', 'gates' => [ { 'key' => 'boolean', 'value' => 'true'}, + {"key" => "expression", "value" => nil}, { 'key' => 'actors', 'value' => ['10']}, {'key' => 'percentage_of_actors', 'value' => nil}, { 'key' => 'percentage_of_time', 'value' => nil}, diff --git a/spec/flipper/exporters/json/v1_spec.rb b/spec/flipper/exporters/json/v1_spec.rb index f71a07429..616dabf1f 100644 --- a/spec/flipper/exporters/json/v1_spec.rb +++ b/spec/flipper/exporters/json/v1_spec.rb @@ -25,9 +25,9 @@ export = subject.call(adapter) expect(export.features).to eq({ - "google_analytics" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, - "plausible" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, - "search" => {actors: Set["User;1", "User;100"], boolean: nil, groups: Set["admins", "employees"], percentage_of_actors: "10", percentage_of_time: "15"}, + "google_analytics" => {actors: Set.new, boolean: nil, expression: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, + "plausible" => {actors: Set.new, boolean: "true", expression: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil}, + "search" => {actors: Set["User;1", "User;100"], boolean: nil, expression: nil, groups: Set["admins", "employees"], percentage_of_actors: "10", percentage_of_time: "15"}, }) end end diff --git a/spec/flipper/ui/actions/export_spec.rb b/spec/flipper/ui/actions/export_spec.rb index 7e72bad6a..143a3ee8a 100644 --- a/spec/flipper/ui/actions/export_spec.rb +++ b/spec/flipper/ui/actions/export_spec.rb @@ -40,9 +40,9 @@ expect(last_response.headers['Content-Type']).to eq('application/json') expect(data['version']).to eq(1) expect(data['features']).to eq({ - "search"=> {"boolean"=>nil, "groups"=>["admins", "employees"], "actors"=>["User;1", "User;100"], "percentage_of_actors"=>"10", "percentage_of_time"=>"15"}, - "plausible"=> {"boolean"=>"true", "groups"=>[], "actors"=>[], "percentage_of_actors"=>nil, "percentage_of_time"=>nil}, - "google_analytics"=> {"boolean"=>nil, "groups"=>[], "actors"=>[], "percentage_of_actors"=>nil, "percentage_of_time"=>nil}, + "search"=> {"boolean"=>nil, "expression"=>nil, "groups"=>["admins", "employees"], "actors"=>["User;1", "User;100"], "percentage_of_actors"=>"10", "percentage_of_time"=>"15"}, + "plausible"=> {"boolean"=>"true", "expression"=>nil, "groups"=>[], "actors"=>[], "percentage_of_actors"=>nil, "percentage_of_time"=>nil}, + "google_analytics"=> {"boolean"=>nil, "expression"=>nil, "groups"=>[], "actors"=>[], "percentage_of_actors"=>nil, "percentage_of_time"=>nil}, }) end end diff --git a/spec/support/spec_helpers.rb b/spec/support/spec_helpers.rb index 3cd9947bc..9f04d2de1 100644 --- a/spec/support/spec_helpers.rb +++ b/spec/support/spec_helpers.rb @@ -60,7 +60,7 @@ def api_flipper_id_is_missing_response def api_expression_invalid_response { - 'code' => 6, + 'code' => 7, 'message' => 'The provided expression was not valid.', 'more_info' => api_error_code_reference_url, } From 9ffff3c201dbc9da547c36fd363e720b9daa6bbb Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sat, 18 Mar 2023 14:45:37 -0400 Subject: [PATCH 147/176] Add rule to export spec --- spec/flipper/ui/actions/export_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/flipper/ui/actions/export_spec.rb b/spec/flipper/ui/actions/export_spec.rb index 143a3ee8a..86315c1a3 100644 --- a/spec/flipper/ui/actions/export_spec.rb +++ b/spec/flipper/ui/actions/export_spec.rb @@ -21,6 +21,7 @@ flipper.enable_group :search, :employees flipper.enable :plausible flipper.disable :google_analytics + flipper.enable :analytics, Flipper.property(:plan).eq("basic") post '/settings/export', {'authenticity_token' => token}, @@ -40,6 +41,7 @@ expect(last_response.headers['Content-Type']).to eq('application/json') expect(data['version']).to eq(1) expect(data['features']).to eq({ + "analytics" => {"boolean"=>nil, "expression"=>"{\"Equal\"=>[{\"Property\"=>[\"plan\"]}, {\"String\"=>[\"basic\"]}]}", "groups"=>[], "actors"=>[], "percentage_of_actors"=>nil, "percentage_of_time"=>nil}, "search"=> {"boolean"=>nil, "expression"=>nil, "groups"=>["admins", "employees"], "actors"=>["User;1", "User;100"], "percentage_of_actors"=>"10", "percentage_of_time"=>"15"}, "plausible"=> {"boolean"=>"true", "expression"=>nil, "groups"=>[], "actors"=>[], "percentage_of_actors"=>nil, "percentage_of_time"=>nil}, "google_analytics"=> {"boolean"=>nil, "expression"=>nil, "groups"=>[], "actors"=>[], "percentage_of_actors"=>nil, "percentage_of_time"=>nil}, From 4f3054145e5259adbfbb80d6e89eacbe6c4f640e Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sat, 18 Mar 2023 14:47:25 -0400 Subject: [PATCH 148/176] Ensure that expressions will import --- spec/flipper/api/v1/actions/import_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/flipper/api/v1/actions/import_spec.rb b/spec/flipper/api/v1/actions/import_spec.rb index 9877d675b..1b4f613bf 100644 --- a/spec/flipper/api/v1/actions/import_spec.rb +++ b/spec/flipper/api/v1/actions/import_spec.rb @@ -10,6 +10,7 @@ source_flipper = build_flipper source_flipper.disable(:search) source_flipper.enable_actor(:google_analytics, Flipper::Actor.new("User;1")) + source_flipper.enable(:analytics, Flipper.property(:plan).eq("basic")) export = source_flipper.export @@ -23,7 +24,8 @@ it 'imports features' do expect(flipper[:search].boolean_value).to be(false) expect(flipper[:google_analytics].actors_value).to eq(Set["User;1"]) - expect(flipper.features.map(&:key)).to eq(["search", "google_analytics"]) + expect(flipper[:analytics].expression_value).to eq("{\"Equal\"=>[{\"Property\"=>[\"plan\"]}, {\"String\"=>[\"basic\"]}]}") + expect(flipper.features.map(&:key)).to eq(["search", "google_analytics", "analytics"]) end end From 7d1c4067f96e5b0878b6ad5236ec162a558edfa0 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sat, 18 Mar 2023 17:06:27 -0400 Subject: [PATCH 149/176] Ensure that expressions work with import and export stuff --- lib/flipper/spec/shared_adapter_specs.rb | 10 ++++----- lib/flipper/test/shared_adapter_test.rb | 10 ++++----- lib/flipper/typecast.rb | 2 ++ spec/flipper/api/v1/actions/import_spec.rb | 2 +- spec/flipper/typecast_spec.rb | 24 ++++++++++++++++++++++ spec/flipper/ui/actions/export_spec.rb | 2 +- 6 files changed, 38 insertions(+), 12 deletions(-) diff --git a/lib/flipper/spec/shared_adapter_specs.rb b/lib/flipper/spec/shared_adapter_specs.rb index a1b3967e8..34c019fab 100644 --- a/lib/flipper/spec/shared_adapter_specs.rb +++ b/lib/flipper/spec/shared_adapter_specs.rb @@ -274,14 +274,14 @@ expect(subject.add(flipper[:stats])).to eq(true) expect(subject.enable(flipper[:stats], boolean_gate, Flipper::Types::Boolean.new)).to eq(true) expect(subject.add(flipper[:search])).to eq(true) + flipper.enable :analytics, Flipper.property(:plan).eq("pro") result = subject.get_all - expect(result).to be_instance_of(Hash) - stats = result["stats"] - search = result["search"] - expect(stats).to eq(subject.default_config.merge(boolean: 'true')) - expect(search).to eq(subject.default_config) + expect(result).to be_instance_of(Hash) + expect(result["stats"]).to eq(subject.default_config.merge(boolean: 'true')) + expect(result["search"]).to eq(subject.default_config) + expect(result["analytics"]).to eq(subject.default_config.merge(expression: {"Equal"=>[{"Property"=>["plan"]}, {"String"=>["pro"]}]})) end it 'includes explicitly disabled features when getting all features' do diff --git a/lib/flipper/test/shared_adapter_test.rb b/lib/flipper/test/shared_adapter_test.rb index 7d3ba3349..db66281dd 100644 --- a/lib/flipper/test/shared_adapter_test.rb +++ b/lib/flipper/test/shared_adapter_test.rb @@ -271,14 +271,14 @@ def test_can_get_all_features assert @adapter.add(@flipper[:stats]) assert @adapter.enable(@flipper[:stats], @boolean_gate, Flipper::Types::Boolean.new) assert @adapter.add(@flipper[:search]) + @flipper.enable :analytics, Flipper.property(:plan).eq("pro") result = @adapter.get_all - assert_instance_of Hash, result - stats = result["stats"] - search = result["search"] - assert_equal @adapter.default_config.merge(boolean: 'true'), stats - assert_equal @adapter.default_config, search + assert_instance_of Hash, result + assert_equal @adapter.default_config.merge(boolean: 'true'), result["stats"] + assert_equal @adapter.default_config, result["search"] + assert_equal @adapter.default_config.merge(expression: {"Equal"=>[{"Property"=>["plan"]}, {"String"=>["pro"]}]}), result["analytics"] end def test_includes_explicitly_disabled_features_when_getting_all_features diff --git a/lib/flipper/typecast.rb b/lib/flipper/typecast.rb index 78af3c981..ec24f3f30 100644 --- a/lib/flipper/typecast.rb +++ b/lib/flipper/typecast.rb @@ -71,6 +71,8 @@ def self.features_hash(source) normalized_value = case value when Array, Set value.to_set + when Hash + value else value ? value.to_s : value end diff --git a/spec/flipper/api/v1/actions/import_spec.rb b/spec/flipper/api/v1/actions/import_spec.rb index 1b4f613bf..c5c0ba93c 100644 --- a/spec/flipper/api/v1/actions/import_spec.rb +++ b/spec/flipper/api/v1/actions/import_spec.rb @@ -24,7 +24,7 @@ it 'imports features' do expect(flipper[:search].boolean_value).to be(false) expect(flipper[:google_analytics].actors_value).to eq(Set["User;1"]) - expect(flipper[:analytics].expression_value).to eq("{\"Equal\"=>[{\"Property\"=>[\"plan\"]}, {\"String\"=>[\"basic\"]}]}") + expect(flipper[:analytics].expression_value).to eq({"Equal"=>[{"Property"=>["plan"]}, {"String"=>["basic"]}]}) expect(flipper.features.map(&:key)).to eq(["search", "google_analytics", "analytics"]) end end diff --git a/spec/flipper/typecast_spec.rb b/spec/flipper/typecast_spec.rb index f67dbf4c7..57bc98bb0 100644 --- a/spec/flipper/typecast_spec.rb +++ b/spec/flipper/typecast_spec.rb @@ -127,6 +127,30 @@ expect(result["search"]).not_to be(hash["search"]) end + it "converts does not convert expressions" do + hash = { + "search" => { + boolean: nil, + expression: {"Equal"=>[{"Property"=>["plan"]}, {"String"=>["basic"]}]}, + groups: ['a', 'b'], + actors: ['User;1'], + percentage_of_actors: nil, + percentage_of_time: nil, + }, + } + result = described_class.features_hash(hash) + expect(result).to eq({ + "search" => { + boolean: nil, + expression: {"Equal"=>[{"Property"=>["plan"]}, {"String"=>["basic"]}]}, + groups: Set['a', 'b'], + actors: Set['User;1'], + percentage_of_actors: nil, + percentage_of_time: nil, + }, + }) + end + it "converts gate value arrays to sets" do hash = { "search" => { diff --git a/spec/flipper/ui/actions/export_spec.rb b/spec/flipper/ui/actions/export_spec.rb index 86315c1a3..c9a8c67b7 100644 --- a/spec/flipper/ui/actions/export_spec.rb +++ b/spec/flipper/ui/actions/export_spec.rb @@ -41,7 +41,7 @@ expect(last_response.headers['Content-Type']).to eq('application/json') expect(data['version']).to eq(1) expect(data['features']).to eq({ - "analytics" => {"boolean"=>nil, "expression"=>"{\"Equal\"=>[{\"Property\"=>[\"plan\"]}, {\"String\"=>[\"basic\"]}]}", "groups"=>[], "actors"=>[], "percentage_of_actors"=>nil, "percentage_of_time"=>nil}, + "analytics" => {"boolean"=>nil, "expression"=>{"Equal"=>[{"Property"=>["plan"]}, {"String"=>["basic"]}]}, "groups"=>[], "actors"=>[], "percentage_of_actors"=>nil, "percentage_of_time"=>nil}, "search"=> {"boolean"=>nil, "expression"=>nil, "groups"=>["admins", "employees"], "actors"=>["User;1", "User;100"], "percentage_of_actors"=>"10", "percentage_of_time"=>"15"}, "plausible"=> {"boolean"=>"true", "expression"=>nil, "groups"=>[], "actors"=>[], "percentage_of_actors"=>nil, "percentage_of_time"=>nil}, "google_analytics"=> {"boolean"=>nil, "expression"=>nil, "groups"=>[], "actors"=>[], "percentage_of_actors"=>nil, "percentage_of_time"=>nil}, From 4d6d1fdd28ededcf35491c82f993a6715a80af43 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Fri, 24 Mar 2023 10:18:30 -0400 Subject: [PATCH 150/176] a bit of formatting --- spec/flipper_integration_spec.rb | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/spec/flipper_integration_spec.rb b/spec/flipper_integration_spec.rb index fa6d0cc01..7da9c76c2 100644 --- a/spec/flipper_integration_spec.rb +++ b/spec/flipper_integration_spec.rb @@ -8,28 +8,24 @@ let(:dev_group) { flipper.group(:devs) } let(:admin_actor) do - double 'Non Flipper Thing', flipper_id: 1, admin?: true, dev?: false + double 'Non Flipper Thing', flipper_id: 1, admin?: true, dev?: false, flipper_properties: {"admin" => true, "dev" => false} end let(:dev_actor) do - double 'Non Flipper Thing', flipper_id: 10, admin?: false, dev?: true + double 'Non Flipper Thing', flipper_id: 10, admin?: false, dev?: true, flipper_properties: {"admin" => false, "dev" => true} end let(:admin_truthy_actor) do - double 'Non Flipper Thing', flipper_id: 1, admin?: 'true-ish', dev?: false + double 'Non Flipper Thing', flipper_id: 1, admin?: 'true-ish', dev?: false, flipper_properties: {"admin" => "true-ish", "dev" => false} end let(:admin_falsey_actor) do - double 'Non Flipper Thing', flipper_id: 1, admin?: nil, dev?: false + double 'Non Flipper Thing', flipper_id: 1, admin?: nil, dev?: false, flipper_properties: {"admin" => nil, "dev" => false} end let(:basic_plan_actor) do - double 'Non Flipper Thing', flipper_id: 1, flipper_properties: { - "plan" => "basic", - } + double 'Non Flipper Thing', flipper_id: 1, flipper_properties: {"plan" => "basic"} end let(:premium_plan_actor) do - double 'Non Flipper Thing', flipper_id: 10, flipper_properties: { - "plan" => "premium", - } + double 'Non Flipper Thing', flipper_id: 10, flipper_properties: {"plan" => "premium"} end let(:pitt) { Flipper::Actor.new(1) } From 7dbb5fd4e48aa066cd357e406571e6c8612cbe08 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Fri, 24 Mar 2023 10:18:41 -0400 Subject: [PATCH 151/176] Add some actor specs --- spec/flipper/types/actor_spec.rb | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/spec/flipper/types/actor_spec.rb b/spec/flipper/types/actor_spec.rb index d6339c1f4..63a1f1fa1 100644 --- a/spec/flipper/types/actor_spec.rb +++ b/spec/flipper/types/actor_spec.rb @@ -11,12 +11,19 @@ attr_reader :flipper_id def initialize(flipper_id) - @flipper_id = flipper_id + @flipper_id = flipper_id.to_s end def admin? true end + + def flipper_properties + { + "flipper_id" => flipper_id, + "admin" => admin?, + } + end end end @@ -87,6 +94,15 @@ def admin? expect(actor.admin?).to eq(true) end + it 'proxies flipper_properties to actor' do + actor = actor_class.new(10) + actor = described_class.new(actor) + expect(actor.flipper_properties).to eq({ + "flipper_id" => "10", + "admin" => true, + }) + end + it 'exposes actor' do actor = actor_class.new(10) actor_type_instance = described_class.new(actor) @@ -104,6 +120,7 @@ def admin? actor = actor_class.new(10) actor_type_instance = described_class.new(actor) expect(actor_type_instance.respond_to?(:admin?)).to eq(true) + expect(actor_type_instance.respond_to?(:flipper_properties)).to eq(true) end it 'returns false if does not respond to method and actor does not respond to method' do From 98f303bb3d3ba602ad9b392ff559d142f3ee5086 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sat, 25 Mar 2023 12:18:05 -0400 Subject: [PATCH 152/176] Fix expression gate for no actor --- lib/flipper/gates/expression.rb | 2 ++ spec/flipper_integration_spec.rb | 1 + 2 files changed, 3 insertions(+) diff --git a/lib/flipper/gates/expression.rb b/lib/flipper/gates/expression.rb index 7f9f8f6a0..2c2ff85b6 100644 --- a/lib/flipper/gates/expression.rb +++ b/lib/flipper/gates/expression.rb @@ -29,6 +29,8 @@ def open?(context) return false if data.nil? || data.empty? expression = Flipper::Expression.build(data) + return false if context.actors.nil? + context.actors.any? do |actor| result = expression.evaluate( feature_name: context.feature_name, diff --git a/spec/flipper_integration_spec.rb b/spec/flipper_integration_spec.rb index 7da9c76c2..980f633bd 100644 --- a/spec/flipper_integration_spec.rb +++ b/spec/flipper_integration_spec.rb @@ -567,6 +567,7 @@ it "works" do feature.enable Flipper.property(:plan).eq("basic") + expect(feature.enabled?).to be(false) expect(feature.enabled?(basic_plan_actor)).to be(true) expect(feature.enabled?(premium_plan_actor)).to be(false) expect(feature.enabled?(admin_actor)).to be(false) From 4994c3f58f1c7499ec04a72dafdfd65bc55d0d28 Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sat, 25 Mar 2023 12:24:36 -0400 Subject: [PATCH 153/176] Make expressions work with nil or empty array of actors Expression still needs to evaluate. --- lib/flipper/gates/expression.rb | 14 ++++++-------- spec/flipper_integration_spec.rb | 5 +++++ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/flipper/gates/expression.rb b/lib/flipper/gates/expression.rb index 2c2ff85b6..53d4dc31e 100644 --- a/lib/flipper/gates/expression.rb +++ b/lib/flipper/gates/expression.rb @@ -29,14 +29,12 @@ def open?(context) return false if data.nil? || data.empty? expression = Flipper::Expression.build(data) - return false if context.actors.nil? - - context.actors.any? do |actor| - result = expression.evaluate( - feature_name: context.feature_name, - properties: properties(actor) - ) - !!result + if context.actors.nil? || context.actors.empty? + !!expression.evaluate(feature_name: context.feature_name, properties: DEFAULT_PROPERTIES) + else + context.actors.any? do |actor| + !!expression.evaluate(feature_name: context.feature_name, properties: properties(actor)) + end end end diff --git a/spec/flipper_integration_spec.rb b/spec/flipper_integration_spec.rb index 980f633bd..860361847 100644 --- a/spec/flipper_integration_spec.rb +++ b/spec/flipper_integration_spec.rb @@ -573,6 +573,11 @@ expect(feature.enabled?(admin_actor)).to be(false) end + it "works for true expression with no actor" do + feature.enable Flipper.boolean(true) + expect(feature.enabled?).to be(true) + end + it "works for multiple actors" do feature.enable Flipper.property(:plan).eq("basic") From 9da3f8cdcaea31437f2394b03bebb52c1f74378b Mon Sep 17 00:00:00 2001 From: John Nunemaker Date: Sat, 25 Mar 2023 12:25:02 -0400 Subject: [PATCH 154/176] Simplify boolean enabled --- examples/expressions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/expressions.rb b/examples/expressions.rb index 41bb6ef94..6b4c1c692 100644 --- a/examples/expressions.rb +++ b/examples/expressions.rb @@ -130,7 +130,7 @@ class Org < Struct.new(:id, :flipper_properties) refute Flipper.enabled?(:something, other_user) puts "\n\nBoolean Expression" -boolean_expression = Flipper.boolean(true).eq(true) +boolean_expression = Flipper.boolean(true) Flipper.enable :something, boolean_expression assert Flipper.enabled?(:something) assert Flipper.enabled?(:something, user) From 973fe0c976e2021672df6e2bf48aabbfd4fd991f Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Sat, 25 Mar 2023 12:29:48 -0400 Subject: [PATCH 155/176] Add Time, Now, and Duration expression types --- lib/flipper/expressions/duration.rb | 37 +++++++++++++++++ lib/flipper/expressions/now.rb | 15 +++++++ lib/flipper/expressions/time.rb | 15 +++++++ spec/flipper/expressions/duration_spec.rb | 49 +++++++++++++++++++++++ spec/flipper/expressions/now_spec.rb | 13 ++++++ spec/flipper/expressions/time_spec.rb | 27 +++++++++++++ 6 files changed, 156 insertions(+) create mode 100644 lib/flipper/expressions/duration.rb create mode 100644 lib/flipper/expressions/now.rb create mode 100644 lib/flipper/expressions/time.rb create mode 100644 spec/flipper/expressions/duration_spec.rb create mode 100644 spec/flipper/expressions/now_spec.rb create mode 100644 spec/flipper/expressions/time_spec.rb diff --git a/lib/flipper/expressions/duration.rb b/lib/flipper/expressions/duration.rb new file mode 100644 index 000000000..c87b52bad --- /dev/null +++ b/lib/flipper/expressions/duration.rb @@ -0,0 +1,37 @@ +require "flipper/expression" + +module Flipper + module Expressions + class Duration < Expression + SECONDS_PER = { + "second" => 1, + "minute" => 60, + "hour" => 3600, + "day" => 86400, + "week" => 604_800, + "month" => 26_29_746, # 1/12 of a gregorian year + "year" => 31_556_952 # length of a gregorian year (365.2425 days) + }.freeze + + def initialize(args) + scalar, unit = args + super [scalar, unit || 'second'] + end + + def evaluate(context = {}) + scalar = evaluate_arg(0, context) + unit = evaluate_arg(1, context) || 'second' + unit = unit.to_s.downcase.chomp("s") + + unless scalar.is_a?(Numeric) + raise ArgumentError.new("Duration value must be a number but was #{scalar.inspect}") + end + unless SECONDS_PER[unit] + raise ArgumentError.new("Duration unit #{unit.inspect} must be one of: #{SECONDS_PER.keys.join(', ')}") + end + + scalar * SECONDS_PER[unit] + end + end + end +end diff --git a/lib/flipper/expressions/now.rb b/lib/flipper/expressions/now.rb new file mode 100644 index 000000000..d110023f7 --- /dev/null +++ b/lib/flipper/expressions/now.rb @@ -0,0 +1,15 @@ +require "flipper/expression" + +module Flipper + module Expressions + class Now < Expression + def initialize(_ = nil) + super [] + end + + def evaluate(context = {}) + ::Time.now + end + end + end +end diff --git a/lib/flipper/expressions/time.rb b/lib/flipper/expressions/time.rb new file mode 100644 index 000000000..904a7fe44 --- /dev/null +++ b/lib/flipper/expressions/time.rb @@ -0,0 +1,15 @@ +require "flipper/expression" + +module Flipper + module Expressions + class Time < Expression + def initialize(args) + super [args].flatten.map(&:to_s) + end + + def evaluate(context = {}) + ::Time.parse(evaluate_arg(0, context)) + end + end + end +end diff --git a/spec/flipper/expressions/duration_spec.rb b/spec/flipper/expressions/duration_spec.rb new file mode 100644 index 000000000..66667c789 --- /dev/null +++ b/spec/flipper/expressions/duration_spec.rb @@ -0,0 +1,49 @@ +RSpec.describe Flipper::Expressions::Duration do + describe "#initialize" do + it 'initializes with scalar and unit' do + expect(described_class.new([10, 'second']).args).to eq([10, 'second']) + end + + it 'defaults unit to seconds' do + expect(described_class.new([1]).args).to eq([1, 'second']) + end + end + + describe "#evaluate" do + it "raises error with invalid value" do + expect { described_class.new([false, 'minute']).evaluate }.to raise_error(ArgumentError) + end + + it "raises error with invalid unit" do + expect { described_class.new([4, 'score']).evaluate }.to raise_error(ArgumentError) + end + + it "evaluates seconds" do + expect(described_class.new([10, 'seconds']).evaluate).to eq(10) + end + + it "evaluates minutes" do + expect(described_class.new([2, 'minutes']).evaluate).to eq(120) + end + + it "evaluates hours" do + expect(described_class.new([2, 'hours']).evaluate).to eq(7200) + end + + it "evaluates days" do + expect(described_class.new([2, 'days']).evaluate).to eq(172_800) + end + + it "evaluates weeks" do + expect(described_class.new([2, 'weeks']).evaluate).to eq(1_209_600) + end + + it "evaluates months" do + expect(described_class.new([2, 'months']).evaluate).to eq(5_259_492) + end + + it "evaluates years" do + expect(described_class.new([2, 'years']).evaluate).to eq(63_113_904) + end + end +end diff --git a/spec/flipper/expressions/now_spec.rb b/spec/flipper/expressions/now_spec.rb new file mode 100644 index 000000000..d234e8709 --- /dev/null +++ b/spec/flipper/expressions/now_spec.rb @@ -0,0 +1,13 @@ +RSpec.describe Flipper::Expressions::Now do + describe "#initialize" do + it "ignores arguments" do + expect(described_class.new("foo").args).to eq([]) + end + end + + describe "#evaluate" do + it "returns current time" do + expect(described_class.new.evaluate.round).to eq(Time.now.round) + end + end +end diff --git a/spec/flipper/expressions/time_spec.rb b/spec/flipper/expressions/time_spec.rb new file mode 100644 index 000000000..8c5bf144d --- /dev/null +++ b/spec/flipper/expressions/time_spec.rb @@ -0,0 +1,27 @@ +RSpec.describe Flipper::Expressions::Time do + let(:time) { Time.now.round } + + describe "#initialize" do + it "works with time" do + expect(described_class.new(time).args).to eq([time.to_s]) + end + + it "works with Time#to_s" do + expect(described_class.new(time.to_s).args).to eq([time.to_s]) + end + + it "works with array" do + expect(described_class.new([time]).args).to eq([time.to_s]) + end + end + + describe "#evaluate" do + it "returns time for #to_s format" do + expect(described_class.new(time.to_s).evaluate).to eq(time) + end + + it "returns time for #iso8601 format" do + expect(described_class.new(time.iso8601).evaluate).to eq(time) + end + end +end From b285c4df2d04634dd2937cd388d90c5bc8748f7f Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Sat, 25 Mar 2023 19:19:16 -0400 Subject: [PATCH 156/176] Refactor expressions --- lib/flipper.rb | 6 +- lib/flipper/expression.rb | 54 +++++++++--------- lib/flipper/expressions/boolean.rb | 15 ----- lib/flipper/expressions/comparable.rb | 15 +++++ lib/flipper/expressions/constant.rb | 19 +++++++ lib/flipper/expressions/duration.rb | 9 +-- lib/flipper/expressions/equal.rb | 11 +--- lib/flipper/expressions/greater_than.rb | 13 +---- .../expressions/greater_than_or_equal_to.rb | 13 +---- lib/flipper/expressions/less_than.rb | 13 +---- .../expressions/less_than_or_equal_to.rb | 13 +---- lib/flipper/expressions/not_equal.rb | 11 +--- lib/flipper/expressions/now.rb | 6 +- lib/flipper/expressions/number.rb | 15 ----- lib/flipper/expressions/percentage.rb | 15 +---- .../expressions/percentage_of_actors.rb | 9 +-- lib/flipper/expressions/property.rb | 10 +--- lib/flipper/expressions/random.rb | 8 +-- lib/flipper/expressions/string.rb | 15 ----- lib/flipper/expressions/time.rb | 8 +-- spec/flipper/adapters/read_only_spec.rb | 2 +- spec/flipper/expression_spec.rb | 57 ++++++++----------- spec/flipper/expressions/boolean_spec.rb | 48 ---------------- spec/flipper/expressions/duration_spec.rb | 14 ++--- spec/flipper/expressions/equal_spec.rb | 53 ++++------------- .../greater_than_or_equal_to_spec.rb | 20 +++---- spec/flipper/expressions/greater_than_spec.rb | 22 ++++--- .../expressions/less_than_or_equal_to_spec.rb | 20 +++---- spec/flipper/expressions/less_than_spec.rb | 20 +++---- spec/flipper/expressions/not_equal_spec.rb | 10 +--- spec/flipper/expressions/now_spec.rb | 6 -- spec/flipper/expressions/number_spec.rb | 32 ----------- .../expressions/percentage_of_actors_spec.rb | 4 +- spec/flipper/expressions/percentage_spec.rb | 22 +------ spec/flipper/expressions/property_spec.rb | 16 ------ spec/flipper/expressions/random_spec.rb | 34 +++-------- spec/flipper/expressions/string_spec.rb | 32 ----------- spec/flipper/expressions/time_spec.rb | 14 ----- spec/flipper_spec.rb | 6 +- 39 files changed, 195 insertions(+), 515 deletions(-) delete mode 100644 lib/flipper/expressions/boolean.rb create mode 100644 lib/flipper/expressions/comparable.rb create mode 100644 lib/flipper/expressions/constant.rb delete mode 100644 lib/flipper/expressions/number.rb delete mode 100644 lib/flipper/expressions/string.rb delete mode 100644 spec/flipper/expressions/boolean_spec.rb delete mode 100644 spec/flipper/expressions/number_spec.rb delete mode 100644 spec/flipper/expressions/string_spec.rb diff --git a/lib/flipper.rb b/lib/flipper.rb index d5f2aa804..23aa70acd 100644 --- a/lib/flipper.rb +++ b/lib/flipper.rb @@ -81,15 +81,15 @@ def property(name) end def string(value) - Flipper::Expressions::String.new(value) + Flipper::Expressions::Constant.new(value) end def number(value) - Flipper::Expressions::Number.new(value) + Flipper::Expressions::Constant.new(value) end def boolean(value) - Flipper::Expressions::Boolean.new(value) + Flipper::Expressions::Constant.new(value) end def random(name) diff --git a/lib/flipper/expression.rb b/lib/flipper/expression.rb index 195c1f3d4..c9ed5d641 100644 --- a/lib/flipper/expression.rb +++ b/lib/flipper/expression.rb @@ -1,6 +1,6 @@ module Flipper class Expression - def self.build(object, convert_to_values: false) + def self.build(object) return object if object.is_a?(Flipper::Expression) case object @@ -13,14 +13,10 @@ def self.build(object, convert_to_values: false) raise ArgumentError, "#{object.inspect} cannot be converted into an expression" end Expressions.const_get(type).new(args) - when String - convert_to_values ? Expressions::String.new(object.to_s) : object + when String, Numeric, FalseClass, TrueClass + Expressions::Constant.new(object) when Symbol - convert_to_values ? Expressions::String.new(object.to_s) : object.to_s - when Numeric - convert_to_values ? Expressions::Number.new(object.to_f) : object - when TrueClass, FalseClass - convert_to_values ? Expressions::Boolean.new(object) : object + Expressions::Constant.new(object.to_s) else raise ArgumentError, "#{object.inspect} cannot be converted into an expression" end @@ -28,13 +24,19 @@ def self.build(object, convert_to_values: false) attr_reader :args - def initialize(args) - unless args.is_a?(Array) - raise ArgumentError, "args must be an Array but was #{args.inspect}" - end + def initialize(args = []) + args = [args] unless args.is_a?(Array) @args = self.class.build(args) end + def evaluate(context = {}) + if call_with_context? + call(*args.map { |arg| arg.evaluate(context) }, context: context) + else + call(*args.map { |arg| arg.evaluate(context) }) + end + end + def eql?(other) self.class.eql?(other.class) && @args == other.args end @@ -42,9 +44,7 @@ def eql?(other) def value { - self.class.name.split("::").last => args.map { |arg| - arg.is_a?(Expression) ? arg.value : arg - } + self.class.name.split("::").last => args.map(&:value) } end @@ -65,50 +65,50 @@ def all end def equal(object) - Expressions::Equal.new([self, self.class.build(object, convert_to_values: true)]) + Expressions::Equal.new([self, self.class.build(object)]) end alias eq equal def not_equal(object) - Expressions::NotEqual.new([self, self.class.build(object, convert_to_values: true)]) + Expressions::NotEqual.new([self, self.class.build(object)]) end alias neq not_equal def greater_than(object) - Expressions::GreaterThan.new([self, self.class.build(object, convert_to_values: true)]) + Expressions::GreaterThan.new([self, self.class.build(object)]) end alias gt greater_than def greater_than_or_equal_to(object) - Expressions::GreaterThanOrEqualTo.new([self, self.class.build(object, convert_to_values: true)]) + Expressions::GreaterThanOrEqualTo.new([self, self.class.build(object)]) end alias gte greater_than_or_equal_to alias greater_than_or_equal greater_than_or_equal_to def less_than(object) - Expressions::LessThan.new([self, self.class.build(object, convert_to_values: true)]) + Expressions::LessThan.new([self, self.class.build(object)]) end alias lt less_than def less_than_or_equal_to(object) - Expressions::LessThanOrEqualTo.new([self, self.class.build(object, convert_to_values: true)]) + Expressions::LessThanOrEqualTo.new([self, self.class.build(object)]) end alias lte less_than_or_equal_to alias less_than_or_equal less_than_or_equal_to def percentage_of_actors(object) - Expressions::PercentageOfActors.new([self, self.class.build(object, convert_to_values: true)]) + Expressions::PercentageOfActors.new([self, self.class.build(object)]) end private def evaluate_arg(index, context = {}) - object = args[index] + object = args[index].evaluate(context) + end - if object.is_a?(Flipper::Expression) - object.evaluate(context) - else - object + def call_with_context? + method(:call).parameters.any? do |type, name| + name == :context && [:key, :keyreq].include?(type) end end end diff --git a/lib/flipper/expressions/boolean.rb b/lib/flipper/expressions/boolean.rb deleted file mode 100644 index c01cac864..000000000 --- a/lib/flipper/expressions/boolean.rb +++ /dev/null @@ -1,15 +0,0 @@ -require "flipper/expression" - -module Flipper - module Expressions - class Boolean < Expression - def initialize(args) - super Array(args) - end - - def evaluate(context = {}) - !!evaluate_arg(0, context) - end - end - end -end diff --git a/lib/flipper/expressions/comparable.rb b/lib/flipper/expressions/comparable.rb new file mode 100644 index 000000000..444530b03 --- /dev/null +++ b/lib/flipper/expressions/comparable.rb @@ -0,0 +1,15 @@ +require "flipper/expression" + +module Flipper + module Expressions + class Comparable < Expression + def operator + raise NotImplementedError + end + + def call(left, right) + left.respond_to?(operator) && right.respond_to?(operator) && left.public_send(operator, right) + end + end + end +end diff --git a/lib/flipper/expressions/constant.rb b/lib/flipper/expressions/constant.rb new file mode 100644 index 000000000..d0c92b024 --- /dev/null +++ b/lib/flipper/expressions/constant.rb @@ -0,0 +1,19 @@ +module Flipper + module Expressions + # Public: Represents a constant value. + class Constant < Expression + # Override initialize to avoid trying to build args + def initialize(value) + @args = Array(value) + end + + def evaluate(context = {}) + args[0] + end + + def value + args[0] + end + end + end +end diff --git a/lib/flipper/expressions/duration.rb b/lib/flipper/expressions/duration.rb index c87b52bad..c1e2d0b2b 100644 --- a/lib/flipper/expressions/duration.rb +++ b/lib/flipper/expressions/duration.rb @@ -13,14 +13,7 @@ class Duration < Expression "year" => 31_556_952 # length of a gregorian year (365.2425 days) }.freeze - def initialize(args) - scalar, unit = args - super [scalar, unit || 'second'] - end - - def evaluate(context = {}) - scalar = evaluate_arg(0, context) - unit = evaluate_arg(1, context) || 'second' + def call(scalar, unit = 'second', context = {}) unit = unit.to_s.downcase.chomp("s") unless scalar.is_a?(Numeric) diff --git a/lib/flipper/expressions/equal.rb b/lib/flipper/expressions/equal.rb index 0f968e4b7..3601079b6 100644 --- a/lib/flipper/expressions/equal.rb +++ b/lib/flipper/expressions/equal.rb @@ -2,14 +2,9 @@ module Flipper module Expressions - class Equal < Expression - def evaluate(context = {}) - return false unless args[0] && args[1] - - left = evaluate_arg(0, context) - right = evaluate_arg(1, context) - - left == right + class Equal < Comparable + def operator + :== end end end diff --git a/lib/flipper/expressions/greater_than.rb b/lib/flipper/expressions/greater_than.rb index 292f073c5..ca964efdc 100644 --- a/lib/flipper/expressions/greater_than.rb +++ b/lib/flipper/expressions/greater_than.rb @@ -2,16 +2,9 @@ module Flipper module Expressions - class GreaterThan < Expression - def evaluate(context = {}) - return false unless args[0] && args[1] - - left = evaluate_arg(0, context) - right = evaluate_arg(1, context) - - return false unless left && right - - left > right + class GreaterThan < Comparable + def operator + :> end end end diff --git a/lib/flipper/expressions/greater_than_or_equal_to.rb b/lib/flipper/expressions/greater_than_or_equal_to.rb index 9a76282cb..cd91825c0 100644 --- a/lib/flipper/expressions/greater_than_or_equal_to.rb +++ b/lib/flipper/expressions/greater_than_or_equal_to.rb @@ -2,16 +2,9 @@ module Flipper module Expressions - class GreaterThanOrEqualTo < Expression - def evaluate(context = {}) - return false unless args[0] && args[1] - - left = evaluate_arg(0, context) - right = evaluate_arg(1, context) - - return false unless left && right - - left >= right + class GreaterThanOrEqualTo < Comparable + def operator + :>= end end end diff --git a/lib/flipper/expressions/less_than.rb b/lib/flipper/expressions/less_than.rb index dffb6e563..1b7f16d31 100644 --- a/lib/flipper/expressions/less_than.rb +++ b/lib/flipper/expressions/less_than.rb @@ -2,16 +2,9 @@ module Flipper module Expressions - class LessThan < Expression - def evaluate(context = {}) - return false unless args[0] && args[1] - - left = evaluate_arg(0, context) - right = evaluate_arg(1, context) - - return false unless left && right - - left < right + class LessThan < Comparable + def operator + :< end end end diff --git a/lib/flipper/expressions/less_than_or_equal_to.rb b/lib/flipper/expressions/less_than_or_equal_to.rb index 83685391a..73061c791 100644 --- a/lib/flipper/expressions/less_than_or_equal_to.rb +++ b/lib/flipper/expressions/less_than_or_equal_to.rb @@ -2,16 +2,9 @@ module Flipper module Expressions - class LessThanOrEqualTo < Expression - def evaluate(context = {}) - return false unless args[0] && args[1] - - left = evaluate_arg(0, context) - right = evaluate_arg(1, context) - - return false unless left && right - - left <= right + class LessThanOrEqualTo < Comparable + def operator + :<= end end end diff --git a/lib/flipper/expressions/not_equal.rb b/lib/flipper/expressions/not_equal.rb index 95e913d0c..b5fbb1b1d 100644 --- a/lib/flipper/expressions/not_equal.rb +++ b/lib/flipper/expressions/not_equal.rb @@ -2,14 +2,9 @@ module Flipper module Expressions - class NotEqual < Expression - def evaluate(context = {}) - return false unless args[0] && args[1] - - left = evaluate_arg(0, context) - right = evaluate_arg(1, context) - - left != right + class NotEqual < Comparable + def operator + :!= end end end diff --git a/lib/flipper/expressions/now.rb b/lib/flipper/expressions/now.rb index d110023f7..90786e4ec 100644 --- a/lib/flipper/expressions/now.rb +++ b/lib/flipper/expressions/now.rb @@ -3,11 +3,7 @@ module Flipper module Expressions class Now < Expression - def initialize(_ = nil) - super [] - end - - def evaluate(context = {}) + def call ::Time.now end end diff --git a/lib/flipper/expressions/number.rb b/lib/flipper/expressions/number.rb deleted file mode 100644 index a5e6e8b3a..000000000 --- a/lib/flipper/expressions/number.rb +++ /dev/null @@ -1,15 +0,0 @@ -require "flipper/expression" - -module Flipper - module Expressions - class Number < Expression - def initialize(args) - super Array(args) - end - - def evaluate(context = {}) - evaluate_arg(0, context).to_f - end - end - end -end diff --git a/lib/flipper/expressions/percentage.rb b/lib/flipper/expressions/percentage.rb index 71b1fe935..ea5b80a94 100644 --- a/lib/flipper/expressions/percentage.rb +++ b/lib/flipper/expressions/percentage.rb @@ -2,18 +2,9 @@ module Flipper module Expressions - class Percentage < Number - def initialize(args) - super Array(args) - end - - def evaluate(context = {}) - value = super - - value = 0 if value < 0 - value = 100 if value > 100 - - value + class Percentage < Expression + def call(value) + value.to_f.clamp(0, 100) end end end diff --git a/lib/flipper/expressions/percentage_of_actors.rb b/lib/flipper/expressions/percentage_of_actors.rb index c11727b15..9d47c5d55 100644 --- a/lib/flipper/expressions/percentage_of_actors.rb +++ b/lib/flipper/expressions/percentage_of_actors.rb @@ -5,14 +5,7 @@ module Expressions class PercentageOfActors < Expression SCALING_FACTOR = 1_000 - def evaluate(context = {}) - return false unless args[0] && args[1] - - text = evaluate_arg(0, context) - percentage = evaluate_arg(1, context) - - return false unless text && percentage - + def call(text, percentage, context: {}) prefix = context[:feature_name] || "" Zlib.crc32("#{prefix}#{text}") % (100 * SCALING_FACTOR) < percentage * SCALING_FACTOR end diff --git a/lib/flipper/expressions/property.rb b/lib/flipper/expressions/property.rb index f07652361..1f12c19c9 100644 --- a/lib/flipper/expressions/property.rb +++ b/lib/flipper/expressions/property.rb @@ -3,15 +3,9 @@ module Flipper module Expressions class Property < Expression - def initialize(args) - super Array(args) - end - - def evaluate(context = {}) - key = evaluate_arg(0, context) - + def call(key, context:) if properties = context[:properties] - properties[key] + properties[key.to_s] else nil end diff --git a/lib/flipper/expressions/random.rb b/lib/flipper/expressions/random.rb index 22182f35f..87acc569e 100644 --- a/lib/flipper/expressions/random.rb +++ b/lib/flipper/expressions/random.rb @@ -3,12 +3,8 @@ module Flipper module Expressions class Random < Expression - def initialize(args) - super Array(args) - end - - def evaluate(context = {}) - rand evaluate_arg(0, context) + def call(max = 0) + rand max end end end diff --git a/lib/flipper/expressions/string.rb b/lib/flipper/expressions/string.rb deleted file mode 100644 index 8f6abd864..000000000 --- a/lib/flipper/expressions/string.rb +++ /dev/null @@ -1,15 +0,0 @@ -require "flipper/expression" - -module Flipper - module Expressions - class String < Expression - def initialize(args) - super Array(args) - end - - def evaluate(context = {}) - evaluate_arg(0, context).to_s - end - end - end -end diff --git a/lib/flipper/expressions/time.rb b/lib/flipper/expressions/time.rb index 904a7fe44..2c92d17cb 100644 --- a/lib/flipper/expressions/time.rb +++ b/lib/flipper/expressions/time.rb @@ -3,12 +3,8 @@ module Flipper module Expressions class Time < Expression - def initialize(args) - super [args].flatten.map(&:to_s) - end - - def evaluate(context = {}) - ::Time.parse(evaluate_arg(0, context)) + def call(value) + ::Time.parse(value) end end end diff --git a/spec/flipper/adapters/read_only_spec.rb b/spec/flipper/adapters/read_only_spec.rb index df8fa5e2f..dcc054a50 100644 --- a/spec/flipper/adapters/read_only_spec.rb +++ b/spec/flipper/adapters/read_only_spec.rb @@ -58,7 +58,7 @@ expression: { "Equal" => [ {"Property" => ["plan"]}, - {"String" => ["basic"]}, + "basic", ] }, percentage_of_actors: '25', diff --git a/spec/flipper/expression_spec.rb b/spec/flipper/expression_spec.rb index 811562dc1..8f9762b9b 100644 --- a/spec/flipper/expression_spec.rb +++ b/spec/flipper/expression_spec.rb @@ -5,8 +5,8 @@ it "can build Equal" do expression = Flipper::Expression.build({ "Equal" => [ - {"String" => ["basic"]}, - {"String" => ["basic"]}, + "basic", + "basic", ] }) @@ -20,8 +20,8 @@ it "can build GreaterThanOrEqualTo" do expression = Flipper::Expression.build({ "GreaterThanOrEqualTo" => [ - {"Number" => [2]}, - {"Number" => [1]}, + 2, + 1, ] }) @@ -35,8 +35,8 @@ it "can build GreaterThan" do expression = Flipper::Expression.build({ "GreaterThan" => [ - {"Number" => [2]}, - {"Number" => [1]}, + 2, + 1, ] }) @@ -50,8 +50,8 @@ it "can build LessThanOrEqualTo" do expression = Flipper::Expression.build({ "LessThanOrEqualTo" => [ - {"Number" => [2]}, - {"Number" => [1]}, + 2, + 1, ] }) @@ -64,10 +64,7 @@ it "can build LessThan" do expression = Flipper::Expression.build({ - "LessThan" => [ - {"Number" => [2]}, - {"Number" => [1]}, - ] + "LessThan" => [2, 1] }) expect(expression).to be_instance_of(Flipper::Expressions::LessThan) @@ -80,8 +77,8 @@ it "can build NotEqual" do expression = Flipper::Expression.build({ "NotEqual" => [ - {"String" => ["basic"]}, - {"String" => ["plus"]}, + "basic", + "plus", ] }) @@ -93,12 +90,10 @@ end it "can build Number" do - expression = Flipper::Expression.build({ - "Number" => [1] - }) + expression = Flipper::Expression.build(1) - expect(expression).to be_instance_of(Flipper::Expressions::Number) - expect(expression.args).to eq([1]) + expect(expression).to be_instance_of(Flipper::Expressions::Constant) + expect(expression.value).to eq(1) end it "can build Percentage" do @@ -107,14 +102,14 @@ }) expect(expression).to be_instance_of(Flipper::Expressions::Percentage) - expect(expression.args).to eq([1]) + expect(expression.args).to eq([Flipper.number(1)]) end it "can build PercentageOfActors" do expression = Flipper::Expression.build({ "PercentageOfActors" => [ - {"String" => ["User;1"]}, - {"Number" => [40]}, + "User;1", + 40, ] }) @@ -126,11 +121,9 @@ end it "can build String" do - expression = Flipper::Expression.build({ - "String" => ["basic"] - }) + expression = Flipper::Expression.build("basic") - expect(expression).to be_instance_of(Flipper::Expressions::String) + expect(expression).to be_instance_of(Flipper::Expressions::Constant) expect(expression.args).to eq(["basic"]) end @@ -140,24 +133,22 @@ }) expect(expression).to be_instance_of(Flipper::Expressions::Property) - expect(expression.args).to eq(["flipper_id"]) + expect(expression.args).to eq([Flipper.string("flipper_id")]) end end describe "#initialize" do it "works with Array" do - expect(described_class.new([1]).args).to eq([1]) + expect(described_class.new([1]).args).to eq([Flipper.number(1)]) end - it "raises ArgumentError if not Array" do + it "casts single argument to array" do [ "asdf", 1, - {"foo" => "bar"}, + {"Property" => "bar"}, ].each do |value| - expect { - described_class.new(value) - }.to raise_error(ArgumentError, /args must be an Array but was #{value.inspect}/) + expect(described_class.new(value).args).to eq([Flipper::Expression.build(value)]) end end end diff --git a/spec/flipper/expressions/boolean_spec.rb b/spec/flipper/expressions/boolean_spec.rb deleted file mode 100644 index 6ef245f5d..000000000 --- a/spec/flipper/expressions/boolean_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -RSpec.describe Flipper::Expressions::Boolean do - describe "#initialize" do - it "works with TrueClass" do - expect(described_class.new(true).args).to eq([true]) - end - - it "works with FalseClass" do - expect(described_class.new(false).args).to eq([false]) - end - - it "works with array" do - expect(described_class.new([true]).args).to eq([true]) - end - end - - describe "#evaluate" do - it "returns a true" do - expression = described_class.new([true]) - result = expression.evaluate - expect(result).to be(true) - end - - it "returns a false" do - expression = described_class.new([false]) - result = expression.evaluate - expect(result).to be(false) - end - - it "returns true for String" do - expression = described_class.new([""]) - result = expression.evaluate - expect(result).to be(true) - end - - it "returns true for Numeric" do - expression = described_class.new([0]) - result = expression.evaluate - expect(result).to be(true) - end - end - - describe "#value" do - it "returns Hash" do - expression = described_class.new([true]) - expect(expression.value).to eq({"Boolean" => [true]}) - end - end -end diff --git a/spec/flipper/expressions/duration_spec.rb b/spec/flipper/expressions/duration_spec.rb index 66667c789..1f660770f 100644 --- a/spec/flipper/expressions/duration_spec.rb +++ b/spec/flipper/expressions/duration_spec.rb @@ -1,14 +1,4 @@ RSpec.describe Flipper::Expressions::Duration do - describe "#initialize" do - it 'initializes with scalar and unit' do - expect(described_class.new([10, 'second']).args).to eq([10, 'second']) - end - - it 'defaults unit to seconds' do - expect(described_class.new([1]).args).to eq([1, 'second']) - end - end - describe "#evaluate" do it "raises error with invalid value" do expect { described_class.new([false, 'minute']).evaluate }.to raise_error(ArgumentError) @@ -18,6 +8,10 @@ expect { described_class.new([4, 'score']).evaluate }.to raise_error(ArgumentError) end + it 'defaults unit to seconds' do + expect(described_class.new(10).evaluate).to eq(10) + end + it "evaluates seconds" do expect(described_class.new([10, 'seconds']).evaluate).to eq(10) end diff --git a/spec/flipper/expressions/equal_spec.rb b/spec/flipper/expressions/equal_spec.rb index eb886e4e9..c3d025c15 100644 --- a/spec/flipper/expressions/equal_spec.rb +++ b/spec/flipper/expressions/equal_spec.rb @@ -1,28 +1,4 @@ RSpec.describe Flipper::Expressions::Equal do - it "can be built" do - expression = described_class.build({ - "Equal" => [ - {"String" => ["basic"]}, - {"String" => ["basic"]}, - ] - }) - - expect(expression).to be_instance_of(Flipper::Expressions::Equal) - expect(expression.args).to eq([ - Flipper.string("basic"), - Flipper.string("basic"), - ]) - end - - it "can be built with primitives" do - expression = described_class.build({ - "Equal" => ["basic", "basic"], - }) - - expect(expression).to be_instance_of(Flipper::Expressions::Equal) - expect(expression.args).to eq(["basic", "basic"]) - end - describe "#evaluate" do it "returns true when equal" do expression = described_class.new([ @@ -48,10 +24,10 @@ it "works when nested" do expression = described_class.new([ - Flipper.boolean(true), - Flipper.all( + a = Flipper.boolean(true), + b = Flipper.all( Flipper.property(:stinky).eq(true), - Flipper.string("admin").eq(Flipper.property(:role)), + Flipper.string("admin").eq(Flipper.property(:role)) ), ]) @@ -59,6 +35,7 @@ "stinky" => true, "role" => "admin", } + expect(expression.evaluate(properties: properties)).to be(true) end @@ -84,23 +61,17 @@ expect(expression.evaluate(properties: properties)).to be(false) end - it "returns false when no args" do - expression = described_class.new([]) - expect(expression.evaluate).to be(false) + it "returns false when value evaluates to nil" do + expect(described_class.new([Flipper.number(nil), 1]).evaluate).to be(false) + expect(described_class.new([1, Flipper.number(nil)]).evaluate).to be(false) end - it "returns false when one arg" do - expression = described_class.new([Flipper.number(10)]) - expect(expression.evaluate).to be(false) + it "raises ArgumentError with no arguments" do + expect { described_class.new([]).evaluate }.to raise_error(ArgumentError) end - it "only evaluates first two arguments equality" do - expression = described_class.new([ - Flipper.number(20), - Flipper.number(20), - Flipper.number(30), - ]) - expect(expression.evaluate).to be(true) + it "raises ArgumentError with one argument" do + expect { described_class.new([10]).evaluate }.to raise_error(ArgumentError) end end @@ -114,7 +85,7 @@ expect(expression.value).to eq({ "Equal" => [ {"Property" => ["plan"]}, - {"String" => ["basic"]}, + "basic", ], }) end diff --git a/spec/flipper/expressions/greater_than_or_equal_to_spec.rb b/spec/flipper/expressions/greater_than_or_equal_to_spec.rb index 1d87eacc7..087c24fc4 100644 --- a/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +++ b/spec/flipper/expressions/greater_than_or_equal_to_spec.rb @@ -24,14 +24,17 @@ expect(expression.evaluate).to be(false) end - it "returns false with no arguments" do - expression = described_class.new([]) - expect(expression.evaluate).to be(false) + it "returns false when value evaluates to nil" do + expect(described_class.new([Flipper.number(nil), 1]).evaluate).to be(false) + expect(described_class.new([1, Flipper.number(nil)]).evaluate).to be(false) end - it "returns false with one argument" do - expression = described_class.new([10]) - expect(expression.evaluate).to be(false) + it "raises ArgumentError with no arguments" do + expect { described_class.new([]).evaluate }.to raise_error(ArgumentError) + end + + it "raises ArgumentError with one argument" do + expect { described_class.new([10]).evaluate }.to raise_error(ArgumentError) end end @@ -43,10 +46,7 @@ ]) expect(expression.value).to eq({ - "GreaterThanOrEqualTo" => [ - {"Number" => [20]}, - {"Number" => [10]}, - ], + "GreaterThanOrEqualTo" => [20, 10] }) end end diff --git a/spec/flipper/expressions/greater_than_spec.rb b/spec/flipper/expressions/greater_than_spec.rb index 3cf04da76..94f4a38fb 100644 --- a/spec/flipper/expressions/greater_than_spec.rb +++ b/spec/flipper/expressions/greater_than_spec.rb @@ -23,16 +23,17 @@ expect(expression.evaluate).to be(false) end - it "returns false with no arguments" do - expression = described_class.new([]) - expect(expression.evaluate).to be(false) + it "returns false when value evaluates to nil" do + expect(described_class.new([Flipper.number(nil), 1]).evaluate).to be(false) + expect(described_class.new([1, Flipper.number(nil)]).evaluate).to be(false) end - it "returns false with one argument" do - expression = described_class.new([ - Flipper.number(10), - ]) - expect(expression.evaluate).to be(false) + it "raises ArgumentError with no arguments" do + expect { described_class.new([]).evaluate }.to raise_error(ArgumentError) + end + + it "raises ArgumentError with one argument" do + expect { described_class.new([10]).evaluate }.to raise_error(ArgumentError) end end @@ -44,10 +45,7 @@ ]) expect(expression.value).to eq({ - "GreaterThan" => [ - {"Number" => [20]}, - {"Number" => [10]}, - ], + "GreaterThan" => [20, 10] }) end end diff --git a/spec/flipper/expressions/less_than_or_equal_to_spec.rb b/spec/flipper/expressions/less_than_or_equal_to_spec.rb index 53adbcae7..588f1f0f4 100644 --- a/spec/flipper/expressions/less_than_or_equal_to_spec.rb +++ b/spec/flipper/expressions/less_than_or_equal_to_spec.rb @@ -26,14 +26,17 @@ expect(expression.evaluate).to be(false) end - it "returns false with no arguments" do - expression = described_class.new([]) - expect(expression.evaluate).to be(false) + it "returns false when value evaluates to nil" do + expect(described_class.new([Flipper.number(nil), 1]).evaluate).to be(false) + expect(described_class.new([1, Flipper.number(nil)]).evaluate).to be(false) end - it "returns false with one argument" do - expression = described_class.new([10]) - expect(expression.evaluate).to be(false) + it "raises ArgumentError with no arguments" do + expect { described_class.new([]).evaluate }.to raise_error(ArgumentError) + end + + it "raises ArgumentError with one argument" do + expect { described_class.new([10]).evaluate }.to raise_error(ArgumentError) end end @@ -45,10 +48,7 @@ ]) expect(expression.value).to eq({ - "LessThanOrEqualTo" => [ - {"Number" => [20]}, - {"Number" => [10]}, - ], + "LessThanOrEqualTo" => [20, 10], }) end end diff --git a/spec/flipper/expressions/less_than_spec.rb b/spec/flipper/expressions/less_than_spec.rb index 71da1678c..7a0348bf9 100644 --- a/spec/flipper/expressions/less_than_spec.rb +++ b/spec/flipper/expressions/less_than_spec.rb @@ -20,14 +20,17 @@ expect(expression.evaluate).to be(false) end - it "returns false with no arguments" do - expression = described_class.new([]) - expect(expression.evaluate).to be(false) + it "returns false when value evaluates to nil" do + expect(described_class.new([Flipper.number(nil), 1]).evaluate).to be(false) + expect(described_class.new([1, Flipper.number(nil)]).evaluate).to be(false) end - it "returns false with one argument" do - expression = described_class.new([10]) - expect(expression.evaluate).to be(false) + it "raises ArgumentError with no arguments" do + expect { described_class.new([]).evaluate }.to raise_error(ArgumentError) + end + + it "raises ArgumentError with one argument" do + expect { described_class.new([10]).evaluate }.to raise_error(ArgumentError) end end @@ -39,10 +42,7 @@ ]) expect(expression.value).to eq({ - "LessThan" => [ - {"Number" => [20]}, - {"Number" => [10]}, - ], + "LessThan" => [20, 10] }) end end diff --git a/spec/flipper/expressions/not_equal_spec.rb b/spec/flipper/expressions/not_equal_spec.rb index df59511ef..4ec035b1a 100644 --- a/spec/flipper/expressions/not_equal_spec.rb +++ b/spec/flipper/expressions/not_equal_spec.rb @@ -36,9 +36,8 @@ expect(expression.evaluate(properties: properties)).to be(false) end - it "only evaluates first two arguments equality" do - expression = described_class.new([20, 10, 20]) - expect(expression.evaluate).to be(true) + it "raises ArgumentError for more arguments" do + expect { described_class.new([20, 10, 20]).evaluate }.to raise_error(ArgumentError) end end @@ -50,10 +49,7 @@ ]) expect(expression.value).to eq({ - "NotEqual" => [ - {"Number" => [20]}, - {"Number" => [10]}, - ], + "NotEqual" => [20, 10] }) end end diff --git a/spec/flipper/expressions/now_spec.rb b/spec/flipper/expressions/now_spec.rb index d234e8709..a625544b8 100644 --- a/spec/flipper/expressions/now_spec.rb +++ b/spec/flipper/expressions/now_spec.rb @@ -1,10 +1,4 @@ RSpec.describe Flipper::Expressions::Now do - describe "#initialize" do - it "ignores arguments" do - expect(described_class.new("foo").args).to eq([]) - end - end - describe "#evaluate" do it "returns current time" do expect(described_class.new.evaluate.round).to eq(Time.now.round) diff --git a/spec/flipper/expressions/number_spec.rb b/spec/flipper/expressions/number_spec.rb deleted file mode 100644 index 434520a75..000000000 --- a/spec/flipper/expressions/number_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -RSpec.describe Flipper::Expressions::Number do - describe "#initialize" do - it "works with number" do - expect(described_class.new(1).args).to eq([1]) - end - - it "works with array" do - expect(described_class.new([1]).args).to eq([1]) - end - end - - describe "#evaluate" do - it "returns Numeric" do - expression = described_class.new([10]) - result = expression.evaluate - expect(result).to be(10.0) - end - - it "returns Numeric for String" do - expression = described_class.new(['10']) - result = expression.evaluate - expect(result).to be(10.0) - end - end - - describe "#value" do - it "returns Hash" do - expression = described_class.new([99]) - expect(expression.value).to eq({"Number" => [99]}) - end - end -end diff --git a/spec/flipper/expressions/percentage_of_actors_spec.rb b/spec/flipper/expressions/percentage_of_actors_spec.rb index d91d966ef..8392b275f 100644 --- a/spec/flipper/expressions/percentage_of_actors_spec.rb +++ b/spec/flipper/expressions/percentage_of_actors_spec.rb @@ -59,8 +59,8 @@ expect(expression.value).to eq({ "PercentageOfActors" => [ - {"String" => ["User;1"]}, - {"Number" => [10]}, + "User;1", + 10 ], }) end diff --git a/spec/flipper/expressions/percentage_spec.rb b/spec/flipper/expressions/percentage_spec.rb index 8aafde9b7..e03e8a90c 100644 --- a/spec/flipper/expressions/percentage_spec.rb +++ b/spec/flipper/expressions/percentage_spec.rb @@ -1,31 +1,15 @@ RSpec.describe Flipper::Expressions::Percentage do - describe "#initialize" do - it "works with number" do - expect(described_class.new(1).args).to eq([1]) - end - - it "works with array" do - expect(described_class.new([1]).args).to eq([1]) - end - end - describe "#evaluate" do it "returns numeric" do - expression = described_class.new([10]) - result = expression.evaluate - expect(result).to be(10.0) + expect(described_class.new(10).evaluate).to be(10.0) end it "returns 0 if less than 0" do - expression = described_class.new([-1]) - result = expression.evaluate - expect(result).to be(0) + expect(described_class.new(-1).evaluate).to be(0) end it "returns 100 if greater than 100" do - expression = described_class.new([101]) - result = expression.evaluate - expect(result).to be(100) + expect(described_class.new(101).evaluate).to be(100) end end diff --git a/spec/flipper/expressions/property_spec.rb b/spec/flipper/expressions/property_spec.rb index c97b19a47..3c8699343 100644 --- a/spec/flipper/expressions/property_spec.rb +++ b/spec/flipper/expressions/property_spec.rb @@ -1,20 +1,4 @@ RSpec.describe Flipper::Expressions::Property do - it "can initialize with string" do - expect(described_class.new("flipper_id").args).to eq(["flipper_id"]) - end - - it "can initialize with symbol" do - expect(described_class.new(:flipper_id).args).to eq(["flipper_id"]) - end - - it "can initialize with array of string" do - expect(described_class.new(["flipper_id"]).args).to eq(["flipper_id"]) - end - - it "can initialize with array of symbol" do - expect(described_class.new([:flipper_id]).args).to eq(["flipper_id"]) - end - describe "#evaluate" do it "returns value for property key" do expression = described_class.new("flipper_id") diff --git a/spec/flipper/expressions/random_spec.rb b/spec/flipper/expressions/random_spec.rb index 94b24c927..30a7b557e 100644 --- a/spec/flipper/expressions/random_spec.rb +++ b/spec/flipper/expressions/random_spec.rb @@ -1,37 +1,21 @@ RSpec.describe Flipper::Expressions::Random do - describe "#initialize" do - it "works with number" do - expect(described_class.new(1).args).to eq([1]) - end - - it "works with array" do - expect(described_class.new([1]).args).to eq([1]) - end - end - describe "#evaluate" do - it "returns random number based on seed" do - expression = described_class.new([10]) - result = expression.evaluate - expect(result).to be >= 0 - expect(result).to be <= 10 + it "returns random number based on max" do + 100.times do + expect(described_class.new(10).evaluate).to be_between(0, 10) + end end - it "returns random number based on seed that is Value" do - expression = described_class.new([Flipper.number(10)]) - result = expression.evaluate - expect(result).to be >= 0 - expect(result).to be <= 10 + it "returns random number based on max that is Value" do + 100.times do + expect(described_class.new([Flipper.number(10)]).evaluate).to be_between(0, 10) + end end end describe "#value" do it "returns Hash" do - expression = described_class.new([100]) - - expect(expression.value).to eq({ - "Random" => [100], - }) + expect(described_class.new(100).value).to eq({ "Random" => [100] }) end end end diff --git a/spec/flipper/expressions/string_spec.rb b/spec/flipper/expressions/string_spec.rb deleted file mode 100644 index bc5610b0e..000000000 --- a/spec/flipper/expressions/string_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -RSpec.describe Flipper::Expressions::String do - describe "#initialize" do - it "works with string" do - expect(described_class.new("test").args).to eq(["test"]) - end - - it "works with array" do - expect(described_class.new(["test"]).args).to eq(["test"]) - end - end - - describe "#evaluate" do - it "returns String for Numeric" do - expression = described_class.new([10]) - result = expression.evaluate - expect(result).to eq("10") - end - - it "returns String" do - expression = described_class.new(["test"]) - result = expression.evaluate - expect(result).to eq("test") - end - end - - describe "#value" do - it "returns Hash" do - expression = described_class.new(["test"]) - expect(expression.value).to eq({"String" => ["test"]}) - end - end -end diff --git a/spec/flipper/expressions/time_spec.rb b/spec/flipper/expressions/time_spec.rb index 8c5bf144d..2c4999561 100644 --- a/spec/flipper/expressions/time_spec.rb +++ b/spec/flipper/expressions/time_spec.rb @@ -1,20 +1,6 @@ RSpec.describe Flipper::Expressions::Time do let(:time) { Time.now.round } - describe "#initialize" do - it "works with time" do - expect(described_class.new(time).args).to eq([time.to_s]) - end - - it "works with Time#to_s" do - expect(described_class.new(time.to_s).args).to eq([time.to_s]) - end - - it "works with array" do - expect(described_class.new([time]).args).to eq([time.to_s]) - end - end - describe "#evaluate" do it "returns time for #to_s format" do expect(described_class.new(time.to_s).evaluate).to eq(time) diff --git a/spec/flipper_spec.rb b/spec/flipper_spec.rb index 7903d246c..43c18833c 100644 --- a/spec/flipper_spec.rb +++ b/spec/flipper_spec.rb @@ -364,9 +364,9 @@ end describe ".boolean" do - it "returns Flipper::Expressions::Boolean instance" do - expect(described_class.boolean(true)).to eq(Flipper::Expressions::Boolean.new(true)) - expect(described_class.boolean(false)).to eq(Flipper::Expressions::Boolean.new(false)) + it "returns Flipper::Expressions::Constant instance" do + expect(described_class.boolean(true)).to eq(Flipper::Expressions::Constant.new(true)) + expect(described_class.boolean(false)).to eq(Flipper::Expressions::Constant.new(false)) end end From c35a4880a5385bf58c42beada8f4118e298b03bf Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Mon, 27 Mar 2023 05:40:25 -0400 Subject: [PATCH 157/176] Remove unused context arg --- lib/flipper/expressions/duration.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/flipper/expressions/duration.rb b/lib/flipper/expressions/duration.rb index c1e2d0b2b..672fa3a13 100644 --- a/lib/flipper/expressions/duration.rb +++ b/lib/flipper/expressions/duration.rb @@ -13,7 +13,7 @@ class Duration < Expression "year" => 31_556_952 # length of a gregorian year (365.2425 days) }.freeze - def call(scalar, unit = 'second', context = {}) + def call(scalar, unit = 'second') unit = unit.to_s.downcase.chomp("s") unless scalar.is_a?(Numeric) From 6929351e2b30bd3186cca2874712ec0948ab21de Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Mon, 27 Mar 2023 05:43:01 -0400 Subject: [PATCH 158/176] Remove unused #evaluate_arg --- lib/flipper/expression.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/flipper/expression.rb b/lib/flipper/expression.rb index c9ed5d641..9f9d27703 100644 --- a/lib/flipper/expression.rb +++ b/lib/flipper/expression.rb @@ -102,10 +102,6 @@ def percentage_of_actors(object) private - def evaluate_arg(index, context = {}) - object = args[index].evaluate(context) - end - def call_with_context? method(:call).parameters.any? do |type, name| name == :context && [:key, :keyreq].include?(type) From bb9d342763ca77ce4008d477ccadce805b093d63 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Mon, 27 Mar 2023 11:09:54 -0400 Subject: [PATCH 159/176] Refactor Expressions to be simple proc with no base class --- lib/flipper.rb | 16 +- lib/flipper/expression.rb | 97 ++----- lib/flipper/expression/builder.rb | 73 ++++++ lib/flipper/expression/constant.rb | 26 ++ lib/flipper/expressions/all.rb | 18 +- lib/flipper/expressions/any.rb | 18 +- lib/flipper/expressions/boolean.rb | 9 + lib/flipper/expressions/comparable.rb | 6 +- lib/flipper/expressions/constant.rb | 19 -- lib/flipper/expressions/duration.rb | 4 +- lib/flipper/expressions/equal.rb | 2 +- lib/flipper/expressions/greater_than.rb | 2 +- .../expressions/greater_than_or_equal_to.rb | 2 +- lib/flipper/expressions/less_than.rb | 2 +- .../expressions/less_than_or_equal_to.rb | 2 +- lib/flipper/expressions/not_equal.rb | 2 +- lib/flipper/expressions/now.rb | 4 +- lib/flipper/expressions/number.rb | 10 + lib/flipper/expressions/percentage.rb | 4 +- .../expressions/percentage_of_actors.rb | 4 +- lib/flipper/expressions/property.rb | 10 +- lib/flipper/expressions/random.rb | 4 +- lib/flipper/expressions/time.rb | 4 +- spec/flipper/expression/builder_spec.rb | 248 ++++++++++++++++++ spec/flipper/expression_spec.rb | 181 +++---------- spec/flipper/expressions/all_spec.rb | 98 +------ spec/flipper/expressions/any_spec.rb | 99 +------ spec/flipper/expressions/duration_spec.rb | 22 +- spec/flipper/expressions/equal_spec.rb | 81 +----- .../greater_than_or_equal_to_spec.rb | 41 +-- spec/flipper/expressions/greater_than_spec.rb | 38 +-- .../expressions/less_than_or_equal_to_spec.rb | 43 +-- spec/flipper/expressions/less_than_spec.rb | 35 +-- spec/flipper/expressions/not_equal_spec.rb | 49 +--- spec/flipper/expressions/now_spec.rb | 4 +- .../expressions/percentage_of_actors_spec.rb | 58 +--- spec/flipper/expressions/percentage_spec.rb | 16 +- spec/flipper/expressions/property_spec.rb | 34 +-- spec/flipper/expressions/random_spec.rb | 16 +- spec/flipper/expressions/time_spec.rb | 6 +- spec/flipper/feature_spec.rb | 24 +- spec/flipper/gates/expression_spec.rb | 5 +- spec/flipper_spec.rb | 14 +- 43 files changed, 577 insertions(+), 873 deletions(-) create mode 100644 lib/flipper/expression/builder.rb create mode 100644 lib/flipper/expression/constant.rb create mode 100644 lib/flipper/expressions/boolean.rb delete mode 100644 lib/flipper/expressions/constant.rb create mode 100644 lib/flipper/expressions/number.rb create mode 100644 spec/flipper/expression/builder_spec.rb diff --git a/lib/flipper.rb b/lib/flipper.rb index 23aa70acd..283623108 100644 --- a/lib/flipper.rb +++ b/lib/flipper.rb @@ -69,31 +69,31 @@ def instance=(flipper) :sync, :sync_secret # For Flipper::Cloud. Will error for OSS Flipper. def any(*args) - Flipper::Expressions::Any.new(args) + Expression.build({ Any: args.flatten }) end def all(*args) - Flipper::Expressions::All.new(args) + Expression.build({ All: args.flatten }) end def property(name) - Flipper::Expressions::Property.new(name) + Expression.build({ Property: name }) end def string(value) - Flipper::Expressions::Constant.new(value) + Expression.build(value) end def number(value) - Flipper::Expressions::Constant.new(value) + Expression.build(value) end def boolean(value) - Flipper::Expressions::Constant.new(value) + Expression.build(value) end - def random(name) - Flipper::Expressions::Random.new(name) + def random(max) + Expression.build({ Random: max }) end # Public: Use this to register a group by name. diff --git a/lib/flipper/expression.rb b/lib/flipper/expression.rb index 9f9d27703..0aa597148 100644 --- a/lib/flipper/expression.rb +++ b/lib/flipper/expression.rb @@ -1,109 +1,62 @@ +require "flipper/expression/builder" +require "flipper/expression/constant" + module Flipper class Expression + include Builder + def self.build(object) - return object if object.is_a?(Flipper::Expression) + return object if object.is_a?(self) || object.is_a?(Constant) case object - when Array - object.map { |o| build(o) } when Hash - type = object.keys.first + name = object.keys.first args = object.values.first - unless type + unless name raise ArgumentError, "#{object.inspect} cannot be converted into an expression" end - Expressions.const_get(type).new(args) + + new(name, Array(args).map { |o| build(o) }) when String, Numeric, FalseClass, TrueClass - Expressions::Constant.new(object) + Expression::Constant.new(object) when Symbol - Expressions::Constant.new(object.to_s) + Expression::Constant.new(object.to_s) else raise ArgumentError, "#{object.inspect} cannot be converted into an expression" end end - attr_reader :args + # Use #build + private_class_method :new + + attr_reader :name, :function, :args - def initialize(args = []) - args = [args] unless args.is_a?(Array) - @args = self.class.build(args) + def initialize(name, args = []) + @name = name.to_s + @function = Expressions.const_get(name) + @args = args end def evaluate(context = {}) - if call_with_context? - call(*args.map { |arg| arg.evaluate(context) }, context: context) - else - call(*args.map { |arg| arg.evaluate(context) }) - end + kwargs = { context: (context if call_with_context?) }.compact + function.call(*args.map {|arg| arg.evaluate(context) }, **kwargs) end def eql?(other) - self.class.eql?(other.class) && @args == other.args + other.is_a?(self.class) && @function == other.function && @args == other.args end alias_method :==, :eql? def value { - self.class.name.split("::").last => args.map(&:value) + name => args.map(&:value) } end - def add(*expressions) - any.add(*expressions) - end - - def remove(*expressions) - any.remove(*expressions) - end - - def any - Expressions::Any.new([self]) - end - - def all - Expressions::All.new([self]) - end - - def equal(object) - Expressions::Equal.new([self, self.class.build(object)]) - end - alias eq equal - - def not_equal(object) - Expressions::NotEqual.new([self, self.class.build(object)]) - end - alias neq not_equal - - def greater_than(object) - Expressions::GreaterThan.new([self, self.class.build(object)]) - end - alias gt greater_than - - def greater_than_or_equal_to(object) - Expressions::GreaterThanOrEqualTo.new([self, self.class.build(object)]) - end - alias gte greater_than_or_equal_to - alias greater_than_or_equal greater_than_or_equal_to - - def less_than(object) - Expressions::LessThan.new([self, self.class.build(object)]) - end - alias lt less_than - - def less_than_or_equal_to(object) - Expressions::LessThanOrEqualTo.new([self, self.class.build(object)]) - end - alias lte less_than_or_equal_to - alias less_than_or_equal less_than_or_equal_to - - def percentage_of_actors(object) - Expressions::PercentageOfActors.new([self, self.class.build(object)]) - end - private def call_with_context? - method(:call).parameters.any? do |type, name| + function.method(:call).parameters.any? do |type, name| name == :context && [:key, :keyreq].include?(type) end end diff --git a/lib/flipper/expression/builder.rb b/lib/flipper/expression/builder.rb new file mode 100644 index 000000000..fd177db0c --- /dev/null +++ b/lib/flipper/expression/builder.rb @@ -0,0 +1,73 @@ +module Flipper + class Expression + module Builder + def build(object) + Expression.build(object) + end + + def add(*expressions) + group? ? build(name => args + expressions.flatten) : any.add(*expressions) + end + + def remove(*expressions) + group? ? build(name => args - expressions.flatten) : any.remove(*expressions) + end + + def any + any? ? self : Expression.build({ Any: [self] }) + end + + def all + all? ? self : Expression.build({ All: [self] }) + end + + def equal(object) + Expression.build({ Equal: [self, object] }) + end + alias eq equal + + def not_equal(object) + Expression.build({ NotEqual: [self, object] }) + end + alias neq not_equal + + def greater_than(object) + Expression.build({ GreaterThan: [self, object] }) + end + alias gt greater_than + + def greater_than_or_equal_to(object) + Expression.build({ GreaterThanOrEqualTo: [self, object] }) + end + alias gte greater_than_or_equal_to + alias greater_than_or_equal greater_than_or_equal_to + + def less_than(object) + Expression.build({ LessThan: [self, object] }) + end + alias lt less_than + + def less_than_or_equal_to(object) + Expression.build({ LessThanOrEqualTo: [self, object] }) + end + alias lte less_than_or_equal_to + alias less_than_or_equal less_than_or_equal_to + + def percentage_of_actors(object) + Expression.build({ PercentageOfActors: [self, Expression.build(object)] }) + end + + def any? + is_a?(Expression) && function == Expressions::Any + end + + def all? + is_a?(Expression) && function == Expressions::All + end + + def group? + any? || all? + end + end + end +end diff --git a/lib/flipper/expression/constant.rb b/lib/flipper/expression/constant.rb new file mode 100644 index 000000000..1ff4467ab --- /dev/null +++ b/lib/flipper/expression/constant.rb @@ -0,0 +1,26 @@ +module Flipper + class Expression + # Public: A constant value like a "string", Number (1, 3.5), Boolean (true, false). + # + # Implements the same interface as Expression + class Constant + include Expression::Builder + + attr_reader :value + + # Override initialize to avoid trying to build args + def initialize(value) + @value = value + end + + def evaluate(context = {}) + value + end + + def eql?(other) + other.is_a?(self.class) && other.value == value + end + alias_method :==, :eql? + end + end +end diff --git a/lib/flipper/expressions/all.rb b/lib/flipper/expressions/all.rb index a32c6dc8c..aa11f5202 100644 --- a/lib/flipper/expressions/all.rb +++ b/lib/flipper/expressions/all.rb @@ -2,21 +2,9 @@ module Flipper module Expressions - class All < Expression - def evaluate(context = {}) - args.all? { |arg| arg.evaluate(context) == true } - end - - def all - self - end - - def add(*expressions) - self.class.new(args + expressions.flatten) - end - - def remove(*expressions) - self.class.new(args - expressions.flatten) + class All + def self.call(*args) + args.all? end end end diff --git a/lib/flipper/expressions/any.rb b/lib/flipper/expressions/any.rb index b5fdcfb0b..71cd6d6ed 100644 --- a/lib/flipper/expressions/any.rb +++ b/lib/flipper/expressions/any.rb @@ -2,21 +2,9 @@ module Flipper module Expressions - class Any < Expression - def evaluate(context = {}) - args.any? { |arg| arg.evaluate(context) == true } - end - - def any - self - end - - def add(*expressions) - self.class.new(args + expressions.flatten) - end - - def remove(*expressions) - self.class.new(args - expressions.flatten) + class Any + def self.call(*args) + args.any? end end end diff --git a/lib/flipper/expressions/boolean.rb b/lib/flipper/expressions/boolean.rb new file mode 100644 index 000000000..59ad1142e --- /dev/null +++ b/lib/flipper/expressions/boolean.rb @@ -0,0 +1,9 @@ +module Flipper + module Expressions + class Boolean + def self.call(value) + Flipper::Typecast.to_boolean(value) + end + end + end +end diff --git a/lib/flipper/expressions/comparable.rb b/lib/flipper/expressions/comparable.rb index 444530b03..4620b66f4 100644 --- a/lib/flipper/expressions/comparable.rb +++ b/lib/flipper/expressions/comparable.rb @@ -2,12 +2,12 @@ module Flipper module Expressions - class Comparable < Expression - def operator + class Comparable + def self.operator raise NotImplementedError end - def call(left, right) + def self.call(left, right) left.respond_to?(operator) && right.respond_to?(operator) && left.public_send(operator, right) end end diff --git a/lib/flipper/expressions/constant.rb b/lib/flipper/expressions/constant.rb deleted file mode 100644 index d0c92b024..000000000 --- a/lib/flipper/expressions/constant.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Flipper - module Expressions - # Public: Represents a constant value. - class Constant < Expression - # Override initialize to avoid trying to build args - def initialize(value) - @args = Array(value) - end - - def evaluate(context = {}) - args[0] - end - - def value - args[0] - end - end - end -end diff --git a/lib/flipper/expressions/duration.rb b/lib/flipper/expressions/duration.rb index 672fa3a13..7e2207b44 100644 --- a/lib/flipper/expressions/duration.rb +++ b/lib/flipper/expressions/duration.rb @@ -2,7 +2,7 @@ module Flipper module Expressions - class Duration < Expression + class Duration SECONDS_PER = { "second" => 1, "minute" => 60, @@ -13,7 +13,7 @@ class Duration < Expression "year" => 31_556_952 # length of a gregorian year (365.2425 days) }.freeze - def call(scalar, unit = 'second') + def self.call(scalar, unit = 'second') unit = unit.to_s.downcase.chomp("s") unless scalar.is_a?(Numeric) diff --git a/lib/flipper/expressions/equal.rb b/lib/flipper/expressions/equal.rb index 3601079b6..1f8015fc4 100644 --- a/lib/flipper/expressions/equal.rb +++ b/lib/flipper/expressions/equal.rb @@ -3,7 +3,7 @@ module Flipper module Expressions class Equal < Comparable - def operator + def self.operator :== end end diff --git a/lib/flipper/expressions/greater_than.rb b/lib/flipper/expressions/greater_than.rb index ca964efdc..3a61d68dd 100644 --- a/lib/flipper/expressions/greater_than.rb +++ b/lib/flipper/expressions/greater_than.rb @@ -3,7 +3,7 @@ module Flipper module Expressions class GreaterThan < Comparable - def operator + def self.operator :> end end diff --git a/lib/flipper/expressions/greater_than_or_equal_to.rb b/lib/flipper/expressions/greater_than_or_equal_to.rb index cd91825c0..e5d400181 100644 --- a/lib/flipper/expressions/greater_than_or_equal_to.rb +++ b/lib/flipper/expressions/greater_than_or_equal_to.rb @@ -3,7 +3,7 @@ module Flipper module Expressions class GreaterThanOrEqualTo < Comparable - def operator + def self.operator :>= end end diff --git a/lib/flipper/expressions/less_than.rb b/lib/flipper/expressions/less_than.rb index 1b7f16d31..994f28165 100644 --- a/lib/flipper/expressions/less_than.rb +++ b/lib/flipper/expressions/less_than.rb @@ -3,7 +3,7 @@ module Flipper module Expressions class LessThan < Comparable - def operator + def self.operator :< end end diff --git a/lib/flipper/expressions/less_than_or_equal_to.rb b/lib/flipper/expressions/less_than_or_equal_to.rb index 73061c791..162a031f6 100644 --- a/lib/flipper/expressions/less_than_or_equal_to.rb +++ b/lib/flipper/expressions/less_than_or_equal_to.rb @@ -3,7 +3,7 @@ module Flipper module Expressions class LessThanOrEqualTo < Comparable - def operator + def self.operator :<= end end diff --git a/lib/flipper/expressions/not_equal.rb b/lib/flipper/expressions/not_equal.rb index b5fbb1b1d..f46061f87 100644 --- a/lib/flipper/expressions/not_equal.rb +++ b/lib/flipper/expressions/not_equal.rb @@ -3,7 +3,7 @@ module Flipper module Expressions class NotEqual < Comparable - def operator + def self.operator :!= end end diff --git a/lib/flipper/expressions/now.rb b/lib/flipper/expressions/now.rb index 90786e4ec..b083fa66e 100644 --- a/lib/flipper/expressions/now.rb +++ b/lib/flipper/expressions/now.rb @@ -2,8 +2,8 @@ module Flipper module Expressions - class Now < Expression - def call + class Now + def self.call ::Time.now end end diff --git a/lib/flipper/expressions/number.rb b/lib/flipper/expressions/number.rb new file mode 100644 index 000000000..98d133ada --- /dev/null +++ b/lib/flipper/expressions/number.rb @@ -0,0 +1,10 @@ +module Flipper + module Expressions + class Number + def self.call(value) + # FIXME: rename to_percentage to to_number, but it does what we want + Flipper::Typecast.to_percentage(value) + end + end + end +end diff --git a/lib/flipper/expressions/percentage.rb b/lib/flipper/expressions/percentage.rb index ea5b80a94..e6bc8cab9 100644 --- a/lib/flipper/expressions/percentage.rb +++ b/lib/flipper/expressions/percentage.rb @@ -2,8 +2,8 @@ module Flipper module Expressions - class Percentage < Expression - def call(value) + class Percentage + def self.call(value) value.to_f.clamp(0, 100) end end diff --git a/lib/flipper/expressions/percentage_of_actors.rb b/lib/flipper/expressions/percentage_of_actors.rb index 9d47c5d55..669cb99b0 100644 --- a/lib/flipper/expressions/percentage_of_actors.rb +++ b/lib/flipper/expressions/percentage_of_actors.rb @@ -2,10 +2,10 @@ module Flipper module Expressions - class PercentageOfActors < Expression + class PercentageOfActors SCALING_FACTOR = 1_000 - def call(text, percentage, context: {}) + def self.call(text, percentage, context: {}) prefix = context[:feature_name] || "" Zlib.crc32("#{prefix}#{text}") % (100 * SCALING_FACTOR) < percentage * SCALING_FACTOR end diff --git a/lib/flipper/expressions/property.rb b/lib/flipper/expressions/property.rb index 1f12c19c9..f467add01 100644 --- a/lib/flipper/expressions/property.rb +++ b/lib/flipper/expressions/property.rb @@ -2,13 +2,9 @@ module Flipper module Expressions - class Property < Expression - def call(key, context:) - if properties = context[:properties] - properties[key.to_s] - else - nil - end + class Property + def self.call(key, context:) + context.dig(:properties, key.to_s) end end end diff --git a/lib/flipper/expressions/random.rb b/lib/flipper/expressions/random.rb index 87acc569e..4b8bcd1b9 100644 --- a/lib/flipper/expressions/random.rb +++ b/lib/flipper/expressions/random.rb @@ -2,8 +2,8 @@ module Flipper module Expressions - class Random < Expression - def call(max = 0) + class Random + def self.call(max = 0) rand max end end diff --git a/lib/flipper/expressions/time.rb b/lib/flipper/expressions/time.rb index 2c92d17cb..5854e38c5 100644 --- a/lib/flipper/expressions/time.rb +++ b/lib/flipper/expressions/time.rb @@ -2,8 +2,8 @@ module Flipper module Expressions - class Time < Expression - def call(value) + class Time + def self.call(value) ::Time.parse(value) end end diff --git a/spec/flipper/expression/builder_spec.rb b/spec/flipper/expression/builder_spec.rb new file mode 100644 index 000000000..450896445 --- /dev/null +++ b/spec/flipper/expression/builder_spec.rb @@ -0,0 +1,248 @@ +RSpec.describe Flipper::Expression::Builder do + def build(object) + Flipper::Expression.build(object) + end + + describe "#add" do + it "converts to Any and adds new expressions" do + expression = build("something") + first = Flipper.boolean(true).eq(true) + second = Flipper.boolean(false).eq(false) + new_expression = expression.add(first, second) + expect(new_expression).to eq(build({ Any: ["something", first, second] })) + end + end + + describe "#remove" do + it "converts to Any and removes any expressions that match" do + expression = build("something") + first = Flipper.boolean(true).eq(true) + second = Flipper.boolean(false).eq(false) + new_expression = expression.remove(build("something"), first, second) + expect(new_expression).to eq(build(Any: [])) + end + end + + it "can convert to Any" do + expression = build("something") + converted = expression.any + expect(converted).to be_instance_of(Flipper::Expression) + expect(converted.function).to be(Flipper::Expressions::Any) + expect(converted.args).to eq([expression]) + end + + it "can convert to All" do + expression = build("something") + converted = expression.all + expect(converted).to eq(build(All: ["something"])) + end + + context "Any" do + describe "#any" do + it "returns self" do + expression = build(Any: [ + Flipper.boolean(true), + Flipper.string("yep").eq("yep"), + ]) + expect(expression.any).to be(expression) + end + end + + describe "#add" do + it "returns new instance with expression added" do + expression = Flipper.boolean(true) + other = Flipper.string("yep").eq("yep") + + result = expression.add(other) + expect(result.args).to eq([ + Flipper.boolean(true), + Flipper.string("yep").eq("yep"), + ]) + end + + it "returns new instance with many expressions added" do + expression = Flipper.boolean(true) + second = Flipper.string("yep").eq("yep") + third = Flipper.number(1).lte(20) + + result = expression.add(second, third) + expect(result.args).to eq([ + Flipper.boolean(true), + Flipper.string("yep").eq("yep"), + Flipper.number(1).lte(20), + ]) + end + + it "returns new instance with array of expressions added" do + expression = Flipper.boolean(true) + second = Flipper.string("yep").eq("yep") + third = Flipper.number(1).lte(20) + + result = expression.add([second, third]) + expect(result.args).to eq([ + Flipper.boolean(true), + Flipper.string("yep").eq("yep"), + Flipper.number(1).lte(20), + ]) + end + end + + describe "#remove" do + it "returns new instance with expression removed" do + first = Flipper.boolean(true) + second = Flipper.string("yep").eq("yep") + third = Flipper.number(1).lte(20) + expression = Flipper.any([first, second, third]) + + result = expression.remove(second) + expect(expression.args).to eq([first, second, third]) + expect(result.args).to eq([first, third]) + end + + it "returns new instance with many expressions removed" do + first = Flipper.boolean(true) + second = Flipper.string("yep").eq("yep") + third = Flipper.number(1).lte(20) + expression = Flipper.any([first, second, third]) + + result = expression.remove(second, third) + expect(expression.args).to eq([first, second, third]) + expect(result.args).to eq([first]) + end + + it "returns new instance with array of expressions removed" do + first = Flipper.boolean(true) + second = Flipper.string("yep").eq("yep") + third = Flipper.number(1).lte(20) + expression = Flipper.any([first, second, third]) + + result = expression.remove([second, third]) + expect(expression.args).to eq([first, second, third]) + expect(result.args).to eq([first]) + end + end + end + + [ + [2, 3, "equal", "eq", :Equal], + [2, 3, "not_equal", "neq", :NotEqual], + [2, 3, "greater_than", "gt", :GreaterThan], + [2, 3, "greater_than_or_equal_to", "gte", :GreaterThanOrEqualTo], + [2, 3, "greater_than_or_equal_to", "greater_than_or_equal", :GreaterThanOrEqualTo], + [2, 3, "less_than", "lt", :LessThan], + [2, 3, "less_than_or_equal_to", "lte", :LessThanOrEqualTo], + [2, 3, "less_than_or_equal_to", "less_than_or_equal", :LessThanOrEqualTo], + ].each do |(left, right, method_name, shortcut_name, function)| + it "can convert to #{function}" do + expression = build(left) + other = build(right) + converted = expression.send(method_name, other) + expect(converted).to eq(build({ function => [ left, right] })) + end + + it "can convert to #{function} using #{shortcut_name}" do + expression = build(left) + other = build(right) + converted = expression.send(shortcut_name, other) + expect(converted).to eq(build({ function => [ left, right] })) + end + + it "builds args into expressions when converting to #{function}" do + expression = build(left) + other = Flipper.property(:age) + converted = expression.send(method_name, other.value) + expect(converted).to eq(build({ function => [ left, other.value] })) + end + end + + it "can convert to PercentageOfActors" do + expression = Flipper.string("User;1").percentage_of_actors(40) + expect(expression).to eq(build({ PercentageOfActors: [ "User;1", 40 ] })) + end + + context "All" do + describe "#all" do + it "returns self" do + expression = Flipper.all([ + Flipper.boolean(true), + Flipper.string("yep").eq("yep"), + ]) + expect(expression.all).to be(expression) + end + end + + describe "#add" do + it "returns new instance with expression added" do + expression = Flipper.all([Flipper.boolean(true)]) + other = Flipper.string("yep").eq("yep") + + result = expression.add(other) + expect(result.args).to eq([ + Flipper.boolean(true), + Flipper.string("yep").eq("yep"), + ]) + end + + it "returns new instance with many expressions added" do + expression = Flipper.all([Flipper.boolean(true)]) + second = Flipper.string("yep").eq("yep") + third = Flipper.number(1).lte(20) + + result = expression.add(second, third) + expect(result.args).to eq([ + Flipper.boolean(true), + Flipper.string("yep").eq("yep"), + Flipper.number(1).lte(20), + ]) + end + + it "returns new instance with array of expressions added" do + expression = Flipper.all([Flipper.boolean(true)]) + second = Flipper.string("yep").eq("yep") + third = Flipper.number(1).lte(20) + + result = expression.add([second, third]) + expect(result.args).to eq([ + Flipper.boolean(true), + Flipper.string("yep").eq("yep"), + Flipper.number(1).lte(20), + ]) + end + end + + describe "#remove" do + it "returns new instance with expression removed" do + first = Flipper.boolean(true) + second = Flipper.string("yep").eq("yep") + third = Flipper.number(1).lte(20) + expression = Flipper.all([first, second, third]) + + result = expression.remove(second) + expect(expression.args).to eq([first, second, third]) + expect(result.args).to eq([first, third]) + end + + it "returns new instance with many expressions removed" do + first = Flipper.boolean(true) + second = Flipper.string("yep").eq("yep") + third = Flipper.number(1).lte(20) + expression = Flipper.all([first, second, third]) + + result = expression.remove(second, third) + expect(expression.args).to eq([first, second, third]) + expect(result.args).to eq([first]) + end + + it "returns new instance with array of expressions removed" do + first = Flipper.boolean(true) + second = Flipper.string("yep").eq("yep") + third = Flipper.number(1).lte(20) + expression = Flipper.all([first, second, third]) + + result = expression.remove([second, third]) + expect(expression.args).to eq([first, second, third]) + expect(result.args).to eq([first]) + end + end + end +end diff --git a/spec/flipper/expression_spec.rb b/spec/flipper/expression_spec.rb index 8f9762b9b..b46cbf429 100644 --- a/spec/flipper/expression_spec.rb +++ b/spec/flipper/expression_spec.rb @@ -3,14 +3,15 @@ RSpec.describe Flipper::Expression do describe "#build" do it "can build Equal" do - expression = Flipper::Expression.build({ + expression = described_class.build({ "Equal" => [ "basic", "basic", ] }) - expect(expression).to be_instance_of(Flipper::Expressions::Equal) + expect(expression).to be_instance_of(Flipper::Expression) + expect(expression.function).to be(Flipper::Expressions::Equal) expect(expression.args).to eq([ Flipper.string("basic"), Flipper.string("basic"), @@ -18,14 +19,15 @@ end it "can build GreaterThanOrEqualTo" do - expression = Flipper::Expression.build({ + expression = described_class.build({ "GreaterThanOrEqualTo" => [ 2, 1, ] }) - expect(expression).to be_instance_of(Flipper::Expressions::GreaterThanOrEqualTo) + expect(expression).to be_instance_of(Flipper::Expression) + expect(expression.function).to be(Flipper::Expressions::GreaterThanOrEqualTo) expect(expression.args).to eq([ Flipper.number(2), Flipper.number(1), @@ -33,14 +35,15 @@ end it "can build GreaterThan" do - expression = Flipper::Expression.build({ + expression = described_class.build({ "GreaterThan" => [ 2, 1, ] }) - expect(expression).to be_instance_of(Flipper::Expressions::GreaterThan) + expect(expression).to be_instance_of(Flipper::Expression) + expect(expression.function).to be(Flipper::Expressions::GreaterThan) expect(expression.args).to eq([ Flipper.number(2), Flipper.number(1), @@ -48,14 +51,15 @@ end it "can build LessThanOrEqualTo" do - expression = Flipper::Expression.build({ + expression = described_class.build({ "LessThanOrEqualTo" => [ 2, 1, ] }) - expect(expression).to be_instance_of(Flipper::Expressions::LessThanOrEqualTo) + expect(expression).to be_instance_of(Flipper::Expression) + expect(expression.function).to be(Flipper::Expressions::LessThanOrEqualTo) expect(expression.args).to eq([ Flipper.number(2), Flipper.number(1), @@ -63,11 +67,12 @@ end it "can build LessThan" do - expression = Flipper::Expression.build({ + expression = described_class.build({ "LessThan" => [2, 1] }) - expect(expression).to be_instance_of(Flipper::Expressions::LessThan) + expect(expression).to be_instance_of(Flipper::Expression) + expect(expression.function).to be(Flipper::Expressions::LessThan) expect(expression.args).to eq([ Flipper.number(2), Flipper.number(1), @@ -75,14 +80,15 @@ end it "can build NotEqual" do - expression = Flipper::Expression.build({ + expression = described_class.build({ "NotEqual" => [ "basic", "plus", ] }) - expect(expression).to be_instance_of(Flipper::Expressions::NotEqual) + expect(expression).to be_instance_of(Flipper::Expression) + expect(expression.function).to be(Flipper::Expressions::NotEqual) expect(expression.args).to eq([ Flipper.string("basic"), Flipper.string("plus"), @@ -90,30 +96,32 @@ end it "can build Number" do - expression = Flipper::Expression.build(1) + expression = described_class.build(1) - expect(expression).to be_instance_of(Flipper::Expressions::Constant) + expect(expression).to be_instance_of(Flipper::Expression::Constant) expect(expression.value).to eq(1) end it "can build Percentage" do - expression = Flipper::Expression.build({ + expression = described_class.build({ "Percentage" => [1] }) - expect(expression).to be_instance_of(Flipper::Expressions::Percentage) + expect(expression).to be_instance_of(Flipper::Expression) + expect(expression.function).to be(Flipper::Expressions::Percentage) expect(expression.args).to eq([Flipper.number(1)]) end it "can build PercentageOfActors" do - expression = Flipper::Expression.build({ + expression = described_class.build({ "PercentageOfActors" => [ "User;1", 40, ] }) - expect(expression).to be_instance_of(Flipper::Expressions::PercentageOfActors) + expect(expression).to be_instance_of(Flipper::Expression) + expect(expression.function).to be(Flipper::Expressions::PercentageOfActors) expect(expression.args).to eq([ Flipper.string("User;1"), Flipper.number(40), @@ -121,165 +129,60 @@ end it "can build String" do - expression = Flipper::Expression.build("basic") + expression = described_class.build("basic") - expect(expression).to be_instance_of(Flipper::Expressions::Constant) - expect(expression.args).to eq(["basic"]) + expect(expression).to be_instance_of(Flipper::Expression::Constant) + expect(expression.value).to eq("basic") end it "can build Property" do - expression = Flipper::Expression.build({ + expression = described_class.build({ "Property" => ["flipper_id"] }) - expect(expression).to be_instance_of(Flipper::Expressions::Property) + expect(expression).to be_instance_of(Flipper::Expression) + expect(expression.function).to be(Flipper::Expressions::Property) expect(expression.args).to eq([Flipper.string("flipper_id")]) end end - describe "#initialize" do - it "works with Array" do - expect(described_class.new([1]).args).to eq([Flipper.number(1)]) - end - - it "casts single argument to array" do - [ - "asdf", - 1, - {"Property" => "bar"}, - ].each do |value| - expect(described_class.new(value).args).to eq([Flipper::Expression.build(value)]) - end - end - end - describe "#eql?" do it "returns true for same class and args" do - expression = Flipper::Expression.new(["foo"]) - other = Flipper::Expression.new(["foo"]) + expression = described_class.build("foo") + other = described_class.build("foo") expect(expression.eql?(other)).to be(true) end it "returns false for different class" do - expression = Flipper::Expression.new(["foo"]) + expression = described_class.build("foo") other = Object.new expect(expression.eql?(other)).to be(false) end it "returns false for different args" do - expression = Flipper::Expression.new(["foo"]) - other = Flipper::Expression.new(["bar"]) + expression = described_class.build("foo") + other = described_class.build("bar") expect(expression.eql?(other)).to be(false) end end describe "#==" do it "returns true for same class and args" do - expression = Flipper::Expression.new(["foo"]) - other = Flipper::Expression.new(["foo"]) + expression = described_class.build("foo") + other = described_class.build("foo") expect(expression == other).to be(true) end it "returns false for different class" do - expression = Flipper::Expression.new(["foo"]) + expression = described_class.build("foo") other = Object.new expect(expression == other).to be(false) end it "returns false for different args" do - expression = Flipper::Expression.new(["foo"]) - other = Flipper::Expression.new(["bar"]) + expression = described_class.build("foo") + other = described_class.build("bar") expect(expression == other).to be(false) end end - - describe "#add" do - it "converts to Any and adds new expressions" do - expression = described_class.new(["something"]) - first = Flipper.boolean(true).eq(true) - second = Flipper.boolean(false).eq(false) - new_expression = expression.add(first, second) - expect(new_expression).to be_instance_of(Flipper::Expressions::Any) - expect(new_expression.args).to eq([ - expression, - first, - second, - ]) - end - end - - describe "#remove" do - it "converts to Any and removes any expressions that match" do - expression = described_class.new(["something"]) - first = Flipper.boolean(true).eq(true) - second = Flipper.boolean(false).eq(false) - new_expression = expression.remove(described_class.new(["something"]), first, second) - expect(new_expression).to be_instance_of(Flipper::Expressions::Any) - expect(new_expression.args).to eq([]) - end - end - - it "can convert to Any" do - expression = described_class.new(["something"]) - converted = expression.any - expect(converted).to be_instance_of(Flipper::Expressions::Any) - expect(converted.args).to eq([expression]) - end - - it "can convert to All" do - expression = described_class.new(["something"]) - converted = expression.all - expect(converted).to be_instance_of(Flipper::Expressions::All) - expect(converted.args).to eq([expression]) - end - - [ - [[2], [3], "equal", "eq", Flipper::Expressions::Equal], - [[2], [3], "not_equal", "neq", Flipper::Expressions::NotEqual], - [[2], [3], "greater_than", "gt", Flipper::Expressions::GreaterThan], - [[2], [3], "greater_than_or_equal_to", "gte", Flipper::Expressions::GreaterThanOrEqualTo], - [[2], [3], "greater_than_or_equal_to", "greater_than_or_equal", Flipper::Expressions::GreaterThanOrEqualTo], - [[2], [3], "less_than", "lt", Flipper::Expressions::LessThan], - [[2], [3], "less_than_or_equal_to", "lte", Flipper::Expressions::LessThanOrEqualTo], - [[2], [3], "less_than_or_equal_to", "less_than_or_equal", Flipper::Expressions::LessThanOrEqualTo], - ].each do |(args, other_args, method_name, shortcut_name, klass)| - it "can convert to #{klass}" do - expression = described_class.new(args) - other = described_class.new(other_args) - converted = expression.send(method_name, other) - expect(converted).to be_instance_of(klass) - expect(converted.args).to eq([expression, other]) - end - - it "builds args into expressions when converting to #{klass}" do - expression = described_class.new(args) - other = Flipper.property(:age) - converted = expression.send(method_name, other.value) - expect(converted).to be_instance_of(klass) - expect(converted.args).to eq([expression, other]) - end - - it "builds array args into expressions when converting to #{klass}" do - expression = described_class.new(args) - other = Flipper.random(100) - converted = expression.send(method_name, [other.value]) - expect(converted).to be_instance_of(klass) - expect(converted.args).to eq([expression, [other]]) - end - - it "can convert to #{klass} using #{shortcut_name}" do - expression = described_class.new(args) - other = described_class.new(other_args) - converted = expression.send(shortcut_name, other) - expect(converted).to be_instance_of(klass) - expect(converted.args).to eq([expression, other]) - end - end - - it "can convert to PercentageOfActors" do - expression = Flipper.string("User;1") - converted = expression.percentage_of_actors(40) - expect(converted).to be_instance_of(Flipper::Expressions::PercentageOfActors) - expect(converted.args).to eq([expression, Flipper.number(40)]) - end end diff --git a/spec/flipper/expressions/all_spec.rb b/spec/flipper/expressions/all_spec.rb index f3fd55c2f..c12fe54ac 100644 --- a/spec/flipper/expressions/all_spec.rb +++ b/spec/flipper/expressions/all_spec.rb @@ -1,103 +1,15 @@ RSpec.describe Flipper::Expressions::All do - describe "#evaluate" do + describe "#call" do it "returns true if all args evaluate as true" do - expression = described_class.new([ - Flipper.boolean(true), - Flipper.string("yep").eq("yep"), - ]) - expect(expression.evaluate).to be(true) + expect(described_class.call(true, true)).to be(true) end it "returns false if any args evaluate as false" do - expression = described_class.new([ - Flipper.boolean(false), - Flipper.string("yep").eq("yep"), - ]) - expect(expression.evaluate).to be(false) + expect(described_class.call(false, true)).to be(false) end - end - - describe "#all" do - it "returns self" do - expression = described_class.new([ - Flipper.boolean(true), - Flipper.string("yep").eq("yep"), - ]) - expect(expression.all).to be(expression) - end - end - - describe "#add" do - it "returns new instance with expression added" do - expression = described_class.new([Flipper.boolean(true)]) - other = Flipper.string("yep").eq("yep") - - result = expression.add(other) - expect(result.args).to eq([ - Flipper.boolean(true), - Flipper.string("yep").eq("yep"), - ]) - end - - it "returns new instance with many expressions added" do - expression = described_class.new([Flipper.boolean(true)]) - second = Flipper.string("yep").eq("yep") - third = Flipper.number(1).lte(20) - - result = expression.add(second, third) - expect(result.args).to eq([ - Flipper.boolean(true), - Flipper.string("yep").eq("yep"), - Flipper.number(1).lte(20), - ]) - end - - it "returns new instance with array of expressions added" do - expression = described_class.new([Flipper.boolean(true)]) - second = Flipper.string("yep").eq("yep") - third = Flipper.number(1).lte(20) - - result = expression.add([second, third]) - expect(result.args).to eq([ - Flipper.boolean(true), - Flipper.string("yep").eq("yep"), - Flipper.number(1).lte(20), - ]) - end - end - - describe "#remove" do - it "returns new instance with expression removed" do - first = Flipper.boolean(true) - second = Flipper.string("yep").eq("yep") - third = Flipper.number(1).lte(20) - expression = described_class.new([first, second, third]) - - result = expression.remove(second) - expect(expression.args).to eq([first, second, third]) - expect(result.args).to eq([first, third]) - end - - it "returns new instance with many expressions removed" do - first = Flipper.boolean(true) - second = Flipper.string("yep").eq("yep") - third = Flipper.number(1).lte(20) - expression = described_class.new([first, second, third]) - - result = expression.remove(second, third) - expect(expression.args).to eq([first, second, third]) - expect(result.args).to eq([first]) - end - - it "returns new instance with array of expressions removed" do - first = Flipper.boolean(true) - second = Flipper.string("yep").eq("yep") - third = Flipper.number(1).lte(20) - expression = described_class.new([first, second, third]) - result = expression.remove([second, third]) - expect(expression.args).to eq([first, second, third]) - expect(result.args).to eq([first]) + it "returns true with empty args" do + expect(described_class.call).to be(true) end end end diff --git a/spec/flipper/expressions/any_spec.rb b/spec/flipper/expressions/any_spec.rb index d7af12165..cd6b07f8a 100644 --- a/spec/flipper/expressions/any_spec.rb +++ b/spec/flipper/expressions/any_spec.rb @@ -1,104 +1,15 @@ RSpec.describe Flipper::Expressions::Any do - describe "#evaluate" do + describe "#call" do it "returns true if any args evaluate as true" do - expression = described_class.new([ - Flipper.boolean(true), - Flipper.string("yep").eq("nope"), - Flipper.number(1).gte(10), - ]) - expect(expression.evaluate).to be(true) + expect(described_class.call(true, false)).to be(true) end it "returns false if all args evaluate as false" do - expression = described_class.new([ - Flipper.boolean(false), - Flipper.string("yep").eq("nope"), - ]) - expect(expression.evaluate).to be(false) + expect(described_class.call(false, false)).to be(false) end - end - - describe "#any" do - it "returns self" do - expression = described_class.new([ - Flipper.boolean(true), - Flipper.string("yep").eq("yep"), - ]) - expect(expression.any).to be(expression) - end - end - - describe "#add" do - it "returns new instance with expression added" do - expression = described_class.new([Flipper.boolean(true)]) - other = Flipper.string("yep").eq("yep") - - result = expression.add(other) - expect(result.args).to eq([ - Flipper.boolean(true), - Flipper.string("yep").eq("yep"), - ]) - end - - it "returns new instance with many expressions added" do - expression = described_class.new([Flipper.boolean(true)]) - second = Flipper.string("yep").eq("yep") - third = Flipper.number(1).lte(20) - - result = expression.add(second, third) - expect(result.args).to eq([ - Flipper.boolean(true), - Flipper.string("yep").eq("yep"), - Flipper.number(1).lte(20), - ]) - end - - it "returns new instance with array of expressions added" do - expression = described_class.new([Flipper.boolean(true)]) - second = Flipper.string("yep").eq("yep") - third = Flipper.number(1).lte(20) - - result = expression.add([second, third]) - expect(result.args).to eq([ - Flipper.boolean(true), - Flipper.string("yep").eq("yep"), - Flipper.number(1).lte(20), - ]) - end - end - - describe "#remove" do - it "returns new instance with expression removed" do - first = Flipper.boolean(true) - second = Flipper.string("yep").eq("yep") - third = Flipper.number(1).lte(20) - expression = described_class.new([first, second, third]) - - result = expression.remove(second) - expect(expression.args).to eq([first, second, third]) - expect(result.args).to eq([first, third]) - end - - it "returns new instance with many expressions removed" do - first = Flipper.boolean(true) - second = Flipper.string("yep").eq("yep") - third = Flipper.number(1).lte(20) - expression = described_class.new([first, second, third]) - - result = expression.remove(second, third) - expect(expression.args).to eq([first, second, third]) - expect(result.args).to eq([first]) - end - - it "returns new instance with array of expressions removed" do - first = Flipper.boolean(true) - second = Flipper.string("yep").eq("yep") - third = Flipper.number(1).lte(20) - expression = described_class.new([first, second, third]) - result = expression.remove([second, third]) - expect(expression.args).to eq([first, second, third]) - expect(result.args).to eq([first]) + it "returns false with empty args" do + expect(described_class.call).to be(false) end end end diff --git a/spec/flipper/expressions/duration_spec.rb b/spec/flipper/expressions/duration_spec.rb index 1f660770f..e19e2aff0 100644 --- a/spec/flipper/expressions/duration_spec.rb +++ b/spec/flipper/expressions/duration_spec.rb @@ -1,43 +1,43 @@ RSpec.describe Flipper::Expressions::Duration do - describe "#evaluate" do + describe "#call" do it "raises error with invalid value" do - expect { described_class.new([false, 'minute']).evaluate }.to raise_error(ArgumentError) + expect { described_class.call(false, 'minute') }.to raise_error(ArgumentError) end it "raises error with invalid unit" do - expect { described_class.new([4, 'score']).evaluate }.to raise_error(ArgumentError) + expect { described_class.call(4, 'score') }.to raise_error(ArgumentError) end it 'defaults unit to seconds' do - expect(described_class.new(10).evaluate).to eq(10) + expect(described_class.call(10)).to eq(10) end it "evaluates seconds" do - expect(described_class.new([10, 'seconds']).evaluate).to eq(10) + expect(described_class.call(10, 'seconds')).to eq(10) end it "evaluates minutes" do - expect(described_class.new([2, 'minutes']).evaluate).to eq(120) + expect(described_class.call(2, 'minutes')).to eq(120) end it "evaluates hours" do - expect(described_class.new([2, 'hours']).evaluate).to eq(7200) + expect(described_class.call(2, 'hours')).to eq(7200) end it "evaluates days" do - expect(described_class.new([2, 'days']).evaluate).to eq(172_800) + expect(described_class.call(2, 'days')).to eq(172_800) end it "evaluates weeks" do - expect(described_class.new([2, 'weeks']).evaluate).to eq(1_209_600) + expect(described_class.call(2, 'weeks')).to eq(1_209_600) end it "evaluates months" do - expect(described_class.new([2, 'months']).evaluate).to eq(5_259_492) + expect(described_class.call(2, 'months')).to eq(5_259_492) end it "evaluates years" do - expect(described_class.new([2, 'years']).evaluate).to eq(63_113_904) + expect(described_class.call(2, 'years')).to eq(63_113_904) end end end diff --git a/spec/flipper/expressions/equal_spec.rb b/spec/flipper/expressions/equal_spec.rb index c3d025c15..96caba84b 100644 --- a/spec/flipper/expressions/equal_spec.rb +++ b/spec/flipper/expressions/equal_spec.rb @@ -1,93 +1,24 @@ RSpec.describe Flipper::Expressions::Equal do describe "#evaluate" do it "returns true when equal" do - expression = described_class.new([ - Flipper.string("basic"), - Flipper.string("basic"), - ]) - - expect(expression.evaluate).to be(true) - end - - it "returns true when properties equal" do - expression = described_class.new([ - Flipper.property(:first), - Flipper.property(:second), - ]) - - properties = { - "first" => "foo", - "second" => "foo", - } - expect(expression.evaluate(properties: properties)).to be(true) - end - - it "works when nested" do - expression = described_class.new([ - a = Flipper.boolean(true), - b = Flipper.all( - Flipper.property(:stinky).eq(true), - Flipper.string("admin").eq(Flipper.property(:role)) - ), - ]) - - properties = { - "stinky" => true, - "role" => "admin", - } - - expect(expression.evaluate(properties: properties)).to be(true) + expect(described_class.call("basic", "basic")).to be(true) end it "returns false when not equal" do - expression = described_class.new([ - Flipper.string("basic"), - Flipper.string("plus"), - ]) - - expect(expression.evaluate).to be(false) - end - - it "returns false when properties not equal" do - expression = described_class.new([ - Flipper.property(:first), - Flipper.property(:second), - ]) - - properties = { - "first" => "foo", - "second" => "bar", - } - expect(expression.evaluate(properties: properties)).to be(false) + expect(described_class.call("basic", "plus")).to be(false) end it "returns false when value evaluates to nil" do - expect(described_class.new([Flipper.number(nil), 1]).evaluate).to be(false) - expect(described_class.new([1, Flipper.number(nil)]).evaluate).to be(false) + expect(described_class.call(nil, 1)).to be(false) + expect(described_class.call(1, nil)).to be(false) end it "raises ArgumentError with no arguments" do - expect { described_class.new([]).evaluate }.to raise_error(ArgumentError) + expect { described_class.call }.to raise_error(ArgumentError) end it "raises ArgumentError with one argument" do - expect { described_class.new([10]).evaluate }.to raise_error(ArgumentError) - end - end - - describe "#value" do - it "returns Hash" do - expression = described_class.new([ - Flipper::Expressions::Property.new(["plan"]), - Flipper.string("basic"), - ]) - - expect(expression.value).to eq({ - "Equal" => [ - {"Property" => ["plan"]}, - "basic", - ], - }) + expect { described_class.call(10) }.to raise_error(ArgumentError) end end end diff --git a/spec/flipper/expressions/greater_than_or_equal_to_spec.rb b/spec/flipper/expressions/greater_than_or_equal_to_spec.rb index 087c24fc4..7e2e065a8 100644 --- a/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +++ b/spec/flipper/expressions/greater_than_or_equal_to_spec.rb @@ -1,53 +1,28 @@ RSpec.describe Flipper::Expressions::GreaterThanOrEqualTo do - describe "#evaluate" do + describe "#call" do it "returns true when equal" do - expression = described_class.new([2, 2]) - expect(expression.evaluate).to be(true) - end - - it "returns true when equal with args that need evaluation" do - expression = described_class.new([ - Flipper.number(2), - Flipper.number(2), - ]) - - expect(expression.evaluate).to be(true) + expect(described_class.call(2, 2)).to be(true) end it "returns true when greater" do - expression = described_class.new([2, 1]) - expect(expression.evaluate).to be(true) + expect(described_class.call(2, 1)).to be(true) end it "returns false when less" do - expression = described_class.new([1, 2]) - expect(expression.evaluate).to be(false) + expect(described_class.call(1, 2)).to be(false) end it "returns false when value evaluates to nil" do - expect(described_class.new([Flipper.number(nil), 1]).evaluate).to be(false) - expect(described_class.new([1, Flipper.number(nil)]).evaluate).to be(false) + expect(described_class.call(nil, 1)).to be(false) + expect(described_class.call(1, nil)).to be(false) end it "raises ArgumentError with no arguments" do - expect { described_class.new([]).evaluate }.to raise_error(ArgumentError) + expect { described_class.call }.to raise_error(ArgumentError) end it "raises ArgumentError with one argument" do - expect { described_class.new([10]).evaluate }.to raise_error(ArgumentError) - end - end - - describe "#value" do - it "returns Hash" do - expression = described_class.new([ - Flipper.number(20), - Flipper.number(10), - ]) - - expect(expression.value).to eq({ - "GreaterThanOrEqualTo" => [20, 10] - }) + expect { described_class.call(10) }.to raise_error(ArgumentError) end end end diff --git a/spec/flipper/expressions/greater_than_spec.rb b/spec/flipper/expressions/greater_than_spec.rb index 94f4a38fb..1c5bbcc1a 100644 --- a/spec/flipper/expressions/greater_than_spec.rb +++ b/spec/flipper/expressions/greater_than_spec.rb @@ -1,52 +1,28 @@ RSpec.describe Flipper::Expressions::GreaterThan do describe "#evaluate" do it "returns false when equal" do - expression = described_class.new([2, 2]) - expect(expression.evaluate).to be(false) + expect(described_class.call(2, 2)).to be(false) end it "returns true when greater" do - expression = described_class.new([2, 1]) - expect(expression.evaluate).to be(true) - end - - it "returns true when greater with args that need evaluation" do - expression = described_class.new([ - Flipper.number(2), - Flipper.number(1), - ]) - expect(expression.evaluate).to be(true) + expect(described_class.call(2, 1)).to be(true) end it "returns false when less" do - expression = described_class.new([1, 2]) - expect(expression.evaluate).to be(false) + expect(described_class.call(1, 2)).to be(false) end it "returns false when value evaluates to nil" do - expect(described_class.new([Flipper.number(nil), 1]).evaluate).to be(false) - expect(described_class.new([1, Flipper.number(nil)]).evaluate).to be(false) + expect(described_class.call(nil, 1)).to be(false) + expect(described_class.call(1, nil)).to be(false) end it "raises ArgumentError with no arguments" do - expect { described_class.new([]).evaluate }.to raise_error(ArgumentError) + expect { described_class.call }.to raise_error(ArgumentError) end it "raises ArgumentError with one argument" do - expect { described_class.new([10]).evaluate }.to raise_error(ArgumentError) - end - end - - describe "#value" do - it "returns Hash" do - expression = described_class.new([ - Flipper.number(20), - Flipper.number(10), - ]) - - expect(expression.value).to eq({ - "GreaterThan" => [20, 10] - }) + expect { described_class.call(10) }.to raise_error(ArgumentError) end end end diff --git a/spec/flipper/expressions/less_than_or_equal_to_spec.rb b/spec/flipper/expressions/less_than_or_equal_to_spec.rb index 588f1f0f4..9fa1182cc 100644 --- a/spec/flipper/expressions/less_than_or_equal_to_spec.rb +++ b/spec/flipper/expressions/less_than_or_equal_to_spec.rb @@ -1,55 +1,28 @@ RSpec.describe Flipper::Expressions::LessThanOrEqualTo do - describe "#evaluate" do + describe "#call" do it "returns true when equal" do - expression = described_class.new([2, 2]) - - expect(expression.evaluate).to be(true) - end - - it "returns true when equal with args that need evaluation" do - expression = described_class.new([ - Flipper.number(2), - Flipper.number(2), - ]) - - expect(expression.evaluate).to be(true) + expect(described_class.call(2, 2)).to be(true) end it "returns true when less" do - expression = described_class.new([1, 2]) - - expect(expression.evaluate).to be(true) + expect(described_class.call(1, 2)).to be(true) end it "returns false when greater" do - expression = described_class.new([2, 1]) - expect(expression.evaluate).to be(false) + expect(described_class.call(2, 1)).to be(false) end it "returns false when value evaluates to nil" do - expect(described_class.new([Flipper.number(nil), 1]).evaluate).to be(false) - expect(described_class.new([1, Flipper.number(nil)]).evaluate).to be(false) + expect(described_class.call(nil, 1)).to be(false) + expect(described_class.call(1, nil)).to be(false) end it "raises ArgumentError with no arguments" do - expect { described_class.new([]).evaluate }.to raise_error(ArgumentError) + expect { described_class.call }.to raise_error(ArgumentError) end it "raises ArgumentError with one argument" do - expect { described_class.new([10]).evaluate }.to raise_error(ArgumentError) - end - end - - describe "#value" do - it "returns Hash" do - expression = described_class.new([ - Flipper.number(20), - Flipper.number(10), - ]) - - expect(expression.value).to eq({ - "LessThanOrEqualTo" => [20, 10], - }) + expect { described_class.call(10) }.to raise_error(ArgumentError) end end end diff --git a/spec/flipper/expressions/less_than_spec.rb b/spec/flipper/expressions/less_than_spec.rb index 7a0348bf9..414c0eaea 100644 --- a/spec/flipper/expressions/less_than_spec.rb +++ b/spec/flipper/expressions/less_than_spec.rb @@ -1,49 +1,32 @@ RSpec.describe Flipper::Expressions::LessThan do - describe "#evaluate" do + describe "#call" do it "returns false when equal" do - expression = described_class.new([2, 2]) - expect(expression.evaluate).to be(false) + expect(described_class.call(2, 2)).to be(false) end it "returns true when less" do - expression = described_class.new([1, 2]) - expect(expression.evaluate).to be(true) + expect(described_class.call(1, 2)).to be(true) end it "returns true when less with args that need evaluation" do - expression = described_class.new([1, 2]) - expect(expression.evaluate).to be(true) + expect(described_class.call(1, 2)).to be(true) end it "returns false when greater" do - expression = described_class.new([2, 1]) - expect(expression.evaluate).to be(false) + expect(described_class.call(2, 1)).to be(false) end it "returns false when value evaluates to nil" do - expect(described_class.new([Flipper.number(nil), 1]).evaluate).to be(false) - expect(described_class.new([1, Flipper.number(nil)]).evaluate).to be(false) + expect(described_class.call(nil, 1)).to be(false) + expect(described_class.call(1, nil)).to be(false) end it "raises ArgumentError with no arguments" do - expect { described_class.new([]).evaluate }.to raise_error(ArgumentError) + expect { described_class.call }.to raise_error(ArgumentError) end it "raises ArgumentError with one argument" do - expect { described_class.new([10]).evaluate }.to raise_error(ArgumentError) - end - end - - describe "#value" do - it "returns Hash" do - expression = described_class.new([ - Flipper.number(20), - Flipper.number(10), - ]) - - expect(expression.value).to eq({ - "LessThan" => [20, 10] - }) + expect { described_class.call(10) }.to raise_error(ArgumentError) end end end diff --git a/spec/flipper/expressions/not_equal_spec.rb b/spec/flipper/expressions/not_equal_spec.rb index 4ec035b1a..91d8584ba 100644 --- a/spec/flipper/expressions/not_equal_spec.rb +++ b/spec/flipper/expressions/not_equal_spec.rb @@ -1,56 +1,15 @@ RSpec.describe Flipper::Expressions::NotEqual do - describe "#evaluate" do + describe "#call" do it "returns true when not equal" do - expression = described_class.new(["basic", "plus"]) - expect(expression.evaluate).to be(true) - end - - it "returns true when properties not equal" do - expression = described_class.new([ - Flipper.property(:first), - Flipper.property(:second), - ]) - - properties = { - "first" => "foo", - "second" => "bar", - } - expect(expression.evaluate(properties: properties)).to be(true) + expect(described_class.call("basic", "plus")).to be(true) end it "returns false when equal" do - expression = described_class.new(["basic", "basic"]) - expect(expression.evaluate).to be(false) - end - - it "returns false when properties are equal" do - expression = described_class.new([ - Flipper.property(:first), - Flipper.property(:second), - ]) - - properties = { - "first" => "foo", - "second" => "foo", - } - expect(expression.evaluate(properties: properties)).to be(false) + expect(described_class.call("basic", "basic")).to be(false) end it "raises ArgumentError for more arguments" do - expect { described_class.new([20, 10, 20]).evaluate }.to raise_error(ArgumentError) - end - end - - describe "#value" do - it "returns Hash" do - expression = described_class.new([ - Flipper.number(20), - Flipper.number(10), - ]) - - expect(expression.value).to eq({ - "NotEqual" => [20, 10] - }) + expect { described_class.call(20, 10, 20).evaluate }.to raise_error(ArgumentError) end end end diff --git a/spec/flipper/expressions/now_spec.rb b/spec/flipper/expressions/now_spec.rb index a625544b8..e7ea9f545 100644 --- a/spec/flipper/expressions/now_spec.rb +++ b/spec/flipper/expressions/now_spec.rb @@ -1,7 +1,7 @@ RSpec.describe Flipper::Expressions::Now do - describe "#evaluate" do + describe "#call" do it "returns current time" do - expect(described_class.new.evaluate.round).to eq(Time.now.round) + expect(described_class.call.round).to eq(Time.now.round) end end end diff --git a/spec/flipper/expressions/percentage_of_actors_spec.rb b/spec/flipper/expressions/percentage_of_actors_spec.rb index 8392b275f..83314e6f4 100644 --- a/spec/flipper/expressions/percentage_of_actors_spec.rb +++ b/spec/flipper/expressions/percentage_of_actors_spec.rb @@ -1,68 +1,20 @@ RSpec.describe Flipper::Expressions::PercentageOfActors do describe "#evaluate" do it "returns true when string in percentage enabled" do - expression = described_class.new([ - Flipper.string("User;1"), - Flipper.number(42), - ]) - - expect(expression.evaluate).to be(true) + expect(described_class.call("User;1", 42)).to be(true) end it "returns true when string in fractional percentage enabled" do - expression = described_class.new([ - Flipper.string("User;1"), - Flipper.number(41.687), - ]) - - expect(expression.evaluate).to be(true) - end - - it "returns true when property evalutes to string that is percentage enabled" do - expression = described_class.new([ - Flipper.property(:flipper_id), - Flipper.number(42), - ]) - - properties = { - "flipper_id" => "User;1", - } - expect(expression.evaluate(properties: properties)).to be(true) + expect(described_class.call("User;1", 41.687)).to be(true) end it "returns false when string in percentage enabled" do - expression = described_class.new([ - Flipper.string("User;1"), - Flipper.number(0), - ]) - - expect(expression.evaluate).to be(false) + expect(described_class.call("User;1", 0)).to be(false) end it "changes value based on feature_name so not all actors get all features first" do - expression = described_class.new([ - Flipper.string("User;1"), - Flipper.number(70), - ]) - - expect(expression.evaluate(feature_name: "a")).to be(true) - expect(expression.evaluate(feature_name: "b")).to be(false) - end - end - - describe "#value" do - it "returns Hash" do - expression = described_class.new([ - Flipper.string("User;1"), - Flipper.number(10), - ]) - - expect(expression.value).to eq({ - "PercentageOfActors" => [ - "User;1", - 10 - ], - }) + expect(described_class.call("User;1", 70, context: {feature_name: "a"})).to be(true) + expect(described_class.call("User;1", 70, context: {feature_name: "b"})).to be(false) end end end diff --git a/spec/flipper/expressions/percentage_spec.rb b/spec/flipper/expressions/percentage_spec.rb index e03e8a90c..1b31347b5 100644 --- a/spec/flipper/expressions/percentage_spec.rb +++ b/spec/flipper/expressions/percentage_spec.rb @@ -1,23 +1,15 @@ RSpec.describe Flipper::Expressions::Percentage do - describe "#evaluate" do + describe "#call" do it "returns numeric" do - expect(described_class.new(10).evaluate).to be(10.0) + expect(described_class.call(10)).to be(10.0) end it "returns 0 if less than 0" do - expect(described_class.new(-1).evaluate).to be(0) + expect(described_class.call(-1)).to be(0) end it "returns 100 if greater than 100" do - expect(described_class.new(101).evaluate).to be(100) - end - end - - describe "#value" do - it "returns Hash" do - expression = described_class.new([99]) - - expect(expression.value).to eq({"Percentage" => [99]}) + expect(described_class.call(101)).to be(100) end end end diff --git a/spec/flipper/expressions/property_spec.rb b/spec/flipper/expressions/property_spec.rb index 3c8699343..3d168f215 100644 --- a/spec/flipper/expressions/property_spec.rb +++ b/spec/flipper/expressions/property_spec.rb @@ -1,37 +1,13 @@ RSpec.describe Flipper::Expressions::Property do - describe "#evaluate" do + describe "#call" do it "returns value for property key" do - expression = described_class.new("flipper_id") - properties = { - "flipper_id" => "User;1", - } - expect(expression.evaluate(properties: properties)).to eq("User;1") - end - - it "can evalute arg and use result for property name" do - expression = described_class.new(Flipper.property(:rollout_key)) - properties = { - "rollout_key" => "flipper_id", - "flipper_id" => "User;1", - } - expect(expression.evaluate(properties: properties)).to eq("User;1") + context = { properties: { "flipper_id" => "User;1" } } + expect(described_class.call("flipper_id", context: context)).to eq("User;1") end it "returns nil if key not found in properties" do - expression = described_class.new("flipper_id") - expect(expression.evaluate).to be(nil) - end - end - - describe "#value" do - it "returns Hash" do - expression = described_class.new([ - "flipper_id", - ]) - - expect(expression.value).to eq({ - "Property" => ["flipper_id"], - }) + context = { properties: { } } + expect(described_class.call("flipper_id", context: context)).to be(nil) end end end diff --git a/spec/flipper/expressions/random_spec.rb b/spec/flipper/expressions/random_spec.rb index 30a7b557e..708f0f716 100644 --- a/spec/flipper/expressions/random_spec.rb +++ b/spec/flipper/expressions/random_spec.rb @@ -1,21 +1,9 @@ RSpec.describe Flipper::Expressions::Random do - describe "#evaluate" do + describe "#call" do it "returns random number based on max" do 100.times do - expect(described_class.new(10).evaluate).to be_between(0, 10) + expect(described_class.call(10)).to be_between(0, 10) end end - - it "returns random number based on max that is Value" do - 100.times do - expect(described_class.new([Flipper.number(10)]).evaluate).to be_between(0, 10) - end - end - end - - describe "#value" do - it "returns Hash" do - expect(described_class.new(100).value).to eq({ "Random" => [100] }) - end end end diff --git a/spec/flipper/expressions/time_spec.rb b/spec/flipper/expressions/time_spec.rb index 2c4999561..55674cd08 100644 --- a/spec/flipper/expressions/time_spec.rb +++ b/spec/flipper/expressions/time_spec.rb @@ -1,13 +1,13 @@ RSpec.describe Flipper::Expressions::Time do let(:time) { Time.now.round } - describe "#evaluate" do + describe "#call" do it "returns time for #to_s format" do - expect(described_class.new(time.to_s).evaluate).to eq(time) + expect(described_class.call(time.to_s)).to eq(time) end it "returns time for #iso8601 format" do - expect(described_class.new(time.iso8601).evaluate).to eq(time) + expect(described_class.call(time.iso8601)).to eq(time) end end end diff --git a/spec/flipper/feature_spec.rb b/spec/flipper/feature_spec.rb index d881058c3..81b0f1ecf 100644 --- a/spec/flipper/feature_spec.rb +++ b/spec/flipper/feature_spec.rb @@ -693,7 +693,7 @@ it "sets expression to Expression" do expression = Flipper.property(:plan).eq("basic") subject.add_expression(expression) - expect(subject.expression).to be_instance_of(Flipper::Expressions::Equal) + expect(subject.expression).to be_instance_of(Flipper::Expression) expect(subject.expression).to eq(expression) end end @@ -702,7 +702,7 @@ it "sets expression to Any" do expression = Flipper.any(Flipper.property(:plan).eq("basic")) subject.add_expression(expression) - expect(subject.expression).to be_instance_of(Flipper::Expressions::Any) + expect(subject.expression).to be_instance_of(Flipper::Expression) expect(subject.expression).to eq(expression) end end @@ -711,7 +711,7 @@ it "sets expression to All" do expression = Flipper.all(Flipper.property(:plan).eq("basic")) subject.add_expression(expression) - expect(subject.expression).to be_instance_of(Flipper::Expressions::All) + expect(subject.expression).to be_instance_of(Flipper::Expression) expect(subject.expression).to eq(expression) end end @@ -728,7 +728,7 @@ it "changes expression to Any and adds new Expression" do new_expression = Flipper.property(:age).gte(21) subject.add_expression(new_expression) - expect(subject.expression).to be_instance_of(Flipper::Expressions::Any) + expect(subject.expression).to be_instance_of(Flipper::Expression) expect(subject.expression.args).to include(expression) expect(subject.expression.args).to include(new_expression) end @@ -738,7 +738,7 @@ it "changes expression to Any and adds new Any" do new_expression = Flipper.any(Flipper.property(:age).eq(21)) subject.add_expression new_expression - expect(subject.expression).to be_instance_of(Flipper::Expressions::Any) + expect(subject.expression).to be_instance_of(Flipper::Expression) expect(subject.expression.args).to include(expression) expect(subject.expression.args).to include(new_expression) end @@ -748,7 +748,7 @@ it "changes expression to Any and adds new All" do new_expression = Flipper.all(Flipper.property(:plan).eq("basic")) subject.add_expression new_expression - expect(subject.expression).to be_instance_of(Flipper::Expressions::Any) + expect(subject.expression).to be_instance_of(Flipper::Expression) expect(subject.expression.args).to include(expression) expect(subject.expression.args).to include(new_expression) end @@ -767,7 +767,7 @@ it "adds Expression to Any" do new_expression = Flipper.property(:age).gte(21) subject.add_expression(new_expression) - expect(subject.expression).to be_instance_of(Flipper::Expressions::Any) + expect(subject.expression).to be_instance_of(Flipper::Expression) expect(subject.expression.args).to include(condition) expect(subject.expression.args).to include(new_expression) end @@ -777,7 +777,7 @@ it "adds Any to Any" do new_expression = Flipper.any(Flipper.property(:age).gte(21)) subject.add_expression(new_expression) - expect(subject.expression).to be_instance_of(Flipper::Expressions::Any) + expect(subject.expression).to be_instance_of(Flipper::Expression) expect(subject.expression.args).to include(condition) expect(subject.expression.args).to include(new_expression) end @@ -787,7 +787,7 @@ it "adds All to Any" do new_expression = Flipper.all(Flipper.property(:age).gte(21)) subject.add_expression(new_expression) - expect(subject.expression).to be_instance_of(Flipper::Expressions::Any) + expect(subject.expression).to be_instance_of(Flipper::Expression) expect(subject.expression.args).to include(condition) expect(subject.expression.args).to include(new_expression) end @@ -806,7 +806,7 @@ it "adds Expression to All" do new_expression = Flipper.property(:age).gte(21) subject.add_expression(new_expression) - expect(subject.expression).to be_instance_of(Flipper::Expressions::All) + expect(subject.expression).to be_instance_of(Flipper::Expression) expect(subject.expression.args).to include(condition) expect(subject.expression.args).to include(new_expression) end @@ -816,7 +816,7 @@ it "adds Any to All" do new_expression = Flipper.any(Flipper.property(:age).gte(21)) subject.add_expression(new_expression) - expect(subject.expression).to be_instance_of(Flipper::Expressions::All) + expect(subject.expression).to be_instance_of(Flipper::Expression) expect(subject.expression.args).to include(condition) expect(subject.expression.args).to include(new_expression) end @@ -826,7 +826,7 @@ it "adds All to All" do new_expression = Flipper.all(Flipper.property(:age).gte(21)) subject.add_expression(new_expression) - expect(subject.expression).to be_instance_of(Flipper::Expressions::All) + expect(subject.expression).to be_instance_of(Flipper::Expression) expect(subject.expression.args).to include(condition) expect(subject.expression.args).to include(new_expression) end diff --git a/spec/flipper/gates/expression_spec.rb b/spec/flipper/gates/expression_spec.rb index 40f32bf81..70a7bc787 100644 --- a/spec/flipper/gates/expression_spec.rb +++ b/spec/flipper/gates/expression_spec.rb @@ -96,12 +96,13 @@ def context(expression, properties: {}) describe '#wrap' do it 'returns self for Flipper::Expression' do expression = Flipper.number(20).eq(20) - expect(subject.wrap(expression)).to be_instance_of(Flipper::Expressions::Equal) + expect(subject.wrap(expression)).to be(expression) end it 'returns Flipper::Expression for Hash' do expression = Flipper.number(20).eq(20) - expect(subject.wrap(expression.value)).to be_instance_of(Flipper::Expressions::Equal) + expect(subject.wrap(expression.value)).to be_instance_of(Flipper::Expression) + expect(subject.wrap(expression.value)).to eq(expression) end end end diff --git a/spec/flipper_spec.rb b/spec/flipper_spec.rb index 43c18833c..30da20543 100644 --- a/spec/flipper_spec.rb +++ b/spec/flipper_spec.rb @@ -359,20 +359,20 @@ describe ".property" do it "returns Flipper::Expressions::Property instance" do - expect(Flipper.property("name")).to eq(Flipper::Expressions::Property.new("name")) + expect(Flipper.property("name")).to eq(Flipper::Expression.build(Property: "name")) end end describe ".boolean" do - it "returns Flipper::Expressions::Constant instance" do - expect(described_class.boolean(true)).to eq(Flipper::Expressions::Constant.new(true)) - expect(described_class.boolean(false)).to eq(Flipper::Expressions::Constant.new(false)) + it "returns Flipper::Expression::Constant instance" do + expect(described_class.boolean(true)).to eq(Flipper::Expression::Constant.new(true)) + expect(described_class.boolean(false)).to eq(Flipper::Expression::Constant.new(false)) end end describe ".random" do it "returns Flipper::Expressions::Random instance" do - expect(Flipper.random(100)).to eq(Flipper::Expressions::Random.new(100)) + expect(Flipper.random(100)).to eq(Flipper::Expression.build(Random: 100)) end end @@ -382,7 +382,7 @@ it "returns Flipper::Expressions::Any instance" do expect(Flipper.any(age_expression, plan_expression)).to eq( - Flipper::Expressions::Any.new([age_expression, plan_expression]) + Flipper::Expression.build({Any: [age_expression, plan_expression]}) ) end end @@ -393,7 +393,7 @@ it "returns Flipper::Expressions::All instance" do expect(Flipper.all(age_expression, plan_expression)).to eq( - Flipper::Expressions::All.new([age_expression, plan_expression]) + Flipper::Expression.build({All: [age_expression, plan_expression]}) ) end end From c6f2e01c757043f777c6aa33f3db6646e73313f6 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Mon, 27 Mar 2023 11:23:02 -0400 Subject: [PATCH 160/176] Restore String, Boolean, Number expressions for casting --- lib/flipper.rb | 10 ++++-- lib/flipper/expression/builder.rb | 2 +- lib/flipper/expressions/number.rb | 3 +- lib/flipper/expressions/string.rb | 9 ++++++ lib/flipper/gate_values.rb | 4 +-- lib/flipper/typecast.rb | 20 ++++++++---- lib/flipper/types/percentage.rb | 2 +- spec/flipper/adapters/rollout_spec.rb | 2 +- spec/flipper/expression/builder_spec.rb | 2 +- spec/flipper/expression_spec.rb | 32 +++++++++---------- spec/flipper/expressions/boolean_spec.rb | 15 +++++++++ spec/flipper/expressions/equal_spec.rb | 2 +- spec/flipper/expressions/greater_than_spec.rb | 2 +- spec/flipper/expressions/number_spec.rb | 21 ++++++++++++ .../expressions/percentage_of_actors_spec.rb | 2 +- spec/flipper/expressions/string_spec.rb | 11 +++++++ spec/flipper/gate_values_spec.rb | 4 +-- spec/flipper/typecast_spec.rb | 14 ++++---- spec/flipper_spec.rb | 17 +++++++--- 19 files changed, 124 insertions(+), 50 deletions(-) create mode 100644 lib/flipper/expressions/string.rb create mode 100644 spec/flipper/expressions/boolean_spec.rb create mode 100644 spec/flipper/expressions/number_spec.rb create mode 100644 spec/flipper/expressions/string_spec.rb diff --git a/lib/flipper.rb b/lib/flipper.rb index 283623108..fd0f30892 100644 --- a/lib/flipper.rb +++ b/lib/flipper.rb @@ -76,20 +76,24 @@ def all(*args) Expression.build({ All: args.flatten }) end + def constant(value) + Expression.build(value) + end + def property(name) Expression.build({ Property: name }) end def string(value) - Expression.build(value) + Expression.build({ String: value }) end def number(value) - Expression.build(value) + Expression.build({ Number: value }) end def boolean(value) - Expression.build(value) + Expression.build({ Boolean: value }) end def random(max) diff --git a/lib/flipper/expression/builder.rb b/lib/flipper/expression/builder.rb index fd177db0c..00a3db9f2 100644 --- a/lib/flipper/expression/builder.rb +++ b/lib/flipper/expression/builder.rb @@ -14,7 +14,7 @@ def remove(*expressions) end def any - any? ? self : Expression.build({ Any: [self] }) + any? ? self : Expression.build({ Any: [self] }) end def all diff --git a/lib/flipper/expressions/number.rb b/lib/flipper/expressions/number.rb index 98d133ada..4a9308ed7 100644 --- a/lib/flipper/expressions/number.rb +++ b/lib/flipper/expressions/number.rb @@ -2,8 +2,7 @@ module Flipper module Expressions class Number def self.call(value) - # FIXME: rename to_percentage to to_number, but it does what we want - Flipper::Typecast.to_percentage(value) + Flipper::Typecast.to_number(value) end end end diff --git a/lib/flipper/expressions/string.rb b/lib/flipper/expressions/string.rb new file mode 100644 index 000000000..035f99f50 --- /dev/null +++ b/lib/flipper/expressions/string.rb @@ -0,0 +1,9 @@ +module Flipper + module Expressions + class String + def self.call(value) + value.to_s + end + end + end +end diff --git a/lib/flipper/gate_values.rb b/lib/flipper/gate_values.rb index cc1974044..1d9df3b16 100644 --- a/lib/flipper/gate_values.rb +++ b/lib/flipper/gate_values.rb @@ -15,8 +15,8 @@ def initialize(adapter_values) @actors = Typecast.to_set(adapter_values[:actors]) @groups = Typecast.to_set(adapter_values[:groups]) @expression = adapter_values[:expression] - @percentage_of_actors = Typecast.to_percentage(adapter_values[:percentage_of_actors]) - @percentage_of_time = Typecast.to_percentage(adapter_values[:percentage_of_time]) + @percentage_of_actors = Typecast.to_number(adapter_values[:percentage_of_actors]) + @percentage_of_time = Typecast.to_number(adapter_values[:percentage_of_time]) end def eql?(other) diff --git a/lib/flipper/typecast.rb b/lib/flipper/typecast.rb index 3aea1c35e..44f30c40f 100644 --- a/lib/flipper/typecast.rb +++ b/lib/flipper/typecast.rb @@ -36,17 +36,25 @@ def self.to_float(value) raise ArgumentError, "#{value.inspect} cannot be converted to a float" end - # Internal: Convert value to a percentage. + # Internal: Convert value to a number. # # Returns a Integer or Float representation of the value. # Raises ArgumentError if conversion is not possible. - def self.to_percentage(value) - result_to_f = value.to_f - result_to_i = result_to_f.to_i - result_to_f == result_to_i ? result_to_i : result_to_f + def self.to_number(value) + case value + when Numeric + value + when String + value.include?('.') ? to_float(value) : to_integer(value) + when NilClass + 0 + else + value.to_f + end rescue NoMethodError - raise ArgumentError, "#{value.inspect} cannot be converted to a percentage" + raise ArgumentError, "#{value.inspect} cannot be converted to a number" end + singleton_class.send(:alias_method, :to_percentage, :to_number) # Internal: Convert value to a set. # diff --git a/lib/flipper/types/percentage.rb b/lib/flipper/types/percentage.rb index faaaba997..b7c94c2b0 100644 --- a/lib/flipper/types/percentage.rb +++ b/lib/flipper/types/percentage.rb @@ -4,7 +4,7 @@ module Flipper module Types class Percentage < Type def initialize(value) - value = Typecast.to_percentage(value) + value = Typecast.to_number(value) if value < 0 || value > 100 raise ArgumentError, diff --git a/spec/flipper/adapters/rollout_spec.rb b/spec/flipper/adapters/rollout_spec.rb index 45ddc6ea6..0638f852e 100644 --- a/spec/flipper/adapters/rollout_spec.rb +++ b/spec/flipper/adapters/rollout_spec.rb @@ -118,7 +118,7 @@ expect(feature.boolean_value).to eq(false) expect(feature.actors_value).to eq(Set.new) expect(feature.groups_value).to eq(Set.new) - expect(feature.percentage_of_actors_value).to be(25) + expect(feature.percentage_of_actors_value).to be(25.0) feature = destination_flipper[:verbose_logging] expect(feature.boolean_value).to eq(false) diff --git a/spec/flipper/expression/builder_spec.rb b/spec/flipper/expression/builder_spec.rb index 450896445..e76a6501f 100644 --- a/spec/flipper/expression/builder_spec.rb +++ b/spec/flipper/expression/builder_spec.rb @@ -156,7 +156,7 @@ def build(object) end it "can convert to PercentageOfActors" do - expression = Flipper.string("User;1").percentage_of_actors(40) + expression = Flipper.constant("User;1").percentage_of_actors(40) expect(expression).to eq(build({ PercentageOfActors: [ "User;1", 40 ] })) end diff --git a/spec/flipper/expression_spec.rb b/spec/flipper/expression_spec.rb index b46cbf429..c6fb34ff4 100644 --- a/spec/flipper/expression_spec.rb +++ b/spec/flipper/expression_spec.rb @@ -13,8 +13,8 @@ expect(expression).to be_instance_of(Flipper::Expression) expect(expression.function).to be(Flipper::Expressions::Equal) expect(expression.args).to eq([ - Flipper.string("basic"), - Flipper.string("basic"), + Flipper::Expression::Constant.new("basic"), + Flipper::Expression::Constant.new("basic"), ]) end @@ -29,8 +29,8 @@ expect(expression).to be_instance_of(Flipper::Expression) expect(expression.function).to be(Flipper::Expressions::GreaterThanOrEqualTo) expect(expression.args).to eq([ - Flipper.number(2), - Flipper.number(1), + Flipper::Expression::Constant.new(2), + Flipper::Expression::Constant.new(1), ]) end @@ -45,8 +45,8 @@ expect(expression).to be_instance_of(Flipper::Expression) expect(expression.function).to be(Flipper::Expressions::GreaterThan) expect(expression.args).to eq([ - Flipper.number(2), - Flipper.number(1), + Flipper::Expression::Constant.new(2), + Flipper::Expression::Constant.new(1), ]) end @@ -61,8 +61,8 @@ expect(expression).to be_instance_of(Flipper::Expression) expect(expression.function).to be(Flipper::Expressions::LessThanOrEqualTo) expect(expression.args).to eq([ - Flipper.number(2), - Flipper.number(1), + Flipper::Expression::Constant.new(2), + Flipper::Expression::Constant.new(1), ]) end @@ -74,8 +74,8 @@ expect(expression).to be_instance_of(Flipper::Expression) expect(expression.function).to be(Flipper::Expressions::LessThan) expect(expression.args).to eq([ - Flipper.number(2), - Flipper.number(1), + Flipper::Expression::Constant.new(2), + Flipper::Expression::Constant.new(1), ]) end @@ -90,8 +90,8 @@ expect(expression).to be_instance_of(Flipper::Expression) expect(expression.function).to be(Flipper::Expressions::NotEqual) expect(expression.args).to eq([ - Flipper.string("basic"), - Flipper.string("plus"), + Flipper::Expression::Constant.new("basic"), + Flipper::Expression::Constant.new("plus"), ]) end @@ -109,7 +109,7 @@ expect(expression).to be_instance_of(Flipper::Expression) expect(expression.function).to be(Flipper::Expressions::Percentage) - expect(expression.args).to eq([Flipper.number(1)]) + expect(expression.args).to eq([Flipper::Expression::Constant.new(1)]) end it "can build PercentageOfActors" do @@ -123,8 +123,8 @@ expect(expression).to be_instance_of(Flipper::Expression) expect(expression.function).to be(Flipper::Expressions::PercentageOfActors) expect(expression.args).to eq([ - Flipper.string("User;1"), - Flipper.number(40), + Flipper::Expression::Constant.new("User;1"), + Flipper::Expression::Constant.new(40), ]) end @@ -142,7 +142,7 @@ expect(expression).to be_instance_of(Flipper::Expression) expect(expression.function).to be(Flipper::Expressions::Property) - expect(expression.args).to eq([Flipper.string("flipper_id")]) + expect(expression.args).to eq([Flipper::Expression::Constant.new("flipper_id")]) end end diff --git a/spec/flipper/expressions/boolean_spec.rb b/spec/flipper/expressions/boolean_spec.rb new file mode 100644 index 000000000..c85921e79 --- /dev/null +++ b/spec/flipper/expressions/boolean_spec.rb @@ -0,0 +1,15 @@ +RSpec.describe Flipper::Expressions::Boolean do + describe "#call" do + [true, 'true', 1, '1'].each do |value| + it "returns a true for #{value.inspect}" do + expect(described_class.call(value)).to be(true) + end + end + + [false, 'false', 0, '0', nil].each do |value| + it "returns a true for #{value.inspect}" do + expect(described_class.call(value)).to be(false) + end + end + end +end diff --git a/spec/flipper/expressions/equal_spec.rb b/spec/flipper/expressions/equal_spec.rb index 96caba84b..a2722810d 100644 --- a/spec/flipper/expressions/equal_spec.rb +++ b/spec/flipper/expressions/equal_spec.rb @@ -1,5 +1,5 @@ RSpec.describe Flipper::Expressions::Equal do - describe "#evaluate" do + describe "#call" do it "returns true when equal" do expect(described_class.call("basic", "basic")).to be(true) end diff --git a/spec/flipper/expressions/greater_than_spec.rb b/spec/flipper/expressions/greater_than_spec.rb index 1c5bbcc1a..9a89a858a 100644 --- a/spec/flipper/expressions/greater_than_spec.rb +++ b/spec/flipper/expressions/greater_than_spec.rb @@ -1,5 +1,5 @@ RSpec.describe Flipper::Expressions::GreaterThan do - describe "#evaluate" do + describe "#call" do it "returns false when equal" do expect(described_class.call(2, 2)).to be(false) end diff --git a/spec/flipper/expressions/number_spec.rb b/spec/flipper/expressions/number_spec.rb new file mode 100644 index 000000000..003e61cc9 --- /dev/null +++ b/spec/flipper/expressions/number_spec.rb @@ -0,0 +1,21 @@ +RSpec.describe Flipper::Expressions::Number do + describe "#call" do + it "returns Integer for Integer" do + expect(described_class.call(10)).to be(10) + end + + it "returns Float for Float" do + expect(described_class.call(10.1)).to be(10.1) + expect(described_class.call(10.0)).to be(10.0) + end + + it "returns Integer for String" do + expect(described_class.call('10')).to be(10) + end + + it "returns Float for String" do + expect(described_class.call('10.0')).to be(10.0) + expect(described_class.call('10.1')).to be(10.1) + end + end +end diff --git a/spec/flipper/expressions/percentage_of_actors_spec.rb b/spec/flipper/expressions/percentage_of_actors_spec.rb index 83314e6f4..180a4b881 100644 --- a/spec/flipper/expressions/percentage_of_actors_spec.rb +++ b/spec/flipper/expressions/percentage_of_actors_spec.rb @@ -1,5 +1,5 @@ RSpec.describe Flipper::Expressions::PercentageOfActors do - describe "#evaluate" do + describe "#call" do it "returns true when string in percentage enabled" do expect(described_class.call("User;1", 42)).to be(true) end diff --git a/spec/flipper/expressions/string_spec.rb b/spec/flipper/expressions/string_spec.rb new file mode 100644 index 000000000..9107e33fd --- /dev/null +++ b/spec/flipper/expressions/string_spec.rb @@ -0,0 +1,11 @@ +RSpec.describe Flipper::Expressions::String do + describe "#call" do + it "returns String for Numeric" do + expect(described_class.call(10)).to eq("10") + end + + it "returns String" do + expect(described_class.call("test")).to eq("test") + end + end +end diff --git a/spec/flipper/gate_values_spec.rb b/spec/flipper/gate_values_spec.rb index 308933aff..1875263b3 100644 --- a/spec/flipper/gate_values_spec.rb +++ b/spec/flipper/gate_values_spec.rb @@ -80,13 +80,13 @@ it 'raises argument error for percentage of time value that cannot be converted to an integer' do expect do described_class.new(percentage_of_time: ['asdf']) - end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a percentage)) + end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a number)) end it 'raises argument error for percentage of actors value that cannot be converted to an int' do expect do described_class.new(percentage_of_actors: ['asdf']) - end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a percentage)) + end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a number)) end it 'raises argument error for actors value that cannot be converted to a set' do diff --git a/spec/flipper/typecast_spec.rb b/spec/flipper/typecast_spec.rb index d49e036ae..f47dc77ce 100644 --- a/spec/flipper/typecast_spec.rb +++ b/spec/flipper/typecast_spec.rb @@ -56,7 +56,7 @@ nil => 0, '' => 0, 0 => 0, - 0.0 => 0, + 0.0 => 0.0, 1 => 1, 1.1 => 1.1, '0.01' => 0.01, @@ -65,9 +65,9 @@ '99' => 99, '99.9' => 99.9, }.each do |value, expected| - context "#to_percentage for #{value.inspect}" do + context "#to_number for #{value.inspect}" do it "returns #{expected}" do - expect(described_class.to_percentage(value)).to be(expected) + expect(described_class.to_number(value)).to be(expected) end end end @@ -99,14 +99,14 @@ it 'raises argument error for bad integer percentage' do expect do - described_class.to_percentage(['asdf']) - end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a percentage)) + described_class.to_number(['asdf']) + end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a number)) end it 'raises argument error for bad float percentage' do expect do - described_class.to_percentage(['asdf.0']) - end.to raise_error(ArgumentError, %(["asdf.0"] cannot be converted to a percentage)) + described_class.to_number(['asdf.0']) + end.to raise_error(ArgumentError, %(["asdf.0"] cannot be converted to a number)) end it 'raises argument error for set value that cannot be converted to a set' do diff --git a/spec/flipper_spec.rb b/spec/flipper_spec.rb index 30da20543..ae4a4bd43 100644 --- a/spec/flipper_spec.rb +++ b/spec/flipper_spec.rb @@ -357,21 +357,28 @@ end end + describe ".constant" do + it "returns Flipper::Expression::Constant instance" do + expect(described_class.constant(false)).to eq(Flipper::Expression::Constant.new(false)) + expect(described_class.constant("string")).to eq(Flipper::Expression::Constant.new("string")) + end + end + describe ".property" do - it "returns Flipper::Expressions::Property instance" do + it "returns Flipper::Expressions::Property expression" do expect(Flipper.property("name")).to eq(Flipper::Expression.build(Property: "name")) end end describe ".boolean" do - it "returns Flipper::Expression::Constant instance" do - expect(described_class.boolean(true)).to eq(Flipper::Expression::Constant.new(true)) - expect(described_class.boolean(false)).to eq(Flipper::Expression::Constant.new(false)) + it "returns Flipper::Expressions::Boolean expression" do + expect(described_class.boolean(true)).to eq(Flipper::Expression.build(Boolean: true)) + expect(described_class.boolean(false)).to eq(Flipper::Expression.build(Boolean: false)) end end describe ".random" do - it "returns Flipper::Expressions::Random instance" do + it "returns Flipper::Expressions::Random expression" do expect(Flipper.random(100)).to eq(Flipper::Expression.build(Random: 100)) end end From 4d9a5c9a72ba70c74623744e8f50e432085abb00 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Mon, 27 Mar 2023 11:29:26 -0400 Subject: [PATCH 161/176] Remove unnecessary class reference --- lib/flipper/expression/builder.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/flipper/expression/builder.rb b/lib/flipper/expression/builder.rb index 00a3db9f2..8dc45605f 100644 --- a/lib/flipper/expression/builder.rb +++ b/lib/flipper/expression/builder.rb @@ -14,47 +14,47 @@ def remove(*expressions) end def any - any? ? self : Expression.build({ Any: [self] }) + any? ? self : build({ Any: [self] }) end def all - all? ? self : Expression.build({ All: [self] }) + all? ? self : build({ All: [self] }) end def equal(object) - Expression.build({ Equal: [self, object] }) + build({ Equal: [self, object] }) end alias eq equal def not_equal(object) - Expression.build({ NotEqual: [self, object] }) + build({ NotEqual: [self, object] }) end alias neq not_equal def greater_than(object) - Expression.build({ GreaterThan: [self, object] }) + build({ GreaterThan: [self, object] }) end alias gt greater_than def greater_than_or_equal_to(object) - Expression.build({ GreaterThanOrEqualTo: [self, object] }) + build({ GreaterThanOrEqualTo: [self, object] }) end alias gte greater_than_or_equal_to alias greater_than_or_equal greater_than_or_equal_to def less_than(object) - Expression.build({ LessThan: [self, object] }) + build({ LessThan: [self, object] }) end alias lt less_than def less_than_or_equal_to(object) - Expression.build({ LessThanOrEqualTo: [self, object] }) + build({ LessThanOrEqualTo: [self, object] }) end alias lte less_than_or_equal_to alias less_than_or_equal less_than_or_equal_to def percentage_of_actors(object) - Expression.build({ PercentageOfActors: [self, Expression.build(object)] }) + build({ PercentageOfActors: [self, build(object)] }) end def any? From c690bc4db6047a853f32221aae1df27c9242f89d Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Mon, 27 Mar 2023 11:31:05 -0400 Subject: [PATCH 162/176] Remove unnecessary require --- lib/flipper/expressions/any.rb | 2 -- lib/flipper/expressions/comparable.rb | 2 -- lib/flipper/expressions/duration.rb | 2 -- lib/flipper/expressions/equal.rb | 2 -- lib/flipper/expressions/greater_than.rb | 2 -- lib/flipper/expressions/greater_than_or_equal_to.rb | 2 -- lib/flipper/expressions/less_than.rb | 2 -- lib/flipper/expressions/less_than_or_equal_to.rb | 2 -- lib/flipper/expressions/not_equal.rb | 2 -- lib/flipper/expressions/now.rb | 2 -- lib/flipper/expressions/percentage.rb | 2 -- lib/flipper/expressions/percentage_of_actors.rb | 2 -- lib/flipper/expressions/property.rb | 2 -- lib/flipper/expressions/random.rb | 2 -- lib/flipper/expressions/time.rb | 2 -- 15 files changed, 30 deletions(-) diff --git a/lib/flipper/expressions/any.rb b/lib/flipper/expressions/any.rb index 71cd6d6ed..1af270d09 100644 --- a/lib/flipper/expressions/any.rb +++ b/lib/flipper/expressions/any.rb @@ -1,5 +1,3 @@ -require "flipper/expression" - module Flipper module Expressions class Any diff --git a/lib/flipper/expressions/comparable.rb b/lib/flipper/expressions/comparable.rb index 4620b66f4..631e1b2ba 100644 --- a/lib/flipper/expressions/comparable.rb +++ b/lib/flipper/expressions/comparable.rb @@ -1,5 +1,3 @@ -require "flipper/expression" - module Flipper module Expressions class Comparable diff --git a/lib/flipper/expressions/duration.rb b/lib/flipper/expressions/duration.rb index 7e2207b44..dda95f332 100644 --- a/lib/flipper/expressions/duration.rb +++ b/lib/flipper/expressions/duration.rb @@ -1,5 +1,3 @@ -require "flipper/expression" - module Flipper module Expressions class Duration diff --git a/lib/flipper/expressions/equal.rb b/lib/flipper/expressions/equal.rb index 1f8015fc4..dc7c2ba08 100644 --- a/lib/flipper/expressions/equal.rb +++ b/lib/flipper/expressions/equal.rb @@ -1,5 +1,3 @@ -require "flipper/expression" - module Flipper module Expressions class Equal < Comparable diff --git a/lib/flipper/expressions/greater_than.rb b/lib/flipper/expressions/greater_than.rb index 3a61d68dd..6313a0002 100644 --- a/lib/flipper/expressions/greater_than.rb +++ b/lib/flipper/expressions/greater_than.rb @@ -1,5 +1,3 @@ -require "flipper/expression" - module Flipper module Expressions class GreaterThan < Comparable diff --git a/lib/flipper/expressions/greater_than_or_equal_to.rb b/lib/flipper/expressions/greater_than_or_equal_to.rb index e5d400181..a81d92238 100644 --- a/lib/flipper/expressions/greater_than_or_equal_to.rb +++ b/lib/flipper/expressions/greater_than_or_equal_to.rb @@ -1,5 +1,3 @@ -require "flipper/expression" - module Flipper module Expressions class GreaterThanOrEqualTo < Comparable diff --git a/lib/flipper/expressions/less_than.rb b/lib/flipper/expressions/less_than.rb index 994f28165..dd5da8db5 100644 --- a/lib/flipper/expressions/less_than.rb +++ b/lib/flipper/expressions/less_than.rb @@ -1,5 +1,3 @@ -require "flipper/expression" - module Flipper module Expressions class LessThan < Comparable diff --git a/lib/flipper/expressions/less_than_or_equal_to.rb b/lib/flipper/expressions/less_than_or_equal_to.rb index 162a031f6..4ca9d7411 100644 --- a/lib/flipper/expressions/less_than_or_equal_to.rb +++ b/lib/flipper/expressions/less_than_or_equal_to.rb @@ -1,5 +1,3 @@ -require "flipper/expression" - module Flipper module Expressions class LessThanOrEqualTo < Comparable diff --git a/lib/flipper/expressions/not_equal.rb b/lib/flipper/expressions/not_equal.rb index f46061f87..1090bb282 100644 --- a/lib/flipper/expressions/not_equal.rb +++ b/lib/flipper/expressions/not_equal.rb @@ -1,5 +1,3 @@ -require "flipper/expression" - module Flipper module Expressions class NotEqual < Comparable diff --git a/lib/flipper/expressions/now.rb b/lib/flipper/expressions/now.rb index b083fa66e..0b6b99986 100644 --- a/lib/flipper/expressions/now.rb +++ b/lib/flipper/expressions/now.rb @@ -1,5 +1,3 @@ -require "flipper/expression" - module Flipper module Expressions class Now diff --git a/lib/flipper/expressions/percentage.rb b/lib/flipper/expressions/percentage.rb index e6bc8cab9..ec79713a5 100644 --- a/lib/flipper/expressions/percentage.rb +++ b/lib/flipper/expressions/percentage.rb @@ -1,5 +1,3 @@ -require "flipper/expression" - module Flipper module Expressions class Percentage diff --git a/lib/flipper/expressions/percentage_of_actors.rb b/lib/flipper/expressions/percentage_of_actors.rb index 669cb99b0..869c2a2ad 100644 --- a/lib/flipper/expressions/percentage_of_actors.rb +++ b/lib/flipper/expressions/percentage_of_actors.rb @@ -1,5 +1,3 @@ -require "flipper/expression" - module Flipper module Expressions class PercentageOfActors diff --git a/lib/flipper/expressions/property.rb b/lib/flipper/expressions/property.rb index f467add01..e60ad2435 100644 --- a/lib/flipper/expressions/property.rb +++ b/lib/flipper/expressions/property.rb @@ -1,5 +1,3 @@ -require "flipper/expression" - module Flipper module Expressions class Property diff --git a/lib/flipper/expressions/random.rb b/lib/flipper/expressions/random.rb index 4b8bcd1b9..bfae53c4f 100644 --- a/lib/flipper/expressions/random.rb +++ b/lib/flipper/expressions/random.rb @@ -1,5 +1,3 @@ -require "flipper/expression" - module Flipper module Expressions class Random diff --git a/lib/flipper/expressions/time.rb b/lib/flipper/expressions/time.rb index 5854e38c5..93780749a 100644 --- a/lib/flipper/expressions/time.rb +++ b/lib/flipper/expressions/time.rb @@ -1,5 +1,3 @@ -require "flipper/expression" - module Flipper module Expressions class Time From 311b04008326fea188e5c1f08a55d52a49be99e4 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Mon, 27 Mar 2023 11:34:14 -0400 Subject: [PATCH 163/176] Use Flipper.constant shortcut --- spec/flipper/expression_spec.rb | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/spec/flipper/expression_spec.rb b/spec/flipper/expression_spec.rb index c6fb34ff4..54f9f72ee 100644 --- a/spec/flipper/expression_spec.rb +++ b/spec/flipper/expression_spec.rb @@ -13,8 +13,8 @@ expect(expression).to be_instance_of(Flipper::Expression) expect(expression.function).to be(Flipper::Expressions::Equal) expect(expression.args).to eq([ - Flipper::Expression::Constant.new("basic"), - Flipper::Expression::Constant.new("basic"), + Flipper.constant("basic"), + Flipper.constant("basic"), ]) end @@ -29,8 +29,8 @@ expect(expression).to be_instance_of(Flipper::Expression) expect(expression.function).to be(Flipper::Expressions::GreaterThanOrEqualTo) expect(expression.args).to eq([ - Flipper::Expression::Constant.new(2), - Flipper::Expression::Constant.new(1), + Flipper.constant(2), + Flipper.constant(1), ]) end @@ -45,8 +45,8 @@ expect(expression).to be_instance_of(Flipper::Expression) expect(expression.function).to be(Flipper::Expressions::GreaterThan) expect(expression.args).to eq([ - Flipper::Expression::Constant.new(2), - Flipper::Expression::Constant.new(1), + Flipper.constant(2), + Flipper.constant(1), ]) end @@ -61,8 +61,8 @@ expect(expression).to be_instance_of(Flipper::Expression) expect(expression.function).to be(Flipper::Expressions::LessThanOrEqualTo) expect(expression.args).to eq([ - Flipper::Expression::Constant.new(2), - Flipper::Expression::Constant.new(1), + Flipper.constant(2), + Flipper.constant(1), ]) end @@ -74,8 +74,8 @@ expect(expression).to be_instance_of(Flipper::Expression) expect(expression.function).to be(Flipper::Expressions::LessThan) expect(expression.args).to eq([ - Flipper::Expression::Constant.new(2), - Flipper::Expression::Constant.new(1), + Flipper.constant(2), + Flipper.constant(1), ]) end @@ -90,8 +90,8 @@ expect(expression).to be_instance_of(Flipper::Expression) expect(expression.function).to be(Flipper::Expressions::NotEqual) expect(expression.args).to eq([ - Flipper::Expression::Constant.new("basic"), - Flipper::Expression::Constant.new("plus"), + Flipper.constant("basic"), + Flipper.constant("plus"), ]) end @@ -109,7 +109,7 @@ expect(expression).to be_instance_of(Flipper::Expression) expect(expression.function).to be(Flipper::Expressions::Percentage) - expect(expression.args).to eq([Flipper::Expression::Constant.new(1)]) + expect(expression.args).to eq([Flipper.constant(1)]) end it "can build PercentageOfActors" do @@ -123,8 +123,8 @@ expect(expression).to be_instance_of(Flipper::Expression) expect(expression.function).to be(Flipper::Expressions::PercentageOfActors) expect(expression.args).to eq([ - Flipper::Expression::Constant.new("User;1"), - Flipper::Expression::Constant.new(40), + Flipper.constant("User;1"), + Flipper.constant(40), ]) end @@ -142,7 +142,7 @@ expect(expression).to be_instance_of(Flipper::Expression) expect(expression.function).to be(Flipper::Expressions::Property) - expect(expression.args).to eq([Flipper::Expression::Constant.new("flipper_id")]) + expect(expression.args).to eq([Flipper.constant("flipper_id")]) end end From ba869a14c3b9effec21f93538b20961f464e5d9c Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Mon, 27 Mar 2023 15:57:46 -0400 Subject: [PATCH 164/176] Add to_number to typecast_ips --- benchmark/typecast_ips.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/benchmark/typecast_ips.rb b/benchmark/typecast_ips.rb index e433febb7..dabcefd91 100644 --- a/benchmark/typecast_ips.rb +++ b/benchmark/typecast_ips.rb @@ -16,4 +16,12 @@ x.report("Typecast.to_float '1'") { Flipper::Typecast.to_float('1'.freeze) } x.report("Typecast.to_float 1.01") { Flipper::Typecast.to_float(1) } x.report("Typecast.to_float '1.01'") { Flipper::Typecast.to_float('1'.freeze) } + + x.report("Typecast.to_number 1") { Flipper::Typecast.to_number(1) } + x.report("Typecast.to_number 1.1") { Flipper::Typecast.to_number(1.1) } + x.report("Typecast.to_number '1'") { Flipper::Typecast.to_number('1'.freeze) } + x.report("Typecast.to_number '1.1'") { Flipper::Typecast.to_number('1.1'.freeze) } + x.report("Typecast.to_number nil") { Flipper::Typecast.to_number(nil) } + time = Time.now + x.report("Typecast.to_number Time.now") { Flipper::Typecast.to_number(time) } end From 734399ee54a31637ac0954dc8f739e9aca9b5074 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Mon, 27 Mar 2023 15:59:27 -0400 Subject: [PATCH 165/176] Avoid Hash allocation in Constant --- lib/flipper/expression/constant.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/flipper/expression/constant.rb b/lib/flipper/expression/constant.rb index 1ff4467ab..8171d7dfd 100644 --- a/lib/flipper/expression/constant.rb +++ b/lib/flipper/expression/constant.rb @@ -13,7 +13,7 @@ def initialize(value) @value = value end - def evaluate(context = {}) + def evaluate(_ = nil) value end From fcef13b7fed566bf5dd5d3efd831cc73b05cb461 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Mon, 27 Mar 2023 15:59:50 -0400 Subject: [PATCH 166/176] Fix number formatting --- lib/flipper/expressions/duration.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/flipper/expressions/duration.rb b/lib/flipper/expressions/duration.rb index dda95f332..b1ca50747 100644 --- a/lib/flipper/expressions/duration.rb +++ b/lib/flipper/expressions/duration.rb @@ -7,7 +7,7 @@ class Duration "hour" => 3600, "day" => 86400, "week" => 604_800, - "month" => 26_29_746, # 1/12 of a gregorian year + "month" => 2_629_746, # 1/12 of a gregorian year "year" => 31_556_952 # length of a gregorian year (365.2425 days) }.freeze From 522e8d0aadc9d0b9130428d4de04a5245a1d8948 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Mon, 27 Mar 2023 16:00:01 -0400 Subject: [PATCH 167/176] Remove outdated comment --- lib/flipper/expression/constant.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/flipper/expression/constant.rb b/lib/flipper/expression/constant.rb index 8171d7dfd..35edca9e4 100644 --- a/lib/flipper/expression/constant.rb +++ b/lib/flipper/expression/constant.rb @@ -8,7 +8,6 @@ class Constant attr_reader :value - # Override initialize to avoid trying to build args def initialize(value) @value = value end From c1397ad51ffbd07a249b760d1a56519e76c32633 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Mon, 27 Mar 2023 16:20:55 -0400 Subject: [PATCH 168/176] Merge latest learn-the-rules, fix failing spec --- lib/flipper/spec/shared_adapter_specs.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/flipper/spec/shared_adapter_specs.rb b/lib/flipper/spec/shared_adapter_specs.rb index 34c019fab..2eb8a18ca 100644 --- a/lib/flipper/spec/shared_adapter_specs.rb +++ b/lib/flipper/spec/shared_adapter_specs.rb @@ -281,7 +281,7 @@ expect(result).to be_instance_of(Hash) expect(result["stats"]).to eq(subject.default_config.merge(boolean: 'true')) expect(result["search"]).to eq(subject.default_config) - expect(result["analytics"]).to eq(subject.default_config.merge(expression: {"Equal"=>[{"Property"=>["plan"]}, {"String"=>["pro"]}]})) + expect(result["analytics"]).to eq(subject.default_config.merge(expression: {"Equal"=>[{"Property"=>["plan"]}, "pro"]})) end it 'includes explicitly disabled features when getting all features' do From b1cd631ad52afc6a2238f5538411b33dcc386107 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Mon, 27 Mar 2023 18:36:46 -0400 Subject: [PATCH 169/176] Fix import/export specs --- spec/flipper/api/v1/actions/import_spec.rb | 6 ++++-- spec/flipper/ui/actions/export_spec.rb | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/spec/flipper/api/v1/actions/import_spec.rb b/spec/flipper/api/v1/actions/import_spec.rb index c5c0ba93c..a36ade543 100644 --- a/spec/flipper/api/v1/actions/import_spec.rb +++ b/spec/flipper/api/v1/actions/import_spec.rb @@ -2,6 +2,8 @@ let(:app) { build_api(flipper) } describe 'post' do + let(:expression) { Flipper.property(:plan).eq("basic") } + context 'succesful request' do before do flipper.enable(:search) @@ -10,7 +12,7 @@ source_flipper = build_flipper source_flipper.disable(:search) source_flipper.enable_actor(:google_analytics, Flipper::Actor.new("User;1")) - source_flipper.enable(:analytics, Flipper.property(:plan).eq("basic")) + source_flipper.enable(:analytics, expression) export = source_flipper.export @@ -24,7 +26,7 @@ it 'imports features' do expect(flipper[:search].boolean_value).to be(false) expect(flipper[:google_analytics].actors_value).to eq(Set["User;1"]) - expect(flipper[:analytics].expression_value).to eq({"Equal"=>[{"Property"=>["plan"]}, {"String"=>["basic"]}]}) + expect(flipper[:analytics].expression_value).to eq(expression.value) expect(flipper.features.map(&:key)).to eq(["search", "google_analytics", "analytics"]) end end diff --git a/spec/flipper/ui/actions/export_spec.rb b/spec/flipper/ui/actions/export_spec.rb index c9a8c67b7..62034afac 100644 --- a/spec/flipper/ui/actions/export_spec.rb +++ b/spec/flipper/ui/actions/export_spec.rb @@ -41,7 +41,7 @@ expect(last_response.headers['Content-Type']).to eq('application/json') expect(data['version']).to eq(1) expect(data['features']).to eq({ - "analytics" => {"boolean"=>nil, "expression"=>{"Equal"=>[{"Property"=>["plan"]}, {"String"=>["basic"]}]}, "groups"=>[], "actors"=>[], "percentage_of_actors"=>nil, "percentage_of_time"=>nil}, + "analytics" => {"boolean"=>nil, "expression"=>{"Equal"=>[{"Property"=>["plan"]}, "basic"]}, "groups"=>[], "actors"=>[], "percentage_of_actors"=>nil, "percentage_of_time"=>nil}, "search"=> {"boolean"=>nil, "expression"=>nil, "groups"=>["admins", "employees"], "actors"=>["User;1", "User;100"], "percentage_of_actors"=>"10", "percentage_of_time"=>"15"}, "plausible"=> {"boolean"=>"true", "expression"=>nil, "groups"=>[], "actors"=>[], "percentage_of_actors"=>nil, "percentage_of_time"=>nil}, "google_analytics"=> {"boolean"=>nil, "expression"=>nil, "groups"=>[], "actors"=>[], "percentage_of_actors"=>nil, "percentage_of_time"=>nil}, From e6f8041148e9c4bfa6129a884819fe4f0797fcb1 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Mon, 27 Mar 2023 18:38:49 -0400 Subject: [PATCH 170/176] Fix adapter spec --- lib/flipper/test/shared_adapter_test.rb | 2 +- spec/flipper/typecast_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/flipper/test/shared_adapter_test.rb b/lib/flipper/test/shared_adapter_test.rb index db66281dd..2dbf8e99b 100644 --- a/lib/flipper/test/shared_adapter_test.rb +++ b/lib/flipper/test/shared_adapter_test.rb @@ -278,7 +278,7 @@ def test_can_get_all_features assert_instance_of Hash, result assert_equal @adapter.default_config.merge(boolean: 'true'), result["stats"] assert_equal @adapter.default_config, result["search"] - assert_equal @adapter.default_config.merge(expression: {"Equal"=>[{"Property"=>["plan"]}, {"String"=>["pro"]}]}), result["analytics"] + assert_equal @adapter.default_config.merge(expression: {"Equal"=>[{"Property"=>["plan"]}, "pro"]}), result["analytics"] end def test_includes_explicitly_disabled_features_when_getting_all_features diff --git a/spec/flipper/typecast_spec.rb b/spec/flipper/typecast_spec.rb index bcdb3a8b7..7a1c7179c 100644 --- a/spec/flipper/typecast_spec.rb +++ b/spec/flipper/typecast_spec.rb @@ -131,7 +131,7 @@ hash = { "search" => { boolean: nil, - expression: {"Equal"=>[{"Property"=>["plan"]}, {"String"=>["basic"]}]}, + expression: {"Equal"=>[{"Property"=>["plan"]}, "basic"]}, groups: ['a', 'b'], actors: ['User;1'], percentage_of_actors: nil, @@ -142,7 +142,7 @@ expect(result).to eq({ "search" => { boolean: nil, - expression: {"Equal"=>[{"Property"=>["plan"]}, {"String"=>["basic"]}]}, + expression: {"Equal"=>[{"Property"=>["plan"]}, "basic"]}, groups: Set['a', 'b'], actors: Set['User;1'], percentage_of_actors: nil, From 64f95b83dc9cfffb07d42e28a4ccf183b4757fdc Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Mon, 27 Mar 2023 19:05:55 -0400 Subject: [PATCH 171/176] Fix issue with Ruby 2.6 and kwargs --- lib/flipper/expression.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/flipper/expression.rb b/lib/flipper/expression.rb index 0aa597148..8d08988c0 100644 --- a/lib/flipper/expression.rb +++ b/lib/flipper/expression.rb @@ -38,8 +38,11 @@ def initialize(name, args = []) end def evaluate(context = {}) - kwargs = { context: (context if call_with_context?) }.compact - function.call(*args.map {|arg| arg.evaluate(context) }, **kwargs) + if call_with_context? + function.call(*args.map {|arg| arg.evaluate(context) }, context: context) + else + function.call(*args.map {|arg| arg.evaluate(context) }) + end end def eql?(other) From d86e9f34d843ed71a7bb96cc27d974aabd2a3a8e Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Mon, 27 Mar 2023 19:36:15 -0400 Subject: [PATCH 172/176] Ensure Now expression returns in utc --- lib/flipper/expressions/now.rb | 2 +- spec/flipper/expressions/now_spec.rb | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/flipper/expressions/now.rb b/lib/flipper/expressions/now.rb index 0b6b99986..4a513d03d 100644 --- a/lib/flipper/expressions/now.rb +++ b/lib/flipper/expressions/now.rb @@ -2,7 +2,7 @@ module Flipper module Expressions class Now def self.call - ::Time.now + ::Time.now.utc end end end diff --git a/spec/flipper/expressions/now_spec.rb b/spec/flipper/expressions/now_spec.rb index e7ea9f545..dcb77358b 100644 --- a/spec/flipper/expressions/now_spec.rb +++ b/spec/flipper/expressions/now_spec.rb @@ -3,5 +3,9 @@ it "returns current time" do expect(described_class.call.round).to eq(Time.now.round) end + + it "defaults to UTC" do + expect(described_class.call.zone).to eq("UTC") + end end end From 56cbdaa35a4f4dee21faccf1d18648afbcd64de3 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Wed, 29 Mar 2023 11:51:19 -0400 Subject: [PATCH 173/176] Allow time to be flexible Co-authored-by: John Nunemaker --- spec/flipper/expressions/now_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/flipper/expressions/now_spec.rb b/spec/flipper/expressions/now_spec.rb index dcb77358b..95868f2bd 100644 --- a/spec/flipper/expressions/now_spec.rb +++ b/spec/flipper/expressions/now_spec.rb @@ -1,7 +1,7 @@ RSpec.describe Flipper::Expressions::Now do describe "#call" do it "returns current time" do - expect(described_class.call.round).to eq(Time.now.round) + expect(described_class.call).to be_within(2).of(Time.now.utc) end it "defaults to UTC" do From 3755d8d8f4ed1799b94d490013c0826b7dbb6f6c Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Mon, 17 Jul 2023 09:30:56 -0400 Subject: [PATCH 174/176] Restore json support to Redis adapter after bad merge --- lib/flipper/adapters/redis.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/flipper/adapters/redis.rb b/lib/flipper/adapters/redis.rb index 9b6037f19..6dfacb9c1 100644 --- a/lib/flipper/adapters/redis.rb +++ b/lib/flipper/adapters/redis.rb @@ -97,6 +97,8 @@ def enable(feature, gate, thing) @client.hset feature_key, gate.key, thing.value.to_s when :set @client.hset feature_key, to_field(gate, thing), 1 + when :json + @client.hset feature_key, gate.key, JSON.dump(thing.value) else unsupported_data_type gate.data_type end @@ -120,6 +122,8 @@ def disable(feature, gate, thing) @client.hset feature_key, gate.key, thing.value.to_s when :set @client.hdel feature_key, to_field(gate, thing) + when :json + @client.hdel feature_key, gate.key else unsupported_data_type gate.data_type end From 16831bc7a45a297b607aae5b9573609b665af56d Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Mon, 17 Jul 2023 10:21:47 -0400 Subject: [PATCH 175/176] change_column :flipper_gates, :text --- lib/generators/flipper/templates/migration.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/generators/flipper/templates/migration.erb b/lib/generators/flipper/templates/migration.erb index 1b6d0e3df..f51e80ffc 100644 --- a/lib/generators/flipper/templates/migration.erb +++ b/lib/generators/flipper/templates/migration.erb @@ -9,7 +9,7 @@ class CreateFlipperTables < ActiveRecord::Migration<%= migration_version %> create_table :flipper_gates do |t| t.string :feature_key, null: false t.string :key, null: false - t.string :value + t.text :value t.timestamps null: false end add_index :flipper_gates, [:feature_key, :key, :value], unique: true From 189df7f86787d2b3f533346c16c9392cb73e341d Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Mon, 17 Jul 2023 10:44:31 -0400 Subject: [PATCH 176/176] Update changelog --- Changelog.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Changelog.md b/Changelog.md index c8917168e..072fb6923 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,12 +2,11 @@ All notable changes to this project will be documented in this file. -## 0.28.1 +## Unreleased ### Additions/Changes -* Use new method of making logs bold for rails (https://github.com/jnunemaker/flipper/pull/726) -* Bundle bootstrap, jquery and poppler with the library. (https://github.com/jnunemaker/flipper/pull/731) +* Expressions are now available and considered "alpha". They are not yet documented, but you can see examples in [examples/expressions.rb](examples/expressions.rb). (https://github.com/jnunemaker/flipper/pull/692) ### Breaking Changes @@ -20,6 +19,13 @@ All notable changes to this project will be documented in this file. * Change Flipper.percentage_of_time => Flipper::Types::PercentageOfTime.new * Change Flipper.time => Flipper::Types::PercentageOfTime.new +## 0.28.1 + +### Additions/Changes + +* Use new method of making logs bold for rails (https://github.com/jnunemaker/flipper/pull/726) +* Bundle bootstrap, jquery and poppler with the library. (https://github.com/jnunemaker/flipper/pull/731) + ## 0.28.0 ### Additions/Changes