From 51d455d8265ffdf6f3bbd3fbbbf3f5c6c502bbc1 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Thu, 25 Jul 2024 18:48:30 +0200 Subject: [PATCH 01/19] feat: Init GO Feature Flag ruby provider Signed-off-by: Thomas Poignant --- .gitignore | 3 + .release-please-manifest.json | 3 +- .../.rubocop.yml | 5 + .../Gemfile | 3 + .../Gemfile.lock | 102 ++++ .../README.md | 132 +++++ .../Rakefile | 10 + .../go-feature-flag/error/errors.rb | 76 +++ .../go_feature_flag_provider.rb | 89 +++ .../openfeature/go-feature-flag/goff_api.rb | 149 +++++ .../model/ofrep_api_response.rb | 32 + .../openfeature/go-feature-flag/options.rb | 28 + .../openfeature/go-feature-flag/version.rb | 5 + ...enfeature-go-feature-flag-provider.gemspec | 41 ++ .../gofeatureflag/goff_api_spec.rb | 282 +++++++++ .../openfeature/gofeatureflag/options_spec.rb | 19 + .../gofeatureflag/provider_spec.rb | 550 ++++++++++++++++++ .../spec/spec_helper.rb | 103 ++++ release-please-config.json | 10 + ruby-sdk-contrib.code-workspace | 3 + 20 files changed, 1644 insertions(+), 1 deletion(-) create mode 100644 providers/openfeature-go-feature-flag-provider/.rubocop.yml create mode 100644 providers/openfeature-go-feature-flag-provider/Gemfile create mode 100644 providers/openfeature-go-feature-flag-provider/Gemfile.lock create mode 100644 providers/openfeature-go-feature-flag-provider/README.md create mode 100644 providers/openfeature-go-feature-flag-provider/Rakefile create mode 100644 providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/error/errors.rb create mode 100644 providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/go_feature_flag_provider.rb create mode 100644 providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/goff_api.rb create mode 100644 providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/model/ofrep_api_response.rb create mode 100644 providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/options.rb create mode 100644 providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/version.rb create mode 100644 providers/openfeature-go-feature-flag-provider/openfeature-go-feature-flag-provider.gemspec create mode 100644 providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/goff_api_spec.rb create mode 100644 providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/options_spec.rb create mode 100644 providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/provider_spec.rb create mode 100644 providers/openfeature-go-feature-flag-provider/spec/spec_helper.rb diff --git a/.gitignore b/.gitignore index 71983ae..74d9d73 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,6 @@ build-iPhoneSimulator/ # .rubocop-https?--* .DS_Store + +# Ignore jetbrains files +.idea/ \ No newline at end of file diff --git a/.release-please-manifest.json b/.release-please-manifest.json index a05acb4..cf71449 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,4 +1,5 @@ { "providers/openfeature-flagd-provider": "0.1.1", - "providers/openfeature-meta_provider": "0.0.3" + "providers/openfeature-meta_provider": "0.0.3", + "providers/openfeature-go-feature-flag-provider": "0.1.0" } diff --git a/providers/openfeature-go-feature-flag-provider/.rubocop.yml b/providers/openfeature-go-feature-flag-provider/.rubocop.yml new file mode 100644 index 0000000..feec135 --- /dev/null +++ b/providers/openfeature-go-feature-flag-provider/.rubocop.yml @@ -0,0 +1,5 @@ +inherit_from: ../../shared_config/.rubocop.yml + +inherit_mode: + merge: + - Exclude diff --git a/providers/openfeature-go-feature-flag-provider/Gemfile b/providers/openfeature-go-feature-flag-provider/Gemfile new file mode 100644 index 0000000..b4e2a20 --- /dev/null +++ b/providers/openfeature-go-feature-flag-provider/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gemspec diff --git a/providers/openfeature-go-feature-flag-provider/Gemfile.lock b/providers/openfeature-go-feature-flag-provider/Gemfile.lock new file mode 100644 index 0000000..2b1ff92 --- /dev/null +++ b/providers/openfeature-go-feature-flag-provider/Gemfile.lock @@ -0,0 +1,102 @@ +PATH + remote: . + specs: + openfeature-go-feature-flag-provider (0.1.0) + openfeature-sdk (~> 0.3.1) + +GEM + remote: https://rubygems.org/ + specs: + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + ast (2.4.2) + bigdecimal (3.1.8) + crack (1.0.0) + bigdecimal + rexml + diff-lcs (1.5.1) + docile (1.4.1) + hashdiff (1.1.1) + json (2.7.2) + language_server-protocol (3.17.0.3) + lint_roller (1.1.0) + openfeature-sdk (0.3.1) + parallel (1.24.0) + parser (3.3.0.5) + ast (~> 2.4.1) + racc + public_suffix (6.0.1) + racc (1.7.3) + rainbow (3.1.1) + rake (13.2.1) + regexp_parser (2.9.0) + rexml (3.2.8) + 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.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) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-html (0.12.3) + simplecov_json_formatter (0.1.4) + 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) + unicode-display_width (2.5.0) + webmock (3.23.1) + addressable (>= 2.8.0) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) + +PLATFORMS + arm64-darwin-23 + ruby + +DEPENDENCIES + openfeature-go-feature-flag-provider! + rake (~> 13.0) + rspec (~> 3.12.0) + rubocop + simplecov + standard + webmock + +BUNDLED WITH + 2.5.11 diff --git a/providers/openfeature-go-feature-flag-provider/README.md b/providers/openfeature-go-feature-flag-provider/README.md new file mode 100644 index 0000000..2012521 --- /dev/null +++ b/providers/openfeature-go-feature-flag-provider/README.md @@ -0,0 +1,132 @@ +

+ go-feature-flag logo + +

+ +# GO Feature Flag - OpenFeature Ruby provider +

+ Documentation + Issues + Join us on slack +

