Skip to content

Commit 0a148f6

Browse files
josecolellaclaude
andauthored
feat: add InMemoryProvider context callbacks and event emission (#224)
Signed-off-by: Jose Colella <jose.colella@gusto.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f9c32ad commit 0a148f6

3 files changed

Lines changed: 115 additions & 16 deletions

File tree

lib/open_feature/sdk/provider/in_memory_provider.rb

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
module OpenFeature
44
module SDK
55
module Provider
6-
# TODO: Add evaluation context support
76
class InMemoryProvider
7+
include Provider::EventEmitter
8+
89
NAME = "In-memory Provider"
910

1011
attr_reader :metadata
@@ -24,7 +25,12 @@ def shutdown
2425

2526
def add_flag(flag_key:, value:)
2627
flags[flag_key] = value
27-
# TODO: Emit PROVIDER_CONFIGURATION_CHANGED event once events are implemented
28+
emit_provider_changed([flag_key])
29+
end
30+
31+
def update_flags(new_flags)
32+
@flags = new_flags.dup
33+
emit_provider_changed(new_flags.keys)
2834
end
2935

3036
def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
@@ -56,13 +62,24 @@ def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
5662
attr_reader :flags
5763

5864
def fetch_value(flag_key:, default_value:, evaluation_context:)
59-
value = flags[flag_key]
65+
raw_value = flags[flag_key]
6066

61-
if value.nil?
67+
if raw_value.nil?
6268
return ResolutionDetails.new(value: default_value, error_code: ErrorCode::FLAG_NOT_FOUND, reason: Reason::ERROR)
6369
end
6470

65-
ResolutionDetails.new(value:, reason: Reason::STATIC)
71+
if raw_value.respond_to?(:call)
72+
value = raw_value.call(evaluation_context)
73+
ResolutionDetails.new(value: value, reason: Reason::TARGETING_MATCH)
74+
else
75+
ResolutionDetails.new(value: raw_value, reason: Reason::STATIC)
76+
end
77+
end
78+
79+
def emit_provider_changed(flag_keys)
80+
return unless configuration_attached?
81+
82+
emit_event(ProviderEvent::PROVIDER_CONFIGURATION_CHANGED, flags_changed: flag_keys)
6683
end
6784
end
6885
end

spec/open_feature/sdk/provider/in_memory_provider_spec.rb

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,94 @@
3535
expect(fetched.value).to eq(false)
3636
end
3737
end
38+
39+
context "when attached to configuration" do
40+
it "emits PROVIDER_CONFIGURATION_CHANGED event" do
41+
config = instance_double("OpenFeature::SDK::Configuration")
42+
allow(config).to receive(:dispatch_provider_event)
43+
provider.send(:attach, config)
44+
45+
provider.add_flag(flag_key: "new_flag", value: "new_value")
46+
47+
expect(config).to have_received(:dispatch_provider_event).with(
48+
provider,
49+
OpenFeature::SDK::ProviderEvent::PROVIDER_CONFIGURATION_CHANGED,
50+
flags_changed: ["new_flag"]
51+
)
52+
end
53+
end
54+
55+
context "when not attached to configuration" do
56+
it "does not emit events" do
57+
expect { provider.add_flag(flag_key: "new_flag", value: "new_value") }.not_to raise_error
58+
end
59+
end
60+
end
61+
62+
describe "#update_flags" do
63+
it "replaces all flags" do
64+
provider.update_flags({"new_key" => "new_value"})
65+
66+
fetched = provider.fetch_string_value(flag_key: "new_key", default_value: "fallback")
67+
expect(fetched.value).to eq("new_value")
68+
69+
old_fetched = provider.fetch_string_value(flag_key: "str", default_value: "fallback")
70+
expect(old_fetched.value).to eq("fallback")
71+
expect(old_fetched.error_code).to eq(OpenFeature::SDK::Provider::ErrorCode::FLAG_NOT_FOUND)
72+
end
73+
74+
context "when attached to configuration" do
75+
it "emits PROVIDER_CONFIGURATION_CHANGED event with all flag keys" do
76+
config = instance_double("OpenFeature::SDK::Configuration")
77+
allow(config).to receive(:dispatch_provider_event)
78+
provider.send(:attach, config)
79+
80+
provider.update_flags({"flag_a" => true, "flag_b" => false})
81+
82+
expect(config).to have_received(:dispatch_provider_event).with(
83+
provider,
84+
OpenFeature::SDK::ProviderEvent::PROVIDER_CONFIGURATION_CHANGED,
85+
flags_changed: contain_exactly("flag_a", "flag_b")
86+
)
87+
end
88+
end
89+
90+
context "when not attached to configuration" do
91+
it "does not emit events" do
92+
expect { provider.update_flags({"flag_a" => true}) }.not_to raise_error
93+
end
94+
end
95+
end
96+
97+
describe "context callbacks (callable flag values)" do
98+
it "calls a Proc flag value with evaluation_context" do
99+
context = OpenFeature::SDK::EvaluationContext.new(targeting_key: "user-123")
100+
flag_proc = proc { |ctx| ctx.targeting_key == "user-123" }
101+
provider = described_class.new({"dynamic_flag" => flag_proc})
102+
103+
fetched = provider.fetch_boolean_value(flag_key: "dynamic_flag", default_value: false, evaluation_context: context)
104+
105+
expect(fetched.value).to eq(true)
106+
expect(fetched.reason).to eq(OpenFeature::SDK::Provider::Reason::TARGETING_MATCH)
107+
end
108+
109+
it "calls a lambda flag value with evaluation_context" do
110+
context = OpenFeature::SDK::EvaluationContext.new(targeting_key: "user-456")
111+
flag_lambda = ->(ctx) { "hello-#{ctx.targeting_key}" }
112+
provider = described_class.new({"greeting" => flag_lambda})
113+
114+
fetched = provider.fetch_string_value(flag_key: "greeting", default_value: "default", evaluation_context: context)
115+
116+
expect(fetched.value).to eq("hello-user-456")
117+
expect(fetched.reason).to eq(OpenFeature::SDK::Provider::Reason::TARGETING_MATCH)
118+
end
119+
120+
it "returns STATIC reason for non-callable values" do
121+
fetched = provider.fetch_string_value(flag_key: "str", default_value: "fallback")
122+
123+
expect(fetched.value).to eq("testing")
124+
expect(fetched.reason).to eq(OpenFeature::SDK::Provider::Reason::STATIC)
125+
end
38126
end
39127

40128
describe "#fetch_boolean_value" do

spec/open_feature/sdk/provider_compatibility_spec.rb

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@
3232
expect { provider.shutdown }.not_to raise_error
3333
end
3434

35-
it "does not automatically gain event capabilities" do
36-
expect(provider).not_to respond_to(:attach)
37-
expect(provider).not_to respond_to(:emit_event)
35+
it "has event capabilities via EventEmitter" do
36+
expect(provider).to respond_to(:emit_event)
37+
expect(provider).to respond_to(:configuration_attached?)
3838
end
3939

4040
it "fetch methods continue to work" do
@@ -63,13 +63,6 @@
6363
end
6464

6565
RSpec.describe "Provider Interface Detection" do
66-
# Create a test provider that implements the new interfaces
67-
let(:event_capable_provider) do
68-
Class.new(OpenFeature::SDK::Provider::InMemoryProvider) do
69-
include OpenFeature::SDK::Provider::EventEmitter
70-
end.new
71-
end
72-
7366
it "can check if provider implements lifecycle methods using duck typing" do
7467
noop_provider = OpenFeature::SDK::Provider::NoOpProvider.new
7568
inmemory_provider = OpenFeature::SDK::Provider::InMemoryProvider.new
@@ -81,9 +74,10 @@
8174

8275
it "can check if provider implements EventEmitter" do
8376
noop_provider = OpenFeature::SDK::Provider::NoOpProvider.new
77+
inmemory_provider = OpenFeature::SDK::Provider::InMemoryProvider.new
8478

8579
# Check using is_a? with module
8680
expect(noop_provider.class.included_modules).not_to include(OpenFeature::SDK::Provider::EventEmitter)
87-
expect(event_capable_provider.class.included_modules).to include(OpenFeature::SDK::Provider::EventEmitter)
81+
expect(inmemory_provider.class.included_modules).to include(OpenFeature::SDK::Provider::EventEmitter)
8882
end
8983
end

0 commit comments

Comments
 (0)