From 9818e1e6ae458f3ae090303e9bb5960f13f95a21 Mon Sep 17 00:00:00 2001 From: Firdaus Al Ghifari Date: Wed, 9 Apr 2025 10:51:22 +0900 Subject: [PATCH 1/8] feat: Add initial Flipt Provider Signed-off-by: Firdaus Al Ghifari --- .../openfeature-flipt-provider/.gitignore | 11 ++ providers/openfeature-flipt-provider/.rspec | 4 + .../openfeature-flipt-provider/.rubocop.yml | 5 + .../openfeature-flipt-provider/.ruby-version | 1 + .../openfeature-flipt-provider/CHANGELOG.md | 0 providers/openfeature-flipt-provider/Gemfile | 6 + .../openfeature-flipt-provider/Gemfile.lock | 108 +++++++++++ .../openfeature-flipt-provider/README.md | 50 +++++ providers/openfeature-flipt-provider/Rakefile | 10 + .../openfeature-flipt-provider/bin/console | 11 ++ providers/openfeature-flipt-provider/bin/rake | 27 +++ .../openfeature-flipt-provider/bin/setup | 8 + .../lib/openfeature/flipt/provider.rb | 172 ++++++++++++++++++ .../lib/openfeature/flipt/version.rb | 7 + .../openfeature-flipt-provider.gemspec | 40 ++++ .../spec/spec_helper.rb | 23 +++ 16 files changed, 483 insertions(+) create mode 100644 providers/openfeature-flipt-provider/.gitignore create mode 100644 providers/openfeature-flipt-provider/.rspec create mode 100644 providers/openfeature-flipt-provider/.rubocop.yml create mode 100644 providers/openfeature-flipt-provider/.ruby-version create mode 100644 providers/openfeature-flipt-provider/CHANGELOG.md create mode 100644 providers/openfeature-flipt-provider/Gemfile create mode 100644 providers/openfeature-flipt-provider/Gemfile.lock create mode 100644 providers/openfeature-flipt-provider/README.md create mode 100644 providers/openfeature-flipt-provider/Rakefile create mode 100755 providers/openfeature-flipt-provider/bin/console create mode 100755 providers/openfeature-flipt-provider/bin/rake create mode 100755 providers/openfeature-flipt-provider/bin/setup create mode 100644 providers/openfeature-flipt-provider/lib/openfeature/flipt/provider.rb create mode 100644 providers/openfeature-flipt-provider/lib/openfeature/flipt/version.rb create mode 100644 providers/openfeature-flipt-provider/openfeature-flipt-provider.gemspec create mode 100644 providers/openfeature-flipt-provider/spec/spec_helper.rb diff --git a/providers/openfeature-flipt-provider/.gitignore b/providers/openfeature-flipt-provider/.gitignore new file mode 100644 index 0000000..b04a8c8 --- /dev/null +++ b/providers/openfeature-flipt-provider/.gitignore @@ -0,0 +1,11 @@ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ + +# rspec failure tracking +.rspec_status diff --git a/providers/openfeature-flipt-provider/.rspec b/providers/openfeature-flipt-provider/.rspec new file mode 100644 index 0000000..44b132b --- /dev/null +++ b/providers/openfeature-flipt-provider/.rspec @@ -0,0 +1,4 @@ +-I lib +--format documentation +--color +--require spec_helper diff --git a/providers/openfeature-flipt-provider/.rubocop.yml b/providers/openfeature-flipt-provider/.rubocop.yml new file mode 100644 index 0000000..feec135 --- /dev/null +++ b/providers/openfeature-flipt-provider/.rubocop.yml @@ -0,0 +1,5 @@ +inherit_from: ../../shared_config/.rubocop.yml + +inherit_mode: + merge: + - Exclude diff --git a/providers/openfeature-flipt-provider/.ruby-version b/providers/openfeature-flipt-provider/.ruby-version new file mode 100644 index 0000000..fa7adc7 --- /dev/null +++ b/providers/openfeature-flipt-provider/.ruby-version @@ -0,0 +1 @@ +3.3.5 diff --git a/providers/openfeature-flipt-provider/CHANGELOG.md b/providers/openfeature-flipt-provider/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/providers/openfeature-flipt-provider/Gemfile b/providers/openfeature-flipt-provider/Gemfile new file mode 100644 index 0000000..3db0240 --- /dev/null +++ b/providers/openfeature-flipt-provider/Gemfile @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +# Specify your gem's dependencies in openfeature-flipt-provider.gemspec +gemspec diff --git a/providers/openfeature-flipt-provider/Gemfile.lock b/providers/openfeature-flipt-provider/Gemfile.lock new file mode 100644 index 0000000..c28b351 --- /dev/null +++ b/providers/openfeature-flipt-provider/Gemfile.lock @@ -0,0 +1,108 @@ +PATH + remote: . + specs: + openfeature-flipt-provider (0.1.0) + ffi (~> 1.17) + flipt_client (~> 0.10.0) + openfeature-sdk (~> 0.4.0) + +GEM + remote: https://rubygems.org/ + specs: + ast (2.4.3) + diff-lcs (1.6.1) + ffi (1.17.1) + ffi (1.17.1-aarch64-linux-gnu) + ffi (1.17.1-aarch64-linux-musl) + ffi (1.17.1-arm-linux-gnu) + ffi (1.17.1-arm-linux-musl) + ffi (1.17.1-arm64-darwin) + ffi (1.17.1-x86-linux-gnu) + ffi (1.17.1-x86-linux-musl) + ffi (1.17.1-x86_64-darwin) + ffi (1.17.1-x86_64-linux-gnu) + ffi (1.17.1-x86_64-linux-musl) + flipt_client (0.10.0) + json (2.10.2) + language_server-protocol (3.17.0.4) + lint_roller (1.1.0) + openfeature-sdk (0.4.0) + parallel (1.26.3) + parser (3.3.7.3) + ast (~> 2.4.1) + racc + prism (1.4.0) + racc (1.8.1) + rainbow (3.1.1) + rake (13.2.1) + regexp_parser (2.10.0) + rspec (3.12.0) + rspec-core (~> 3.12.0) + rspec-expectations (~> 3.12.0) + rspec-mocks (~> 3.12.0) + rspec-core (3.12.3) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.4) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.7) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-support (3.12.2) + rubocop (1.73.2) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.38.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.43.0) + parser (>= 3.3.7.2) + prism (~> 1.4) + rubocop-performance (1.24.0) + lint_roller (~> 1.1) + rubocop (>= 1.72.1, < 2.0) + rubocop-ast (>= 1.38.0, < 2.0) + ruby-progressbar (1.13.0) + standard (1.47.0) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.73.0) + standard-custom (~> 1.0.0) + standard-performance (~> 1.7) + standard-custom (1.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.50) + standard-performance (1.7.0) + lint_roller (~> 1.1) + rubocop-performance (~> 1.24.0) + unicode-display_width (3.1.4) + unicode-emoji (~> 4.0, >= 4.0.4) + unicode-emoji (4.0.4) + +PLATFORMS + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin + ruby + x86-linux-gnu + x86-linux-musl + x86_64-darwin + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + openfeature-flipt-provider! + rake (~> 13.0) + rspec (~> 3.12.0) + rubocop + standard (>= 1.35.1) + +BUNDLED WITH + 2.5.16 diff --git a/providers/openfeature-flipt-provider/README.md b/providers/openfeature-flipt-provider/README.md new file mode 100644 index 0000000..97ef2f6 --- /dev/null +++ b/providers/openfeature-flipt-provider/README.md @@ -0,0 +1,50 @@ +

