Skip to content

Commit 25680a4

Browse files
authored
feat: adds InMemoryProvider (#102)
Signed-off-by: Max VelDink <[email protected]>
1 parent db48cd5 commit 25680a4

File tree

10 files changed

+262
-16
lines changed

10 files changed

+262
-16
lines changed

Gemfile.lock

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,15 @@ GEM
99
ast (2.4.2)
1010
base64 (0.1.1)
1111
concurrent-ruby (1.2.3)
12+
debug (1.9.1)
13+
irb (~> 1.10)
14+
reline (>= 0.3.8)
1215
diff-lcs (1.5.0)
1316
docile (1.4.0)
17+
io-console (0.7.2)
18+
irb (1.11.2)
19+
rdoc
20+
reline (>= 0.4.2)
1421
json (2.6.3)
1522
language_server-protocol (3.17.0.3)
1623
lint_roller (1.1.0)
@@ -19,10 +26,16 @@ GEM
1926
parser (3.2.2.3)
2027
ast (~> 2.4.1)
2128
racc
29+
psych (5.1.2)
30+
stringio
2231
racc (1.7.1)
2332
rainbow (3.1.1)
2433
rake (13.0.6)
34+
rdoc (6.6.2)
35+
psych (>= 4.0.0)
2536
regexp_parser (2.8.1)
37+
reline (0.4.3)
38+
io-console (~> 0.5)
2639
rexml (3.2.6)
2740
rspec (3.12.0)
2841
rspec-core (~> 3.12.0)
@@ -76,6 +89,7 @@ GEM
7689
standard-performance (1.2.0)
7790
lint_roller (~> 1.1)
7891
rubocop-performance (~> 1.19.0)
92+
stringio (3.1.0)
7993
unicode-display_width (2.4.2)
8094

8195
PLATFORMS
@@ -91,6 +105,7 @@ PLATFORMS
91105

92106
DEPENDENCIES
93107
concurrent-ruby
108+
debug
94109
markly
95110
openfeature-sdk!
96111
rake (~> 13.0)

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,12 @@ require 'json' # For JSON.dump
4343

4444
OpenFeature::SDK.configure do |config|
4545
# your provider of choice
46-
config.provider = OpenFeature::SDK::Provider::NoOpProvider.new
46+
config.provider = OpenFeature::SDK::Provider::InMemoryProvider.new(
47+
{
48+
"flag1" => true,
49+
"flag2" => 1
50+
}
51+
)
4752
end
4853

4954
# Create a client
@@ -69,7 +74,7 @@ For complete documentation, visit: https://openfeature.dev/docs/category/concept
6974

7075
Providers are the abstraction layer between OpenFeature and different flag management systems.
7176

72-
The `NoOpProvider` is an example of a minimalist provider. For complete documentation on the Provider interface, visit: https://openfeature.dev/specification/sections/providers.
77+
The `NoOpProvider` is an example of a minimalist provider. The `InMemoryProvider` is a provider that can be initialized with flags and used to store flags in process. For complete documentation on the Provider interface, visit: https://openfeature.dev/specification/sections/providers.
7378

7479
In addition to the `fetch_*` methods, providers can optionally implement lifecycle methods that are invoked when the underlying provider is switched out. For example:
7580

lib/open_feature/sdk/provider.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
require_relative "provider/reason"
33
require_relative "provider/resolution_details"
44
require_relative "provider/no_op_provider"
5+
require_relative "provider/in_memory_provider"
56

67
module OpenFeature
78
module SDK
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
module OpenFeature
2+
module SDK
3+
module Provider
4+
# TODO: Add evaluation context support
5+
class InMemoryProvider
6+
NAME = "In-memory Provider"
7+
8+
def initialize(flags = {})
9+
@metadata = Metadata.new(name: NAME).freeze
10+
@flags = flags
11+
end
12+
13+
def init
14+
# Intentional no-op, used for testing
15+
end
16+
17+
def shutdown
18+
# Intentional no-op, used for testing
19+
end
20+
21+
def add_flag(flag_key:, value:)
22+
flags[flag_key] = value
23+
# TODO: Emit PROVIDER_CONFIGURATION_CHANGED event once events are implemented
24+
end
25+
26+
def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
27+
fetch_value(allowed_classes: [TrueClass, FalseClass], flag_key:, default_value:, evaluation_context:)
28+
end
29+
30+
def fetch_string_value(flag_key:, default_value:, evaluation_context: nil)
31+
fetch_value(allowed_classes: [String], flag_key:, default_value:, evaluation_context:)
32+
end
33+
34+
def fetch_number_value(flag_key:, default_value:, evaluation_context: nil)
35+
fetch_value(allowed_classes: [Integer, Float], flag_key:, default_value:, evaluation_context:)
36+
end
37+
38+
def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
39+
fetch_value(allowed_classes: [Array, Hash], flag_key:, default_value:, evaluation_context:)
40+
end
41+
42+
private
43+
44+
attr_reader :flags
45+
46+
def fetch_value(allowed_classes:, flag_key:, default_value:, evaluation_context:)
47+
value = flags[flag_key]
48+
49+
if value.nil?
50+
return ResolutionDetails.new(value: default_value, error_code: ErrorCode::FLAG_NOT_FOUND, reason: Reason::ERROR)
51+
end
52+
53+
if allowed_classes.include?(value.class)
54+
ResolutionDetails.new(value:, reason: Reason::STATIC)
55+
else
56+
ResolutionDetails.new(value: default_value, error_code: ErrorCode::TYPE_MISMATCH, reason: Reason::ERROR)
57+
end
58+
end
59+
end
60+
end
61+
end
62+
end

openfeature-sdk.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Gem::Specification.new do |spec|
3131
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
3232
spec.require_paths = ["lib"]
3333

34+
spec.add_development_dependency "debug"
3435
spec.add_development_dependency "markly"
3536
spec.add_development_dependency "rake", "~> 13.0"
3637
spec.add_development_dependency "rspec", "~> 3.12.0"

spec/open_feature/sdk/configuration_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
describe "#provider=" do
99
context "when provider has an init method" do
10-
let(:provider) { TestProvider.new }
10+
let(:provider) { OpenFeature::SDK::Provider::InMemoryProvider.new }
1111

1212
it "inits and sets the provider" do
1313
expect(provider).to receive(:init)
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
require "spec_helper"
2+
3+
RSpec.describe OpenFeature::SDK::Provider::InMemoryProvider do
4+
subject(:provider) do
5+
described_class.new(
6+
{
7+
"bool" => true,
8+
"str" => "testing",
9+
"num" => 1,
10+
"struct" => {"more" => "config"}
11+
}
12+
)
13+
end
14+
15+
describe "#add_flag" do
16+
context "when flag doesn't exist" do
17+
it "adds flag" do
18+
provider.add_flag(flag_key: "new_flag", value: "new_value")
19+
20+
fetched = provider.fetch_string_value(flag_key: "new_flag", default_value: "fallback")
21+
22+
expect(fetched.value).to eq("new_value")
23+
end
24+
end
25+
26+
context "when flag exists" do
27+
it "updates flag" do
28+
provider.add_flag(flag_key: "bool", value: false)
29+
30+
fetched = provider.fetch_boolean_value(flag_key: "bool", default_value: true)
31+
32+
expect(fetched.value).to eq(false)
33+
end
34+
end
35+
end
36+
37+
describe "#fetch_boolean_value" do
38+
context "when flag is found" do
39+
context "when type matches" do
40+
it "returns value as static" do
41+
fetched = provider.fetch_boolean_value(flag_key: "bool", default_value: false)
42+
43+
expect(fetched.value).to eq(true)
44+
expect(fetched.reason).to eq(OpenFeature::SDK::Provider::Reason::STATIC)
45+
end
46+
end
47+
48+
context "when type does not match" do
49+
it "returns default as type mismatch" do
50+
fetched = provider.fetch_boolean_value(flag_key: "str", default_value: false)
51+
52+
expect(fetched.value).to eq(false)
53+
expect(fetched.error_code).to eq(OpenFeature::SDK::Provider::ErrorCode::TYPE_MISMATCH)
54+
expect(fetched.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR)
55+
end
56+
end
57+
end
58+
59+
context "when flag is not found" do
60+
it "returns default as flag not found" do
61+
fetched = provider.fetch_boolean_value(flag_key: "not here", default_value: false)
62+
63+
expect(fetched.value).to eq(false)
64+
expect(fetched.error_code).to eq(OpenFeature::SDK::Provider::ErrorCode::FLAG_NOT_FOUND)
65+
expect(fetched.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR)
66+
end
67+
end
68+
end
69+
70+
describe "#fetch_string_value" do
71+
context "when flag is found" do
72+
context "when type matches" do
73+
it "returns value as static" do
74+
fetched = provider.fetch_string_value(flag_key: "str", default_value: "fallback")
75+
76+
expect(fetched.value).to eq("testing")
77+
expect(fetched.reason).to eq(OpenFeature::SDK::Provider::Reason::STATIC)
78+
end
79+
end
80+
81+
context "when type does not match" do
82+
it "returns default as type mismatch" do
83+
fetched = provider.fetch_string_value(flag_key: "bool", default_value: "fallback")
84+
85+
expect(fetched.value).to eq("fallback")
86+
expect(fetched.error_code).to eq(OpenFeature::SDK::Provider::ErrorCode::TYPE_MISMATCH)
87+
expect(fetched.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR)
88+
end
89+
end
90+
end
91+
92+
context "when flag is not found" do
93+
it "returns default as flag not found" do
94+
fetched = provider.fetch_string_value(flag_key: "not here", default_value: "fallback")
95+
96+
expect(fetched.value).to eq("fallback")
97+
expect(fetched.error_code).to eq(OpenFeature::SDK::Provider::ErrorCode::FLAG_NOT_FOUND)
98+
expect(fetched.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR)
99+
end
100+
end
101+
end
102+
103+
describe "#fetch_number_value" do
104+
context "when flag is found" do
105+
context "when type matches" do
106+
it "returns value as static" do
107+
fetched = provider.fetch_number_value(flag_key: "num", default_value: 0)
108+
109+
expect(fetched.value).to eq(1)
110+
expect(fetched.reason).to eq(OpenFeature::SDK::Provider::Reason::STATIC)
111+
end
112+
end
113+
114+
context "when type does not match" do
115+
it "returns default as type mismatch" do
116+
fetched = provider.fetch_number_value(flag_key: "str", default_value: 0)
117+
118+
expect(fetched.value).to eq(0)
119+
expect(fetched.error_code).to eq(OpenFeature::SDK::Provider::ErrorCode::TYPE_MISMATCH)
120+
expect(fetched.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR)
121+
end
122+
end
123+
end
124+
125+
context "when flag is not found" do
126+
it "returns default as flag not found" do
127+
fetched = provider.fetch_number_value(flag_key: "not here", default_value: 0)
128+
129+
expect(fetched.value).to eq(0)
130+
expect(fetched.error_code).to eq(OpenFeature::SDK::Provider::ErrorCode::FLAG_NOT_FOUND)
131+
expect(fetched.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR)
132+
end
133+
end
134+
end
135+
136+
describe "#fetch_object_value" do
137+
context "when flag is found" do
138+
context "when type matches" do
139+
it "returns value as static" do
140+
fetched = provider.fetch_object_value(flag_key: "struct", default_value: {})
141+
142+
expect(fetched.value).to eq({"more" => "config"})
143+
expect(fetched.reason).to eq(OpenFeature::SDK::Provider::Reason::STATIC)
144+
end
145+
end
146+
147+
context "when type does not match" do
148+
it "returns default as type mismatch" do
149+
fetched = provider.fetch_object_value(flag_key: "num", default_value: {})
150+
151+
expect(fetched.value).to eq({})
152+
expect(fetched.error_code).to eq(OpenFeature::SDK::Provider::ErrorCode::TYPE_MISMATCH)
153+
expect(fetched.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR)
154+
end
155+
end
156+
end
157+
158+
context "when flag is not found" do
159+
it "returns default as flag not found" do
160+
fetched = provider.fetch_object_value(flag_key: "not here", default_value: {})
161+
162+
expect(fetched.value).to eq({})
163+
expect(fetched.error_code).to eq(OpenFeature::SDK::Provider::ErrorCode::FLAG_NOT_FOUND)
164+
expect(fetched.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR)
165+
end
166+
end
167+
end
168+
end

spec/spec_helper.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
require "markly"
88

9+
require "debug"
10+
911
RSpec.configure do |config|
1012
# Enable flags like --only-failures and --next-failure
1113
config.example_status_persistence_file_path = ".rspec_status"
@@ -17,6 +19,8 @@
1719
c.syntax = :expect
1820
end
1921

22+
config.filter_run_when_matching :focus
23+
2024
# ie for GitHub Actions
2125
# see https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables
2226
if ENV["CI"] == "true"

spec/specification/flag_evaluation_api_spec.rb

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# frozen_string_literal: true
22

33
require "spec_helper"
4-
require_relative "../support/test_provider"
54

65
RSpec.describe "Flag Evaluation API" do
76
context "1.1 - API Initialization and Configuration" do
@@ -23,7 +22,7 @@
2322

2423
context "Requirement 1.1.2.2" do
2524
specify "the provider mutator must invoke an initialize function on the provider" do
26-
provider = TestProvider.new
25+
provider = OpenFeature::SDK::Provider::InMemoryProvider.new
2726
expect(provider).to receive(:init)
2827

2928
OpenFeature::SDK.provider = provider
@@ -32,8 +31,8 @@
3231

3332
context "Requirement 1.1.2.3" do
3433
specify "the provider mutator must invoke a shutdown function on previously registered provider" do
35-
previous_provider = TestProvider.new
36-
new_provider = TestProvider.new
34+
previous_provider = OpenFeature::SDK::Provider::InMemoryProvider.new
35+
new_provider = OpenFeature::SDK::Provider::InMemoryProvider.new
3736

3837
expect(previous_provider).to receive(:shutdown)
3938
expect(new_provider).not_to receive(:shutdown)

spec/support/test_provider.rb

Lines changed: 0 additions & 9 deletions
This file was deleted.

0 commit comments

Comments
 (0)