Skip to content

Commit

Permalink
feat: adds InMemoryProvider (#102)
Browse files Browse the repository at this point in the history
Signed-off-by: Max VelDink <maxveldink@gmail.com>
  • Loading branch information
maxveldink authored Mar 6, 2024
1 parent db48cd5 commit 25680a4
Show file tree
Hide file tree
Showing 10 changed files with 262 additions and 16 deletions.
15 changes: 15 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@ GEM
ast (2.4.2)
base64 (0.1.1)
concurrent-ruby (1.2.3)
debug (1.9.1)
irb (~> 1.10)
reline (>= 0.3.8)
diff-lcs (1.5.0)
docile (1.4.0)
io-console (0.7.2)
irb (1.11.2)
rdoc
reline (>= 0.4.2)
json (2.6.3)
language_server-protocol (3.17.0.3)
lint_roller (1.1.0)
Expand All @@ -19,10 +26,16 @@ GEM
parser (3.2.2.3)
ast (~> 2.4.1)
racc
psych (5.1.2)
stringio
racc (1.7.1)
rainbow (3.1.1)
rake (13.0.6)
rdoc (6.6.2)
psych (>= 4.0.0)
regexp_parser (2.8.1)
reline (0.4.3)
io-console (~> 0.5)
rexml (3.2.6)
rspec (3.12.0)
rspec-core (~> 3.12.0)
Expand Down Expand Up @@ -76,6 +89,7 @@ GEM
standard-performance (1.2.0)
lint_roller (~> 1.1)
rubocop-performance (~> 1.19.0)
stringio (3.1.0)
unicode-display_width (2.4.2)

PLATFORMS
Expand All @@ -91,6 +105,7 @@ PLATFORMS

DEPENDENCIES
concurrent-ruby
debug
markly
openfeature-sdk!
rake (~> 13.0)
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,12 @@ require 'json' # For JSON.dump

OpenFeature::SDK.configure do |config|
# your provider of choice
config.provider = OpenFeature::SDK::Provider::NoOpProvider.new
config.provider = OpenFeature::SDK::Provider::InMemoryProvider.new(
{
"flag1" => true,
"flag2" => 1
}
)
end

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

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

The `NoOpProvider` is an example of a minimalist provider. For complete documentation on the Provider interface, visit: https://openfeature.dev/specification/sections/providers.
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.

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

Expand Down
1 change: 1 addition & 0 deletions lib/open_feature/sdk/provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
require_relative "provider/reason"
require_relative "provider/resolution_details"
require_relative "provider/no_op_provider"
require_relative "provider/in_memory_provider"

module OpenFeature
module SDK
Expand Down
62 changes: 62 additions & 0 deletions lib/open_feature/sdk/provider/in_memory_provider.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
module OpenFeature
module SDK
module Provider
# TODO: Add evaluation context support
class InMemoryProvider
NAME = "In-memory Provider"

def initialize(flags = {})
@metadata = Metadata.new(name: NAME).freeze
@flags = flags
end

def init
# Intentional no-op, used for testing
end

def shutdown
# Intentional no-op, used for testing
end

def add_flag(flag_key:, value:)
flags[flag_key] = value
# TODO: Emit PROVIDER_CONFIGURATION_CHANGED event once events are implemented
end

def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
fetch_value(allowed_classes: [TrueClass, FalseClass], flag_key:, default_value:, evaluation_context:)
end

def fetch_string_value(flag_key:, default_value:, evaluation_context: nil)
fetch_value(allowed_classes: [String], flag_key:, default_value:, evaluation_context:)
end

def fetch_number_value(flag_key:, default_value:, evaluation_context: nil)
fetch_value(allowed_classes: [Integer, Float], flag_key:, default_value:, evaluation_context:)
end

def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
fetch_value(allowed_classes: [Array, Hash], flag_key:, default_value:, evaluation_context:)
end

private

attr_reader :flags

def fetch_value(allowed_classes:, flag_key:, default_value:, evaluation_context:)
value = flags[flag_key]

if value.nil?
return ResolutionDetails.new(value: default_value, error_code: ErrorCode::FLAG_NOT_FOUND, reason: Reason::ERROR)
end

if allowed_classes.include?(value.class)
ResolutionDetails.new(value:, reason: Reason::STATIC)
else
ResolutionDetails.new(value: default_value, error_code: ErrorCode::TYPE_MISMATCH, reason: Reason::ERROR)
end
end
end
end
end
end
1 change: 1 addition & 0 deletions openfeature-sdk.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Gem::Specification.new do |spec|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]

spec.add_development_dependency "debug"
spec.add_development_dependency "markly"
spec.add_development_dependency "rake", "~> 13.0"
spec.add_development_dependency "rspec", "~> 3.12.0"
Expand Down
2 changes: 1 addition & 1 deletion spec/open_feature/sdk/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

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

it "inits and sets the provider" do
expect(provider).to receive(:init)
Expand Down
168 changes: 168 additions & 0 deletions spec/open_feature/sdk/provider/in_memory_provider_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
require "spec_helper"

RSpec.describe OpenFeature::SDK::Provider::InMemoryProvider do
subject(:provider) do
described_class.new(
{
"bool" => true,
"str" => "testing",
"num" => 1,
"struct" => {"more" => "config"}
}
)
end

describe "#add_flag" do
context "when flag doesn't exist" do
it "adds flag" do
provider.add_flag(flag_key: "new_flag", value: "new_value")

fetched = provider.fetch_string_value(flag_key: "new_flag", default_value: "fallback")

expect(fetched.value).to eq("new_value")
end
end

context "when flag exists" do
it "updates flag" do
provider.add_flag(flag_key: "bool", value: false)

fetched = provider.fetch_boolean_value(flag_key: "bool", default_value: true)

