Skip to content

Commit

Permalink
feat: Add MetaProvider gem and basic implementation (#17)
Browse files Browse the repository at this point in the history
Signed-off-by: Max VelDink <mveldink@justworks.com>
Signed-off-by: Alexandre Chakroun <alexandre.chakroun@doctolib.com>
Co-authored-by: Alexandre Chakroun <11556013+alxckn@users.noreply.github.com>
  • Loading branch information
maxveldink and alxckn authored Apr 16, 2024
1 parent 80d6d02 commit 61b8c10
Show file tree
Hide file tree
Showing 18 changed files with 514 additions and 2 deletions.
28 changes: 26 additions & 2 deletions .github/workflows/ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@ jobs:
working-directory: ./providers/openfeature-flagd-provider
strategy:
matrix:
ruby-version: ['3.1']
ruby-version:
- "3.3"
- "3.2"
- "3.1"
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
Expand All @@ -38,3 +41,24 @@ jobs:
working-directory: ./providers/openfeature-flagd-provider/docker
- name: Run tests
run: bundle exec rspec
test_meta_provider:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./providers/openfeature-sdk-meta_provider
strategy:
matrix:
ruby-version:
- "3.3"
- "3.2"
- "3.1"
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby-version }}
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
working-directory: ./providers/openfeature-sdk-meta_provider
- name: Lint and test
run: bin/rake
12 changes: 12 additions & 0 deletions providers/openfeature-sdk-meta_provider/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/.bundle/
/.yardoc
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/

# rspec failure tracking
.rspec_status
spec/examples.txt
4 changes: 4 additions & 0 deletions providers/openfeature-sdk-meta_provider/.rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-I lib
--format documentation
--color
--require spec_helper
1 change: 1 addition & 0 deletions providers/openfeature-sdk-meta_provider/.ruby-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.3.0
3 changes: 3 additions & 0 deletions providers/openfeature-sdk-meta_provider/.standard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
parallel: true
format: progress
ruby_version: 3.1
1 change: 1 addition & 0 deletions providers/openfeature-sdk-meta_provider/.tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ruby 3.3.0
6 changes: 6 additions & 0 deletions providers/openfeature-sdk-meta_provider/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true

source "https://rubygems.org"

# Specify your gem's dependencies in openfeature-sdk-meta_provider.gemspec
gemspec
95 changes: 95 additions & 0 deletions providers/openfeature-sdk-meta_provider/Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
PATH
remote: .
specs:
openfeature-sdk-meta_provider (0.1.0)
openfeature-sdk (~> 0.3.0)

GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
debug (1.9.2)
irb (~> 1.10)
reline (>= 0.3.8)
diff-lcs (1.5.1)
io-console (0.7.2)
irb (1.12.0)
rdoc
reline (>= 0.4.2)
json (2.7.2)
language_server-protocol (3.17.0.3)
lint_roller (1.1.0)
openfeature-sdk (0.3.0)
parallel (1.24.0)
parser (3.3.0.5)
ast (~> 2.4.1)
racc
psych (5.1.2)
stringio
racc (1.7.3)
rainbow (3.1.1)
rake (13.2.1)
rdoc (6.6.3.1)
psych (>= 4.0.0)
regexp_parser (2.9.0)
reline (0.5.0)
io-console (~> 0.5)
rexml (3.2.6)
rspec (3.13.0)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0)
rspec-core (3.13.0)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-support (3.13.1)
rubocop (1.62.1)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.31.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.31.2)
parser (>= 3.3.0.4)
rubocop-performance (1.20.2)
rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.30.0, < 2.0)
ruby-progressbar (1.13.0)
standard (1.35.1)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.0)
rubocop (~> 1.62.0)
standard-custom (~> 1.0.0)
standard-performance (~> 1.3)
standard-custom (1.0.2)
lint_roller (~> 1.0)
rubocop (~> 1.50)
standard-performance (1.3.1)
lint_roller (~> 1.1)
rubocop-performance (~> 1.20.2)
stringio (3.1.0)
unicode-display_width (2.5.0)

PLATFORMS
arm64-darwin-23
ruby

DEPENDENCIES
debug (~> 1.9.2)
openfeature-sdk-meta_provider!
rake (~> 13.0)
rspec (~> 3.12)
standard (~> 1.34)

BUNDLED WITH
2.5.6
38 changes: 38 additions & 0 deletions providers/openfeature-sdk-meta_provider/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# OpenFeature::SDK::Provider::MetaProvider for Ruby

