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 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..007450e --- /dev/null +++ b/providers/openfeature-flipt-provider/README.md @@ -0,0 +1,57 @@ +

+ 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( + namespace: "flipt-namespace", + options: { + url: "https://url-to-flipt-server", + update_interval: 60, + authentication: "token" + } + ) + ) +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 +``` + +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 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..bdc787e --- /dev/null +++ b/providers/openfeature-flipt-provider/lib/openfeature/flipt/provider.rb @@ -0,0 +1,167 @@ +# 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: {}) + @client = ::Flipt::EvaluationClient.new(namespace, 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 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/openfeature/flipt/provider_spec.rb b/providers/openfeature-flipt-provider/spec/openfeature/flipt/provider_spec.rb new file mode 100644 index 0000000..4ca3234 --- /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(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).with("test-namespace", {}).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 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 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" } ] }