diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..4633a7ac
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,30 @@
+root = true
+
+[*]
+end_of_line = lf
+indent_size = 4
+indent_style = space
+insert_final_newline = true
+tab_width = 8
+trim_trailing_whitespace = true
+
+[*.bat]
+end_of_line = crlf
+
+[*.gemspec]
+indent_size = 2
+
+[*.rb]
+indent_size = 2
+
+[*.yml]
+indent_size = 2
+
+[{*[Mm]akefile*,*.mak,*.mk,depend}]
+indent_style = tab
+
+[enc/*]
+indent_size = 2
+
+[reg*.[ch]]
+indent_size = 2
diff --git a/.rubocop.yml b/.rubocop.yml
index 030f2943..1c951a20 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -11,4 +11,11 @@ Style/StringLiteralsInInterpolation:
EnforcedStyle: double_quotes
Layout/LineLength:
- Max: 120
+ Enabled: false
+ Max: 270
+
+Style/AccessorGrouping:
+ Enabled: false
+
+Metrics/BlockLength:
+ Enabled: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 4d8d55e4..a778957c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -2,6 +2,7 @@ PATH
remote: .
specs:
openfeature-sdk (0.0.1)
+ sorbet-runtime (~> 0.5.10539)
GEM
remote: https://rubygems.org/
@@ -42,6 +43,12 @@ GEM
rubocop-ast (1.23.0)
parser (>= 3.1.1.0)
ruby-progressbar (1.11.0)
+ sorbet (0.5.10539)
+ sorbet-static (= 0.5.10539)
+ sorbet-runtime (0.5.10539)
+ sorbet-static (0.5.10539-universal-darwin-21)
+ sorbet-static (0.5.10539-universal-darwin-22)
+ sorbet-static (0.5.10539-x86_64-linux)
unicode-display_width (2.3.0)
PLATFORMS
@@ -59,6 +66,7 @@ DEPENDENCIES
rake (~> 13.0)
rspec (~> 3.12.0)
rubocop (~> 1.37.1)
+ sorbet (~> 0.5.10539)
BUNDLED WITH
2.3.25
diff --git a/lib/openfeature/sdk.rb b/lib/openfeature/sdk.rb
index 46085e2f..a132bdf8 100644
--- a/lib/openfeature/sdk.rb
+++ b/lib/openfeature/sdk.rb
@@ -1,10 +1,65 @@
# frozen_string_literal: true
+require "sorbet-runtime"
+require "forwardable"
+
require_relative "sdk/version"
+require_relative "sdk/configuration"
+require_relative "sdk/client"
+require_relative "sdk/metadata"
+require_relative "sdk/provider/no_op_provider"
module OpenFeature
+ # API Initialization and Configuration
+ #
+ # Represents the entry point to the SDK, including configuration of Provider,Hook,
+ # and building the Client
+ #
+ # To use the SDK, you can optionally configure a Provider, with Hook
+ #
+ # OpenFeature::SDK.configure do |config|
+ # config.provider = NoOpProvider.new
+ # end
+ #
+ # If no provider is specified, the NoOpProvider is set as the default Provider.
+ # Once the SDK has been configured, a client can be built
+ #
+ # client = OpenFeature::SDK.build_client(name: 'my-open-feature-client')
module SDK
- class Error < StandardError; end
- # Your code goes here...
+ class << self
+ extend T::Sig
+ extend Forwardable
+
+ def_delegator :@configuration, :provider
+ def_delegator :@configuration, :hooks
+ def_delegator :@configuration, :context
+
+ sig { returns(Configuration) }
+ def configuration
+ @configuration ||= T.let(Configuration.new, Configuration)
+ end
+
+ # rubocop:disable Lint/UnusedMethodArgument
+ sig { params(block: T.proc.params(arg0: Configuration).void).void }
+ def configure(&block)
+ return unless block_given?
+
+ yield(configuration)
+ end
+ # rubocop:enable Lint/UnusedMethodArgument
+
+ sig do
+ params(
+ name: T.nilable(String),
+ version: T.nilable(String),
+ context: T.nilable(EvaluationContext)
+ ).returns(SDK::Client)
+ end
+ def build_client(name: nil, version: nil, context: nil)
+ client_options = Metadata.new(name: name, version: version)
+ provider = Provider::NoOpProvider.new if provider.nil?
+ SDK::Client.new(provider, client_options, context)
+ end
+ end
end
end
diff --git a/lib/openfeature/sdk/client.rb b/lib/openfeature/sdk/client.rb
new file mode 100644
index 00000000..a34b03bb
--- /dev/null
+++ b/lib/openfeature/sdk/client.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+# typed: true
+
+require "sorbet-runtime"
+require "forwardable"
+require "json"
+
+require_relative "./provider/provider"
+require_relative "./evaluation_context"
+require_relative "./metadata"
+require_relative "./hook/hook"
+require_relative "./hook/hook_context"
+require_relative "./evaluation_options"
+require_relative "./resolver/boolean_resolver"
+require_relative "./resolver/number_resolver"
+require_relative "./resolver/object_resolver"
+require_relative "./resolver/string_resolver"
+
+module OpenFeature
+ module SDK
+ # TODO: Write
+ #
+ class Client
+ extend T::Sig
+ extend Forwardable
+
+ class OpenFeatureOptions < T::Struct
+ const :name, T.nilable(String)
+ const :version, T.nilable(String)
+ end
+
+ sig { returns(Metadata) }
+ attr_reader :metadata
+
+ sig { returns(T::Array[Hook]) }
+ attr_accessor :hooks
+
+ def_delegator :@boolean_resolver, :fetch_value, :fetch_boolean_value
+ def_delegator :@boolean_resolver, :fetch_detailed_value, :fetch_boolean_details
+
+ def_delegator :@number_resolver, :fetch_value, :fetch_number_value
+ def_delegator :@number_resolver, :fetch_detailed_value, :fetch_number_details
+
+ def_delegator :@string_resolver, :fetch_value, :fetch_string_value
+ def_delegator :@string_resolver, :fetch_detailed_value, :fetch_string_details
+
+ def_delegator :@object_resolver, :fetch_value, :fetch_object_value
+ def_delegator :@object_resolver, :fetch_detailed_value, :fetch_object_details
+
+ sig do
+ params(
+ provider: Provider,
+ client_options: Metadata,
+ context: T.nilable(EvaluationContext)
+ ).void
+ end
+ def initialize(provider, client_options, context)
+ @provider = provider
+ @metadata = client_options.dup.freeze
+ @context = context.dup.freeze
+ @hooks = []
+
+ @boolean_resolver = Resolver::BooleanResolver.new(provider)
+ @number_resolver = Resolver::NumberResolver.new(provider)
+ @string_resolver = Resolver::StringResolver.new(provider)
+ @object_resolver = Resolver::ObjectResolver.new(provider)
+ end
+ end
+ end
+end
diff --git a/lib/openfeature/sdk/configuration.rb b/lib/openfeature/sdk/configuration.rb
new file mode 100644
index 00000000..7cfb0119
--- /dev/null
+++ b/lib/openfeature/sdk/configuration.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+# typed: true
+
+require "forwardable"
+
+require_relative "./provider/provider"
+require_relative "./provider/no_op_provider"
+
+module OpenFeature
+ module SDK
+ # TODO: Write documentation
+ #
+ class Configuration
+ extend T::Sig
+ extend Forwardable
+
+ sig { returns(T.nilable(EvaluationContext)) }
+ attr_accessor :context
+
+ sig { returns(SDK::Provider) }
+ attr_accessor :provider
+
+ sig { returns(T::Array[Hook]) }
+ attr_accessor :hooks
+
+ def_delegator :@provider, :metadata
+
+ def initialize
+ @hooks = []
+ end
+ end
+ end
+end
diff --git a/lib/openfeature/sdk/evaluation_context.rb b/lib/openfeature/sdk/evaluation_context.rb
new file mode 100644
index 00000000..0462a3ac
--- /dev/null
+++ b/lib/openfeature/sdk/evaluation_context.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+# typed: true
+
+# frozen_literal: true
+
+require "sorbet-runtime"
+require "date"
+
+class EvaluationContext < T::Struct
+ CustomFieldValues = T.type_alias { T.any(T::Boolean, String, Integer, Float, T.untyped, DateTime) }
+ CustomField = T.type_alias { T::Hash[String, CustomFieldValues] }
+
+ const :targeting_key, T.nilable(String)
+ const :custom_fields, T.nilable(CustomField)
+end
diff --git a/lib/openfeature/sdk/evaluation_details.rb b/lib/openfeature/sdk/evaluation_details.rb
new file mode 100644
index 00000000..3358d741
--- /dev/null
+++ b/lib/openfeature/sdk/evaluation_details.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+# typed: strict
+
+require "sorbet-runtime"
+require_relative("./resolution_details")
+
+class EvaluationDetails < ResolutionDetails
+ const :flag_key, String
+end
diff --git a/lib/openfeature/sdk/evaluation_options.rb b/lib/openfeature/sdk/evaluation_options.rb
new file mode 100644
index 00000000..e26fc59f
--- /dev/null
+++ b/lib/openfeature/sdk/evaluation_options.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+# typed: strict
+
+require "sorbet-runtime"
+require_relative("./hook/hook")
+
+class EvaluationOptions < T::Struct
+ const :hooks, T::Array[Hook], default: []
+ const :hook_hints, T.nilable(T::Hash[String, T.untyped])
+end
diff --git a/lib/openfeature/sdk/feature_flag_error_code.rb b/lib/openfeature/sdk/feature_flag_error_code.rb
new file mode 100644
index 00000000..79ab8c1a
--- /dev/null
+++ b/lib/openfeature/sdk/feature_flag_error_code.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+# typed: true
+
+require "sorbet-runtime"
+
+class FeatureFlagErrorCode < T::Enum
+ enums do
+ PROVIDER_NOT_READY = new("PROVIDER_NOT_READY")
+ FLAG_NOT_FOUND = new("FLAG_NOT_FOUND")
+ PARSE_ERROR = new("PARSE_ERROR")
+ TYPE_MISMATCH = new("TYPE_MISMATCH")
+ TARGETING_KEY_MISSING = new("TARGETING_KEY_MISSING")
+ INVALID_CONTEXT = new("INVALID_CONTEXT")
+ GENERAL = new("GENERAL")
+ end
+end
diff --git a/lib/openfeature/sdk/feature_flag_evaluation_details.rb b/lib/openfeature/sdk/feature_flag_evaluation_details.rb
new file mode 100644
index 00000000..e8002f18
--- /dev/null
+++ b/lib/openfeature/sdk/feature_flag_evaluation_details.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+# typed: strict
+
+require "sorbet-runtime"
+require_relative("./feature_flag_error_code")
+
+class FeatureFlagEvaluationDetails < T::Struct
+ const :reason, T.nilable(String)
+ const :variant, T.nilable(String)
+ const :error_code, T.nilable(FeatureFlagErrorCode)
+ const :error_message, T.nilable(String)
+end
diff --git a/lib/openfeature/sdk/flag_evaluation_options.rb b/lib/openfeature/sdk/flag_evaluation_options.rb
new file mode 100644
index 00000000..ddb72f6e
--- /dev/null
+++ b/lib/openfeature/sdk/flag_evaluation_options.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+# typed: true
+
+require "sorbet-runtime"
+require_relative("./hook")
+
+class FlagEvaluationOptions < T::Struct
+ const :hooks, T.nilable(T::Array[Hook])
+ const :hook_hints, T.nilable(T::Hash[String, T.untyped])
+end
diff --git a/lib/openfeature/sdk/hook/hook.rb b/lib/openfeature/sdk/hook/hook.rb
new file mode 100644
index 00000000..48fd790a
--- /dev/null
+++ b/lib/openfeature/sdk/hook/hook.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+# typed: true
+
+require "sorbet-runtime"
+
+require_relative("./hook_context")
+
+module Hook
+ extend T::Sig
+ extend T::Helpers
+ interface!
+
+ sig do
+ abstract.params(
+ hook_context: HookContext,
+ hook_hints: T.nilable(T::Hash[Symbol, T.untyped])
+ ).returns(EvaluationContext)
+ end
+ def before(hook_context:, hook_hints: nil); end
+
+ sig do
+ abstract.params(
+ hook_context: HookContext,
+ hook_hints: T.nilable(T::Hash[Symbol, T.untyped])
+ ).returns(EvaluationContext)
+ end
+ def after(hook_context:, hook_hints: nil); end
+
+ sig do
+ abstract.params(
+ hook_context: HookContext,
+ hook_hints: T.nilable(T::Hash[Symbol, T.untyped])
+ ).returns(EvaluationContext)
+ end
+ def error(hook_context:, hook_hints: nil); end
+
+ sig do
+ abstract.params(
+ hook_context: HookContext,
+ hook_hints: T.nilable(T::Hash[Symbol, T.untyped])
+ ).returns(EvaluationContext)
+ end
+ def finally(hook_context:, hook_hints: nil); end
+end
diff --git a/lib/openfeature/sdk/hook/hook_context.rb b/lib/openfeature/sdk/hook/hook_context.rb
new file mode 100644
index 00000000..858231b1
--- /dev/null
+++ b/lib/openfeature/sdk/hook/hook_context.rb
@@ -0,0 +1,21 @@
+# typed: true
+# frozen_string_literal: true
+
+require_relative("../metadata")
+require_relative("../evaluation_context")
+
+module OpenFeature
+ module SDK
+ module Hook
+ class HookContext < T::Struct
+ const :flag_key, String
+ const :default_value, T.any(T::Boolean, String, Integer, Integer, Float)
+ const :flag_value_type, T.any(String, Integer, Float, TrueClass, FalseClass)
+ const :context, T.nilable(EvaluationContext)
+ const :client_metadata, SDK::Metadata
+ const :provider_metadata, SDK::Metadata
+ const :logger, T.nilable(T.untyped)
+ end
+ end
+ end
+end
diff --git a/lib/openfeature/sdk/metadata.rb b/lib/openfeature/sdk/metadata.rb
new file mode 100644
index 00000000..01a5b81b
--- /dev/null
+++ b/lib/openfeature/sdk/metadata.rb
@@ -0,0 +1,43 @@
+# typed: true
+# frozen_string_literal: true
+
+require "sorbet-runtime"
+
+module OpenFeature
+ module SDK
+ # Metadata structure that defines general metadata relating to a Provider or Client
+ #
+ # Within the Metadata structure you have access to the following attribute reader:
+ #
+ # * name - Allows you to specify name of the Metadata structure
+ #
+ # * version - Allows you to specify version of the Metadata structure
+ #
+ # Usage:
+ #
+ # metadata = Metadata.new(name: 'name-for-metadata')
+ # metadata.name # 'name-for-metadata'
+ # metadata_two = Metadata.new(name: 'name-for-metadata')
+ # metadata_two == metadata # true - equality based on values
+ class Metadata
+ extend T::Sig
+
+ sig { returns(String) }
+ attr_reader :name
+
+ sig { returns(T.nilable(String)) }
+ attr_reader :version
+
+ sig { params(name: String, version: T.nilable(String)).void }
+ def initialize(name:, version: nil)
+ @name = T.let(name.dup, String)
+ @version = T.let(version.dup, T.nilable(String))
+ end
+
+ sig { params(other: Metadata).returns(T::Boolean) }
+ def ==(other)
+ @name == other.name && @version == other.version
+ end
+ end
+ end
+end
diff --git a/lib/openfeature/sdk/provider/no_op_provider.rb b/lib/openfeature/sdk/provider/no_op_provider.rb
new file mode 100644
index 00000000..7f4808b2
--- /dev/null
+++ b/lib/openfeature/sdk/provider/no_op_provider.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+# typed: true
+
+# frozen_literal: true
+
+require "sorbet-runtime"
+require "json"
+
+require_relative("./provider")
+require_relative("../metadata")
+require_relative("../evaluation_context")
+require_relative("../resolution_details")
+
+# rubocop:disable Lint/UnusedMethodArgument
+module OpenFeature
+ module SDK
+ module Provider
+ # TODO: Write documentation
+ #
+ class NoOpProvider
+ extend T::Sig
+ include Provider
+
+ Number = T.type_alias { T.any(Integer, Float) }
+
+ REASON_NO_OP = "No-op"
+ NAME = "No-op Provider"
+
+ def initialize
+ @metadata = SDK::Metadata.new(name: NAME).freeze
+ end
+
+ sig do
+ override.params(
+ flag_key: String,
+ default_value: T::Boolean,
+ evaluation_context: T.nilable(EvaluationContext)
+ ).returns(ResolutionDetails)
+ end
+ def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
+ no_op(default_value)
+ end
+
+ sig do
+ override.params(
+ flag_key: String,
+ default_value: String,
+ evaluation_context: T.nilable(EvaluationContext)
+ ).returns(ResolutionDetails)
+ end
+ def fetch_string_value(flag_key:, default_value:, evaluation_context: nil)
+ no_op(default_value)
+ end
+
+ sig do
+ override.params(
+ flag_key: String,
+ default_value: Number,
+ evaluation_context: T.nilable(EvaluationContext)
+ ).returns(ResolutionDetails)
+ end
+ def fetch_number_value(flag_key:, default_value:, evaluation_context: nil)
+ no_op(default_value)
+ end
+
+ sig do
+ override.params(
+ flag_key: String,
+ default_value: T.untyped,
+ evaluation_context: T.nilable(EvaluationContext)
+ ).returns(ResolutionDetails)
+ end
+ def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
+ no_op(default_value)
+ end
+
+ private
+
+ sig { params(default_value: T.untyped, variant: T.nilable(String)).returns(ResolutionDetails) }
+ def no_op(default_value)
+ ResolutionDetails.new(value: default_value, reason: REASON_NO_OP)
+ end
+ end
+ end
+ end
+end
+# rubocop:enable Lint/UnusedMethodArgument
diff --git a/lib/openfeature/sdk/provider/provider.rb b/lib/openfeature/sdk/provider/provider.rb
new file mode 100644
index 00000000..22fb50d8
--- /dev/null
+++ b/lib/openfeature/sdk/provider/provider.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+# typed: true
+
+# frozen_literal: true
+
+require "sorbet-runtime"
+require_relative("../feature_flag_evaluation_details")
+require_relative("../evaluation_context")
+require_relative("../hook/hook")
+
+module OpenFeature
+ module SDK
+ # TODO: Write documentation
+ #
+ module Provider
+ extend T::Sig
+ extend T::Helpers
+ interface!
+
+ Number = T.type_alias { T.any(Integer, Float) }
+
+ sig { returns(Metadata) }
+ attr_reader :metadata
+
+ sig { returns(T.nilable(T::Array[Hook])) }
+ attr_accessor :hooks
+
+ sig do
+ abstract.params(
+ flag_key: String,
+ default_value: T::Boolean,
+ evaluation_context: T.nilable(EvaluationContext)
+ ).returns(ResolutionDetails)
+ end
+ def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil); end
+
+ sig do
+ abstract.params(
+ flag_key: String,
+ default_value: T::Boolean,
+ evaluation_details: FeatureFlagEvaluationDetails
+ ).returns(ResolutionDetails)
+ end
+ def fetch_boolean_details(flag_key:, default_value:, evaluation_details:); end
+
+ sig do
+ abstract.params(
+ flag_key: String,
+ default_value: String,
+ evaluation_context: T.nilable(EvaluationContext)
+ ).returns(ResolutionDetails)
+ end
+ def fetch_string_value(flag_key:, default_value:, evaluation_context: nil); end
+
+ sig do
+ abstract.params(
+ flag_key: String,
+ default_value: String,
+ evaluation_details: FeatureFlagEvaluationDetails
+ ).returns(ResolutionDetails)
+ end
+ def fetch_string_details(flag_key:, default_value:, evaluation_details:); end
+
+ sig do
+ abstract.params(
+ flag_key: String,
+ default_value: Number,
+ evaluation_context: T.nilable(EvaluationContext)
+ ).returns(ResolutionDetails)
+ end
+ def fetch_number_value(flag_key:, default_value:, evaluation_context: nil); end
+
+ sig do
+ abstract.params(
+ flag_key: String,
+ default_value: Number,
+ evaluation_details: FeatureFlagEvaluationDetails
+ ).returns(ResolutionDetails)
+ end
+ def fetch_number_details(flag_key:, default_value:, evaluation_details:); end
+
+ sig do
+ abstract.params(
+ flag_key: String,
+ default_value: T.untyped,
+ evaluation_context: T.nilable(EvaluationContext)
+ ).returns(ResolutionDetails)
+ end
+ def fetch_object_value(flag_key:, default_value:, evaluation_context: nil); end
+
+ sig do
+ abstract.params(
+ flag_key: String,
+ default_value: T.untyped,
+ evaluation_details: FeatureFlagEvaluationDetails
+ ).returns(Object)
+ end
+ def fetch_object_details(flag_key:, default_value:, evaluation_details:); end
+ end
+ end
+end
diff --git a/lib/openfeature/sdk/resolution_details.rb b/lib/openfeature/sdk/resolution_details.rb
new file mode 100644
index 00000000..49b1f1a2
--- /dev/null
+++ b/lib/openfeature/sdk/resolution_details.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+# typed: strict
+
+require "sorbet-runtime"
+require_relative("./feature_flag_error_code")
+require_relative("./resolution_reason")
+
+class ResolutionDetails < T::Struct
+ const :value, T.any(String, T::Boolean, Integer, Float, T::Hash[String, T.untyped], T::Array[T.untyped])
+ const :reason, T.nilable(T.any(ResolutionReason, String))
+ const :variant, T.nilable(String)
+ const :error_code, T.nilable(FeatureFlagErrorCode)
+ const :error_message, T.nilable(String)
+end
diff --git a/lib/openfeature/sdk/resolution_reason.rb b/lib/openfeature/sdk/resolution_reason.rb
new file mode 100644
index 00000000..52a41a8a
--- /dev/null
+++ b/lib/openfeature/sdk/resolution_reason.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+# typed: strict
+
+require "sorbet-runtime"
+require_relative("./resolution_details")
+
+class ResolutionReason < T::Enum
+ enums do
+ DEFAULT = new("DEFAULT")
+ TARGETING_MATCH = new("TARGETING_MATCH")
+ SPLIT = new("SPLIT")
+ DISABLED = new("DISABLED")
+ UNKNOWN = new("UNKNOWN")
+ ERROR = new("ERROR")
+ end
+end
diff --git a/lib/openfeature/sdk/resolver/boolean_resolver.rb b/lib/openfeature/sdk/resolver/boolean_resolver.rb
new file mode 100644
index 00000000..5b64da5c
--- /dev/null
+++ b/lib/openfeature/sdk/resolver/boolean_resolver.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+# typed: true
+
+require "sorbet-runtime"
+
+require_relative "../provider/provider"
+require_relative "../evaluation_context"
+require_relative "../metadata"
+require_relative "../hook/hook"
+require_relative "../hook/hook_context"
+require_relative "../evaluation_options"
+
+module OpenFeature
+ module Resolver
+ # TODO: Write documentation
+ #
+ class BooleanResolver
+ extend T::Sig
+
+ sig do
+ params(
+ provider: SDK::Provider
+ ).void
+ end
+ def initialize(provider)
+ @provider = provider
+ end
+
+ sig do
+ params(
+ flag_key: String,
+ default_value: T::Boolean,
+ evaluation_context: T.nilable(EvaluationContext),
+ evaluation_options: T.nilable(EvaluationOptions)
+ ).returns(T::Boolean)
+ end
+ def fetch_value(flag_key:, default_value:, evaluation_context: nil, evaluation_options: nil)
+ resolution_details = @provider.fetch_boolean_value(flag_key: flag_key, default_value: default_value, evaluation_context: evaluation_context, evaluation_options: evaluation_options)
+ correct_type?(resolution_details.value) ? resolution_details.value : default_value
+ end
+
+ sig do
+ params(
+ flag_key: String,
+ default_value: T::Boolean,
+ evaluation_context: T.nilable(EvaluationContext),
+ evaluation_options: T.nilable(EvaluationOptions)
+ ).returns(ResolutionDetails)
+ end
+ def fetch_detailed_value(flag_key:, default_value:, evaluation_context: nil, evaluation_options: nil)
+ @provider.fetch_boolean_value(flag_key: flag_key, default_value: default_value, evaluation_context: evaluation_context, evaluation_options: evaluation_options)
+ end
+
+ private
+
+ sig { params(value: T.untyped).returns(T::Boolean) }
+ def correct_type?(value)
+ [TrueClass, FalseClass].include?(value.class)
+ end
+ end
+ end
+end
diff --git a/lib/openfeature/sdk/resolver/number_resolver.rb b/lib/openfeature/sdk/resolver/number_resolver.rb
new file mode 100644
index 00000000..c75adfbe
--- /dev/null
+++ b/lib/openfeature/sdk/resolver/number_resolver.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+# typed: true
+
+require "sorbet-runtime"
+require_relative "../provider/provider"
+require_relative("../evaluation_context")
+require_relative("../metadata")
+require_relative("../hook/hook")
+require_relative("../hook/hook_context")
+require_relative("../evaluation_options")
+
+module OpenFeature
+ module Resolver
+ # TODO: Write documentation
+ #
+ class NumberResolver
+ extend T::Sig
+
+ Number = T.type_alias { T.any(Integer, Float) }
+
+ sig do
+ params(
+ provider: SDK::Provider
+ ).void
+ end
+ def initialize(provider)
+ @provider = provider
+ end
+
+ sig do
+ params(
+ flag_key: String,
+ default_value: Number,
+ evaluation_context: T.nilable(EvaluationContext),
+ evaluation_options: T.nilable(EvaluationOptions)
+ ).returns(Number)
+ end
+ def fetch_value(flag_key:, default_value:, evaluation_context: nil, evaluation_options: nil)
+ resolution_details = @provider.fetch_number_value(flag_key: flag_key, default_value: default_value, evaluation_context: evaluation_context, evaluation_options: evaluation_options)
+ correct_type?(resolution_details.value) ? resolution_details.value : default_value
+ end
+
+ sig do
+ params(
+ flag_key: String,
+ default_value: Number,
+ evaluation_context: T.nilable(EvaluationContext),
+ evaluation_options: T.nilable(EvaluationOptions)
+ ).returns(ResolutionDetails)
+ end
+ def fetch_detailed_value(flag_key:, default_value:, evaluation_context: nil, evaluation_options: nil)
+ @provider.fetch_number_value(flag_key: flag_key, default_value: default_value, evaluation_context: evaluation_context, evaluation_options: evaluation_options)
+ end
+
+ private
+
+ def correct_type?(value)
+ [Float, Integer].include?(value.class)
+ end
+ end
+ end
+end
diff --git a/lib/openfeature/sdk/resolver/object_resolver.rb b/lib/openfeature/sdk/resolver/object_resolver.rb
new file mode 100644
index 00000000..286da26d
--- /dev/null
+++ b/lib/openfeature/sdk/resolver/object_resolver.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+# typed: true
+
+require "sorbet-runtime"
+require_relative "../provider/provider"
+require_relative("../evaluation_context")
+require_relative("../metadata")
+require_relative("../hook/hook")
+require_relative("../hook/hook_context")
+require_relative("../evaluation_options")
+
+module OpenFeature
+ module Resolver
+ # TODO: Write documentation
+ #
+ class ObjectResolver
+ extend T::Sig
+
+ sig do
+ params(
+ provider: SDK::Provider
+ ).void
+ end
+ def initialize(provider)
+ @provider = provider
+ end
+
+ sig do
+ params(
+ flag_key: String,
+ default_value: T.untyped,
+ evaluation_context: T.nilable(EvaluationContext),
+ evaluation_options: T.nilable(EvaluationOptions)
+ ).returns(String)
+ end
+ def fetch_value(flag_key:, default_value:, evaluation_context: nil, evaluation_options: nil)
+ resolution_details = @provider.fetch_object_value(flag_key: flag_key, default_value: default_value, evaluation_context: evaluation_context, evaluation_options: evaluation_options)
+ correct_type?(resolution_details.value) ? resolution_details.value : default_value
+ end
+
+ sig do
+ params(
+ flag_key: String,
+ default_value: T.untyped,
+ evaluation_context: T.nilable(EvaluationContext),
+ evaluation_options: T.nilable(EvaluationOptions)
+ ).returns(ResolutionDetails)
+ end
+ def fetch_detailed_value(flag_key:, default_value:, evaluation_context: nil, evaluation_options: nil)
+ @provider.fetch_object_value(flag_key: flag_key, default_value: default_value, evaluation_context: evaluation_context, evaluation_options: evaluation_options)
+ end
+
+ private
+
+ def correct_type?(value)
+ result = JSON.parse(value)
+ result.is_a?(Hash) || result.is_a?(Array)
+ rescue JSON::ParserError
+ false
+ end
+ end
+ end
+end
diff --git a/lib/openfeature/sdk/resolver/string_resolver.rb b/lib/openfeature/sdk/resolver/string_resolver.rb
new file mode 100644
index 00000000..a71a89b2
--- /dev/null
+++ b/lib/openfeature/sdk/resolver/string_resolver.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+# typed: true
+
+require "sorbet-runtime"
+require_relative "../provider/provider"
+require_relative("../evaluation_context")
+require_relative("../metadata")
+require_relative("../hook/hook")
+require_relative("../hook/hook_context")
+require_relative("../evaluation_options")
+
+module OpenFeature
+ module Resolver
+ # TODO: Write documentation
+ #
+ class StringResolver
+ extend T::Sig
+
+ sig do
+ params(
+ provider: SDK::Provider
+ ).void
+ end
+ def initialize(provider)
+ @provider = provider
+ end
+
+ sig do
+ params(
+ flag_key: String,
+ default_value: String,
+ evaluation_context: T.nilable(EvaluationContext),
+ evaluation_options: T.nilable(EvaluationOptions)
+ ).returns(String)
+ end
+ def fetch_value(flag_key:, default_value:, evaluation_context: nil, evaluation_options: nil)
+ resolution_details = @provider.fetch_string_value(flag_key: flag_key, default_value: default_value,
+ evaluation_context: evaluation_context,
+ evaluation_options: evaluation_options)
+ is_correct_type?(resolution_details.value) ? resolution_details.value : default_value
+ end
+
+ sig do
+ params(
+ flag_key: String,
+ default_value: String,
+ evaluation_context: T.nilable(EvaluationContext),
+ evaluation_options: T.nilable(EvaluationOptions)
+ ).returns(ResolutionDetails)
+ end
+ def fetch_detailed_value(flag_key:, default_value:, evaluation_context: nil, evaluation_options: nil)
+ @provider.fetch_string_value(flag_key: flag_key, default_value: default_value,
+ evaluation_context: evaluation_context,
+ evaluation_options: evaluation_options)
+ end
+
+ private
+
+ def correct_type?(value)
+ value.is_a?(String)
+ end
+ end
+ end
+end
diff --git a/openfeature-sdk.gemspec b/openfeature-sdk.gemspec
index 98942e83..873b9986 100644
--- a/openfeature-sdk.gemspec
+++ b/openfeature-sdk.gemspec
@@ -31,8 +31,11 @@ Gem::Specification.new do |spec|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
+ spec.add_dependency "sorbet-runtime", "~> 0.5.10539"
+
spec.add_development_dependency "rake", "~> 13.0"
spec.add_development_dependency "rspec", "~> 3.12.0"
spec.add_development_dependency "rubocop", "~> 1.37.1"
+ spec.add_development_dependency "sorbet", "~> 0.5.10539"
spec.metadata["rubygems_mfa_required"] = "true"
end
diff --git a/spec/openfeature/client_spec.rb b/spec/openfeature/client_spec.rb
new file mode 100644
index 00000000..a553f23e
--- /dev/null
+++ b/spec/openfeature/client_spec.rb
@@ -0,0 +1,238 @@
+# frozen_string_literal: true
+
+require "./src/open_feature"
+require "./src/no_op_provider"
+require "./src/provider"
+require "./src/hook/hook"
+require "./src/metadata"
+require "./src/client"
+
+require "spec_helper"
+
+describe OpenFeature::Client do
+ before do
+ subject
+ end
+
+ context "Requirement 1.2.1" do
+ subject do
+ OpenFeature.configure do |config|
+ config.provider = NoOpProvider.new
+ config.hooks << api_hook1
+ config.hooks << api_hook2
+ end
+
+ client = OpenFeature.build_client(name: "my-openfeature-client")
+ client.hooks << client_hook1
+ client
+ end
+
+ let(:api_hook1) do
+ Class.new do
+ include Hook
+ end
+ end
+ let(:api_hook2) do
+ Class.new do
+ include Hook
+ end
+ end
+ let(:client_hook1) do
+ Class.new do
+ include Hook
+ end
+ end
+
+ it "MUST provide a method to add hooks which accepts one or more API-conformant hooks, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed." do
+ expect(subject).to respond_to(:hooks)
+ expect(subject.hooks).to have_attributes(size: 1).and eq([client_hook1])
+ end
+ end
+
+ context "Requirement 1.2.2" do
+ subject do
+ OpenFeature.configure do |config|
+ config.provider = NoOpProvider.new
+ end
+
+ OpenFeature.build_client(name: "my-openfeature-client")
+ end
+
+ it "MUST define a metadata member or accessor, containing an immutable name field or accessor of type string, which corresponds to the name value supplied during client creation." do
+ expect(subject).to respond_to(:metadata)
+ expect(subject.metadata).to respond_to(:name)
+ expect(subject.metadata.name).to eq("my-openfeature-client")
+ end
+ end
+
+ context "Flag evaluation" do
+ context "Requirement 1.3.1" do
+ context "Provide methods for typed flag evaluation, including boolean, numeric, string, and structure, with parameters flag key (string, required), default value (boolean | number | string | structure, required), evaluation context (optional), and evaluation options (optional), which returns the flag value." do
+ subject(:client) do
+ OpenFeature.build_client(name: "client")
+ end
+ let(:flag_key) { "my-awesome-feature-flag-key" }
+
+ context "boolean value" do
+ it do
+ expect(client).to respond_to(:fetch_boolean_value).with(4).arguments
+ end
+
+ it do
+ expect(client.fetch_boolean_value(flag_key: flag_key, default_value: false)).is_a?(FalseClass)
+ end
+
+ it do
+ expect(client.fetch_boolean_value(flag_key: flag_key, default_value: true)).is_a?(TrueClass)
+ end
+ end
+
+ context "string value" do
+ it do
+ expect(client).to respond_to(:fetch_string_value).with(4).arguments
+ end
+
+ it do
+ expect(client.fetch_string_value(flag_key: flag_key, default_value: "default_value")).is_a?(String)
+ end
+ end
+
+ context "number value" do
+ it do
+ expect(client).to respond_to(:fetch_number_value).with(4).arguments
+ end
+
+ context "Condition 1.3.2 - The implementation language differentiates between floating-point numbers and integers." do
+ it do
+ expect(client.fetch_number_value(flag_key: flag_key, default_value: 4)).is_a?(Integer)
+ end
+
+ it do
+ expect(client.fetch_number_value(flag_key: flag_key, default_value: 95.5)).is_a?(Float)
+ end
+ end
+ end
+
+ context "object value" do
+ it do
+ expect(client).to respond_to(:fetch_object_value).with(4).arguments
+ end
+
+ it do
+ expect(client.fetch_object_value(flag_key: flag_key,
+ default_value: JSON.dump({ data: "some-data" }))).is_a?(String)
+ end
+ end
+ end
+ end
+
+ context "Requirement 1.4.1" do
+ context "MUST provide methods for detailed flag value evaluation with parameters flag key (string, required), default value (boolean | number | string | structure, required), evaluation context (optional), and evaluation options (optional), which returns an evaluation details structure." do
+ subject(:client) do
+ OpenFeature.build_client(name: "client")
+ end
+ let(:flag_key) { "my-awesome-feature-flag-key" }
+
+ context "boolean value" do
+ it do
+ expect(client).to respond_to(:fetch_boolean_details).with(4).arguments
+ end
+
+ it do
+ expect(client.fetch_boolean_details(flag_key: flag_key, default_value: false)).is_a?(ResolutionDetails)
+ end
+
+ context "Requirement 1.4.2" do
+ it "The evaluation details structure's value field MUST contain the evaluated flag value" do
+ expect(client.fetch_boolean_details(flag_key: flag_key, default_value: true).value).is_a?(TrueClass)
+ expect(client.fetch_boolean_details(flag_key: flag_key, default_value: false).value).is_a?(FalseClass)
+ end
+ end
+
+ context "Requirement 1.4.4" do
+ it "The evaluation details structure's flag key field MUST contain the flag key argument passed to the detailed flag evaluation method." do
+ expect(client).to respond_to(:fetch_boolean_details).with(4).arguments.and_keywords(:flag_key,
+ :default_value, :evaluation_context, :evaluation_options)
+ end
+ end
+ end
+
+ context "number value" do
+ it do
+ expect(client).to respond_to(:fetch_number_details).with(4).arguments.and_keywords(:flag_key,
+ :default_value, :evaluation_context, :evaluation_options)
+ end
+
+ it do
+ expect(client.fetch_number_details(flag_key: flag_key, default_value: 1.2)).is_a?(ResolutionDetails)
+ expect(client.fetch_number_details(flag_key: flag_key, default_value: 1)).is_a?(ResolutionDetails)
+ end
+
+ context "Requirement 1.4.2" do
+ it "The evaluation details structure's value field MUST contain the evaluated flag value" do
+ expect(client.fetch_number_details(flag_key: flag_key, default_value: 1.0).value).is_a?(Float)
+ expect(client.fetch_number_details(flag_key: flag_key, default_value: 1).value).is_a?(Integer)
+ end
+ end
+
+ context "Requirement 1.4.4" do
+ it "The evaluation details structure's flag key field MUST contain the flag key argument passed to the detailed flag evaluation method." do
+ expect(client).to respond_to(:fetch_number_details).with(4).arguments.and_keywords(:flag_key,
+ :default_value, :evaluation_context, :evaluation_options)
+ end
+ end
+ end
+
+ context "string value" do
+ it do
+ expect(client).to respond_to(:fetch_string_details).with(4).arguments.and_keywords(:flag_key,
+ :default_value, :evaluation_context, :evaluation_options)
+ end
+
+ it do
+ expect(client.fetch_string_details(flag_key: flag_key, default_value: "some-string")).is_a?(ResolutionDetails)
+ end
+
+ context "Requirement 1.4.2" do
+ it "The evaluation details structure's value field MUST contain the evaluated flag value" do
+ expect(client.fetch_string_details(flag_key: flag_key, default_value: "some-string").value).is_a?(String)
+ end
+ end
+
+ context "Requirement 1.4.4" do
+ it "The evaluation details structure's flag key field MUST contain the flag key argument passed to the detailed flag evaluation method." do
+ expect(client).to respond_to(:fetch_string_details).with(4).arguments.and_keywords(:flag_key,
+ :default_value, :evaluation_context, :evaluation_options)
+ end
+ end
+ end
+
+ context "object value" do
+ it do
+ expect(client).to respond_to(:fetch_object_details).with(4).arguments.and_keywords(:flag_key,
+ :default_value, :evaluation_context, :evaluation_options)
+ end
+
+ it do
+ expect(client.fetch_object_details(flag_key: flag_key,
+ default_value: JSON.dump({ name: "some-name" }))).is_a?(ResolutionDetails)
+ end
+
+ context "Requirement 1.4.2" do
+ it "The evaluation details structure's value field MUST contain the evaluated flag value" do
+ expect(client.fetch_object_details(flag_key: flag_key,
+ default_value: JSON.dump({ name: "some-name" })).value).is_a?(String)
+ end
+ end
+
+ context "Requirement 1.4.4" do
+ it "The evaluation details structure's flag key field MUST contain the flag key argument passed to the detailed flag evaluation method." do
+ expect(client).to respond_to(:fetch_object_details).with(4).arguments.and_keywords(:flag_key,
+ :default_value, :evaluation_context, :evaluation_options)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/openfeature/provider/no_op_provider_spec.rb b/spec/openfeature/provider/no_op_provider_spec.rb
new file mode 100644
index 00000000..ed2ff637
--- /dev/null
+++ b/spec/openfeature/provider/no_op_provider_spec.rb
@@ -0,0 +1,131 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_relative "../../../lib/openfeature/sdk/provider/no_op_provider"
+
+
+describe OpenFeature::SDK::Provider::NoOpProvider do
+ subject(:provider) { described_class.new }
+ let(:flag_key) { 'some-feature-flag-key' }
+
+ context 'Requirement 2.1.1' do
+ it 'MUST define a metadata member or accessor, containing a name field or accessor of type string, which identifies the provider implementation.' do
+ expect(provider).to respond_to(:metadata)
+ expect(provider.metadata).to respond_to(:name)
+ expect(provider.metadata.name).to eq(described_class::NAME)
+ end
+ end
+
+ context 'Requirement 2.2.1' do
+ context 'MUST define methods to resolve flag values, with parameters flag key (string, required), default value (boolean | number | string | structure, required) and evaluation context (optional), which returns a flag resolution structure.' do
+ context 'boolean value' do
+ it do
+ expect(provider).to respond_to(:fetch_boolean_value).with(3).arguments.and_keywords(:flag_key,
+ :default_value, :evaluation_context)
+ end
+
+ it do
+ expect(provider.fetch_boolean_value(flag_key:, default_value: false)).is_a?(ResolutionDetails)
+ end
+ end
+
+ context 'number value' do
+ it do
+ expect(provider).to respond_to(:fetch_number_value).with(3).arguments.and_keywords(:flag_key,
+ :default_value, :evaluation_context)
+ end
+
+ it do
+ expect(provider.fetch_number_value(flag_key:, default_value: 1.0)).is_a?(ResolutionDetails)
+ expect(provider.fetch_number_value(flag_key:, default_value: 1)).is_a?(ResolutionDetails)
+ end
+ end
+
+ context 'string value' do
+ it do
+ expect(provider).to respond_to(:fetch_string_value).with(3).arguments.and_keywords(:flag_key,
+ :default_value, :evaluation_context)
+ end
+
+ it do
+ expect(provider.fetch_string_value(flag_key:, default_value: 'some-string-value')).is_a?(ResolutionDetails)
+ end
+ end
+
+ context 'boolean value' do
+ it do
+ expect(provider).to respond_to(:fetch_object_value).with(3).arguments.and_keywords(:flag_key,
+ :default_value, :evaluation_context)
+ end
+
+ it do
+ expect(provider.fetch_object_value(flag_key:,
+ default_value: JSON.dump({ example: 'some-cool-object-value' }))).is_a?(ResolutionDetails)
+ end
+ end
+ end
+ end
+
+ context 'Requirement 2.2.3' do
+ context "SHOULD populate the flag resolution structure's variant field with a string identifier corresponding to the returned flag value" do
+ context 'boolean value' do
+ it do
+ expect(provider.fetch_boolean_value(flag_key:, default_value: false).value).is_a?(FalseClass)
+ expect(provider.fetch_boolean_value(flag_key:, default_value: false).value).is_a?(TrueClass)
+ end
+ end
+
+ context 'number value' do
+ it do
+ expect(provider.fetch_number_value(flag_key:, default_value: 1.0).value).is_a?(Float)
+ expect(provider.fetch_number_value(flag_key:, default_value: 1).value).is_a?(Integer)
+ end
+ end
+
+ context 'string value' do
+ it do
+ expect(provider.fetch_string_value(flag_key:, default_value: 'some-string-value').value).is_a?(String)
+ end
+ end
+
+ context 'boolean value' do
+ it do
+ expect(provider.fetch_object_value(flag_key:,
+ default_value: JSON.dump({ example: 'some-cool-object-value' }))).is_a?(String)
+ end
+ end
+ end
+ end
+
+ context 'Requirement 2.2.4' do
+ context "MUST populate the flag resolution structure's value field with the resolved flag value." do
+ context 'boolean value' do
+ it do
+ expect(provider.fetch_boolean_value(flag_key:, default_value: false).value).is_a?(FalseClass)
+ expect(provider.fetch_boolean_value(flag_key:, default_value: false).value).is_a?(TrueClass)
+ end
+ end
+
+ context 'number value' do
+ it do
+ expect(provider.fetch_number_value(flag_key:, default_value: 1.0).value).is_a?(Float)
+ expect(provider.fetch_number_value(flag_key:, default_value: 1).value).is_a?(Integer)
+ end
+ end
+
+ context 'string value' do
+ it do
+ expect(provider.fetch_string_value(flag_key:, default_value: 'some-string-value').value).is_a?(String)
+ end
+ end
+
+ context 'boolean value' do
+ it do
+ expect(provider.fetch_object_value(flag_key:,
+ default_value: JSON.dump({ example: 'some-cool-object-value' }))).is_a?(String)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/openfeature/sdk_spec.rb b/spec/openfeature/sdk_spec.rb
index 96829020..5d3b4b43 100644
--- a/spec/openfeature/sdk_spec.rb
+++ b/spec/openfeature/sdk_spec.rb
@@ -1,7 +1,85 @@
# frozen_string_literal: true
+require_relative "../spec_helper"
+
+require_relative "../../lib/openfeature/sdk/provider/no_op_provider"
+require_relative "../../lib/openfeature/sdk/configuration"
+require_relative "../../lib/openfeature/sdk"
+require_relative "../../lib/openfeature/sdk/metadata"
+
RSpec.describe OpenFeature::SDK do
- it "has a version number" do
- expect(OpenFeature::SDK::VERSION).not_to be nil
+ before do
+ subject
+ end
+
+ context "Requirement 1.1.2" do
+ subject do
+ OpenFeature::SDK.configure do |config|
+ config.provider = OpenFeature::SDK::Provider::NoOpProvider.new
+ end
+ end
+
+ it "must provide a function to set the global provider singleton, which accepts an API-conformant provider implementation" do
+ expect(OpenFeature::SDK).to respond_to(:provider)
+ expect(OpenFeature::SDK.provider).not_to be_nil
+ expect(OpenFeature::SDK.provider).is_a?(OpenFeature::SDK::Provider)
+ end
+ end
+
+ context "Requirement 1.1.3" do
+ subject do
+ OpenFeature::SDK.configure do |config|
+ config.provider = OpenFeature::SDK::Provider::NoOpProvider.new
+ config.hooks << hook_1
+ config.hooks << hook_2
+ end
+ end
+
+ let(:hook1) do
+ Class.new do
+ include Hook
+ end
+ end
+ let(:hook2) do
+ Class.new do
+ include Hook
+ end
+ end
+
+ it "must provide a function that adds hooks which accepts one or more API-conformant `hooks`, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed." do
+ expect(OpenFeature::SDK).to respond_to(:hooks)
+ expect(OpenFeature::SDK.hooks).to have_attributes(size: 2).and eq([hook1, hook2])
+ end
+ end
+
+ context "Requirement 1.1.4" do
+ subject do
+ OpenFeature::SDK.configure do |config|
+ config.provider = OpenFeature::SDK::Provider::NoOpProvider.new
+ end
+ end
+
+ it "must provide a function for retrieving the metadata field of the configured provider" do
+ expect(OpenFeature::SDK.provider.metadata).not_to be_nil
+ expect(OpenFeature::SDK.provider).to respond_to(:metadata)
+ expect(OpenFeature::SDK.provider.metadata).is_a?(OpenFeature::SDK::Metadata)
+
+ expect(OpenFeature::SDK.provider.metadata).to eq(OpenFeature::SDK::Metadata.new(name: OpenFeature::SDK::Provider::NoOpProvider::NAME))
+ end
+ end
+
+ context "Requirement 1.1.5" do
+ subject do
+ OpenFeature::SDK.configure do |config|
+ config.provider = OpenFeature::SDK::Provider::NoOpProvider.new
+ end
+
+ OpenFeature::SDK.build_client(name: "requirement-1.1.5")
+ end
+
+ it "provide a function for creating a client which accepts the following options: * name (optional): A logical string identifier for the client." do
+ expect(OpenFeature::SDK).to respond_to(:build_client).with(1).arguments
+ expect(subject).is_a?(OpenFeature::SDK::Client)
+ end
end
end