expect(fetched.value).to eq(false)
end
end
end

describe "#fetch_boolean_value" do
context "when flag is found" do
context "when type matches" do
it "returns value as static" do
fetched = provider.fetch_boolean_value(flag_key: "bool", default_value: false)

expect(fetched.value).to eq(true)
expect(fetched.reason).to eq(OpenFeature::SDK::Provider::Reason::STATIC)
end
end

context "when type does not match" do
it "returns default as type mismatch" do
fetched = provider.fetch_boolean_value(flag_key: "str", default_value: false)

expect(fetched.value).to eq(false)
expect(fetched.error_code).to eq(OpenFeature::SDK::Provider::ErrorCode::TYPE_MISMATCH)
expect(fetched.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR)
end
end
end

context "when flag is not found" do
it "returns default as flag not found" do
fetched = provider.fetch_boolean_value(flag_key: "not here", default_value: false)

expect(fetched.value).to eq(false)
expect(fetched.error_code).to eq(OpenFeature::SDK::Provider::ErrorCode::FLAG_NOT_FOUND)
expect(fetched.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR)
end
end
end

describe "#fetch_string_value" do
context "when flag is found" do
context "when type matches" do
it "returns value as static" do
fetched = provider.fetch_string_value(flag_key: "str", default_value: "fallback")

expect(fetched.value).to eq("testing")
expect(fetched.reason).to eq(OpenFeature::SDK::Provider::Reason::STATIC)
end
end

context "when type does not match" do
it "returns default as type mismatch" do
fetched = provider.fetch_string_value(flag_key: "bool", default_value: "fallback")

expect(fetched.value).to eq("fallback")
expect(fetched.error_code).to eq(OpenFeature::SDK::Provider::ErrorCode::TYPE_MISMATCH)
expect(fetched.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR)
end
end
end

context "when flag is not found" do
it "returns default as flag not found" do
fetched = provider.fetch_string_value(flag_key: "not here", default_value: "fallback")

expect(fetched.value).to eq("fallback")
expect(fetched.error_code).to eq(OpenFeature::SDK::Provider::ErrorCode::FLAG_NOT_FOUND)
expect(fetched.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR)
end
end
end

describe "#fetch_number_value" do
context "when flag is found" do
context "when type matches" do
it "returns value as static" do
fetched = provider.fetch_number_value(flag_key: "num", default_value: 0)

expect(fetched.value).to eq(1)
expect(fetched.reason).to eq(OpenFeature::SDK::Provider::Reason::STATIC)
end
end

context "when type does not match" do
it "returns default as type mismatch" do
fetched = provider.fetch_number_value(flag_key: "str", default_value: 0)

expect(fetched.value).to eq(0)
expect(fetched.error_code).to eq(OpenFeature::SDK::Provider::ErrorCode::TYPE_MISMATCH)
expect(fetched.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR)
end
end
end

context "when flag is not found" do
it "returns default as flag not found" do
fetched = provider.fetch_number_value(flag_key: "not here", default_value: 0)

expect(fetched.value).to eq(0)
expect(fetched.error_code).to eq(OpenFeature::SDK::Provider::ErrorCode::FLAG_NOT_FOUND)
expect(fetched.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR)
end
end
end

describe "#fetch_object_value" do
context "when flag is found" do
context "when type matches" do
it "returns value as static" do
fetched = provider.fetch_object_value(flag_key: "struct", default_value: {})

expect(fetched.value).to eq({"more" => "config"})
expect(fetched.reason).to eq(OpenFeature::SDK::Provider::Reason::STATIC)
end
end

context "when type does not match" do
it "returns default as type mismatch" do
fetched = provider.fetch_object_value(flag_key: "num", default_value: {})

expect(fetched.value).to eq({})
expect(fetched.error_code).to eq(OpenFeature::SDK::Provider::ErrorCode::TYPE_MISMATCH)
expect(fetched.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR)
end
end
end

context "when flag is not found" do
it "returns default as flag not found" do
fetched = provider.fetch_object_value(flag_key: "not here", default_value: {})

expect(fetched.value).to eq({})
expect(fetched.error_code).to eq(OpenFeature::SDK::Provider::ErrorCode::FLAG_NOT_FOUND)
expect(fetched.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR)
end
end
end
end
4 changes: 4 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

require "markly"

require "debug"

RSpec.configure do |config|
# Enable flags like --only-failures and --next-failure
config.example_status_persistence_file_path = ".rspec_status"
Expand All @@ -17,6 +19,8 @@
c.syntax = :expect
end

config.filter_run_when_matching :focus

# ie for GitHub Actions
# see https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables
if ENV["CI"] == "true"
Expand Down
7 changes: 3 additions & 4 deletions spec/specification/flag_evaluation_api_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# frozen_string_literal: true

require "spec_helper"
require_relative "../support/test_provider"

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

context "Requirement 1.1.2.2" do
specify "the provider mutator must invoke an initialize function on the provider" do
provider = TestProvider.new
provider = OpenFeature::SDK::Provider::InMemoryProvider.new
expect(provider).to receive(:init)

OpenFeature::SDK.provider = provider
Expand All @@ -32,8 +31,8 @@

context "Requirement 1.1.2.3" do
specify "the provider mutator must invoke a shutdown function on previously registered provider" do
previous_provider = TestProvider.new
new_provider = TestProvider.new
previous_provider = OpenFeature::SDK::Provider::InMemoryProvider.new
new_provider = OpenFeature::SDK::Provider::InMemoryProvider.new

expect(previous_provider).to receive(:shutdown)
expect(new_provider).not_to receive(:shutdown)
Expand Down
9 changes: 0 additions & 9 deletions spec/support/test_provider.rb

This file was deleted.

0 comments on commit 25680a4

Please sign in to comment.