diff --git a/examples/expressions.rb b/examples/expressions.rb index 6b4c1c692..bbb836371 100644 --- a/examples/expressions.rb +++ b/examples/expressions.rb @@ -31,13 +31,9 @@ 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, { @@ -46,7 +42,6 @@ class Org < Struct.new(:id, :flipper_properties) "plan" => "basic", "age" => 39, "team_user" => true, - "now" => NOW, }) admin_user = User.new(2, { @@ -54,7 +49,6 @@ class Org < Struct.new(:id, :flipper_properties) "id" => 2, "admin" => true, "team_user" => true, - "now" => NOW, }) other_user = User.new(3, { @@ -63,7 +57,6 @@ class Org < Struct.new(:id, :flipper_properties) "plan" => "plus", "age" => 18, "org_admin" => true, - "now" => NOW - DAY, }) age_expression = Flipper.property(:age).gte(21) @@ -205,9 +198,40 @@ class Org < Struct.new(:id, :flipper_properties) assert Flipper.enabled?(:something, admin_user) refute Flipper.enabled?(:something, other_user) -puts "\n\nEnabling based on time" -scheduled_time_expression = Flipper.property(:now).gte(NOW) -Flipper.enable :something, scheduled_time_expression +puts "\n\nEnabling based on time (epoch)" +scheduled_epoch = Flipper.now.gte(Flipper.time(Time.now.to_i - 86_400)) +Flipper.enable :something, scheduled_epoch assert Flipper.enabled?(:something, user) assert Flipper.enabled?(:something, admin_user) -refute Flipper.enabled?(:something, other_user) +assert Flipper.enabled?(:something, other_user) +reset + +puts "\n\nEnabling based on time (datetime)" +past_time = (Time.now.utc - 86_400).iso8601 +scheduled_datetime = Flipper.now.gte(Flipper.time(past_time)) +Flipper.enable :something, scheduled_datetime +assert Flipper.enabled?(:something, user) +assert Flipper.enabled?(:something, admin_user) +assert Flipper.enabled?(:something, other_user) +reset + +puts "\n\nDisabling after a time (expiring feature)" +future_time = (Time.now.utc + 86_400).iso8601 +expiring_expression = Flipper.now.lt(Flipper.time(future_time)) +Flipper.enable :something, expiring_expression +assert Flipper.enabled?(:something, user) +assert Flipper.enabled?(:something, admin_user) +assert Flipper.enabled?(:something, other_user) +reset + +puts "\n\nEnabling within a time window" +start_time = (Time.now.utc - 86_400).iso8601 +end_time = (Time.now.utc + 86_400).iso8601 +time_window = Flipper.all( + Flipper.now.gte(Flipper.time(start_time)), + Flipper.now.lt(Flipper.time(end_time)) +) +Flipper.enable :something, time_window +assert Flipper.enabled?(:something, user) +assert Flipper.enabled?(:something, admin_user) +assert Flipper.enabled?(:something, other_user) diff --git a/lib/flipper.rb b/lib/flipper.rb index af9c7c271..c5546abcf 100644 --- a/lib/flipper.rb +++ b/lib/flipper.rb @@ -100,6 +100,18 @@ def random(max) Expression.build({ Random: max }) end + def now + Expression.build({ Now: [] }) + end + + def time(value) + Expression.build({ Time: value }) + end + + def duration(scalar, unit = 'second') + Expression.build({ Duration: [scalar, unit] }) + end + def feature_enabled(name) Expression.build({ FeatureEnabled: name }) end diff --git a/lib/flipper/expressions/time.rb b/lib/flipper/expressions/time.rb index 93780749a..3d54ffe76 100644 --- a/lib/flipper/expressions/time.rb +++ b/lib/flipper/expressions/time.rb @@ -1,8 +1,15 @@ +require "time" + module Flipper module Expressions class Time def self.call(value) - ::Time.parse(value) + case value + when Numeric + ::Time.at(value).utc + else + ::Time.parse(value) + end end end end diff --git a/spec/flipper/expressions/time_spec.rb b/spec/flipper/expressions/time_spec.rb index 55674cd08..6775ee0c8 100644 --- a/spec/flipper/expressions/time_spec.rb +++ b/spec/flipper/expressions/time_spec.rb @@ -9,5 +9,13 @@ it "returns time for #iso8601 format" do expect(described_class.call(time.iso8601)).to eq(time) end + + it "returns time for epoch integer" do + expect(described_class.call(time.to_i)).to eq(Time.at(time.to_i).utc) + end + + it "returns time for epoch float" do + expect(described_class.call(time.to_f)).to be_within(0.001).of(time) + end end end diff --git a/spec/flipper/gates/expression_spec.rb b/spec/flipper/gates/expression_spec.rb index b35523ab2..5a19d9691 100644 --- a/spec/flipper/gates/expression_spec.rb +++ b/spec/flipper/gates/expression_spec.rb @@ -99,6 +99,64 @@ def context(expression, properties: {}) expect(subject.open?(context)).to be(false) end end + + context 'for time-based expressions' do + it 'enables when now is past a scheduled epoch' do + past_epoch = Time.now.to_i - 86_400 + expression = Flipper.now.gte(Flipper.time(past_epoch)) + expect(subject.open?(context(expression.value))).to be(true) + end + + it 'does not enable when now is before a future epoch' do + future_epoch = Time.now.to_i + 86_400 + expression = Flipper.now.gte(Flipper.time(future_epoch)) + expect(subject.open?(context(expression.value))).to be(false) + end + + it 'enables when now is past a scheduled datetime' do + past_time = (Time.now.utc - 86_400).iso8601 + expression = Flipper.now.gte(Flipper.time(past_time)) + expect(subject.open?(context(expression.value))).to be(true) + end + + it 'does not enable when now is before a future datetime' do + future_time = (Time.now.utc + 86_400).iso8601 + expression = Flipper.now.gte(Flipper.time(future_time)) + expect(subject.open?(context(expression.value))).to be(false) + end + + it 'enables expiring features with lt' do + future_time = (Time.now.utc + 86_400).iso8601 + expression = Flipper.now.lt(Flipper.time(future_time)) + expect(subject.open?(context(expression.value))).to be(true) + end + + it 'disables expired features with lt' do + past_time = (Time.now.utc - 86_400).iso8601 + expression = Flipper.now.lt(Flipper.time(past_time)) + expect(subject.open?(context(expression.value))).to be(false) + end + + it 'enables within a time window using all' do + start_time = (Time.now.utc - 86_400).iso8601 + end_time = (Time.now.utc + 86_400).iso8601 + expression = Flipper.all( + Flipper.now.gte(Flipper.time(start_time)), + Flipper.now.lt(Flipper.time(end_time)) + ) + expect(subject.open?(context(expression.value))).to be(true) + end + + it 'does not enable outside a time window' do + start_time = (Time.now.utc + 86_400).iso8601 + end_time = (Time.now.utc + 172_800).iso8601 + expression = Flipper.all( + Flipper.now.gte(Flipper.time(start_time)), + Flipper.now.lt(Flipper.time(end_time)) + ) + expect(subject.open?(context(expression.value))).to be(false) + end + end end describe '#protects?' do