The `OpenFeature::SDK::Provider::MetaProvider` is a utility provider implementation that takes multiple [providers](https://docs.openfeature.dev/docs/specification/sections/providers) for use during flag resolution. This can be helpful when an organization is migrating or consolidating feature flag providers as they transition to OpenFeature. There are usually a combination of internal and vendor providers that are combined together to handle flag resolution. If your organization has different providers for different teams, consider looking at using [domains](https://openfeature.dev/specification/glossary#domain).

## Installation

Coming soon!

## Usage

The `MetaProvider` is initialized with a collection of `Provider`s and a strategy for fetching flags from them.

```ruby
# Create a MetaProvider
meta_provider = OpenFeature::SDK::Provider::MetaProvider.new(
providers: [
OpenFeature::SDK::ProviderInMemoryProvider.new,
MyCustomProvider.new
],
strategy: :first_match
)

# Use it as the default provider
OpenFeature.configure do |c|
c.set_provider(meta_provider)
end
```

### Strategies

#### :first_match

When `:first_match` is given as the strategy, each provider will be evaluated, in the order they were passed in, for the requested `flag_key`. The first provider where the `flag_key` is found will be returned, short-circuiting flag evaluation with the remaining providers. In the case of a provider error, or no matching flags, returns the default value.


## Contributing

https://github.com/open-feature/ruby-sdk-contrib
10 changes: 10 additions & 0 deletions providers/openfeature-sdk-meta_provider/Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

require "bundler/gem_tasks"
require "rspec/core/rake_task"

RSpec::Core::RakeTask.new(:spec)

require "standard/rake"

task default: %i[standard spec]
15 changes: 15 additions & 0 deletions providers/openfeature-sdk-meta_provider/bin/console
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require "bundler/setup"
require "open_feature/sdk/provider/meta_provider"

# You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like.

# (If you use this, don't forget to add pry to your Gemfile!)
# require "pry"
# Pry.start

require "irb"
IRB.start(__FILE__)
27 changes: 27 additions & 0 deletions providers/openfeature-sdk-meta_provider/bin/rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

#
# This file was generated by Bundler.
#
# The application 'rake' is installed as part of a gem, and
# this file is here to facilitate running it.
#

ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)

bundle_binstub = File.expand_path("bundle", __dir__)

if File.file?(bundle_binstub)
if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
load(bundle_binstub)
else
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
end
end

require "rubygems"
require "bundler/setup"

load Gem.bin_path("rake", "rake")
8 changes: 8 additions & 0 deletions providers/openfeature-sdk-meta_provider/bin/setup
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
set -vx

bundle install

# Do any other automated setup that you need to do here
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# frozen_string_literal: true

module OpenFeature
module SDK
module Provider
# Used to pull from multiple providers.
class MetaProvider
# @param providers [Array<Provider>]
# @param strategy [Symbol] When `:first_match`, returns first matched resolution. Providers will be searched
# in the order they were given. Defaults to `:first_match`.
def initialize(providers:, strategy: :first_match)
@providers = providers
@strategy = strategy
end

def metadata
ProviderMetadata.new(name: "MetaProvider: #{providers.map { |provider| provider.metadata.name }.join(", ")}")
end

def init
providers.each { |provider| provider.init }
end

def shutdown
providers.each(&:shutdown)
end

def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
fetch_from_sources(default_value:) do |provider|
provider.fetch_boolean_value(flag_key:, default_value:, evaluation_context:)
end
end

def fetch_number_value(flag_key:, default_value:, evaluation_context: nil)
fetch_from_sources(default_value:) do |provider|
provider.fetch_number_value(flag_key:, default_value:, evaluation_context:)
end
end

def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
fetch_from_sources(default_value:) do |provider|
provider.fetch_object_value(flag_key:, default_value:, evaluation_context:)
end
end

def fetch_string_value(flag_key:, default_value:, evaluation_context: nil)
fetch_from_sources(default_value:) do |provider|
provider.fetch_string_value(flag_key:, default_value:, evaluation_context:)
end
end

private

attr_reader :providers, :strategy

def fetch_from_sources(default_value:, &blk)
case strategy
when :first_match
successful_details = providers.each do |provider|
details = yield(provider)

break details if details.error_code.nil?
rescue
next
end

if successful_details.is_a?(ResolutionDetails)
successful_details
else
ResolutionDetails.new(value: default_value, error_code: ErrorCode::GENERAL, reason: Reason::ERROR)
end
else
ResolutionDetails.new(value: default_value, error_code: ErrorCode::GENERAL, reason: "Unknown strategy for MetaProvider")
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

Gem::Specification.new do |spec|
spec.name = "openfeature-sdk-meta_provider"
spec.version = "0.1.0"
spec.authors = ["OpenFeature Authors"]
spec.email = ["cncf-openfeature-contributors@lists.cncf.io"]

spec.summary = "Meta provider for the OpenFeature Ruby SDK"
spec.description = "The MetaProvider wraps multiple other providers and uses a given strategy to resolve flags using all of them."
spec.homepage = "https://github.com/open-feature/ruby-sdk-contrib/providers/openfeature-sdk-meta_provider"
spec.license = "Apache-2.0"
spec.required_ruby_version = ">= 3.1"

spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = "https://github.com/open-feature/ruby-sdk-contrib/providers/openfeature-sdk-meta_provider"
spec.metadata["changelog_uri"] = "https://github.com/open-feature/ruby-sdk-contrib/blob/main/CHANGELOG.md"
spec.metadata["bug_tracker_uri"] = "https://github.com/open-feature/ruby-sdk-contrib/issues"
spec.metadata["documentation_uri"] = "https://github.com/open-feature/ruby-sdk-contrib/README.md"

# Specify which files should be added to the gem when it is released.
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
spec.files = Dir.chdir(File.expand_path(__dir__)) do
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
end
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]

spec.add_dependency "openfeature-sdk", "~> 0.3.0"

spec.add_development_dependency "rake", "~> 13.0"
spec.add_development_dependency "rspec", "~> 3.12"
spec.add_development_dependency "standard", "~> 1.34"
spec.add_development_dependency "debug", "~> 1.9.2"
end
Loading

0 comments on commit 61b8c10

Please sign in to comment.