+ flipt logo +

+ +# Flipt - OpenFeature Ruby provider + +This repository contains the Ruby provider for [Flipt](https://www.flipt.io/), a feature flagging and experimentation platform. + +In conjunction with the [OpenFeature SDK](https://openfeature.dev/docs/reference/concepts/provider/) you can use this provider to integrate Flipt into your Ruby application. + +For documentation on how to use Flipt, please refer to the [Flipt documentation](https://docs.flipt.io/). + +## Installation +Add this line to your application's Gemfile: + +```ruby +gem 'openfeature-flipt-provider' +``` + +And then execute: +```bash +$ bundle +``` + +## Usage +To use the Flipt provider, you need to create an instance of the provider and pass it to the OpenFeature SDK. + +```ruby +require "open_feature/sdk" +require "openfeature/flipt/provider" + +OpenFeature::SDK.configure do |config| + config.set_provider( + OpenFeature::Flipt::Provider.new(options: { + url: "http://url-to-flipt-server" + }) + ) +end +client = OpenFeature::SDK.build_client + +# Check if a feature is enabled +if client.fetch_boolean_value(flag_key: "featureEnabled", default_value: false) + puts "Feature is enabled" +else + puts "Feature is disabled" +end +``` + +## Contributing +https://github.com/open-feature/ruby-sdk-contrib diff --git a/providers/openfeature-flipt-provider/Rakefile b/providers/openfeature-flipt-provider/Rakefile new file mode 100644 index 0000000..85f5f4d --- /dev/null +++ b/providers/openfeature-flipt-provider/Rakefile @@ -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] diff --git a/providers/openfeature-flipt-provider/bin/console b/providers/openfeature-flipt-provider/bin/console new file mode 100755 index 0000000..05989c4 --- /dev/null +++ b/providers/openfeature-flipt-provider/bin/console @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" +require "openfeature/flipt/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. + +require "irb" +IRB.start(__FILE__) diff --git a/providers/openfeature-flipt-provider/bin/rake b/providers/openfeature-flipt-provider/bin/rake new file mode 100755 index 0000000..4eb7d7b --- /dev/null +++ b/providers/openfeature-flipt-provider/bin/rake @@ -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") diff --git a/providers/openfeature-flipt-provider/bin/setup b/providers/openfeature-flipt-provider/bin/setup new file mode 100755 index 0000000..dce67d8 --- /dev/null +++ b/providers/openfeature-flipt-provider/bin/setup @@ -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 diff --git a/providers/openfeature-flipt-provider/lib/openfeature/flipt/provider.rb b/providers/openfeature-flipt-provider/lib/openfeature/flipt/provider.rb new file mode 100644 index 0000000..3237ad1 --- /dev/null +++ b/providers/openfeature-flipt-provider/lib/openfeature/flipt/provider.rb @@ -0,0 +1,172 @@ +# frozen_string_literal: true + +require "flipt_client" +require "open_feature/sdk" + +module OpenFeature + module Flipt + class Provider + PROVIDER_NAME = "Flipt Provider" + + # @param namespace [String] Namespace to use when fetching flags. + # @param options [Hash] Options to pass to the Flipt client. + def initialize(namespace: "default", options: {}) + @namespace = namespace + @options = options + end + + def metadata + @_metadata ||= SDK::Provider::ProviderMetadata.new(name: PROVIDER_NAME).freeze + end + + def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil) + fetch_value( + flag_key: flag_key, + default_value: default_value, + evaluation_context: evaluation_context, + evaluation_method: :evaluate_boolean, + result_key: "enabled" + ) + end + + def fetch_string_value(flag_key:, default_value:, evaluation_context: nil) + fetch_value( + flag_key: flag_key, + default_value: default_value, + evaluation_context: evaluation_context, + evaluation_method: :evaluate_variant, + result_key: "variant_key" + ) + end + + def fetch_number_value(flag_key:, default_value:, evaluation_context: nil) + fetch_numeric_value(flag_key:, default_value:, evaluation_context:, allowed_types: [Numeric]) + end + + def fetch_integer_value(flag_key:, default_value:, evaluation_context: nil) + fetch_numeric_value(flag_key:, default_value:, evaluation_context:, allowed_types: [Integer]) + end + + def fetch_float_value(flag_key:, default_value:, evaluation_context: nil) + fetch_numeric_value(flag_key:, default_value:, evaluation_context:, allowed_types: [Float]) + end + + def fetch_object_value(flag_key:, default_value:, evaluation_context: nil) + result = fetch_value( + flag_key: flag_key, + default_value: default_value, + evaluation_context: evaluation_context, + evaluation_method: :evaluate_variant, + result_key: "variant_key" + ) + + unless result.value.is_a?(Hash) + begin + result.value = JSON.parse(result.value) + rescue JSON::ParserError, TypeError + return OpenFeature::SDK::Provider::ResolutionDetails.new( + value: default_value, + error_message: "Could not parse '#{result.value}' as JSON", + reason: OpenFeature::SDK::Provider::Reason::ERROR + ) + end + end + result + end + + private + + def client + @_client ||= ::Flipt::EvaluationClient.new(@namespace, @options) + end + + def fetch_value(flag_key:, default_value:, evaluation_context:, evaluation_method:, result_key:) + transformed_eval_context = transform_context(evaluation_context) + + begin + response = client.send(evaluation_method, { + flag_key: flag_key, + entity_id: evaluation_context&.fetch("targeting_key", nil) || "default", + context: transformed_eval_context + }) + + if %w[FLAG_DISABLED_EVALUATION_REASON].include?(response["result"]["reason"]) + OpenFeature::SDK::Provider::ResolutionDetails.new( + value: default_value, + reason: OpenFeature::SDK::Provider::Reason::DISABLED + ) + elsif %w[DEFAULT_EVALUATION_REASON MATCH_EVALUATION_REASON].include?(response["result"]["reason"]) + OpenFeature::SDK::Provider::ResolutionDetails.new( + value: response["result"][result_key], + reason: OpenFeature::SDK::Provider::Reason::TARGETING_MATCH + ) + elsif %w[UNKNOWN_EVALUATION_REASON].include?(response["result"]["reason"]) + OpenFeature::SDK::Provider::ResolutionDetails.new( + value: default_value, + reason: OpenFeature::SDK::Provider::Reason::UNKNOWN + ) + else + OpenFeature::SDK::Provider::ResolutionDetails.new( + value: default_value, + reason: OpenFeature::SDK::Provider::Reason::DEFAULT + ) + end + rescue => e + OpenFeature::SDK::Provider::ResolutionDetails.new( + value: default_value, + error_message: e.message, + reason: OpenFeature::SDK::Provider::Reason::ERROR + ) + end + end + + def fetch_numeric_value(flag_key:, default_value:, evaluation_context:, allowed_types:) + result = fetch_value( + flag_key: flag_key, + default_value: default_value, + evaluation_context: evaluation_context, + evaluation_method: :evaluate_variant, + result_key: "variant_key" + ) + + unless result.value.is_a?(Numeric) + begin + parsed_value = Float(result.value) + # Only convert to integer if it's a whole number and allowed_types is [Integer] + result.value = if allowed_types == [Integer] && parsed_value.to_i == parsed_value + parsed_value.to_i + else + parsed_value + end + rescue ArgumentError, TypeError + return OpenFeature::SDK::Provider::ResolutionDetails.new( + value: default_value, + error_message: "Could not convert '#{result.value}' to #{allowed_types.first.name.downcase}", + reason: OpenFeature::SDK::Provider::Reason::ERROR + ) + end + end + + unless allowed_types.any? { |type| result.value.is_a?(type) } + return OpenFeature::SDK::Provider::ResolutionDetails.new( + value: default_value, + error_message: "Value '#{result.value}' is not a #{allowed_types.first.name.downcase}", + reason: OpenFeature::SDK::Provider::Reason::ERROR + ) + end + + result + end + + def transform_context(context) + eval_context = {} + context&.each do |key, value| + next if key == "targeting_key" + + eval_context[key] = value.to_s + end + eval_context + end + end + end +end diff --git a/providers/openfeature-flipt-provider/lib/openfeature/flipt/version.rb b/providers/openfeature-flipt-provider/lib/openfeature/flipt/version.rb new file mode 100644 index 0000000..759d0a8 --- /dev/null +++ b/providers/openfeature-flipt-provider/lib/openfeature/flipt/version.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module OpenFeature + module Flipt + VERSION = "0.1.0" + end +end diff --git a/providers/openfeature-flipt-provider/openfeature-flipt-provider.gemspec b/providers/openfeature-flipt-provider/openfeature-flipt-provider.gemspec new file mode 100644 index 0000000..e111e14 --- /dev/null +++ b/providers/openfeature-flipt-provider/openfeature-flipt-provider.gemspec @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require_relative "lib/openfeature/flipt/version" + +Gem::Specification.new do |spec| + spec.name = "openfeature-flipt-provider" + spec.version = OpenFeature::Flipt::VERSION + spec.authors = ["Firdaus Al Ghifari"] + spec.email = ["firdaus.alghifari@gmail.com"] + + spec.summary = "OpenFeature Flipt Provider for Ruby" + spec.description = "OpenFeature Flipt Provider for Ruby" + spec.homepage = "https://github.com/open-feature/ruby-sdk-contrib/tree/main/providers/openfeature-flipt-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/tree/main/providers/openfeature-flipt-provider" + spec.metadata["changelog_uri"] = "https://github.com/open-feature/ruby-sdk-contrib/blob/main/providers/openfeature-flipt-provider/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/tree/main/providers/openfeature-flipt-provider/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 "ffi", "~> 1.17" + spec.add_dependency "openfeature-sdk", "~> 0.4.0" + spec.add_dependency "flipt_client", "~> 0.10.0" + + spec.add_development_dependency "rake", "~> 13.0" + spec.add_development_dependency "rspec", "~> 3.12.0" + spec.add_development_dependency "standard", ">= 1.35.1" + spec.add_development_dependency "rubocop" +end diff --git a/providers/openfeature-flipt-provider/spec/spec_helper.rb b/providers/openfeature-flipt-provider/spec/spec_helper.rb new file mode 100644 index 0000000..5eeee39 --- /dev/null +++ b/providers/openfeature-flipt-provider/spec/spec_helper.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require "bundler/setup" +require "open_feature/sdk" +require "openfeature/flipt/provider" + +RSpec.configure do |config| + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true + end + + config.shared_context_metadata_behavior = :apply_to_host_groups + config.filter_run_when_matching :focus + config.example_status_persistence_file_path = "spec/examples.txt" + config.disable_monkey_patching! + config.warnings = true + config.order = :random + Kernel.srand config.seed +end From 90765048e340737b830a7d01252a2e88a8935b6e Mon Sep 17 00:00:00 2001 From: Firdaus Al Ghifari Date: Wed, 9 Apr 2025 10:53:16 +0900 Subject: [PATCH 2/8] test: Add unit tests for Flipt Provider Signed-off-by: Firdaus Al Ghifari --- .../spec/openfeature/flipt/provider_spec.rb | 245 ++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 providers/openfeature-flipt-provider/spec/openfeature/flipt/provider_spec.rb diff --git a/providers/openfeature-flipt-provider/spec/openfeature/flipt/provider_spec.rb b/providers/openfeature-flipt-provider/spec/openfeature/flipt/provider_spec.rb new file mode 100644 index 0000000..86de7eb --- /dev/null +++ b/providers/openfeature-flipt-provider/spec/openfeature/flipt/provider_spec.rb @@ -0,0 +1,245 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe OpenFeature::Flipt::Provider do + let(:provider) { described_class.new } + let(:client_stub) { double(::Flipt::EvaluationClient) } + let(:evaluation_context) { {"targeting_key" => "user123", "some_key" => "some_value"} } + + before do + allow(::Flipt::EvaluationClient).to receive(:new).and_return(client_stub) + end + + context "2.1 - Feature Provider Interface" do + describe "#metadata" do + it "returns a name field which identifies the provider implementation" do + expect(provider.metadata.name).to eq("Flipt Provider") + end + end + end + + context "2.2 - Flag Value Resolution" do + describe "#fetch_boolean_value" do + it "returns the correct resolution details for a matching evaluation" do + response = { + "status" => "success", + "result" => {"enabled" => true, "reason" => "MATCH_EVALUATION_REASON"} + } + allow(client_stub).to receive(:evaluate_boolean).and_return(response) + + result = provider.fetch_boolean_value(flag_key: "test_flag", default_value: false, evaluation_context: evaluation_context) + expect(result.value).to eq(true) + expect(result.reason).to eq(OpenFeature::SDK::Provider::Reason::TARGETING_MATCH) + end + + it "returns the default value for an unknown evaluation reason" do + response = { + "status" => "success", + "result" => {"reason" => "UNKNOWN_EVALUATION_REASON"} + } + allow(client_stub).to receive(:evaluate_boolean).and_return(response) + + result = provider.fetch_boolean_value(flag_key: "test_flag", default_value: false, evaluation_context: evaluation_context) + expect(result.value).to eq(false) + expect(result.reason).to eq(OpenFeature::SDK::Provider::Reason::UNKNOWN) + end + + it "returns the default value for a flag disabled evaluation reason" do + response = { + "status" => "success", + "result" => {"reason" => "FLAG_DISABLED_EVALUATION_REASON"} + } + allow(client_stub).to receive(:evaluate_boolean).and_return(response) + + result = provider.fetch_boolean_value(flag_key: "test_flag", default_value: false, evaluation_context: evaluation_context) + expect(result.value).to eq(false) + expect(result.reason).to eq(OpenFeature::SDK::Provider::Reason::DISABLED) + end + + it "returns the default value for an empty result" do + response = { + "status" => "failed", + "result" => {} + } + allow(client_stub).to receive(:evaluate_boolean).and_return(response) + + result = provider.fetch_boolean_value(flag_key: "test_flag", default_value: false, evaluation_context: evaluation_context) + expect(result.value).to eq(false) + expect(result.reason).to eq(OpenFeature::SDK::Provider::Reason::DEFAULT) + end + + it "returns the default value and error message on exception" do + allow(client_stub).to receive(:evaluate_boolean).and_raise(StandardError.new("Some error")) + + result = provider.fetch_boolean_value(flag_key: "test_flag", default_value: false, evaluation_context: evaluation_context) + expect(result.value).to eq(false) + expect(result.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR) + expect(result.error_message).to eq("Some error") + end + end + + describe "#fetch_string_value" do + it "returns the correct resolution details for a matching evaluation" do + response = { + "status" => "success", + "result" => {"variant_key" => "variant1", "reason" => "MATCH_EVALUATION_REASON"} + } + allow(client_stub).to receive(:evaluate_variant).and_return(response) + + result = provider.fetch_string_value(flag_key: "test_flag", default_value: "default", evaluation_context: evaluation_context) + expect(result.value).to eq("variant1") + expect(result.reason).to eq(OpenFeature::SDK::Provider::Reason::TARGETING_MATCH) + end + + it "returns the default value for an unknown evaluation reason" do + response = { + "status" => "success", + "result" => {"reason" => "UNKNOWN_EVALUATION_REASON"} + } + allow(client_stub).to receive(:evaluate_variant).and_return(response) + + result = provider.fetch_string_value(flag_key: "test_flag", default_value: "default", evaluation_context: evaluation_context) + expect(result.value).to eq("default") + expect(result.reason).to eq(OpenFeature::SDK::Provider::Reason::UNKNOWN) + end + + it "returns the default value for a flag disabled evaluation reason" do + response = { + "status" => "success", + "result" => {"reason" => "FLAG_DISABLED_EVALUATION_REASON"} + } + allow(client_stub).to receive(:evaluate_variant).and_return(response) + + result = provider.fetch_string_value(flag_key: "test_flag", default_value: "default", evaluation_context: evaluation_context) + expect(result.value).to eq("default") + expect(result.reason).to eq(OpenFeature::SDK::Provider::Reason::DISABLED) + end + + it "returns the default value and error message on exception" do + allow(client_stub).to receive(:evaluate_variant).and_raise(StandardError.new("Some error")) + + result = provider.fetch_string_value(flag_key: "test_flag", default_value: "default", evaluation_context: evaluation_context) + expect(result.value).to eq("default") + expect(result.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR) + expect(result.error_message).to eq("Some error") + end + end + + describe "#fetch_number_value" do + it "returns the correct numeric value for a matching evaluation" do + response = { + "status" => "success", + "result" => {"variant_key" => "42.5", "reason" => "MATCH_EVALUATION_REASON"} + } + allow(client_stub).to receive(:evaluate_variant).and_return(response) + + result = provider.fetch_number_value(flag_key: "test_flag", default_value: 0, evaluation_context: evaluation_context) + expect(result.value).to eq(42.5) + expect(result.reason).to eq(OpenFeature::SDK::Provider::Reason::TARGETING_MATCH) + end + + it "returns error when value cannot be converted to number" do + response = { + "status" => "success", + "result" => {"variant_key" => "not_a_number", "reason" => "MATCH_EVALUATION_REASON"} + } + allow(client_stub).to receive(:evaluate_variant).and_return(response) + + result = provider.fetch_number_value(flag_key: "test_flag", default_value: 0, evaluation_context: evaluation_context) + expect(result.value).to eq(0) + expect(result.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR) + end + end + + describe "#fetch_integer_value" do + it "returns the correct integer value for a matching evaluation" do + response = { + "status" => "success", + "result" => {"variant_key" => "42", "reason" => "MATCH_EVALUATION_REASON"} + } + allow(client_stub).to receive(:evaluate_variant).and_return(response) + + result = provider.fetch_integer_value(flag_key: "test_flag", default_value: 0, evaluation_context: evaluation_context) + expect(result.value).to eq(42) + expect(result.reason).to eq(OpenFeature::SDK::Provider::Reason::TARGETING_MATCH) + end + + it "returns error when value cannot be converted to integer" do + response = { + "status" => "success", + "result" => {"variant_key" => "42.5", "reason" => "MATCH_EVALUATION_REASON"} + } + allow(client_stub).to receive(:evaluate_variant).and_return(response) + + result = provider.fetch_integer_value(flag_key: "test_flag", default_value: 0, evaluation_context: evaluation_context) + expect(result.value).to eq(0) + expect(result.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR) + end + end + + describe "#fetch_float_value" do + it "returns the correct float value for a matching evaluation" do + response = { + "status" => "success", + "result" => {"variant_key" => "42.5", "reason" => "MATCH_EVALUATION_REASON"} + } + allow(client_stub).to receive(:evaluate_variant).and_return(response) + + result = provider.fetch_float_value(flag_key: "test_flag", default_value: 0.0, evaluation_context: evaluation_context) + expect(result.value).to eq(42.5) + expect(result.reason).to eq(OpenFeature::SDK::Provider::Reason::TARGETING_MATCH) + end + + it "returns error when value cannot be converted to float" do + response = { + "status" => "success", + "result" => {"variant_key" => "not_a_float", "reason" => "MATCH_EVALUATION_REASON"} + } + allow(client_stub).to receive(:evaluate_variant).and_return(response) + + result = provider.fetch_float_value(flag_key: "test_flag", default_value: 0.0, evaluation_context: evaluation_context) + expect(result.value).to eq(0.0) + expect(result.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR) + end + end + + describe "#fetch_object_value" do + it "returns the correct object value for a matching evaluation" do + response = { + "status" => "success", + "result" => {"variant_key" => '{"key": "value"}', "reason" => "MATCH_EVALUATION_REASON"} + } + allow(client_stub).to receive(:evaluate_variant).and_return(response) + + result = provider.fetch_object_value(flag_key: "test_flag", default_value: {}, evaluation_context: evaluation_context) + expect(result.value).to eq({"key" => "value"}) + expect(result.reason).to eq(OpenFeature::SDK::Provider::Reason::TARGETING_MATCH) + end + + it "returns error when value cannot be parsed as JSON" do + response = { + "status" => "success", + "result" => {"variant_key" => "invalid_json", "reason" => "MATCH_EVALUATION_REASON"} + } + allow(client_stub).to receive(:evaluate_variant).and_return(response) + + result = provider.fetch_object_value(flag_key: "test_flag", default_value: {}, evaluation_context: evaluation_context) + expect(result.value).to eq({}) + expect(result.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR) + end + end + end + + describe "#transform_context" do + it "transforms the context correctly" do + result = provider.send(:transform_context, evaluation_context) + expect(result).to eq({"some_key" => "some_value"}) + end + + it "returns an empty hash if context is nil" do + result = provider.send(:transform_context, nil) + expect(result).to eq({}) + end + end +end From 6cef975bb79c2271cff255b489f2c0b45a02c19a Mon Sep 17 00:00:00 2001 From: Firdaus Al Ghifari Date: Wed, 9 Apr 2025 10:53:43 +0900 Subject: [PATCH 3/8] ci: Add ci for flipt provider Signed-off-by: Firdaus Al Ghifari --- .github/workflows/ruby.yml | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index b66c448..03aeaa4 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -85,4 +85,26 @@ jobs: bundler-cache: true # runs 'bundle install' and caches installed gems automatically working-directory: ./providers/openfeature-go-feature-flag-provider - name: Lint and test - run: bin/rake \ No newline at end of file + run: bin/rake + + test_flipt_provider: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./providers/openfeature-flipt-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-flipt-provider + - name: Lint and test + run: bin/rake From 503ce10aa9f75a06efb78d0398e85be53d802355 Mon Sep 17 00:00:00 2001 From: Firdaus Al Ghifari Date: Wed, 9 Apr 2025 10:54:03 +0900 Subject: [PATCH 4/8] chores: Add flipt provider to code-workspace Signed-off-by: Firdaus Al Ghifari --- ruby-sdk-contrib.code-workspace | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ruby-sdk-contrib.code-workspace b/ruby-sdk-contrib.code-workspace index 2669c9a..31970d5 100644 --- a/ruby-sdk-contrib.code-workspace +++ b/ruby-sdk-contrib.code-workspace @@ -8,6 +8,9 @@ }, { "path": "providers/openfeature-go-feature-flag-provider" + }, + { + "path": "providers/openfeature-flipt-provider" } ] } From 76a05ce118a7103f1d21bc657e16dca75b290a4a Mon Sep 17 00:00:00 2001 From: Firdaus Al Ghifari Date: Sat, 12 Apr 2025 11:31:44 +0900 Subject: [PATCH 5/8] docs: update flipt README.md with authentication example Signed-off-by: Firdaus Al Ghifari --- providers/openfeature-flipt-provider/README.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/providers/openfeature-flipt-provider/README.md b/providers/openfeature-flipt-provider/README.md index 97ef2f6..007450e 100644 --- a/providers/openfeature-flipt-provider/README.md +++ b/providers/openfeature-flipt-provider/README.md @@ -31,9 +31,14 @@ require "openfeature/flipt/provider" OpenFeature::SDK.configure do |config| config.set_provider( - OpenFeature::Flipt::Provider.new(options: { - url: "http://url-to-flipt-server" - }) + OpenFeature::Flipt::Provider.new( + namespace: "flipt-namespace", + options: { + url: "https://url-to-flipt-server", + update_interval: 60, + authentication: "token" + } + ) ) end client = OpenFeature::SDK.build_client @@ -46,5 +51,7 @@ else end ``` +For a complete list of configuration options, such as authentication and error strategies, refer to the [Flipt Client Ruby SDK documentation](https://github.com/flipt-io/flipt-client-sdks/tree/main/flipt-client-ruby#constructor-arguments). + ## Contributing https://github.com/open-feature/ruby-sdk-contrib From 6913d1c98dec0810dcbbc8f8d41d8af942c547f4 Mon Sep 17 00:00:00 2001 From: Firdaus Al Ghifari Date: Sat, 12 Apr 2025 11:34:00 +0900 Subject: [PATCH 6/8] refactor: move client initialization to constructor method Signed-off-by: Firdaus Al Ghifari --- .../lib/openfeature/flipt/provider.rb | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/providers/openfeature-flipt-provider/lib/openfeature/flipt/provider.rb b/providers/openfeature-flipt-provider/lib/openfeature/flipt/provider.rb index 3237ad1..0786631 100644 --- a/providers/openfeature-flipt-provider/lib/openfeature/flipt/provider.rb +++ b/providers/openfeature-flipt-provider/lib/openfeature/flipt/provider.rb @@ -11,8 +11,7 @@ class Provider # @param namespace [String] Namespace to use when fetching flags. # @param options [Hash] Options to pass to the Flipt client. def initialize(namespace: "default", options: {}) - @namespace = namespace - @options = options + @client ||= ::Flipt::EvaluationClient.new(@namespace, @options) end def metadata @@ -76,15 +75,11 @@ def fetch_object_value(flag_key:, default_value:, evaluation_context: nil) private - def client - @_client ||= ::Flipt::EvaluationClient.new(@namespace, @options) - end - def fetch_value(flag_key:, default_value:, evaluation_context:, evaluation_method:, result_key:) transformed_eval_context = transform_context(evaluation_context) begin - response = client.send(evaluation_method, { + response = @client.send(evaluation_method, { flag_key: flag_key, entity_id: evaluation_context&.fetch("targeting_key", nil) || "default", context: transformed_eval_context From 92d68e883671d3cbf6af8017ba2329a3eaddf218 Mon Sep 17 00:00:00 2001 From: Firdaus Al Ghifari Date: Sat, 12 Apr 2025 11:37:55 +0900 Subject: [PATCH 7/8] refactor: remove unnecessary memoization on constructor Signed-off-by: Firdaus Al Ghifari --- .../lib/openfeature/flipt/provider.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/openfeature-flipt-provider/lib/openfeature/flipt/provider.rb b/providers/openfeature-flipt-provider/lib/openfeature/flipt/provider.rb index 0786631..99dbf88 100644 --- a/providers/openfeature-flipt-provider/lib/openfeature/flipt/provider.rb +++ b/providers/openfeature-flipt-provider/lib/openfeature/flipt/provider.rb @@ -11,7 +11,7 @@ class Provider # @param namespace [String] Namespace to use when fetching flags. # @param options [Hash] Options to pass to the Flipt client. def initialize(namespace: "default", options: {}) - @client ||= ::Flipt::EvaluationClient.new(@namespace, @options) + @client = ::Flipt::EvaluationClient.new(@namespace, @options) end def metadata From a5223bc361e00435eef807d18e6243897b79b1fa Mon Sep 17 00:00:00 2001 From: Firdaus Al Ghifari Date: Sat, 12 Apr 2025 15:31:31 +0900 Subject: [PATCH 8/8] fix: flipt ruby provider client initialization logic Signed-off-by: Firdaus Al Ghifari --- .../lib/openfeature/flipt/provider.rb | 2 +- .../spec/openfeature/flipt/provider_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/providers/openfeature-flipt-provider/lib/openfeature/flipt/provider.rb b/providers/openfeature-flipt-provider/lib/openfeature/flipt/provider.rb index 99dbf88..bdc787e 100644 --- a/providers/openfeature-flipt-provider/lib/openfeature/flipt/provider.rb +++ b/providers/openfeature-flipt-provider/lib/openfeature/flipt/provider.rb @@ -11,7 +11,7 @@ class Provider # @param namespace [String] Namespace to use when fetching flags. # @param options [Hash] Options to pass to the Flipt client. def initialize(namespace: "default", options: {}) - @client = ::Flipt::EvaluationClient.new(@namespace, @options) + @client = ::Flipt::EvaluationClient.new(namespace, options) end def metadata diff --git a/providers/openfeature-flipt-provider/spec/openfeature/flipt/provider_spec.rb b/providers/openfeature-flipt-provider/spec/openfeature/flipt/provider_spec.rb index 86de7eb..4ca3234 100644 --- a/providers/openfeature-flipt-provider/spec/openfeature/flipt/provider_spec.rb +++ b/providers/openfeature-flipt-provider/spec/openfeature/flipt/provider_spec.rb @@ -3,12 +3,12 @@ require "spec_helper" RSpec.describe OpenFeature::Flipt::Provider do - let(:provider) { described_class.new } + let(:provider) { described_class.new(namespace: "test-namespace") } let(:client_stub) { double(::Flipt::EvaluationClient) } let(:evaluation_context) { {"targeting_key" => "user123", "some_key" => "some_value"} } before do - allow(::Flipt::EvaluationClient).to receive(:new).and_return(client_stub) + allow(::Flipt::EvaluationClient).to receive(:new).with("test-namespace", {}).and_return(client_stub) end context "2.1 - Feature Provider Interface" do