diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 7a1ade30..7c0eb658 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -26,54 +26,9 @@ jobs:
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
- bundler-cache: true
- run: bundle install
- run: bundle exec rake
-
- rubocop:
- runs-on: ${{ matrix.os }}
- name: Rubocop ${{ matrix.ruby }}
- strategy:
- fail-fast: false
- matrix:
- os: [ubuntu-latest, macos-latest, windows-latest]
- ruby:
- - "3.1.2"
- - "2.7.6"
- - "3.0.4"
- env:
- BUNDLE_GEMFILE: Gemfile
-
- steps:
- - uses: actions/checkout@v3
- - name: Set up Ruby ${{ matrix.ruby }}
- uses: ruby/setup-ruby@v1
- with:
- ruby-version: ${{ matrix.ruby }}
- bundler-cache: true
- name: Run style checks
run: bundle exec rubocop
-
- test:
- runs-on: ${{ matrix.os }}
- name: RSpec ${{ matrix.ruby }}
- strategy:
- fail-fast: false
- matrix:
- os: [ubuntu-latest, macos-latest, windows-latest]
- ruby:
- - "3.1.2"
- - "2.7.6"
- - "3.0.4"
- env:
- BUNDLE_GEMFILE: Gemfile
-
- steps:
- - uses: actions/checkout@v3
- - name: Set up Ruby ${{ matrix.ruby }}
- uses: ruby/setup-ruby@v1
- with:
- ruby-version: ${{ matrix.ruby }}
- bundler-cache: true
- name: Run tests
run: bundle exec rspec
diff --git a/.rspec b/.rspec
index 34c5164d..44b132be 100644
--- a/.rspec
+++ b/.rspec
@@ -1,3 +1,4 @@
+-I lib
--format documentation
--color
--require spec_helper
diff --git a/.rubocop.yml b/.rubocop.yml
index af98bb01..236b1161 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -12,6 +12,13 @@ Style/StringLiteralsInInterpolation:
Layout/LineLength:
Max: 120
+ Exclude:
+ - 'spec/**/*.rb'
+
+Metrics/BlockLength:
+ Exclude:
+ - 'spec/**/*.rb'
+ - 'openfeature-sdk.gemspec'
Gemspec/RequireMFA:
Enabled: false
diff --git a/Gemfile b/Gemfile
index 3b2e7f2c..76a864be 100644
--- a/Gemfile
+++ b/Gemfile
@@ -4,3 +4,5 @@ source "https://rubygems.org"
# Specify your gem's dependencies in openfeature-sdk.gemspec
gemspec
+
+gem "concurrent-ruby", require: "concurrent"
diff --git a/Gemfile.lock b/Gemfile.lock
index c06b2b31..3c35b44a 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,12 +1,13 @@
PATH
remote: .
specs:
- openfeature-sdk (0.0.2)
+ openfeature-sdk (0.0.3)
GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
+ concurrent-ruby (1.1.10)
diff-lcs (1.5.0)
json (2.6.2)
parallel (1.22.1)
@@ -55,6 +56,7 @@ PLATFORMS
x86_64-linux
DEPENDENCIES
+ concurrent-ruby
openfeature-sdk!
rake (~> 13.0)
rspec (~> 3.12.0)
diff --git a/lib/openfeature/sdk.rb b/lib/openfeature/sdk.rb
index 46085e2f..bfd3550c 100644
--- a/lib/openfeature/sdk.rb
+++ b/lib/openfeature/sdk.rb
@@ -1,10 +1,11 @@
# frozen_string_literal: true
require_relative "sdk/version"
+require_relative "sdk/api"
module OpenFeature
+ # TODO: Add documentation
+ #
module SDK
- class Error < StandardError; end
- # Your code goes here...
end
end
diff --git a/lib/openfeature/sdk/api.rb b/lib/openfeature/sdk/api.rb
new file mode 100644
index 00000000..0db9f1f5
--- /dev/null
+++ b/lib/openfeature/sdk/api.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require "forwardable"
+require "singleton"
+
+require_relative "configuration"
+require_relative "client"
+require_relative "metadata"
+require_relative "provider/no_op_provider"
+
+module OpenFeature
+ module SDK
+ # API Initialization and Configuration
+ #
+ # Represents the entry point to the API, including configuration of Provider,Hook,
+ # and building the Client
+ #
+ # To use the SDK, you can optionally configure a Provider, with Hook
+ #
+ # OpenFeature::SDK::API.instance.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::API.instance.build_client(name: 'my-open-feature-client')
+ class API
+ include Singleton
+ extend Forwardable
+
+ def_delegator :@configuration, :provider
+ def_delegator :@configuration, :hooks
+ def_delegator :@configuration, :context
+
+ def configuration
+ @configuration ||= Configuration.new
+ end
+
+ def configure(&block)
+ return unless block_given?
+
+ block.call(configuration)
+ end
+
+ def build_client(name: nil, version: nil)
+ client_options = Metadata.new(name: name, version: version).freeze
+ provider = Provider::NoOpProvider.new if provider.nil?
+ 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..7dfe001c
--- /dev/null
+++ b/lib/openfeature/sdk/client.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module OpenFeature
+ module SDK
+ # TODO: Write documentation
+ #
+ class Client
+ attr_reader :metadata
+
+ attr_accessor :hooks
+
+ def initialize(provider, client_options, context)
+ @provider = provider
+ @client_options = client_options
+ @context = context
+ end
+ end
+ end
+end
diff --git a/lib/openfeature/sdk/configuration.rb b/lib/openfeature/sdk/configuration.rb
new file mode 100644
index 00000000..06ce7d94
--- /dev/null
+++ b/lib/openfeature/sdk/configuration.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require "concurrent"
+
+require_relative "api"
+
+module OpenFeature
+ module SDK
+ # Represents the configuration object for the global API where Provider, Hook,
+ # and Context are configured.
+ # This class is not meant to be interacted with directly but instead through the OpenFeature::SDK.configure
+ # method
+ class Configuration
+ extend Forwardable
+
+ attr_accessor :context, :provider, :hooks
+
+ def_delegator :@provider, :metadata
+
+ def initialize
+ @hooks = Concurrent::Array.new([])
+ end
+ end
+ end
+end
diff --git a/lib/openfeature/sdk/metadata.rb b/lib/openfeature/sdk/metadata.rb
new file mode 100644
index 00000000..411ae1d5
--- /dev/null
+++ b/lib/openfeature/sdk/metadata.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module OpenFeature
+ module SDK
+ # Metadata structure that defines general metadata relating to a Provider or Client
+ #
+ # Within the Metadata structure, the following attribute readers are available:
+ #
+ # * name - Defines the name of the structure
+ #
+ # * version - Allows you to specify version of the Metadata structure
+ #
+ # Usage:
+ #
+ # metadata = Metadata.new(name: 'name-for-metadata', version: 'v1.1.3')
+ # metadata.name # 'name-for-metadata'
+ # metadata.version # version
+ # metadata_two = Metadata.new(name: 'name-for-metadata')
+ # metadata_two == metadata # true - equality based on values
+ class Metadata
+ attr_reader :name, :version
+
+ def initialize(name:, version: nil)
+ @name = name
+ @version = version
+ end
+
+ def ==(other)
+ raise ArgumentError("Expected comparison to be between Metadata object") unless other.is_a?(Metadata)
+
+ @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..781c3c18
--- /dev/null
+++ b/lib/openfeature/sdk/provider/no_op_provider.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require_relative "../metadata"
+
+# rubocop:disable Lint/UnusedMethodArgument
+module OpenFeature
+ module SDK
+ module Provider
+ # Defines the default provider that is set if no provider is specified.
+ #
+ # To use NoOpProvider, it can be set during the configuration of the SDK
+ #
+ # OpenFeature::SDK.configure do |config|
+ # config.provider = NoOpProvider.new
+ # end
+ #
+ # Within the NoOpProvider, the following methods exist
+ #
+ # * fetch_boolean_value - Retrieve feature flag boolean value
+ #
+ # * fetch_string_value - Retrieve feature flag string value
+ #
+ # * fetch_number_value - Retrieve feature flag number value
+ #
+ # * fetch_object_value - Retrieve feature flag object value
+ #
+ class NoOpProvider
+ REASON_NO_OP = "No-op"
+ NAME = "No-op Provider"
+
+ attr_reader :metadata
+
+ def initialize
+ @metadata = Metadata.new(name: NAME).freeze
+ end
+
+ def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
+ no_op(default_value)
+ end
+
+ def fetch_string_value(flag_key:, default_value:, evaluation_context: nil)
+ no_op(default_value)
+ end
+
+ def fetch_number_value(flag_key:, default_value:, evaluation_context: nil)
+ no_op(default_value)
+ end
+
+ def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
+ no_op(default_value)
+ end
+
+ private
+
+ def no_op(default_value)
+ Struct.new("ResolutionDetails", :value, :reason)
+ end
+ end
+ end
+ end
+end
+# rubocop:enable Lint/UnusedMethodArgument
diff --git a/spec/openfeature/sdk/api_spec.rb b/spec/openfeature/sdk/api_spec.rb
new file mode 100644
index 00000000..5491e807
--- /dev/null
+++ b/spec/openfeature/sdk/api_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require_relative "../../spec_helper"
+
+require "openfeature/sdk/configuration"
+require "openfeature/sdk/api"
+require "openfeature/sdk/metadata"
+
+# https://docs.openfeature.dev/docs/specification/sections/flag-evaluation#11-api-initialization-and-configuration
+
+RSpec.describe OpenFeature::SDK::API do
+ subject(:api) { described_class.instance }
+
+ context "with Requirement 1.1.2" do
+ before do
+ api.configure do |config|
+ config.provider = OpenFeature::SDK::Provider::NoOpProvider.new
+ end
+ end
+
+ it do
+ expect(api).to respond_to(:provider)
+ end
+
+ it do
+ expect(api.provider).not_to be_nil
+ end
+
+ it do
+ expect(api.provider).is_a?(OpenFeature::SDK::Provider)
+ end
+ end
+
+ context "with Requirement 1.1.3" do
+ before do
+ api.configure do |config|
+ config.provider = OpenFeature::SDK::Provider::NoOpProvider.new
+ config.hooks << hook1
+ config.hooks << hook2
+ end
+ end
+
+ let(:hook1) { "my_hook" }
+ let(:hook2) { "my_other_hook" }
+
+ it do
+ expect(api).to respond_to(:hooks)
+ expect(api.hooks).to have_attributes(size: 2).and eq([hook1, hook2])
+ end
+ end
+
+ context "with Requirement 1.1.4" do
+ before do
+ api.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(api.provider.metadata).not_to be_nil
+ end
+
+ it do
+ expect(api.provider).to respond_to(:metadata)
+ end
+
+ it do
+ expect(api.provider.metadata).is_a?(OpenFeature::SDK::Metadata)
+ end
+
+ it do
+ expect(api.provider.metadata).to eq(OpenFeature::SDK::Metadata.new(name: OpenFeature::SDK::Provider::NoOpProvider::NAME))
+ end
+ end
+
+ context "with Requirement 1.1.5" do
+ before do
+ api.configure do |config|
+ config.provider = OpenFeature::SDK::Provider::NoOpProvider.new
+ end
+
+ api.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(api).to respond_to(:build_client).with_keywords(:name, :version)
+ end
+
+ it do
+ expect(api).is_a?(OpenFeature::SDK::Client)
+ end
+ end
+end
diff --git a/spec/openfeature/sdk_spec.rb b/spec/openfeature/sdk_spec.rb
index 96829020..1474a9f2 100644
--- a/spec/openfeature/sdk_spec.rb
+++ b/spec/openfeature/sdk_spec.rb
@@ -2,6 +2,6 @@
RSpec.describe OpenFeature::SDK do
it "has a version number" do
- expect(OpenFeature::SDK::VERSION).not_to be nil
+ expect(OpenFeature::SDK::VERSION).not_to be_nil
end
end