+ +This repository contains the official Ruby OpenFeature provider for accessing your feature flags with [GO Feature Flag](https://gofeatureflag.org). + +In conjuction with the [OpenFeature SDK](https://openfeature.dev/docs/reference/concepts/provider) you will be able +to evaluate your feature flags in your Ruby applications. + +For documentation related to flags management in GO Feature Flag, +refer to the [GO Feature Flag documentation website](https://gofeatureflag.org/docs). + +### Functionalities: +- Manage the integration of the OpenFeature Ruby SDK and GO Feature Flag relay-proxy. + +## Dependency Setup + +### Gem Package Manager + +Add this line to your application's Gemfile: +``` +gem 'openfeature-go-feature-flag-provider' +``` +And then execute: +``` +bundle install +``` +Or install it yourself as: +``` +gem install openfeature-go-feature-flag-provider +``` + +## Getting started + +### Initialize the provider + +The `OpenFeature::GoFeatureFlag::Provider` needs some options to be created and then set in the OpenFeature SDK. + +| **Option** | **Description** | +|------------|---------------------------------------------------------------------------------------------------------------------------------------------| +| `endpoint` | **(mandatory)** The URL to access to the relay-proxy.
*(example: `https://relay.proxy.gofeatureflag.org/`)* | +| `headers` | A `Hash` object containing the headers to send to the relay-proxy.
*(example to send APIKey: `{"Authorization" => "Bearer my-api-key"}` | + +The only required option to create a `GoFeatureFlagProvider` is the URL _(`endpoint`)_ to your GO Feature Flag relay-proxy instance. + +```ruby +import GOFeatureFlag +import OpenFeature + +# ... + +options = OpenFeature::GoFeatureFlag::Options.new(endpoint: "http://localhost:1031") +provider = OpenFeature::GoFeatureFlag::Provider.new(options: options) + +evaluation_context = OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16") + +OpenFeature::SDK.configure do |config| + config.set_provider(provider) +end +client = OpenFeature::SDK.build_client() + +bool_value = client.fetch_boolean_value( + flag_key: "my-boolean-flag", + default_value: false, + evaluation_context: evaluation_context +) + +if bool_value + puts "The flag is enabled" +else + puts "The flag is disabled" +end +``` + +The evaluation context is the way for the client to specify contextual data that GO Feature Flag uses to evaluate the feature flags, it allows to define rules on the flag. + +The `targeting_key` is mandatory for GO Feature Flag to evaluate the feature flag, it could be the id of a user, a session ID or anything you find relevant to use as identifier during the evaluation. + + +### Evaluate a feature flag +The client is used to retrieve values for the current `EvaluationContext`. +For example, retrieving a boolean value for the flag **"my-flag"**: + +```swift +client = OpenFeature::SDK.build_client() + +bool_value = client.fetch_boolean_value( + flag_key: "my-boolean-flag", + default_value: false, + evaluation_context: evaluation_context +) +``` + +GO Feature Flag supports different all OpenFeature supported types of feature flags, it means that you can use all the accessor directly +```swift +// Bool +client.fetch_boolean_value(flag_key: 'my-flag', default_value: false, evaluation_context: evaluation_context) + +// String +client.fetch_string_value(flag_key: 'my-flag', default_value: "default", evaluation_context: evaluation_context) + +// Number +client.fetch_number_value(flag_key: 'my-flag', default_value: 0, evaluation_context: evaluation_context) + +// Object +client.fetch_object_value(flag_key: 'my-flag', default_value: {"default" => true}, evaluation_context: evaluation_context) +``` + +## Features status + +| Status | Feature | Description | +|--------|-----------------|----------------------------------------------------------------------------| +| ✅ | Flag evaluation | It is possible to evaluate all the type of flags | +| ❌ | Caching | Mechanism is in place to refresh the cache in case of configuration change | +| ❌ | Event Streaming | Not supported by the SDK | +| ❌ | Logging | Not supported by the SDK | +| ✅ | Flag Metadata | Not supported by the SDK | + + +**Implemented**: ✅ | In-progress: ⚠️ | Not implemented yet: ❌ + +## Contributing +This project welcomes contributions from the community. +If you're interested in contributing, see the [contributors' guide](https://github.com/thomaspoignant/go-feature-flag/blob/main/CONTRIBUTING.md) for some helpful tips. diff --git a/providers/openfeature-go-feature-flag-provider/Rakefile b/providers/openfeature-go-feature-flag-provider/Rakefile new file mode 100644 index 0000000..85f5f4d --- /dev/null +++ b/providers/openfeature-go-feature-flag-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-go-feature-flag-provider/lib/openfeature/go-feature-flag/error/errors.rb b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/error/errors.rb new file mode 100644 index 0000000..ee49d5c --- /dev/null +++ b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/error/errors.rb @@ -0,0 +1,76 @@ +# Define a custom error class +require "open_feature/sdk/provider/error_code" + +module OpenFeature + module GoFeatureFlag + class FlagNotFoundError < StandardError + attr_reader :response, :error_code, :error_message + + def initialize(response, flag_key) + error_message = "Flag not found: #{flag_key}" + super(error_message) + @response = response + @error_code = OpenFeature::SDK::Provider::ErrorCode::FLAG_NOT_FOUND + @error_message = error_message + end + end + + class InternalServerError < StandardError + attr_reader :response, :error_code, :error_message + + def initialize(response) + error_message = "Internal Server Error" + super(error_message) + @response = response + @error_code = OpenFeature::SDK::Provider::ErrorCode::GENERAL + @error_message = error_message + end + end + + class InvalidOptionError < StandardError + attr_reader :error_code, :error_message + + def initialize(error_code, error_message) + super(error_message) + @error_code = error_code + @error_message = error_message + end + end + + class UnauthorizedError < StandardError + attr_reader :response, :error_code, :error_message + + def initialize(response) + error_message = "unauthorized" + super(error_message) + @response = response + @error_code = OpenFeature::SDK::Provider::ErrorCode::GENERAL + @error_message = error_message + end + end + + class ParseError < StandardError + attr_reader :response, :error_code, :error_message + + def initialize(response) + error_message = "Parse error" + super(error_message) + @response = response + @error_code = OpenFeature::SDK::Provider::ErrorCode::PARSE_ERROR + @error_message = error_message + end + end + + class RateLimited < StandardError + attr_reader :response, :error_code, :error_message + + def initialize(response) + error_message = response.nil? ? "Rate limited" : "Rate limited: " + response["Retry-After"].to_s + super(error_message) + @response = response + @error_code = OpenFeature::SDK::Provider::ErrorCode::GENERAL + @error_message = error_message + end + end + end +end diff --git a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/go_feature_flag_provider.rb b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/go_feature_flag_provider.rb new file mode 100644 index 0000000..70e87f6 --- /dev/null +++ b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/go_feature_flag_provider.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true +module OpenFeature + module GoFeatureFlag + # This class is the entry point for the GoFeatureFlagProvider + class Provider + PROVIDER_NAME = "GO Feature Flag Provider" + attr_reader :metadata, :options + + def initialize(options: OpenFeature::GoFeatureFlag::Options.new) + @metadata = OpenFeature::SDK::Provider::ProviderMetadata.new(name: PROVIDER_NAME) + @options = options + @goff_api = OpenFeature::GoFeatureFlag::GoFeatureFlagApi.new(options: options) + end + + def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil) + evaluate(flag_key: flag_key, default_value: default_value, evaluation_context: evaluation_context, allowed_classes: [TrueClass, FalseClass]) + end + + def fetch_string_value(flag_key:, default_value:, evaluation_context: nil) + evaluate(flag_key: flag_key, default_value: default_value, evaluation_context: evaluation_context, allowed_classes: [String]) + end + + def fetch_number_value(flag_key:, default_value:, evaluation_context: nil) + evaluate(flag_key: flag_key, default_value: default_value, evaluation_context: evaluation_context, allowed_classes: [Integer, Float]) + end + + def fetch_object_value(flag_key:, default_value:, evaluation_context: nil) + evaluate(flag_key: flag_key, default_value: default_value, evaluation_context: evaluation_context, allowed_classes: [Array, Hash]) + end + + private + + def evaluate(flag_key:, default_value:, allowed_classes:, evaluation_context: nil) + evaluation_context = OpenFeature::SDK::EvaluationContext.new unless evaluation_context.is_a?(OpenFeature::SDK::EvaluationContext) + validate_parameters(flag_key, evaluation_context) + + # do a http call to the go feature flag server + parsed_response = @goff_api.evaluate_ofrep_api(flag_key: flag_key, evaluation_context: evaluation_context) + parsed_response = OpenFeature::GoFeatureFlag::OfrepApiResponse unless parsed_response.is_a?(OpenFeature::GoFeatureFlag::OfrepApiResponse) + + if parsed_response.has_error? + return OpenFeature::SDK::Provider::ResolutionDetails.new( + value: default_value, + error_code: parsed_response.error_code, + error_message: parsed_response.error_details, + reason: parsed_response.reason + ) + end + + unless allowed_classes.include?(parsed_response.value.class) + return OpenFeature::SDK::Provider::ResolutionDetails.new( + value: default_value, + error_code: OpenFeature::SDK::Provider::ErrorCode::TYPE_MISMATCH, + error_message: "flag type #{parsed_response.value.class} does not match allowed types #{allowed_classes}", + reason: OpenFeature::SDK::Provider::Reason::ERROR + ) + end + + OpenFeature::SDK::Provider::ResolutionDetails.new( + value: parsed_response.value, + reason: parsed_response.reason, + variant: parsed_response.variant, + flag_metadata: parsed_response.metadata + ) + + rescue OpenFeature::GoFeatureFlag::UnauthorizedError, + OpenFeature::GoFeatureFlag::InvalidOptionError, + OpenFeature::GoFeatureFlag::FlagNotFoundError, + OpenFeature::GoFeatureFlag::InternalServerError => e + OpenFeature::SDK::Provider::ResolutionDetails.new( + value: default_value, + error_code: e.error_code, + error_message: e.error_message, + reason: OpenFeature::SDK::Provider::Reason::ERROR + ) + end + + def validate_parameters(flag_key, evaluation_context) + if evaluation_context.nil? || evaluation_context.targeting_key.nil? || evaluation_context.targeting_key.empty? + raise OpenFeature::GoFeatureFlag::InvalidOptionError.new(OpenFeature::SDK::Provider::ErrorCode::INVALID_CONTEXT, "invalid evaluation context provided") + end + + if flag_key.nil? || flag_key.empty? + raise OpenFeature::GoFeatureFlag::InvalidOptionError.new(OpenFeature::SDK::Provider::ErrorCode::GENERAL, "invalid flag key provided") + end + end + end + end +end diff --git a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/goff_api.rb b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/goff_api.rb new file mode 100644 index 0000000..ac6f5c1 --- /dev/null +++ b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/goff_api.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true +require "open_feature/sdk" +require "net/http" +require "json" +require_relative "error/errors" +require_relative "model/ofrep_api_response" + +module OpenFeature + module GoFeatureFlag + # This class is the entry point for the GoFeatureFlagProvider + class GoFeatureFlagApi + attr_reader :options + def initialize(options: {}) + options = OpenFeature::GoFeatureFlag::Options.new unless options.is_a?(OpenFeature::GoFeatureFlag::Options) + @options = options + end + + def evaluate_ofrep_api(flag_key:, evaluation_context:) + unless @retry_after.nil? + if Time.now < @retry_after + raise OpenFeature::GoFeatureFlag::RateLimited.new(nil) + else + @retry_after = nil + end + end + + evaluation_context = OpenFeature::SDK::EvaluationContext.new unless evaluation_context.is_a?(OpenFeature::SDK::EvaluationContext) + # Format the URL to call the Go Feature Flag OFREP API + base_uri = URI.parse(@options.endpoint) + new_path = File.join(base_uri.path, "/ofrep/v1/evaluate/flags/#{flag_key}") + ofrep_uri = base_uri.dup + ofrep_uri.path = new_path + + # Initialize the HTTP client + http = Net::HTTP.new(ofrep_uri.host, ofrep_uri.port) + http.use_ssl = (ofrep_uri.scheme == "https") + + # Prepare the headers + headers = { + "Content-Type" => "application/json" + } + if @options.custom_headers.nil? + headers.merge!(@options.custom_headers) + end + + request = Net::HTTP::Post.new(ofrep_uri.path, headers) + + # replace targetingKey + evaluation_context.fields["targetingKey"] = evaluation_context.targeting_key + evaluation_context.fields.delete("targeting_key") + + request.body = {context: evaluation_context.fields}.to_json + response = http.request(request) + + case response.code.to_i + when 200 + parse_success_response(response) + when 400 + parse_error_response(response) + when 401, 403 + raise OpenFeature::GoFeatureFlag::UnauthorizedError.new(response) + when 404 + raise OpenFeature::GoFeatureFlag::FlagNotFoundError.new(response, flag_key) + when 429 + parse_retry_later_header(response) + raise OpenFeature::GoFeatureFlag::RateLimited.new(response) + else + raise OpenFeature::GoFeatureFlag::InternalServerError.new(response) + end + end + + private + + def parse_error_response(response) + required_keys = %w[key error_code] + parsed = JSON.parse(response.body) + + missing_keys = required_keys - parsed.keys + unless missing_keys.empty? + raise OpenFeature::GoFeatureFlag::ParseError.new(response) + end + + OpenFeature::GoFeatureFlag::OfrepApiResponse.new( + value: nil, + key: parsed["key"], + reason: OpenFeature::SDK::Provider::Reason::ERROR, + variant: nil, + error_code: parsed["error_code"], + error_details: parsed["error_details"], + metadata: nil + ) + end + + def parse_success_response(response) + required_keys = %w[key value reason variant] + parsed = JSON.parse(response.body) + + missing_keys = required_keys - parsed.keys + unless missing_keys.empty? + raise OpenFeature::GoFeatureFlag::ParseError.new(response) + end + + OpenFeature::GoFeatureFlag::OfrepApiResponse.new( + value: parsed["value"], + key: parsed["key"], + reason: reason_mapper(parsed["reason"]), + variant: parsed["variant"], + error_code: nil, + error_details: nil, + metadata: parsed["metadata"] + ) + end + + def reason_mapper(reason_str) + reason_str = reason_str.upcase + reason_map = { + "STATIC" => OpenFeature::SDK::Provider::Reason::STATIC, + "DEFAULT" => OpenFeature::SDK::Provider::Reason::DEFAULT, + "TARGETING_MATCH" => OpenFeature::SDK::Provider::Reason::TARGETING_MATCH, + "SPLIT" => OpenFeature::SDK::Provider::Reason::SPLIT, + "CACHED" => OpenFeature::SDK::Provider::Reason::CACHED, + "DISABLED" => OpenFeature::SDK::Provider::Reason::DISABLED, + "UNKNOWN" => OpenFeature::SDK::Provider::Reason::UNKNOWN, + "STALE" => OpenFeature::SDK::Provider::Reason::STALE, + "ERROR" => OpenFeature::SDK::Provider::Reason::ERROR + } + reason_map[reason_str] || OpenFeature::SDK::Provider::Reason::UNKNOWN + end + + def parse_retry_later_header(response) + retry_after = response["Retry-After"] + return nil if retry_after.nil? + + begin + @retry_after = if /^\d+$/.match?(retry_after) + # Retry-After is in seconds + Time.now + Integer(retry_after) + else + # Retry-After is an HTTP-date + Time.httpdate(retry_after) + end + rescue ArgumentError + # ignore invalid Retry-After header + nil + end + end + end + end +end diff --git a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/model/ofrep_api_response.rb b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/model/ofrep_api_response.rb new file mode 100644 index 0000000..6d33762 --- /dev/null +++ b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/model/ofrep_api_response.rb @@ -0,0 +1,32 @@ +module OpenFeature + module GoFeatureFlag + class OfrepApiResponse + attr_reader :value, :key, :reason, :variant, :error_code, :error_details, :metadata + + def initialize(value:, key:, reason:, variant:, error_code:, error_details:, metadata:) + @value = value + @key = key + @reason = reason + @variant = variant + @error_code = error_code + @error_details = error_details + @metadata = metadata + end + + def has_error? + !@error_code.nil? && !@error_code.empty? + end + + def eql?(other) + return false unless other.is_a?(OpenFeature::GoFeatureFlag::OfrepApiResponse) + key == other.key && + value == other.value && + reason == other.reason && + variant == other.variant && + error_code == other.error_code && + error_details == other.error_details && + metadata == other.metadata + end + end + end +end \ No newline at end of file diff --git a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/options.rb b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/options.rb new file mode 100644 index 0000000..a2b9509 --- /dev/null +++ b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/options.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true +require 'uri' + +module OpenFeature + module GoFeatureFlag + # This class is the configuration class for the GoFeatureFlagProvider + class Options + attr_accessor :endpoint, :custom_headers + + def initialize(endpoint: nil, headers: {}) + validate_endpoint(endpoint: endpoint) + @endpoint = endpoint + @custom_headers = headers + end + + private + + def validate_endpoint(endpoint:nil) + return if endpoint.nil? + + uri = URI.parse(endpoint) + raise ArgumentError, "Invalid URL for endpoint: #{endpoint}" unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS) + rescue URI::InvalidURIError + raise ArgumentError, "Invalid URL for endpoint: #{endpoint}" + end + end + end +end diff --git a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/version.rb b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/version.rb new file mode 100644 index 0000000..b07d460 --- /dev/null +++ b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/version.rb @@ -0,0 +1,5 @@ +module OpenFeature + module GoFeatureFlag + GO_FEATURE_FLAG_PROVIDER_VERSION = "0.1.0" + end +end diff --git a/providers/openfeature-go-feature-flag-provider/openfeature-go-feature-flag-provider.gemspec b/providers/openfeature-go-feature-flag-provider/openfeature-go-feature-flag-provider.gemspec new file mode 100644 index 0000000..30e53e3 --- /dev/null +++ b/providers/openfeature-go-feature-flag-provider/openfeature-go-feature-flag-provider.gemspec @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require_relative 'lib/openfeature/go-feature-flag/version' + +Gem::Specification.new do |spec| + spec.name = 'openfeature-go-feature-flag-provider' + spec.version = OpenFeature::GoFeatureFlag::GO_FEATURE_FLAG_PROVIDER_VERSION + spec.authors = ['Thomas Poignant'] + spec.email = ['contact@gofeatureflag.org'] + + spec.summary = 'The GO Feature Flag provider for the OpenFeature Ruby SDK' + spec.description = 'The GO Feature Flag provider for the OpenFeature Ruby SDK' + spec.homepage = 'https://github.com/open-feature/ruby-sdk-contrib/tree/main/providers/openfeature-go-feature-flag-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-go-feature-flag-provider' + spec.metadata['changelog_uri'] = 'https://github.com/open-feature/ruby-sdk-contrib/blob/main/providers/openfeature-go-feature-flag-provider/CHANGELOG.md' + spec.metadata['bug_tracker_uri'] = 'https://github.com/thomaspoignant/go-feature-flag/issues/new/choose' + spec.metadata['documentation_uri'] = 'https://gofeatureflag.org/docs' + + # 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_runtime_dependency 'openfeature-sdk', '~> 0.3.1' + + spec.add_development_dependency 'rake', '~> 13.0' + spec.add_development_dependency 'rspec', '~> 3.12.0' + spec.add_development_dependency 'standard' + spec.add_development_dependency 'rubocop' + spec.add_development_dependency 'simplecov' + spec.add_development_dependency 'webmock' + +end diff --git a/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/goff_api_spec.rb b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/goff_api_spec.rb new file mode 100644 index 0000000..04f81b6 --- /dev/null +++ b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/goff_api_spec.rb @@ -0,0 +1,282 @@ +require "spec_helper" +require "open_feature/sdk" +require "webmock/rspec" + +describe OpenFeature::GoFeatureFlag::GoFeatureFlagApi do + subject(:goff_api) do + options = OpenFeature::GoFeatureFlag::Options.new(endpoint: "http://localhost:1031") + described_class.new(options: options) + end + + let(:default_evaluation_context) do + OpenFeature::SDK::EvaluationContext.new( + targeting_key: "4f433951-4c8c-42b3-9f18-8c9a5ed8e9eb", + company: "GO Feature Flag", + firstname: "John", + lastname: "Doe" + ) + end + + context "#evaluate" do + it "should raise an error if rate limited" do + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/double_key") + .to_return(status: 429) + + expect { + goff_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) + }.to raise_error(OpenFeature::GoFeatureFlag::RateLimited) + end + + it "should raise an error if not authorized (401)" do + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/double_key") + .to_return(status: 401) + + expect { + goff_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) + }.to raise_error(OpenFeature::GoFeatureFlag::UnauthorizedError) + end + + it "should raise an error if not authorized (403)" do + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/double_key") + .to_return(status: 403) + + expect { + goff_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) + }.to raise_error(OpenFeature::GoFeatureFlag::UnauthorizedError) + end + + it "should raise an error if flag not found (404)" do + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/does-not-exists") + .to_return(status: 404) + + expect { + goff_api.evaluate_ofrep_api(flag_key: "does-not-exists", evaluation_context: default_evaluation_context) + }.to raise_error(OpenFeature::GoFeatureFlag::FlagNotFoundError) + end + + it "should raise an error if unknown http code (500)" do + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/double_key") + .to_return(status: 500) + + expect { + goff_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) + }.to raise_error(OpenFeature::GoFeatureFlag::InternalServerError) + end + + it "should return an error response if 400" do + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/double_key") + .to_return(status: 400, body: + { + key: "double_key", + error_code: "TYPE_MISMATCH", + error_details: "expected type: boolean, got: string" + }.to_json) + + got = goff_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) + want = OpenFeature::GoFeatureFlag::OfrepApiResponse.new( + key: "double_key", + value: nil, + reason: OpenFeature::SDK::Provider::Reason::ERROR, + variant: nil, + error_code: "TYPE_MISMATCH", + error_details: "expected type: boolean, got: string", + metadata: nil + ) + expect(got).to eql(want) + end + + it "should return a valid response if 200" do + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/double_key") + .to_return(status: 200, body: + { + key: "double_key", + metadata: {"website" => "https://gofeatureflag.org"}, + value: 1.15, + reason: "TARGETING_MATCH", + variant: "variantA" + }.to_json) + + got = goff_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) + want = OpenFeature::GoFeatureFlag::OfrepApiResponse.new( + key: "double_key", + value: 1.15, + reason: OpenFeature::SDK::Provider::Reason::TARGETING_MATCH, + variant: "variantA", + error_code: nil, + error_details: nil, + metadata: {"website" => "https://gofeatureflag.org"} + ) + expect(got).to eql(want) + end + + it "should raise an error if 200 and json does not contains the required keys (missing value)" do + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/double_key") + .to_return(status: 200, body: + { + key: "double_key", + metadata: {"website" => "https://gofeatureflag.org"}, + reason: "TARGETING_MATCH", + variant: "variantA" + }.to_json) + + expect { + goff_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) + }.to raise_error(OpenFeature::GoFeatureFlag::ParseError) + end + + it "should raise an error if 200 and json does not contains the required keys (missing key)" do + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/double_key") + .to_return(status: 200, body: + { + value: 1.15, + metadata: {"website" => "https://gofeatureflag.org"}, + reason: "TARGETING_MATCH", + variant: "variantA" + }.to_json) + + expect { + goff_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) + }.to raise_error(OpenFeature::GoFeatureFlag::ParseError) + end + + it "should raise an error if 200 and json does not contains the required keys (missing reason)" do + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/double_key") + .to_return(status: 200, body: + { + value: 1.15, + metadata: {"website" => "https://gofeatureflag.org"}, + key: "double_key", + variant: "variantA" + }.to_json) + + expect { + goff_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) + }.to raise_error(OpenFeature::GoFeatureFlag::ParseError) + end + + it "should raise an error if 200 and json does not contains the required keys (missing variant)" do + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/double_key") + .to_return(status: 200, body: + { + value: 1.15, + metadata: {"website" => "https://gofeatureflag.org"}, + key: "double_key", + reason: "TARGETING_MATCH", + }.to_json) + + expect { + goff_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) + }.to raise_error(OpenFeature::GoFeatureFlag::ParseError) + end + + it "should raise an error if 400 and json does not contains the required keys (missing key)" do + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/double_key") + .to_return(status: 400, body: + { + error_code: "TYPE_MISMATCH" + }.to_json) + + expect { + goff_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) + }.to raise_error(OpenFeature::GoFeatureFlag::ParseError) + end + + it "should raise an error if 400 and json does not contains the required keys (missing error_code)" do + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/double_key") + .to_return(status: 400, body: + { + key: "double_key" + }.to_json) + + expect { + goff_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) + }.to raise_error(OpenFeature::GoFeatureFlag::ParseError) + end + + it "should raise an error if 400 and json does not contains the required keys (missing error_code)" do + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/double_key") + .to_return(status: 400, body: + { + key: "double_key" + }.to_json) + + expect { + goff_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) + }.to raise_error(OpenFeature::GoFeatureFlag::ParseError) + end + + it "should not be able to call the API again if rate-limited (with retry-after int)" do + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/double_key") + .to_return(status: 429, headers: {"Retry-After" => "10"}) + + expect { + goff_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) + }.to raise_error(OpenFeature::GoFeatureFlag::RateLimited) + + expect { + goff_api.evaluate_ofrep_api(flag_key: "random_flag", evaluation_context: default_evaluation_context) + }.to raise_error(OpenFeature::GoFeatureFlag::RateLimited) + end + + it "should be able to call the API again if we wait after the retry-after (as int)" do + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/double_key") + .to_return(status: 429, headers: {"Retry-After" => "1"}) + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/random_flag") + .to_return(status: 200, body: + { + value: 1.15, + metadata: {"website" => "https://gofeatureflag.org"}, + key: "double_key", + reason: "TARGETING_MATCH", + variant: "variantA" + }.to_json) + + expect { + goff_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) + }.to raise_error(OpenFeature::GoFeatureFlag::RateLimited) + + sleep(1.1) + + expect { + goff_api.evaluate_ofrep_api(flag_key: "random_flag", evaluation_context: default_evaluation_context) + }.not_to raise_error + end + + it "should not be able to call the API again if rate-limited (with retry-after date)" do + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/double_key") + .to_return(status: 429, headers: {"Retry-After" => (Time.now + 1).httpdate}) + + expect { + goff_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) + }.to raise_error(OpenFeature::GoFeatureFlag::RateLimited) + + expect { + goff_api.evaluate_ofrep_api(flag_key: "random_flag", evaluation_context: default_evaluation_context) + }.to raise_error(OpenFeature::GoFeatureFlag::RateLimited) + end + + it "should be able to call the API again if we wait after the retry-after (as date)" do + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/double_key") + .to_return(status: 429, headers: {"Retry-After" => (Time.now + 1).httpdate}) + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/random_flag") + .to_return(status: 200, body: + { + value: 1.15, + metadata: {"website" => "https://gofeatureflag.org"}, + key: "double_key", + reason: "TARGETING_MATCH", + variant: "variantA" + }.to_json) + + expect { + goff_api.evaluate_ofrep_api(flag_key: "double_key", evaluation_context: default_evaluation_context) + }.to raise_error(OpenFeature::GoFeatureFlag::RateLimited) + + sleep(1.1) + + expect { + goff_api.evaluate_ofrep_api(flag_key: "random_flag", evaluation_context: default_evaluation_context) + }.not_to raise_error + end + end +end diff --git a/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/options_spec.rb b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/options_spec.rb new file mode 100644 index 0000000..80620cd --- /dev/null +++ b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/options_spec.rb @@ -0,0 +1,19 @@ +require "spec_helper" +require "open_feature/sdk" + +describe OpenFeature::GoFeatureFlag::Options do + context "#endpoint" do + it "should have a valid endpoint set" do + options = OpenFeature::GoFeatureFlag::Options.new(endpoint: "http://localhost:1031") + expect(options.endpoint).to eql("http://localhost:1031") + end + + it "should raise if endpoint is invalid" do + expect { OpenFeature::GoFeatureFlag::Options.new(endpoint: "invalid_url") }.to raise_error(ArgumentError, "Invalid URL for endpoint: invalid_url") + end + + it "should raise if endpoint is not http" do + expect { OpenFeature::GoFeatureFlag::Options.new(endpoint: "ftp://gofeatureflag.org") }.to raise_error(ArgumentError, "Invalid URL for endpoint: ftp://gofeatureflag.org") + end + end +end diff --git a/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/provider_spec.rb b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/provider_spec.rb new file mode 100644 index 0000000..2b723d1 --- /dev/null +++ b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/provider_spec.rb @@ -0,0 +1,550 @@ +require "spec_helper" +require "open_feature/sdk" +require "webmock/rspec" + +describe OpenFeature::GoFeatureFlag::Provider do + subject(:goff_provider) do + options = OpenFeature::GoFeatureFlag::Options.new(endpoint: "http://localhost:1031") + described_class.new(options: options) + end + + context "#metadata" do + it "metadata name is defined" do + expect(goff_provider).to respond_to(:metadata) + expect(goff_provider.metadata).to respond_to(:name) + expect(goff_provider.metadata.name).to eq("GO Feature Flag Provider") + end + end + + context "#options" do + it "should have a valid endpoint set" do + expect(goff_provider.options.endpoint).to eql("http://localhost:1031") + end + + it "should raise if endpoint is invalid" do + expect { OpenFeature::GoFeatureFlag::Options.new(endpoint: "invalid_url") }.to raise_error(ArgumentError, "Invalid URL for endpoint: invalid_url") + end + + it "should raise if endpoint is not http" do + expect { OpenFeature::GoFeatureFlag::Options.new(endpoint: "ftp://gofeatureflag.org") }.to raise_error(ArgumentError, "Invalid URL for endpoint: ftp://gofeatureflag.org") + end + end + + context "#fetch_boolean_value with openfeature" do + it "should return the value of the flag" do + test_name = RSpec.current_example.description + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/boolean_flag") + .to_return(status: 200, body: + { + key: "double_key", + metadata: {"website" => "https://gofeatureflag.org"}, + value: true, + reason: "TARGETING_MATCH", + variant: "variantA" + }.to_json) + + OpenFeature::SDK.configure do |config| + config.set_provider(goff_provider, domain: test_name) + end + client = OpenFeature::SDK.build_client(domain: test_name) + bool_value = client.fetch_boolean_value( + flag_key: "boolean_flag", + default_value: false, + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16") + ) + expect(bool_value).to be_truthy + end + + it "should return the default value if flag is not the right type" do + test_name = RSpec.current_example.description + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/boolean_flag") + .to_return(status: 200, body: + { + key: "double_key", + metadata: {"website" => "https://gofeatureflag.org"}, + value: "default", + reason: "TARGETING_MATCH", + variant: "variantA" + }.to_json) + + OpenFeature::SDK.configure do |config| + config.set_provider(goff_provider, domain: test_name) + end + client = OpenFeature::SDK.build_client(domain: test_name) + bool_value = client.fetch_boolean_value( + flag_key: "boolean_flag", + default_value: false, + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16") + ) + expect(bool_value).to be_falsey + end + + it "should return the default value of the flag if error send by the API (http code 403)" do + test_name = RSpec.current_example.description + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/boolean_flag") + .to_return(status: 403) + + OpenFeature::SDK.configure do |config| + config.set_provider(goff_provider, domain: test_name) + end + client = OpenFeature::SDK.build_client(domain: test_name) + bool_value = client.fetch_boolean_value( + flag_key: "boolean_flag", + default_value: false, + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16") + ) + expect(bool_value).to be_falsey + end + + it "should return the default value of the flag if error send by the API (http code 400)" do + test_name = RSpec.current_example.description + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/boolean_flag") + .to_return(status: 400, body: + { + key: "double_key", + error_code: "INVALID_CONTEXT" + }.to_json) + + OpenFeature::SDK.configure do |config| + config.set_provider(goff_provider, domain: test_name) + end + client = OpenFeature::SDK.build_client(domain: test_name) + bool_value = client.fetch_boolean_value( + flag_key: "boolean_flag", + default_value: false, + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16") + ) + expect(bool_value).to be_falsey + end + + it "should return default value if no evaluation context" do + test_name = RSpec.current_example.description + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/boolean_flag") + .to_return(status: 200, body: + { + key: "double_key", + metadata: {"website" => "https://gofeatureflag.org"}, + value: true, + reason: "TARGETING_MATCH", + variant: "variantA" + }.to_json) + + OpenFeature::SDK.configure do |config| + config.set_provider(goff_provider, domain: test_name) + end + client = OpenFeature::SDK.build_client(domain: test_name) + bool_value = client.fetch_boolean_value( + flag_key: "boolean_flag", + default_value: false, + evaluation_context: nil + ) + expect(bool_value).to be_falsey + end + + it "should return default value if evaluation context has empty string targetingKey" do + test_name = RSpec.current_example.description + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/boolean_flag") + .to_return(status: 200, body: + { + key: "double_key", + metadata: {"website" => "https://gofeatureflag.org"}, + value: true, + reason: "TARGETING_MATCH", + variant: "variantA" + }.to_json) + + OpenFeature::SDK.configure do |config| + config.set_provider(goff_provider, domain: test_name) + end + client = OpenFeature::SDK.build_client(domain: test_name) + bool_value = client.fetch_boolean_value( + flag_key: "boolean_flag", + default_value: false, + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "") + ) + expect(bool_value).to be_falsey + end + + it "should return default value if evaluation context has nil targetingKey" do + test_name = RSpec.current_example.description + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/boolean_flag") + .to_return(status: 200, body: + { + key: "double_key", + metadata: {"website" => "https://gofeatureflag.org"}, + value: true, + reason: "TARGETING_MATCH", + variant: "variantA" + }.to_json) + + OpenFeature::SDK.configure do |config| + config.set_provider(goff_provider, domain: test_name) + end + client = OpenFeature::SDK.build_client(domain: test_name) + bool_value = client.fetch_boolean_value( + flag_key: "boolean_flag", + default_value: false, + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: nil) + ) + expect(bool_value).to be_falsey + end + + it "should return default value if flag_key nil" do + test_name = RSpec.current_example.description + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/boolean_flag") + .to_return(status: 200, body: + { + key: "double_key", + metadata: {"website" => "https://gofeatureflag.org"}, + value: true, + reason: "TARGETING_MATCH", + variant: "variantA" + }.to_json) + + OpenFeature::SDK.configure do |config| + config.set_provider(goff_provider, domain: test_name) + end + client = OpenFeature::SDK.build_client(domain: test_name) + bool_value = client.fetch_boolean_value( + flag_key: nil, + default_value: false, + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "1234") + ) + expect(bool_value).to be_falsey + end + + it "should return default value if flag_key empty string" do + test_name = RSpec.current_example.description + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/boolean_flag") + .to_return(status: 200, body: + { + key: "double_key", + metadata: {"website" => "https://gofeatureflag.org"}, + value: true, + reason: "TARGETING_MATCH", + variant: "variantA" + }.to_json) + + OpenFeature::SDK.configure do |config| + config.set_provider(goff_provider, domain: test_name) + end + client = OpenFeature::SDK.build_client(domain: test_name) + bool_value = client.fetch_boolean_value( + flag_key: "", + default_value: false, + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "1234") + ) + expect(bool_value).to be_falsey + end + end + + context "#fetch_string_value with openfeature" do + it "should return the value of the flag" do + test_name = RSpec.current_example.description + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/flag_key") + .to_return(status: 200, body: + { + key: "flag_key", + metadata: {"website" => "https://gofeatureflag.org"}, + value: "aValue", + reason: "TARGETING_MATCH", + variant: "variantA" + }.to_json) + + OpenFeature::SDK.configure do |config| + config.set_provider(goff_provider, domain: test_name) + end + client = OpenFeature::SDK.build_client(domain: test_name) + value = client.fetch_string_value( + flag_key: "flag_key", + default_value: "default", + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16") + ) + expect(value).to eq("aValue") + end + + it "should return the default value if flag is not the right type" do + test_name = RSpec.current_example.description + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/flag_key") + .to_return(status: 200, body: + { + key: "flag_key", + metadata: {"website" => "https://gofeatureflag.org"}, + value: 15, + reason: "TARGETING_MATCH", + variant: "variantA" + }.to_json) + + OpenFeature::SDK.configure do |config| + config.set_provider(goff_provider, domain: test_name) + end + client = OpenFeature::SDK.build_client(domain: test_name) + value = client.fetch_string_value( + flag_key: "flag_key", + default_value: "default", + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16") + ) + expect(value).to eq("default") + end + end + + context "#fetch_number_value with openfeature" do + it "should return the value of the flag" do + test_name = RSpec.current_example.description + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/flag_key") + .to_return(status: 200, body: + { + key: "flag_key", + metadata: {"website" => "https://gofeatureflag.org"}, + value: 15, + reason: "TARGETING_MATCH", + variant: "variantA" + }.to_json) + + OpenFeature::SDK.configure do |config| + config.set_provider(goff_provider, domain: test_name) + end + client = OpenFeature::SDK.build_client(domain: test_name) + value = client.fetch_number_value( + flag_key: "flag_key", + default_value: 25, + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16") + ) + expect(value).to eq(15) + end + + it "should return the default value if flag is not the right type" do + test_name = RSpec.current_example.description + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/flag_key") + .to_return(status: 200, body: + { + key: "flag_key", + metadata: {"website" => "https://gofeatureflag.org"}, + value: "yoyo", + reason: "TARGETING_MATCH", + variant: "variantA" + }.to_json) + + OpenFeature::SDK.configure do |config| + config.set_provider(goff_provider, domain: test_name) + end + client = OpenFeature::SDK.build_client(domain: test_name) + value = client.fetch_number_value( + flag_key: "flag_key", + default_value: 25, + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16") + ) + expect(value).to eq(25) + end + end + + context "#fetch_object_value with openfeature" do + it "should return the value of the flag" do + test_name = RSpec.current_example.description + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/flag_key") + .to_return(status: 200, body: + { + key: "flag_key", + metadata: {"website" => "https://gofeatureflag.org"}, + value: {"test" => "test"}, + reason: "TARGETING_MATCH", + variant: "variantA" + }.to_json) + + OpenFeature::SDK.configure do |config| + config.set_provider(goff_provider, domain: test_name) + end + client = OpenFeature::SDK.build_client(domain: test_name) + value = client.fetch_object_value( + flag_key: "flag_key", + default_value: {"fail" => true}, + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16") + ) + expect(value).to eq({"test" => "test"}) + end + + it "should return the default value if flag is not the right type" do + test_name = RSpec.current_example.description + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/flag_key") + .to_return(status: 200, body: + { + key: "flag_key", + metadata: {"website" => "https://gofeatureflag.org"}, + value: '{"test" => "test"}', + reason: "TARGETING_MATCH", + variant: "variantA" + }.to_json) + + OpenFeature::SDK.configure do |config| + config.set_provider(goff_provider, domain: test_name) + end + client = OpenFeature::SDK.build_client(domain: test_name) + value = client.fetch_object_value( + flag_key: "flag_key", + default_value: {"fail" => true}, + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16") + ) + expect(value).to eq({"fail" => true}) + end + end + + context "#fetch_boolean_value provider directly" do + it "should return an error if no evaluation context" do + eval = goff_provider.fetch_boolean_value(flag_key: "flag_key", default_value: true, evaluation_context: nil) + want = OpenFeature::SDK::Provider::ResolutionDetails.new( + value: true, + error_code: OpenFeature::SDK::Provider::ErrorCode::INVALID_CONTEXT, + error_message: "invalid evaluation context provided", + reason: OpenFeature::SDK::Provider::Reason::ERROR + ) + expect(eval).to eql(want) + end + + it "should return an error if evaluation context has empty string targetingKey" do + eval = goff_provider.fetch_boolean_value(flag_key: "flag_key", + default_value: true, + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "")) + want = OpenFeature::SDK::Provider::ResolutionDetails.new( + value: true, + error_code: OpenFeature::SDK::Provider::ErrorCode::INVALID_CONTEXT, + error_message: "invalid evaluation context provided", + reason: OpenFeature::SDK::Provider::Reason::ERROR + ) + expect(eval).to eql(want) + end + + it "should return an error if evaluation context has nil targetingKey" do + eval = goff_provider.fetch_boolean_value(flag_key: "flag_key", + default_value: true, + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: nil)) + want = OpenFeature::SDK::Provider::ResolutionDetails.new( + value: true, + error_code: OpenFeature::SDK::Provider::ErrorCode::INVALID_CONTEXT, + error_message: "invalid evaluation context provided", + reason: OpenFeature::SDK::Provider::Reason::ERROR + ) + expect(eval).to eql(want) + end + + it "should return an error if flag_key nil" do + eval = goff_provider.fetch_boolean_value(flag_key: nil, + default_value: true, + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16")) + want = OpenFeature::SDK::Provider::ResolutionDetails.new( + value: true, + error_code: OpenFeature::SDK::Provider::ErrorCode::GENERAL, + error_message: "invalid flag key provided", + reason: OpenFeature::SDK::Provider::Reason::ERROR + ) + expect(eval).to eql(want) + end + + it "should return an error if flag_key empty string" do + eval = goff_provider.fetch_boolean_value(flag_key: "", + default_value: true, + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16")) + want = OpenFeature::SDK::Provider::ResolutionDetails.new( + value: true, + error_code: OpenFeature::SDK::Provider::ErrorCode::GENERAL, + error_message: "invalid flag key provided", + reason: OpenFeature::SDK::Provider::Reason::ERROR + ) + expect(eval).to eql(want) + end + + it "return an error API response if 401" do + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/boolean_flag") + .to_return(status: 401) + eval = goff_provider.fetch_boolean_value( + flag_key: "boolean_flag", + default_value: true, + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16")) + want = OpenFeature::SDK::Provider::ResolutionDetails.new( + value: true, + error_code: OpenFeature::SDK::Provider::ErrorCode::GENERAL, + error_message: "unauthorized", + reason: OpenFeature::SDK::Provider::Reason::ERROR + ) + expect(eval).to eql(want) + end + + it "return an error API response if 403" do + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/boolean_flag") + .to_return(status: 403) + eval = goff_provider.fetch_boolean_value( + flag_key: "boolean_flag", + default_value: true, + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16")) + want = OpenFeature::SDK::Provider::ResolutionDetails.new( + value: true, + error_code: OpenFeature::SDK::Provider::ErrorCode::GENERAL, + error_message: "unauthorized", + reason: OpenFeature::SDK::Provider::Reason::ERROR + ) + expect(eval).to eql(want) + end + + it "return an error API response if 400" do + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/boolean_flag") + .to_return(status: 400, body: + { + key: "boolean_flag", + error_code: OpenFeature::SDK::Provider::ErrorCode::GENERAL.to_s, + error_details: "GENERAL error" + }.to_json) + eval = goff_provider.fetch_boolean_value( + flag_key: "boolean_flag", + default_value: false, + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16")) + want = OpenFeature::SDK::Provider::ResolutionDetails.new( + value: false, + error_code: OpenFeature::SDK::Provider::ErrorCode::GENERAL, + error_message: "GENERAL error", + reason: OpenFeature::SDK::Provider::Reason::ERROR + ) + expect(eval).to eql(want) + end + + it "return an error API response if flag not found 404" do + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/boolean_flag") + .to_return(status: 404, body: + { + key: "boolean_flag", + error_code: OpenFeature::SDK::Provider::ErrorCode::FLAG_NOT_FOUND.to_s, + error_details: "GENERAL error" + }.to_json) + eval = goff_provider.fetch_boolean_value( + flag_key: "boolean_flag", + default_value: false, + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16")) + want = OpenFeature::SDK::Provider::ResolutionDetails.new( + value: false, + error_code: OpenFeature::SDK::Provider::ErrorCode::FLAG_NOT_FOUND, + error_message: "Flag not found: boolean_flag", + reason: OpenFeature::SDK::Provider::Reason::ERROR + ) + expect(eval).to eql(want) + end + + it "return an error API response if 500" do + stub_request(:post, "http://localhost:1031/ofrep/v1/evaluate/flags/boolean_flag") + .to_return(status: 500, body: + { + key: "boolean_flag", + error_code: OpenFeature::SDK::Provider::ErrorCode::GENERAL.to_s, + error_details: "GENERAL error" + }.to_json) + eval = goff_provider.fetch_boolean_value( + flag_key: "boolean_flag", + default_value: false, + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16")) + want = OpenFeature::SDK::Provider::ResolutionDetails.new( + value: false, + error_code: OpenFeature::SDK::Provider::ErrorCode::GENERAL, + error_message: "Internal Server Error", + reason: OpenFeature::SDK::Provider::Reason::ERROR + ) + expect(eval).to eql(want) + end + end +end diff --git a/providers/openfeature-go-feature-flag-provider/spec/spec_helper.rb b/providers/openfeature-go-feature-flag-provider/spec/spec_helper.rb new file mode 100644 index 0000000..e79e5fb --- /dev/null +++ b/providers/openfeature-go-feature-flag-provider/spec/spec_helper.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require 'bundler/setup' +require 'openfeature/go-feature-flag/go_feature_flag_provider' +require 'openfeature/go-feature-flag/options' +require 'openfeature/go-feature-flag/goff_api' + +# This file was generated by the `rspec --init` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + + # The settings below are suggested to provide a good initial experience + # with RSpec, but feel free to customize to your heart's content. + # # This allows you to limit a spec run to individual examples or groups + # # you care about by tagging them with `:focus` metadata. When nothing + # # is tagged with `:focus`, all examples get run. RSpec also provides + # # aliases for `it`, `describe`, and `context` that include `:focus` + # # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + # config.filter_run_when_matching :focus + # + # # Allows RSpec to persist some state between runs in order to support + # # the `--only-failures` and `--next-failure` CLI options. We recommend + # # you configure your source control system to ignore this file. + # config.example_status_persistence_file_path = "spec/examples.txt" + # + # # Limits the available syntax to the non-monkey patched syntax that is + # # recommended. For more details, see: + # # https://relishapp.com/rspec/rspec-core/docs/configuration/zero-monkey-patching-mode + # config.disable_monkey_patching! + # + # # This setting enables warnings. It's recommended, but in some cases may + # # be too noisy due to issues in dependencies. + # config.warnings = true + # + # # Many RSpec users commonly either run the entire suite or an individual + # # file, and it's useful to allow more verbose output when running an + # # individual spec file. + # if config.files_to_run.one? + # # Use the documentation formatter for detailed output, + # # unless a formatter has already been configured + # # (e.g. via a command-line flag). + # config.default_formatter = "doc" + # end + # + # # Print the 10 slowest examples and example groups at the + # # end of the spec run, to help surface which specs are running + # # particularly slow. + # config.profile_examples = 10 + # + # # Run specs in random order to surface order dependencies. If you find an + # # order dependency and want to debug it, you can fix the order by providing + # # the seed, which is printed after each run. + # # --seed 1234 + # config.order = :random + # + # # Seed global randomization in this process using the `--seed` CLI option. + # # Setting this allows you to use `--seed` to deterministically reproduce + # # test failures related to randomization by passing the same `--seed` value + # # as the one that triggered the failure. + # Kernel.srand config.seed +end diff --git a/release-please-config.json b/release-please-config.json index 0b970c2..3148493 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -21,6 +21,16 @@ "extra-files": [ "README.md" ] + }, + "providers/openfeature-go-feature-flag-provider": { + "package-name": "openfeature-go-feature-flag-provider", + "version-file": "lib/openfeature/go-feature-flag/version.rb", + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "versioning": "default", + "extra-files": [ + "README.md" + ] } }, "changelog-sections": [ diff --git a/ruby-sdk-contrib.code-workspace b/ruby-sdk-contrib.code-workspace index 8a3f8ce..2669c9a 100644 --- a/ruby-sdk-contrib.code-workspace +++ b/ruby-sdk-contrib.code-workspace @@ -5,6 +5,9 @@ }, { "path": "providers/openfeature-meta_provider" + }, + { + "path": "providers/openfeature-go-feature-flag-provider" } ] } From c3bbc0fa7221bf4ebddfe94fc4597abc05d620d9 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Mon, 5 Aug 2024 16:23:50 +0200 Subject: [PATCH 02/19] chore(ci): Add CI for GO Feature Flag Signed-off-by: Thomas Poignant --- .github/workflows/ruby.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 3de104a..b9834c9 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -64,3 +64,25 @@ jobs: working-directory: ./providers/openfeature-meta_provider - name: Lint and test run: bin/rake + + test_go_feature_flag_provider: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./providers/openfeature-go-feature-flag-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-go-feature-flag-provider + - name: Lint and test + run: bin/rake \ No newline at end of file From eb41719b7ce47ffa87d2912acdce1135e53d20a5 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Mon, 5 Aug 2024 16:27:08 +0200 Subject: [PATCH 03/19] fix: dependencies failed CI Signed-off-by: Thomas Poignant --- providers/openfeature-go-feature-flag-provider/Gemfile.lock | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/providers/openfeature-go-feature-flag-provider/Gemfile.lock b/providers/openfeature-go-feature-flag-provider/Gemfile.lock index 2b1ff92..b8eb005 100644 --- a/providers/openfeature-go-feature-flag-provider/Gemfile.lock +++ b/providers/openfeature-go-feature-flag-provider/Gemfile.lock @@ -30,7 +30,8 @@ GEM rainbow (3.1.1) rake (13.2.1) regexp_parser (2.9.0) - rexml (3.2.8) + rexml (3.3.4) + strscan rspec (3.12.0) rspec-core (~> 3.12.0) rspec-expectations (~> 3.12.0) @@ -79,6 +80,7 @@ GEM standard-performance (1.3.1) lint_roller (~> 1.1) rubocop-performance (~> 1.20.2) + strscan (3.1.0) unicode-display_width (2.5.0) webmock (3.23.1) addressable (>= 2.8.0) From 51357beb2a7c9f2ed09f3d1ca41617228b6532f4 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Mon, 5 Aug 2024 16:30:45 +0200 Subject: [PATCH 04/19] fix(ci): Add rake file Signed-off-by: Thomas Poignant --- .github/workflows/ruby.yml | 2 +- .../bin/rake | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100755 providers/openfeature-go-feature-flag-provider/bin/rake diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index b9834c9..b66c448 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -37,7 +37,7 @@ jobs: bundler-cache: true # runs 'bundle install' and caches installed gems automatically working-directory: ./providers/openfeature-flagd-provider - name: Launch flagd instance - run: docker-compose up -d flagd + run: docker compose up -d flagd working-directory: ./providers/openfeature-flagd-provider/docker - name: Check linting run: bundle exec rubocop diff --git a/providers/openfeature-go-feature-flag-provider/bin/rake b/providers/openfeature-go-feature-flag-provider/bin/rake new file mode 100755 index 0000000..4eb7d7b --- /dev/null +++ b/providers/openfeature-go-feature-flag-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") From 52f5063e1eb9b11d164e0f3e8db45941340cdb5f Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Mon, 5 Aug 2024 16:33:42 +0200 Subject: [PATCH 05/19] fix lint issue Signed-off-by: Thomas Poignant --- .../go_feature_flag_provider.rb | 2 +- .../openfeature/go-feature-flag/goff_api.rb | 1 + .../model/ofrep_api_response.rb | 2 +- .../openfeature/go-feature-flag/options.rb | 5 +- ...enfeature-go-feature-flag-provider.gemspec | 47 +++++++++---------- .../gofeatureflag/goff_api_spec.rb | 2 +- .../gofeatureflag/provider_spec.rb | 31 +++++++----- .../spec/spec_helper.rb | 8 ++-- 8 files changed, 52 insertions(+), 46 deletions(-) diff --git a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/go_feature_flag_provider.rb b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/go_feature_flag_provider.rb index 70e87f6..972cb94 100644 --- a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/go_feature_flag_provider.rb +++ b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/go_feature_flag_provider.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module OpenFeature module GoFeatureFlag # This class is the entry point for the GoFeatureFlagProvider @@ -62,7 +63,6 @@ def evaluate(flag_key:, default_value:, allowed_classes:, evaluation_context: ni variant: parsed_response.variant, flag_metadata: parsed_response.metadata ) - rescue OpenFeature::GoFeatureFlag::UnauthorizedError, OpenFeature::GoFeatureFlag::InvalidOptionError, OpenFeature::GoFeatureFlag::FlagNotFoundError, diff --git a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/goff_api.rb b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/goff_api.rb index ac6f5c1..b78d2a3 100644 --- a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/goff_api.rb +++ b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/goff_api.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require "open_feature/sdk" require "net/http" require "json" diff --git a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/model/ofrep_api_response.rb b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/model/ofrep_api_response.rb index 6d33762..7fc327b 100644 --- a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/model/ofrep_api_response.rb +++ b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/model/ofrep_api_response.rb @@ -29,4 +29,4 @@ def eql?(other) end end end -end \ No newline at end of file +end diff --git a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/options.rb b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/options.rb index a2b9509..609b5fa 100644 --- a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/options.rb +++ b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/options.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true -require 'uri' + +require "uri" module OpenFeature module GoFeatureFlag @@ -15,7 +16,7 @@ def initialize(endpoint: nil, headers: {}) private - def validate_endpoint(endpoint:nil) + def validate_endpoint(endpoint: nil) return if endpoint.nil? uri = URI.parse(endpoint) diff --git a/providers/openfeature-go-feature-flag-provider/openfeature-go-feature-flag-provider.gemspec b/providers/openfeature-go-feature-flag-provider/openfeature-go-feature-flag-provider.gemspec index 30e53e3..5bcb7b8 100644 --- a/providers/openfeature-go-feature-flag-provider/openfeature-go-feature-flag-provider.gemspec +++ b/providers/openfeature-go-feature-flag-provider/openfeature-go-feature-flag-provider.gemspec @@ -1,41 +1,40 @@ # frozen_string_literal: true -require_relative 'lib/openfeature/go-feature-flag/version' +require_relative "lib/openfeature/go-feature-flag/version" Gem::Specification.new do |spec| - spec.name = 'openfeature-go-feature-flag-provider' + spec.name = "openfeature-go-feature-flag-provider" spec.version = OpenFeature::GoFeatureFlag::GO_FEATURE_FLAG_PROVIDER_VERSION - spec.authors = ['Thomas Poignant'] - spec.email = ['contact@gofeatureflag.org'] + spec.authors = ["Thomas Poignant"] + spec.email = ["contact@gofeatureflag.org"] - spec.summary = 'The GO Feature Flag provider for the OpenFeature Ruby SDK' - spec.description = 'The GO Feature Flag provider for the OpenFeature Ruby SDK' - spec.homepage = 'https://github.com/open-feature/ruby-sdk-contrib/tree/main/providers/openfeature-go-feature-flag-provider' - spec.license = 'Apache-2.0' - spec.required_ruby_version = '>= 3.1' + spec.summary = "The GO Feature Flag provider for the OpenFeature Ruby SDK" + spec.description = "The GO Feature Flag provider for the OpenFeature Ruby SDK" + spec.homepage = "https://github.com/open-feature/ruby-sdk-contrib/tree/main/providers/openfeature-go-feature-flag-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-go-feature-flag-provider' - spec.metadata['changelog_uri'] = 'https://github.com/open-feature/ruby-sdk-contrib/blob/main/providers/openfeature-go-feature-flag-provider/CHANGELOG.md' - spec.metadata['bug_tracker_uri'] = 'https://github.com/thomaspoignant/go-feature-flag/issues/new/choose' - spec.metadata['documentation_uri'] = 'https://gofeatureflag.org/docs' + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = "https://github.com/open-feature/ruby-sdk-contrib/tree/main/providers/openfeature-go-feature-flag-provider" + spec.metadata["changelog_uri"] = "https://github.com/open-feature/ruby-sdk-contrib/blob/main/providers/openfeature-go-feature-flag-provider/CHANGELOG.md" + spec.metadata["bug_tracker_uri"] = "https://github.com/thomaspoignant/go-feature-flag/issues/new/choose" + spec.metadata["documentation_uri"] = "https://gofeatureflag.org/docs" # 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.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } - spec.require_paths = ['lib'] + spec.require_paths = ["lib"] - spec.add_runtime_dependency 'openfeature-sdk', '~> 0.3.1' - - spec.add_development_dependency 'rake', '~> 13.0' - spec.add_development_dependency 'rspec', '~> 3.12.0' - spec.add_development_dependency 'standard' - spec.add_development_dependency 'rubocop' - spec.add_development_dependency 'simplecov' - spec.add_development_dependency 'webmock' + spec.add_runtime_dependency "openfeature-sdk", "~> 0.3.1" + spec.add_development_dependency "rake", "~> 13.0" + spec.add_development_dependency "rspec", "~> 3.12.0" + spec.add_development_dependency "standard" + spec.add_development_dependency "rubocop" + spec.add_development_dependency "simplecov" + spec.add_development_dependency "webmock" end diff --git a/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/goff_api_spec.rb b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/goff_api_spec.rb index 04f81b6..292110c 100644 --- a/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/goff_api_spec.rb +++ b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/goff_api_spec.rb @@ -161,7 +161,7 @@ value: 1.15, metadata: {"website" => "https://gofeatureflag.org"}, key: "double_key", - reason: "TARGETING_MATCH", + reason: "TARGETING_MATCH" }.to_json) expect { diff --git a/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/provider_spec.rb b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/provider_spec.rb index 2b723d1..528b7bb 100644 --- a/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/provider_spec.rb +++ b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/provider_spec.rb @@ -402,8 +402,8 @@ it "should return an error if evaluation context has empty string targetingKey" do eval = goff_provider.fetch_boolean_value(flag_key: "flag_key", - default_value: true, - evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "")) + default_value: true, + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "")) want = OpenFeature::SDK::Provider::ResolutionDetails.new( value: true, error_code: OpenFeature::SDK::Provider::ErrorCode::INVALID_CONTEXT, @@ -415,8 +415,8 @@ it "should return an error if evaluation context has nil targetingKey" do eval = goff_provider.fetch_boolean_value(flag_key: "flag_key", - default_value: true, - evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: nil)) + default_value: true, + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: nil)) want = OpenFeature::SDK::Provider::ResolutionDetails.new( value: true, error_code: OpenFeature::SDK::Provider::ErrorCode::INVALID_CONTEXT, @@ -428,8 +428,8 @@ it "should return an error if flag_key nil" do eval = goff_provider.fetch_boolean_value(flag_key: nil, - default_value: true, - evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16")) + default_value: true, + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16")) want = OpenFeature::SDK::Provider::ResolutionDetails.new( value: true, error_code: OpenFeature::SDK::Provider::ErrorCode::GENERAL, @@ -441,8 +441,8 @@ it "should return an error if flag_key empty string" do eval = goff_provider.fetch_boolean_value(flag_key: "", - default_value: true, - evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16")) + default_value: true, + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16")) want = OpenFeature::SDK::Provider::ResolutionDetails.new( value: true, error_code: OpenFeature::SDK::Provider::ErrorCode::GENERAL, @@ -458,7 +458,8 @@ eval = goff_provider.fetch_boolean_value( flag_key: "boolean_flag", default_value: true, - evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16")) + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16") + ) want = OpenFeature::SDK::Provider::ResolutionDetails.new( value: true, error_code: OpenFeature::SDK::Provider::ErrorCode::GENERAL, @@ -474,7 +475,8 @@ eval = goff_provider.fetch_boolean_value( flag_key: "boolean_flag", default_value: true, - evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16")) + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16") + ) want = OpenFeature::SDK::Provider::ResolutionDetails.new( value: true, error_code: OpenFeature::SDK::Provider::ErrorCode::GENERAL, @@ -495,7 +497,8 @@ eval = goff_provider.fetch_boolean_value( flag_key: "boolean_flag", default_value: false, - evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16")) + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16") + ) want = OpenFeature::SDK::Provider::ResolutionDetails.new( value: false, error_code: OpenFeature::SDK::Provider::ErrorCode::GENERAL, @@ -516,7 +519,8 @@ eval = goff_provider.fetch_boolean_value( flag_key: "boolean_flag", default_value: false, - evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16")) + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16") + ) want = OpenFeature::SDK::Provider::ResolutionDetails.new( value: false, error_code: OpenFeature::SDK::Provider::ErrorCode::FLAG_NOT_FOUND, @@ -537,7 +541,8 @@ eval = goff_provider.fetch_boolean_value( flag_key: "boolean_flag", default_value: false, - evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16")) + evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16") + ) want = OpenFeature::SDK::Provider::ResolutionDetails.new( value: false, error_code: OpenFeature::SDK::Provider::ErrorCode::GENERAL, diff --git a/providers/openfeature-go-feature-flag-provider/spec/spec_helper.rb b/providers/openfeature-go-feature-flag-provider/spec/spec_helper.rb index e79e5fb..3494843 100644 --- a/providers/openfeature-go-feature-flag-provider/spec/spec_helper.rb +++ b/providers/openfeature-go-feature-flag-provider/spec/spec_helper.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -require 'bundler/setup' -require 'openfeature/go-feature-flag/go_feature_flag_provider' -require 'openfeature/go-feature-flag/options' -require 'openfeature/go-feature-flag/goff_api' +require "bundler/setup" +require "openfeature/go-feature-flag/go_feature_flag_provider" +require "openfeature/go-feature-flag/options" +require "openfeature/go-feature-flag/goff_api" # This file was generated by the `rspec --init` command. Conventionally, all # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. From 70714c798c546ed794c3da306807ddf2a725c637 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Mon, 5 Aug 2024 17:04:25 +0200 Subject: [PATCH 06/19] fix readme: Signed-off-by: Thomas Poignant --- .../openfeature-go-feature-flag-provider/README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/providers/openfeature-go-feature-flag-provider/README.md b/providers/openfeature-go-feature-flag-provider/README.md index 2012521..e1ed9f2 100644 --- a/providers/openfeature-go-feature-flag-provider/README.md +++ b/providers/openfeature-go-feature-flag-provider/README.md @@ -5,6 +5,7 @@ # GO Feature Flag - OpenFeature Ruby provider

+ gem Documentation Issues Join us on slack @@ -89,7 +90,7 @@ The `targeting_key` is mandatory for GO Feature Flag to evaluate the feature fla The client is used to retrieve values for the current `EvaluationContext`. For example, retrieving a boolean value for the flag **"my-flag"**: -```swift +```ruby client = OpenFeature::SDK.build_client() bool_value = client.fetch_boolean_value( @@ -100,17 +101,17 @@ bool_value = client.fetch_boolean_value( ``` GO Feature Flag supports different all OpenFeature supported types of feature flags, it means that you can use all the accessor directly -```swift -// Bool +```ruby +# Bool client.fetch_boolean_value(flag_key: 'my-flag', default_value: false, evaluation_context: evaluation_context) -// String +# String client.fetch_string_value(flag_key: 'my-flag', default_value: "default", evaluation_context: evaluation_context) -// Number +# Number client.fetch_number_value(flag_key: 'my-flag', default_value: 0, evaluation_context: evaluation_context) -// Object +# Object client.fetch_object_value(flag_key: 'my-flag', default_value: {"default" => true}, evaluation_context: evaluation_context) ``` From 82088865b631211aa8b1168af605ba62d481c900 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Mon, 5 Aug 2024 17:42:06 +0200 Subject: [PATCH 07/19] Use fetch_*_details instead of value Signed-off-by: Thomas Poignant --- .../openfeature/go-feature-flag/goff_api.rb | 16 +- .../gofeatureflag/provider_spec.rb | 194 +++++++++++++++--- 2 files changed, 179 insertions(+), 31 deletions(-) diff --git a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/goff_api.rb b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/goff_api.rb index b78d2a3..3862c9b 100644 --- a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/goff_api.rb +++ b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/goff_api.rb @@ -86,7 +86,7 @@ def parse_error_response(response) key: parsed["key"], reason: OpenFeature::SDK::Provider::Reason::ERROR, variant: nil, - error_code: parsed["error_code"], + error_code: error_code_mapper(parsed["error_code"]), error_details: parsed["error_details"], metadata: nil ) @@ -128,6 +128,20 @@ def reason_mapper(reason_str) reason_map[reason_str] || OpenFeature::SDK::Provider::Reason::UNKNOWN end + def error_code_mapper(error_code_str) + error_code_str = error_code_str.upcase + error_code_map = { + "PROVIDER_NOT_READY" => OpenFeature::SDK::Provider::ErrorCode::PROVIDER_NOT_READY, + "FLAG_NOT_FOUND" => OpenFeature::SDK::Provider::ErrorCode::FLAG_NOT_FOUND, + "PARSE_ERROR" => OpenFeature::SDK::Provider::ErrorCode::PARSE_ERROR, + "TYPE_MISMATCH" => OpenFeature::SDK::Provider::ErrorCode::TYPE_MISMATCH, + "TARGETING_KEY_MISSING" => OpenFeature::SDK::Provider::ErrorCode::TARGETING_KEY_MISSING, + "INVALID_CONTEXT" => OpenFeature::SDK::Provider::ErrorCode::INVALID_CONTEXT, + "GENERAL" => OpenFeature::SDK::Provider::ErrorCode::GENERAL + } + error_code_map[error_code_str] || OpenFeature::SDK::Provider::ErrorCode::GENERAL + end + def parse_retry_later_header(response) retry_after = response["Retry-After"] return nil if retry_after.nil? diff --git a/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/provider_spec.rb b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/provider_spec.rb index 528b7bb..3643810 100644 --- a/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/provider_spec.rb +++ b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/provider_spec.rb @@ -47,12 +47,21 @@ config.set_provider(goff_provider, domain: test_name) end client = OpenFeature::SDK.build_client(domain: test_name) - bool_value = client.fetch_boolean_value( + got = client.fetch_boolean_details( flag_key: "boolean_flag", default_value: false, evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16") ) - expect(bool_value).to be_truthy + want = OpenFeature::SDK::EvaluationDetails.new( + flag_key: "boolean_flag", + resolution_details: OpenFeature::SDK::Provider::ResolutionDetails.new( + value: true, + reason: OpenFeature::SDK::Provider::Reason::TARGETING_MATCH, + variant: "variantA", + flag_metadata: {"website" => "https://gofeatureflag.org"} + ) + ) + expect(got).to eql(want) end it "should return the default value if flag is not the right type" do @@ -71,12 +80,21 @@ config.set_provider(goff_provider, domain: test_name) end client = OpenFeature::SDK.build_client(domain: test_name) - bool_value = client.fetch_boolean_value( + got = client.fetch_boolean_details( flag_key: "boolean_flag", default_value: false, evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16") ) - expect(bool_value).to be_falsey + want = OpenFeature::SDK::EvaluationDetails.new( + flag_key: "boolean_flag", + resolution_details: OpenFeature::SDK::Provider::ResolutionDetails.new( + value: false, + reason: OpenFeature::SDK::Provider::Reason::ERROR, + error_code: OpenFeature::SDK::Provider::ErrorCode::TYPE_MISMATCH, + error_message: "flag type String does not match allowed types [TrueClass, FalseClass]" + ) + ) + expect(got).to eql(want) end it "should return the default value of the flag if error send by the API (http code 403)" do @@ -88,12 +106,21 @@ config.set_provider(goff_provider, domain: test_name) end client = OpenFeature::SDK.build_client(domain: test_name) - bool_value = client.fetch_boolean_value( + got = client.fetch_boolean_details( flag_key: "boolean_flag", default_value: false, evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16") ) - expect(bool_value).to be_falsey + want = OpenFeature::SDK::EvaluationDetails.new( + flag_key: "boolean_flag", + resolution_details: OpenFeature::SDK::Provider::ResolutionDetails.new( + value: false, + reason: OpenFeature::SDK::Provider::Reason::ERROR, + error_code: OpenFeature::SDK::Provider::ErrorCode::GENERAL, + error_message: "unauthorized" + ) + ) + expect(got).to eql(want) end it "should return the default value of the flag if error send by the API (http code 400)" do @@ -109,12 +136,20 @@ config.set_provider(goff_provider, domain: test_name) end client = OpenFeature::SDK.build_client(domain: test_name) - bool_value = client.fetch_boolean_value( + got = client.fetch_boolean_details( flag_key: "boolean_flag", default_value: false, evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16") ) - expect(bool_value).to be_falsey + want = OpenFeature::SDK::EvaluationDetails.new( + flag_key: "boolean_flag", + resolution_details: OpenFeature::SDK::Provider::ResolutionDetails.new( + value: false, + reason: OpenFeature::SDK::Provider::Reason::ERROR, + error_code: OpenFeature::SDK::Provider::ErrorCode::INVALID_CONTEXT + ) + ) + expect(got).to eql(want) end it "should return default value if no evaluation context" do @@ -133,12 +168,21 @@ config.set_provider(goff_provider, domain: test_name) end client = OpenFeature::SDK.build_client(domain: test_name) - bool_value = client.fetch_boolean_value( + got = client.fetch_boolean_details( flag_key: "boolean_flag", default_value: false, evaluation_context: nil ) - expect(bool_value).to be_falsey + want = OpenFeature::SDK::EvaluationDetails.new( + flag_key: "boolean_flag", + resolution_details: OpenFeature::SDK::Provider::ResolutionDetails.new( + value: false, + reason: OpenFeature::SDK::Provider::Reason::ERROR, + error_code: OpenFeature::SDK::Provider::ErrorCode::INVALID_CONTEXT, + error_message: "invalid evaluation context provided" + ) + ) + expect(got).to eql(want) end it "should return default value if evaluation context has empty string targetingKey" do @@ -157,12 +201,21 @@ config.set_provider(goff_provider, domain: test_name) end client = OpenFeature::SDK.build_client(domain: test_name) - bool_value = client.fetch_boolean_value( + got = client.fetch_boolean_details( flag_key: "boolean_flag", default_value: false, evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "") ) - expect(bool_value).to be_falsey + want = OpenFeature::SDK::EvaluationDetails.new( + flag_key: "boolean_flag", + resolution_details: OpenFeature::SDK::Provider::ResolutionDetails.new( + value: false, + reason: OpenFeature::SDK::Provider::Reason::ERROR, + error_code: OpenFeature::SDK::Provider::ErrorCode::INVALID_CONTEXT, + error_message: "invalid evaluation context provided" + ) + ) + expect(got).to eql(want) end it "should return default value if evaluation context has nil targetingKey" do @@ -181,12 +234,21 @@ config.set_provider(goff_provider, domain: test_name) end client = OpenFeature::SDK.build_client(domain: test_name) - bool_value = client.fetch_boolean_value( + got = client.fetch_boolean_details( flag_key: "boolean_flag", default_value: false, evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: nil) ) - expect(bool_value).to be_falsey + want = OpenFeature::SDK::EvaluationDetails.new( + flag_key: "boolean_flag", + resolution_details: OpenFeature::SDK::Provider::ResolutionDetails.new( + value: false, + reason: OpenFeature::SDK::Provider::Reason::ERROR, + error_code: OpenFeature::SDK::Provider::ErrorCode::INVALID_CONTEXT, + error_message: "invalid evaluation context provided" + ) + ) + expect(got).to eql(want) end it "should return default value if flag_key nil" do @@ -205,12 +267,21 @@ config.set_provider(goff_provider, domain: test_name) end client = OpenFeature::SDK.build_client(domain: test_name) - bool_value = client.fetch_boolean_value( + got = client.fetch_boolean_details( flag_key: nil, default_value: false, evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "1234") ) - expect(bool_value).to be_falsey + want = OpenFeature::SDK::EvaluationDetails.new( + flag_key: nil, + resolution_details: OpenFeature::SDK::Provider::ResolutionDetails.new( + value: false, + reason: OpenFeature::SDK::Provider::Reason::ERROR, + error_code: OpenFeature::SDK::Provider::ErrorCode::GENERAL, + error_message: "invalid flag key provided" + ) + ) + expect(got).to eql(want) end it "should return default value if flag_key empty string" do @@ -229,12 +300,21 @@ config.set_provider(goff_provider, domain: test_name) end client = OpenFeature::SDK.build_client(domain: test_name) - bool_value = client.fetch_boolean_value( + got = client.fetch_boolean_details( flag_key: "", default_value: false, evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "1234") ) - expect(bool_value).to be_falsey + want = OpenFeature::SDK::EvaluationDetails.new( + flag_key: "", + resolution_details: OpenFeature::SDK::Provider::ResolutionDetails.new( + value: false, + reason: OpenFeature::SDK::Provider::Reason::ERROR, + error_code: OpenFeature::SDK::Provider::ErrorCode::GENERAL, + error_message: "invalid flag key provided" + ) + ) + expect(got).to eql(want) end end @@ -255,12 +335,21 @@ config.set_provider(goff_provider, domain: test_name) end client = OpenFeature::SDK.build_client(domain: test_name) - value = client.fetch_string_value( + got = client.fetch_string_details( flag_key: "flag_key", default_value: "default", evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16") ) - expect(value).to eq("aValue") + want = OpenFeature::SDK::EvaluationDetails.new( + flag_key: "flag_key", + resolution_details: OpenFeature::SDK::Provider::ResolutionDetails.new( + value: "aValue", + variant: "variantA", + reason: OpenFeature::SDK::Provider::Reason::TARGETING_MATCH, + flag_metadata: {"website" => "https://gofeatureflag.org"}, + ) + ) + expect(got).to eql(want) end it "should return the default value if flag is not the right type" do @@ -279,12 +368,21 @@ config.set_provider(goff_provider, domain: test_name) end client = OpenFeature::SDK.build_client(domain: test_name) - value = client.fetch_string_value( + got = client.fetch_string_details( flag_key: "flag_key", default_value: "default", evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16") ) - expect(value).to eq("default") + want = OpenFeature::SDK::EvaluationDetails.new( + flag_key: "flag_key", + resolution_details: OpenFeature::SDK::Provider::ResolutionDetails.new( + value: "default", + reason: OpenFeature::SDK::Provider::Reason::ERROR, + error_code: OpenFeature::SDK::Provider::ErrorCode::TYPE_MISMATCH, + error_message: "flag type Integer does not match allowed types [String]" + ) + ) + expect(got).to eql(want) end end @@ -305,12 +403,21 @@ config.set_provider(goff_provider, domain: test_name) end client = OpenFeature::SDK.build_client(domain: test_name) - value = client.fetch_number_value( + got = client.fetch_number_details( flag_key: "flag_key", default_value: 25, evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16") ) - expect(value).to eq(15) + want = OpenFeature::SDK::EvaluationDetails.new( + flag_key: "flag_key", + resolution_details: OpenFeature::SDK::Provider::ResolutionDetails.new( + value: 15, + variant: "variantA", + reason: OpenFeature::SDK::Provider::Reason::TARGETING_MATCH, + flag_metadata: {"website" => "https://gofeatureflag.org"} + ) + ) + expect(got).to eql(want) end it "should return the default value if flag is not the right type" do @@ -329,12 +436,21 @@ config.set_provider(goff_provider, domain: test_name) end client = OpenFeature::SDK.build_client(domain: test_name) - value = client.fetch_number_value( + got = client.fetch_number_details( flag_key: "flag_key", default_value: 25, evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16") ) - expect(value).to eq(25) + want = OpenFeature::SDK::EvaluationDetails.new( + flag_key: "flag_key", + resolution_details: OpenFeature::SDK::Provider::ResolutionDetails.new( + value: 25, + reason: OpenFeature::SDK::Provider::Reason::ERROR, + error_code: OpenFeature::SDK::Provider::ErrorCode::TYPE_MISMATCH, + error_message: "flag type String does not match allowed types [Integer, Float]" + ) + ) + expect(got).to eql(want) end end @@ -355,12 +471,21 @@ config.set_provider(goff_provider, domain: test_name) end client = OpenFeature::SDK.build_client(domain: test_name) - value = client.fetch_object_value( + got = client.fetch_object_details( flag_key: "flag_key", default_value: {"fail" => true}, evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16") ) - expect(value).to eq({"test" => "test"}) + want = OpenFeature::SDK::EvaluationDetails.new( + flag_key: "flag_key", + resolution_details: OpenFeature::SDK::Provider::ResolutionDetails.new( + value: {"test" => "test"}, + variant: "variantA", + reason: OpenFeature::SDK::Provider::Reason::TARGETING_MATCH, + flag_metadata: {"website" => "https://gofeatureflag.org"} + ) + ) + expect(got).to eql(want) end it "should return the default value if flag is not the right type" do @@ -379,12 +504,21 @@ config.set_provider(goff_provider, domain: test_name) end client = OpenFeature::SDK.build_client(domain: test_name) - value = client.fetch_object_value( + got = client.fetch_object_details( flag_key: "flag_key", default_value: {"fail" => true}, evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16") ) - expect(value).to eq({"fail" => true}) + want = OpenFeature::SDK::EvaluationDetails.new( + flag_key: "flag_key", + resolution_details: OpenFeature::SDK::Provider::ResolutionDetails.new( + value: {"fail" => true}, + reason: OpenFeature::SDK::Provider::Reason::ERROR, + error_code: OpenFeature::SDK::Provider::ErrorCode::TYPE_MISMATCH, + error_message: "flag type String does not match allowed types [Array, Hash]" + ) + ) + expect(got).to eql(want) end end From 3ef79bb1a67127fa466ab6d4df38d8540056d0db Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Mon, 5 Aug 2024 17:43:04 +0200 Subject: [PATCH 08/19] Fix lint Signed-off-by: Thomas Poignant --- .../spec/openfeature/gofeatureflag/provider_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/provider_spec.rb b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/provider_spec.rb index 3643810..1a6000f 100644 --- a/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/provider_spec.rb +++ b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/provider_spec.rb @@ -346,7 +346,7 @@ value: "aValue", variant: "variantA", reason: OpenFeature::SDK::Provider::Reason::TARGETING_MATCH, - flag_metadata: {"website" => "https://gofeatureflag.org"}, + flag_metadata: {"website" => "https://gofeatureflag.org"} ) ) expect(got).to eql(want) From 8f63a5c458c0cf501cf9a5e3b5feb018cd397672 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Mon, 5 Aug 2024 17:45:22 +0200 Subject: [PATCH 09/19] Use real type Signed-off-by: Thomas Poignant --- .../spec/openfeature/gofeatureflag/goff_api_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/goff_api_spec.rb b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/goff_api_spec.rb index 292110c..f369334 100644 --- a/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/goff_api_spec.rb +++ b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/goff_api_spec.rb @@ -78,7 +78,7 @@ value: nil, reason: OpenFeature::SDK::Provider::Reason::ERROR, variant: nil, - error_code: "TYPE_MISMATCH", + error_code: OpenFeature::SDK::Provider::ErrorCode::TYPE_MISMATCH, error_details: "expected type: boolean, got: string", metadata: nil ) From d29df641c56d82bf0bfc997b50073e153775bca7 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Wed, 7 Aug 2024 10:10:51 +0200 Subject: [PATCH 10/19] Fix review PR Signed-off-by: Thomas Poignant --- providers/openfeature-go-feature-flag-provider/README.md | 9 ++------- .../go-feature-flag/go_feature_flag_provider.rb | 2 +- .../lib/openfeature/go-feature-flag/goff_api.rb | 2 +- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/providers/openfeature-go-feature-flag-provider/README.md b/providers/openfeature-go-feature-flag-provider/README.md index e1ed9f2..4edc0fd 100644 --- a/providers/openfeature-go-feature-flag-provider/README.md +++ b/providers/openfeature-go-feature-flag-provider/README.md @@ -13,7 +13,7 @@ This repository contains the official Ruby OpenFeature provider for accessing your feature flags with [GO Feature Flag](https://gofeatureflag.org). -In conjuction with the [OpenFeature SDK](https://openfeature.dev/docs/reference/concepts/provider) you will be able +In conjunction with the [OpenFeature SDK](https://openfeature.dev/docs/reference/concepts/provider) you will be able to evaluate your feature flags in your Ruby applications. For documentation related to flags management in GO Feature Flag, @@ -53,11 +53,6 @@ The `OpenFeature::GoFeatureFlag::Provider` needs some options to be created and The only required option to create a `GoFeatureFlagProvider` is the URL _(`endpoint`)_ to your GO Feature Flag relay-proxy instance. ```ruby -import GOFeatureFlag -import OpenFeature - -# ... - options = OpenFeature::GoFeatureFlag::Options.new(endpoint: "http://localhost:1031") provider = OpenFeature::GoFeatureFlag::Provider.new(options: options) @@ -123,7 +118,7 @@ client.fetch_object_value(flag_key: 'my-flag', default_value: {"default" => true | ❌ | Caching | Mechanism is in place to refresh the cache in case of configuration change | | ❌ | Event Streaming | Not supported by the SDK | | ❌ | Logging | Not supported by the SDK | -| ✅ | Flag Metadata | Not supported by the SDK | +| ✅ | Flag Metadata | You can retrieve your flag metadata directly in the evaluation details. | **Implemented**: ✅ | In-progress: ⚠️ | Not implemented yet: ❌ diff --git a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/go_feature_flag_provider.rb b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/go_feature_flag_provider.rb index 972cb94..3ac64fc 100644 --- a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/go_feature_flag_provider.rb +++ b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/go_feature_flag_provider.rb @@ -32,7 +32,7 @@ def fetch_object_value(flag_key:, default_value:, evaluation_context: nil) private def evaluate(flag_key:, default_value:, allowed_classes:, evaluation_context: nil) - evaluation_context = OpenFeature::SDK::EvaluationContext.new unless evaluation_context.is_a?(OpenFeature::SDK::EvaluationContext) + evaluation_context = OpenFeature::SDK::EvaluationContext.new if evaluation_context.nil? validate_parameters(flag_key, evaluation_context) # do a http call to the go feature flag server diff --git a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/goff_api.rb b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/goff_api.rb index 3862c9b..bd640c3 100644 --- a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/goff_api.rb +++ b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/goff_api.rb @@ -25,7 +25,7 @@ def evaluate_ofrep_api(flag_key:, evaluation_context:) end end - evaluation_context = OpenFeature::SDK::EvaluationContext.new unless evaluation_context.is_a?(OpenFeature::SDK::EvaluationContext) + evaluation_context = OpenFeature::SDK::EvaluationContext.new if evaluation_context.nil? # Format the URL to call the Go Feature Flag OFREP API base_uri = URI.parse(@options.endpoint) new_path = File.join(base_uri.path, "/ofrep/v1/evaluate/flags/#{flag_key}") From d47b6d02239fb2299178f74f047b630ae817a3f0 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Mon, 12 Aug 2024 22:23:03 +0200 Subject: [PATCH 11/19] move require in spec_helper.rb Signed-off-by: Thomas Poignant --- .../spec/openfeature/gofeatureflag/goff_api_spec.rb | 2 -- .../spec/openfeature/gofeatureflag/options_spec.rb | 1 - .../spec/openfeature/gofeatureflag/provider_spec.rb | 2 -- .../openfeature-go-feature-flag-provider/spec/spec_helper.rb | 2 ++ 4 files changed, 2 insertions(+), 5 deletions(-) diff --git a/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/goff_api_spec.rb b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/goff_api_spec.rb index f369334..3767e00 100644 --- a/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/goff_api_spec.rb +++ b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/goff_api_spec.rb @@ -1,6 +1,4 @@ require "spec_helper" -require "open_feature/sdk" -require "webmock/rspec" describe OpenFeature::GoFeatureFlag::GoFeatureFlagApi do subject(:goff_api) do diff --git a/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/options_spec.rb b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/options_spec.rb index 80620cd..4b097bd 100644 --- a/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/options_spec.rb +++ b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/options_spec.rb @@ -1,5 +1,4 @@ require "spec_helper" -require "open_feature/sdk" describe OpenFeature::GoFeatureFlag::Options do context "#endpoint" do diff --git a/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/provider_spec.rb b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/provider_spec.rb index 1a6000f..3b881fb 100644 --- a/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/provider_spec.rb +++ b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/provider_spec.rb @@ -1,6 +1,4 @@ require "spec_helper" -require "open_feature/sdk" -require "webmock/rspec" describe OpenFeature::GoFeatureFlag::Provider do subject(:goff_provider) do diff --git a/providers/openfeature-go-feature-flag-provider/spec/spec_helper.rb b/providers/openfeature-go-feature-flag-provider/spec/spec_helper.rb index 3494843..cf06990 100644 --- a/providers/openfeature-go-feature-flag-provider/spec/spec_helper.rb +++ b/providers/openfeature-go-feature-flag-provider/spec/spec_helper.rb @@ -4,6 +4,8 @@ require "openfeature/go-feature-flag/go_feature_flag_provider" require "openfeature/go-feature-flag/options" require "openfeature/go-feature-flag/goff_api" +require "open_feature/sdk" +require "webmock/rspec" # This file was generated by the `rspec --init` command. Conventionally, all # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. From b450c396f1278da58c7aaa19d78cbb919e5e3c36 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Mon, 12 Aug 2024 22:24:47 +0200 Subject: [PATCH 12/19] use idiomatic ruby in example Signed-off-by: Thomas Poignant --- providers/openfeature-go-feature-flag-provider/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/providers/openfeature-go-feature-flag-provider/README.md b/providers/openfeature-go-feature-flag-provider/README.md index 4edc0fd..d09f785 100644 --- a/providers/openfeature-go-feature-flag-provider/README.md +++ b/providers/openfeature-go-feature-flag-provider/README.md @@ -54,19 +54,19 @@ The only required option to create a `GoFeatureFlagProvider` is the URL _(`endpo ```ruby options = OpenFeature::GoFeatureFlag::Options.new(endpoint: "http://localhost:1031") -provider = OpenFeature::GoFeatureFlag::Provider.new(options: options) +provider = OpenFeature::GoFeatureFlag::Provider.new(options:) evaluation_context = OpenFeature::SDK::EvaluationContext.new(targeting_key: "9b9450f8-ab5c-4dcf-872f-feda3f6ccb16") OpenFeature::SDK.configure do |config| config.set_provider(provider) end -client = OpenFeature::SDK.build_client() +client = OpenFeature::SDK.build_client bool_value = client.fetch_boolean_value( flag_key: "my-boolean-flag", default_value: false, - evaluation_context: evaluation_context + evaluation_context: ) if bool_value From bcdfa064d128cc4a4a68cfd0ef5b45c5e5528db3 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Mon, 12 Aug 2024 22:41:58 +0200 Subject: [PATCH 13/19] Removing type forced type check Signed-off-by: Thomas Poignant --- .../lib/openfeature/go-feature-flag/goff_api.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/goff_api.rb b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/goff_api.rb index bd640c3..b704869 100644 --- a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/goff_api.rb +++ b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/goff_api.rb @@ -12,7 +12,6 @@ module GoFeatureFlag class GoFeatureFlagApi attr_reader :options def initialize(options: {}) - options = OpenFeature::GoFeatureFlag::Options.new unless options.is_a?(OpenFeature::GoFeatureFlag::Options) @options = options end From 73ad60ddb8c3ffa063bc7a5ca20e8b8b1482e620 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Mon, 12 Aug 2024 22:57:43 +0200 Subject: [PATCH 14/19] Use faraday as HTTP Client Signed-off-by: Thomas Poignant --- .../Gemfile.lock | 10 ++++++ .../openfeature/go-feature-flag/goff_api.rb | 34 ++++++------------- ...enfeature-go-feature-flag-provider.gemspec | 1 + 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/providers/openfeature-go-feature-flag-provider/Gemfile.lock b/providers/openfeature-go-feature-flag-provider/Gemfile.lock index b8eb005..0c64b8d 100644 --- a/providers/openfeature-go-feature-flag-provider/Gemfile.lock +++ b/providers/openfeature-go-feature-flag-provider/Gemfile.lock @@ -2,6 +2,7 @@ PATH remote: . specs: openfeature-go-feature-flag-provider (0.1.0) + faraday (~> 2.10.1) openfeature-sdk (~> 0.3.1) GEM @@ -16,10 +17,18 @@ GEM rexml diff-lcs (1.5.1) docile (1.4.1) + faraday (2.10.1) + faraday-net_http (>= 2.0, < 3.2) + logger + faraday-net_http (3.1.1) + net-http hashdiff (1.1.1) json (2.7.2) language_server-protocol (3.17.0.3) lint_roller (1.1.0) + logger (1.6.0) + net-http (0.4.1) + uri openfeature-sdk (0.3.1) parallel (1.24.0) parser (3.3.0.5) @@ -82,6 +91,7 @@ GEM rubocop-performance (~> 1.20.2) strscan (3.1.0) unicode-display_width (2.5.0) + uri (0.13.0) webmock (3.23.1) addressable (>= 2.8.0) crack (>= 0.3.2) diff --git a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/goff_api.rb b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/goff_api.rb index b704869..3905985 100644 --- a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/goff_api.rb +++ b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/goff_api.rb @@ -3,6 +3,7 @@ require "open_feature/sdk" require "net/http" require "json" +require "faraday" require_relative "error/errors" require_relative "model/ofrep_api_response" @@ -13,6 +14,10 @@ class GoFeatureFlagApi attr_reader :options def initialize(options: {}) @options = options + @faraday_connection = Faraday.new( + url: @options.endpoint, + headers: {"Content-Type" => "application/json"}.merge(@options.custom_headers || {}) + ) end def evaluate_ofrep_api(flag_key:, evaluation_context:) @@ -25,34 +30,15 @@ def evaluate_ofrep_api(flag_key:, evaluation_context:) end evaluation_context = OpenFeature::SDK::EvaluationContext.new if evaluation_context.nil? - # Format the URL to call the Go Feature Flag OFREP API - base_uri = URI.parse(@options.endpoint) - new_path = File.join(base_uri.path, "/ofrep/v1/evaluate/flags/#{flag_key}") - ofrep_uri = base_uri.dup - ofrep_uri.path = new_path - - # Initialize the HTTP client - http = Net::HTTP.new(ofrep_uri.host, ofrep_uri.port) - http.use_ssl = (ofrep_uri.scheme == "https") - - # Prepare the headers - headers = { - "Content-Type" => "application/json" - } - if @options.custom_headers.nil? - headers.merge!(@options.custom_headers) - end - - request = Net::HTTP::Post.new(ofrep_uri.path, headers) - - # replace targetingKey + # replace targeting_key by targetingKey evaluation_context.fields["targetingKey"] = evaluation_context.targeting_key evaluation_context.fields.delete("targeting_key") - request.body = {context: evaluation_context.fields}.to_json - response = http.request(request) + response = @faraday_connection.post("/ofrep/v1/evaluate/flags/#{flag_key}") do |req| + req.body = {context: evaluation_context.fields}.to_json + end - case response.code.to_i + case response.status when 200 parse_success_response(response) when 400 diff --git a/providers/openfeature-go-feature-flag-provider/openfeature-go-feature-flag-provider.gemspec b/providers/openfeature-go-feature-flag-provider/openfeature-go-feature-flag-provider.gemspec index 5bcb7b8..aece293 100644 --- a/providers/openfeature-go-feature-flag-provider/openfeature-go-feature-flag-provider.gemspec +++ b/providers/openfeature-go-feature-flag-provider/openfeature-go-feature-flag-provider.gemspec @@ -30,6 +30,7 @@ Gem::Specification.new do |spec| spec.require_paths = ["lib"] spec.add_runtime_dependency "openfeature-sdk", "~> 0.3.1" + spec.add_runtime_dependency "faraday", "~> 2.10.1" spec.add_development_dependency "rake", "~> 13.0" spec.add_development_dependency "rspec", "~> 3.12.0" From 84ddc8c25129ccb0e66d06b75cb6d21833e7dc55 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Mon, 12 Aug 2024 23:00:33 +0200 Subject: [PATCH 15/19] Use idiomatic notation in readme Signed-off-by: Thomas Poignant --- .../openfeature-go-feature-flag-provider/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/providers/openfeature-go-feature-flag-provider/README.md b/providers/openfeature-go-feature-flag-provider/README.md index d09f785..274a9fb 100644 --- a/providers/openfeature-go-feature-flag-provider/README.md +++ b/providers/openfeature-go-feature-flag-provider/README.md @@ -86,7 +86,7 @@ The client is used to retrieve values for the current `EvaluationContext`. For example, retrieving a boolean value for the flag **"my-flag"**: ```ruby -client = OpenFeature::SDK.build_client() +client = OpenFeature::SDK.build_client bool_value = client.fetch_boolean_value( flag_key: "my-boolean-flag", @@ -98,16 +98,16 @@ bool_value = client.fetch_boolean_value( GO Feature Flag supports different all OpenFeature supported types of feature flags, it means that you can use all the accessor directly ```ruby # Bool -client.fetch_boolean_value(flag_key: 'my-flag', default_value: false, evaluation_context: evaluation_context) +client.fetch_boolean_value(flag_key: 'my-flag', default_value: false, evaluation_context:) # String -client.fetch_string_value(flag_key: 'my-flag', default_value: "default", evaluation_context: evaluation_context) +client.fetch_string_value(flag_key: 'my-flag', default_value: "default", evaluation_context:) # Number -client.fetch_number_value(flag_key: 'my-flag', default_value: 0, evaluation_context: evaluation_context) +client.fetch_number_value(flag_key: 'my-flag', default_value: 0, evaluation_context:) # Object -client.fetch_object_value(flag_key: 'my-flag', default_value: {"default" => true}, evaluation_context: evaluation_context) +client.fetch_object_value(flag_key: 'my-flag', default_value: {"default" => true}, evaluation_context:) ``` ## Features status From a440f8ec2e82f4b50376026601bceae43d938510 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Mon, 12 Aug 2024 23:02:10 +0200 Subject: [PATCH 16/19] Use directly SDK because we are in OpenFeature namespace already Signed-off-by: Thomas Poignant --- .../lib/openfeature/go-feature-flag/error/errors.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/error/errors.rb b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/error/errors.rb index ee49d5c..a61e1a9 100644 --- a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/error/errors.rb +++ b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/error/errors.rb @@ -10,7 +10,7 @@ def initialize(response, flag_key) error_message = "Flag not found: #{flag_key}" super(error_message) @response = response - @error_code = OpenFeature::SDK::Provider::ErrorCode::FLAG_NOT_FOUND + @error_code = SDK::Provider::ErrorCode::FLAG_NOT_FOUND @error_message = error_message end end @@ -22,7 +22,7 @@ def initialize(response) error_message = "Internal Server Error" super(error_message) @response = response - @error_code = OpenFeature::SDK::Provider::ErrorCode::GENERAL + @error_code = SDK::Provider::ErrorCode::GENERAL @error_message = error_message end end @@ -44,7 +44,7 @@ def initialize(response) error_message = "unauthorized" super(error_message) @response = response - @error_code = OpenFeature::SDK::Provider::ErrorCode::GENERAL + @error_code = SDK::Provider::ErrorCode::GENERAL @error_message = error_message end end @@ -56,7 +56,7 @@ def initialize(response) error_message = "Parse error" super(error_message) @response = response - @error_code = OpenFeature::SDK::Provider::ErrorCode::PARSE_ERROR + @error_code = SDK::Provider::ErrorCode::PARSE_ERROR @error_message = error_message end end @@ -68,7 +68,7 @@ def initialize(response) error_message = response.nil? ? "Rate limited" : "Rate limited: " + response["Retry-After"].to_s super(error_message) @response = response - @error_code = OpenFeature::SDK::Provider::ErrorCode::GENERAL + @error_code = SDK::Provider::ErrorCode::GENERAL @error_message = error_message end end From b6d095091f975b8804d9ca02ed94bdc744710d75 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Mon, 12 Aug 2024 23:05:05 +0200 Subject: [PATCH 17/19] Call to parent should be the last thing in the initialize Signed-off-by: Thomas Poignant --- .../lib/openfeature/go-feature-flag/error/errors.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/error/errors.rb b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/error/errors.rb index a61e1a9..5900b08 100644 --- a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/error/errors.rb +++ b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/error/errors.rb @@ -8,10 +8,10 @@ class FlagNotFoundError < StandardError def initialize(response, flag_key) error_message = "Flag not found: #{flag_key}" - super(error_message) @response = response @error_code = SDK::Provider::ErrorCode::FLAG_NOT_FOUND @error_message = error_message + super(error_message) end end @@ -20,10 +20,10 @@ class InternalServerError < StandardError def initialize(response) error_message = "Internal Server Error" - super(error_message) @response = response @error_code = SDK::Provider::ErrorCode::GENERAL @error_message = error_message + super(error_message) end end @@ -31,9 +31,9 @@ class InvalidOptionError < StandardError attr_reader :error_code, :error_message def initialize(error_code, error_message) - super(error_message) @error_code = error_code @error_message = error_message + super(error_message) end end @@ -42,10 +42,10 @@ class UnauthorizedError < StandardError def initialize(response) error_message = "unauthorized" - super(error_message) @response = response @error_code = SDK::Provider::ErrorCode::GENERAL @error_message = error_message + super(error_message) end end @@ -54,10 +54,10 @@ class ParseError < StandardError def initialize(response) error_message = "Parse error" - super(error_message) @response = response @error_code = SDK::Provider::ErrorCode::PARSE_ERROR @error_message = error_message + super(error_message) end end @@ -66,10 +66,10 @@ class RateLimited < StandardError def initialize(response) error_message = response.nil? ? "Rate limited" : "Rate limited: " + response["Retry-After"].to_s - super(error_message) @response = response @error_code = SDK::Provider::ErrorCode::GENERAL @error_message = error_message + super(error_message) end end end From a05b450b409300a18bf08b83de9493a50d71e4cf Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Mon, 12 Aug 2024 23:06:12 +0200 Subject: [PATCH 18/19] Use directly SDK because we are in OpenFeature namespace already Signed-off-by: Thomas Poignant --- .../go_feature_flag_provider.rb | 34 ++++++++--------- .../openfeature/go-feature-flag/goff_api.rb | 38 +++++++++---------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/go_feature_flag_provider.rb b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/go_feature_flag_provider.rb index 3ac64fc..9cb6f24 100644 --- a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/go_feature_flag_provider.rb +++ b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/go_feature_flag_provider.rb @@ -7,10 +7,10 @@ class Provider PROVIDER_NAME = "GO Feature Flag Provider" attr_reader :metadata, :options - def initialize(options: OpenFeature::GoFeatureFlag::Options.new) - @metadata = OpenFeature::SDK::Provider::ProviderMetadata.new(name: PROVIDER_NAME) + def initialize(options: Options.new) + @metadata = SDK::Provider::ProviderMetadata.new(name: PROVIDER_NAME) @options = options - @goff_api = OpenFeature::GoFeatureFlag::GoFeatureFlagApi.new(options: options) + @goff_api = GoFeatureFlagApi.new(options: options) end def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil) @@ -37,10 +37,10 @@ def evaluate(flag_key:, default_value:, allowed_classes:, evaluation_context: ni # do a http call to the go feature flag server parsed_response = @goff_api.evaluate_ofrep_api(flag_key: flag_key, evaluation_context: evaluation_context) - parsed_response = OpenFeature::GoFeatureFlag::OfrepApiResponse unless parsed_response.is_a?(OpenFeature::GoFeatureFlag::OfrepApiResponse) + parsed_response = OfrepApiResponse unless parsed_response.is_a?(OfrepApiResponse) if parsed_response.has_error? - return OpenFeature::SDK::Provider::ResolutionDetails.new( + return SDK::Provider::ResolutionDetails.new( value: default_value, error_code: parsed_response.error_code, error_message: parsed_response.error_details, @@ -49,39 +49,39 @@ def evaluate(flag_key:, default_value:, allowed_classes:, evaluation_context: ni end unless allowed_classes.include?(parsed_response.value.class) - return OpenFeature::SDK::Provider::ResolutionDetails.new( + return SDK::Provider::ResolutionDetails.new( value: default_value, - error_code: OpenFeature::SDK::Provider::ErrorCode::TYPE_MISMATCH, + error_code: SDK::Provider::ErrorCode::TYPE_MISMATCH, error_message: "flag type #{parsed_response.value.class} does not match allowed types #{allowed_classes}", - reason: OpenFeature::SDK::Provider::Reason::ERROR + reason: SDK::Provider::Reason::ERROR ) end - OpenFeature::SDK::Provider::ResolutionDetails.new( + SDK::Provider::ResolutionDetails.new( value: parsed_response.value, reason: parsed_response.reason, variant: parsed_response.variant, flag_metadata: parsed_response.metadata ) - rescue OpenFeature::GoFeatureFlag::UnauthorizedError, - OpenFeature::GoFeatureFlag::InvalidOptionError, - OpenFeature::GoFeatureFlag::FlagNotFoundError, - OpenFeature::GoFeatureFlag::InternalServerError => e - OpenFeature::SDK::Provider::ResolutionDetails.new( + rescue UnauthorizedError, + InvalidOptionError, + FlagNotFoundError, + InternalServerError => e + SDK::Provider::ResolutionDetails.new( value: default_value, error_code: e.error_code, error_message: e.error_message, - reason: OpenFeature::SDK::Provider::Reason::ERROR + reason: SDK::Provider::Reason::ERROR ) end def validate_parameters(flag_key, evaluation_context) if evaluation_context.nil? || evaluation_context.targeting_key.nil? || evaluation_context.targeting_key.empty? - raise OpenFeature::GoFeatureFlag::InvalidOptionError.new(OpenFeature::SDK::Provider::ErrorCode::INVALID_CONTEXT, "invalid evaluation context provided") + raise InvalidOptionError.new(SDK::Provider::ErrorCode::INVALID_CONTEXT, "invalid evaluation context provided") end if flag_key.nil? || flag_key.empty? - raise OpenFeature::GoFeatureFlag::InvalidOptionError.new(OpenFeature::SDK::Provider::ErrorCode::GENERAL, "invalid flag key provided") + raise InvalidOptionError.new(SDK::Provider::ErrorCode::GENERAL, "invalid flag key provided") end end end diff --git a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/goff_api.rb b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/goff_api.rb index 3905985..ffa8ffc 100644 --- a/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/goff_api.rb +++ b/providers/openfeature-go-feature-flag-provider/lib/openfeature/go-feature-flag/goff_api.rb @@ -69,7 +69,7 @@ def parse_error_response(response) OpenFeature::GoFeatureFlag::OfrepApiResponse.new( value: nil, key: parsed["key"], - reason: OpenFeature::SDK::Provider::Reason::ERROR, + reason: SDK::Provider::Reason::ERROR, variant: nil, error_code: error_code_mapper(parsed["error_code"]), error_details: parsed["error_details"], @@ -100,31 +100,31 @@ def parse_success_response(response) def reason_mapper(reason_str) reason_str = reason_str.upcase reason_map = { - "STATIC" => OpenFeature::SDK::Provider::Reason::STATIC, - "DEFAULT" => OpenFeature::SDK::Provider::Reason::DEFAULT, - "TARGETING_MATCH" => OpenFeature::SDK::Provider::Reason::TARGETING_MATCH, - "SPLIT" => OpenFeature::SDK::Provider::Reason::SPLIT, - "CACHED" => OpenFeature::SDK::Provider::Reason::CACHED, - "DISABLED" => OpenFeature::SDK::Provider::Reason::DISABLED, - "UNKNOWN" => OpenFeature::SDK::Provider::Reason::UNKNOWN, - "STALE" => OpenFeature::SDK::Provider::Reason::STALE, - "ERROR" => OpenFeature::SDK::Provider::Reason::ERROR + "STATIC" => SDK::Provider::Reason::STATIC, + "DEFAULT" => SDK::Provider::Reason::DEFAULT, + "TARGETING_MATCH" => SDK::Provider::Reason::TARGETING_MATCH, + "SPLIT" => SDK::Provider::Reason::SPLIT, + "CACHED" => SDK::Provider::Reason::CACHED, + "DISABLED" => SDK::Provider::Reason::DISABLED, + "UNKNOWN" => SDK::Provider::Reason::UNKNOWN, + "STALE" => SDK::Provider::Reason::STALE, + "ERROR" => SDK::Provider::Reason::ERROR } - reason_map[reason_str] || OpenFeature::SDK::Provider::Reason::UNKNOWN + reason_map[reason_str] || SDK::Provider::Reason::UNKNOWN end def error_code_mapper(error_code_str) error_code_str = error_code_str.upcase error_code_map = { - "PROVIDER_NOT_READY" => OpenFeature::SDK::Provider::ErrorCode::PROVIDER_NOT_READY, - "FLAG_NOT_FOUND" => OpenFeature::SDK::Provider::ErrorCode::FLAG_NOT_FOUND, - "PARSE_ERROR" => OpenFeature::SDK::Provider::ErrorCode::PARSE_ERROR, - "TYPE_MISMATCH" => OpenFeature::SDK::Provider::ErrorCode::TYPE_MISMATCH, - "TARGETING_KEY_MISSING" => OpenFeature::SDK::Provider::ErrorCode::TARGETING_KEY_MISSING, - "INVALID_CONTEXT" => OpenFeature::SDK::Provider::ErrorCode::INVALID_CONTEXT, - "GENERAL" => OpenFeature::SDK::Provider::ErrorCode::GENERAL + "PROVIDER_NOT_READY" => SDK::Provider::ErrorCode::PROVIDER_NOT_READY, + "FLAG_NOT_FOUND" => SDK::Provider::ErrorCode::FLAG_NOT_FOUND, + "PARSE_ERROR" => SDK::Provider::ErrorCode::PARSE_ERROR, + "TYPE_MISMATCH" => SDK::Provider::ErrorCode::TYPE_MISMATCH, + "TARGETING_KEY_MISSING" => SDK::Provider::ErrorCode::TARGETING_KEY_MISSING, + "INVALID_CONTEXT" => SDK::Provider::ErrorCode::INVALID_CONTEXT, + "GENERAL" => SDK::Provider::ErrorCode::GENERAL } - error_code_map[error_code_str] || OpenFeature::SDK::Provider::ErrorCode::GENERAL + error_code_map[error_code_str] || SDK::Provider::ErrorCode::GENERAL end def parse_retry_later_header(response) From c9852a53475025d6b24856e9cf2cfbd6ed81de7c Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Mon, 12 Aug 2024 23:35:28 +0200 Subject: [PATCH 19/19] reconfigure spec_helper.rb with new configuration Signed-off-by: Thomas Poignant --- .../gofeatureflag/goff_api_spec.rb | 2 +- .../openfeature/gofeatureflag/options_spec.rb | 2 +- .../gofeatureflag/provider_spec.rb | 2 +- .../spec/spec_helper.rb | 92 ++----------------- 4 files changed, 10 insertions(+), 88 deletions(-) diff --git a/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/goff_api_spec.rb b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/goff_api_spec.rb index 3767e00..ec939f7 100644 --- a/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/goff_api_spec.rb +++ b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/goff_api_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -describe OpenFeature::GoFeatureFlag::GoFeatureFlagApi do +RSpec.describe OpenFeature::GoFeatureFlag::GoFeatureFlagApi do subject(:goff_api) do options = OpenFeature::GoFeatureFlag::Options.new(endpoint: "http://localhost:1031") described_class.new(options: options) diff --git a/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/options_spec.rb b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/options_spec.rb index 4b097bd..46e55e9 100644 --- a/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/options_spec.rb +++ b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/options_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -describe OpenFeature::GoFeatureFlag::Options do +RSpec.describe OpenFeature::GoFeatureFlag::Options do context "#endpoint" do it "should have a valid endpoint set" do options = OpenFeature::GoFeatureFlag::Options.new(endpoint: "http://localhost:1031") diff --git a/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/provider_spec.rb b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/provider_spec.rb index 3b881fb..30b17d1 100644 --- a/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/provider_spec.rb +++ b/providers/openfeature-go-feature-flag-provider/spec/openfeature/gofeatureflag/provider_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -describe OpenFeature::GoFeatureFlag::Provider do +RSpec.describe OpenFeature::GoFeatureFlag::Provider do subject(:goff_provider) do options = OpenFeature::GoFeatureFlag::Options.new(endpoint: "http://localhost:1031") described_class.new(options: options) diff --git a/providers/openfeature-go-feature-flag-provider/spec/spec_helper.rb b/providers/openfeature-go-feature-flag-provider/spec/spec_helper.rb index cf06990..c0d12a9 100644 --- a/providers/openfeature-go-feature-flag-provider/spec/spec_helper.rb +++ b/providers/openfeature-go-feature-flag-provider/spec/spec_helper.rb @@ -1,105 +1,27 @@ # frozen_string_literal: true require "bundler/setup" +require "rspec" require "openfeature/go-feature-flag/go_feature_flag_provider" require "openfeature/go-feature-flag/options" require "openfeature/go-feature-flag/goff_api" require "open_feature/sdk" require "webmock/rspec" -# This file was generated by the `rspec --init` command. Conventionally, all -# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. -# The generated `.rspec` file contains `--require spec_helper` which will cause -# this file to always be loaded, without a need to explicitly require it in any -# files. -# -# Given that it is always loaded, you are encouraged to keep this file as -# light-weight as possible. Requiring heavyweight dependencies from this file -# will add to the boot time of your test suite on EVERY test run, even for an -# individual file that may not need all of that loaded. Instead, consider making -# a separate helper file that requires the additional dependencies and performs -# the additional setup, and require it from the spec files that actually need -# it. -# -# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration RSpec.configure do |config| - # rspec-expectations config goes here. You can use an alternate - # assertion/expectation library such as wrong or the stdlib/minitest - # assertions if you prefer. config.expect_with :rspec do |expectations| - # This option will default to `true` in RSpec 4. It makes the `description` - # and `failure_message` of custom matchers include text for helper methods - # defined using `chain`, e.g.: - # be_bigger_than(2).and_smaller_than(4).description - # # => "be bigger than 2 and smaller than 4" - # ...rather than: - # # => "be bigger than 2" expectations.include_chain_clauses_in_custom_matcher_descriptions = true end - # rspec-mocks config goes here. You can use an alternate test double - # library (such as bogus or mocha) by changing the `mock_with` option here. config.mock_with :rspec do |mocks| - # Prevents you from mocking or stubbing a method that does not exist on - # a real object. This is generally recommended, and will default to - # `true` in RSpec 4. mocks.verify_partial_doubles = true end - # This option will default to `:apply_to_host_groups` in RSpec 4 (and will - # have no way to turn it off -- the option exists only for backwards - # compatibility in RSpec 3). It causes shared context metadata to be - # inherited by the metadata hash of host groups and examples, rather than - # triggering implicit auto-inclusion in groups with matching metadata. config.shared_context_metadata_behavior = :apply_to_host_groups - - # The settings below are suggested to provide a good initial experience - # with RSpec, but feel free to customize to your heart's content. - # # This allows you to limit a spec run to individual examples or groups - # # you care about by tagging them with `:focus` metadata. When nothing - # # is tagged with `:focus`, all examples get run. RSpec also provides - # # aliases for `it`, `describe`, and `context` that include `:focus` - # # metadata: `fit`, `fdescribe` and `fcontext`, respectively. - # config.filter_run_when_matching :focus - # - # # Allows RSpec to persist some state between runs in order to support - # # the `--only-failures` and `--next-failure` CLI options. We recommend - # # you configure your source control system to ignore this file. - # config.example_status_persistence_file_path = "spec/examples.txt" - # - # # Limits the available syntax to the non-monkey patched syntax that is - # # recommended. For more details, see: - # # https://relishapp.com/rspec/rspec-core/docs/configuration/zero-monkey-patching-mode - # config.disable_monkey_patching! - # - # # This setting enables warnings. It's recommended, but in some cases may - # # be too noisy due to issues in dependencies. - # config.warnings = true - # - # # Many RSpec users commonly either run the entire suite or an individual - # # file, and it's useful to allow more verbose output when running an - # # individual spec file. - # if config.files_to_run.one? - # # Use the documentation formatter for detailed output, - # # unless a formatter has already been configured - # # (e.g. via a command-line flag). - # config.default_formatter = "doc" - # end - # - # # Print the 10 slowest examples and example groups at the - # # end of the spec run, to help surface which specs are running - # # particularly slow. - # config.profile_examples = 10 - # - # # Run specs in random order to surface order dependencies. If you find an - # # order dependency and want to debug it, you can fix the order by providing - # # the seed, which is printed after each run. - # # --seed 1234 - # config.order = :random - # - # # Seed global randomization in this process using the `--seed` CLI option. - # # Setting this allows you to use `--seed` to deterministically reproduce - # # test failures related to randomization by passing the same `--seed` value - # # as the one that triggered the failure. - # Kernel.srand config.seed + 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