Skip to content

Commit

Permalink
feat: Add client object (#34)
Browse files Browse the repository at this point in the history
Signed-off-by: Jose Colella <jose.colella@gusto.com>
  • Loading branch information
josecolella authored Dec 12, 2022
1 parent d6b0922 commit 92f8d0d
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 4 deletions.
4 changes: 4 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@ Metrics/BlockLength:

Gemspec/RequireMFA:
Enabled: false

Style/DocumentDynamicEvalDefinition:
# TODO re-enable after figuring out what it actually wants
Enabled: false
2 changes: 1 addition & 1 deletion lib/openfeature/sdk/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def configure(&block)
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)
Client.new(provider: provider, client_options: client_options, context: context)
end
end
end
Expand Down
22 changes: 20 additions & 2 deletions lib/openfeature/sdk/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,32 @@ module SDK
# TODO: Write documentation
#
class Client
RESULT_TYPE = %i[boolean string number object].freeze
SUFFIXES = %i[value details].freeze

attr_reader :metadata

attr_accessor :hooks

def initialize(provider, client_options, context)
def initialize(provider:, client_options: nil, context: nil)
@provider = provider
@client_options = client_options
@metadata = client_options
@context = context
@hooks = []
end

RESULT_TYPE.each do |result_type|
SUFFIXES.each do |suffix|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
# def fetch_boolean_details(flag_key:, default_value:, evaluation_context: nil)
# result = @provider.fetch_boolean_value(flag_key: flag_key, default_value: default_value, evaluation_context: evaluation_context)
# end
def fetch_#{result_type}_#{suffix}(flag_key:, default_value:, evaluation_context: nil)
result = @provider.fetch_#{result_type}_value(flag_key: flag_key, default_value: default_value, evaluation_context: evaluation_context)
#{"result.value" if suffix == :value}
end
RUBY
end
end
end
end
Expand Down
4 changes: 3 additions & 1 deletion lib/openfeature/sdk/provider/no_op_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ class NoOpProvider

attr_reader :metadata

ResolutionDetails = Struct.new(:value, :reason, :variant, :error_code, :error_message)

def initialize
@metadata = Metadata.new(name: NAME).freeze
end
Expand All @@ -53,7 +55,7 @@ def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
private

def no_op(default_value)
Struct.new("ResolutionDetails", :value, :reason)
ResolutionDetails.new(value: default_value, reason: REASON_NO_OP)
end
end
end
Expand Down
195 changes: 195 additions & 0 deletions spec/openfeature/sdk/client_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# frozen_string_literal: true

require_relative "../../spec_helper"

require "openfeature/sdk/client"
require "openfeature/sdk/metadata"
require "openfeature/sdk/provider/no_op_provider"

# https://docs.openfeature.dev/docs/specification/sections/flag-evaluation#12-client-usage

RSpec.describe OpenFeature::SDK::Client do
subject(:client) { described_class.new(provider: provider, client_options: client_metadata) }
let(:provider) { OpenFeature::SDK::Provider::NoOpProvider.new }
let(:client_metadata) { OpenFeature::SDK::Metadata.new(name: name) }
let(:name) { "my-openfeature-client" }

context "Requirement 1.2.1" do
before do
client.hooks << client_hook
end

let(:client_hook) { "some_hook" }

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(client).to respond_to(:hooks)
expect(client.hooks).to have_attributes(size: 1).and eq([client_hook])
end
end

context "Requirement 1.2.2" do
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(client).to respond_to(:metadata)
expect(client.metadata).to respond_to(:name)
expect(client.metadata.name).to eq(name)
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
let(:flag_key) { "my-awesome-feature-flag-key" }

context "boolean value" do
it do
expect(client).to respond_to(:fetch_boolean_value)
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)
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)
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)
end

it do
expect(client.fetch_object_value(flag_key: flag_key,
default_value: { data: "some-data" })).is_a?(Hash)
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
let(:flag_key) { "my-awesome-feature-flag-key" }

context "boolean value" do
it do
expect(client).to respond_to(:fetch_boolean_details)
end

it do
expect(client.fetch_boolean_details(flag_key: flag_key, default_value: false)).is_a?(OpenFeature::SDK::Provider::NoOpProvider::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)
end
end
end

context "number value" do
it do
expect(client).to respond_to(:fetch_number_details)
end

it do
expect(client.fetch_number_details(flag_key: flag_key, default_value: 1.2)).is_a?(OpenFeature::SDK::Provider::NoOpProvider::ResolutionDetails)
expect(client.fetch_number_details(flag_key: flag_key, default_value: 1)).is_a?(OpenFeature::SDK::Provider::NoOpProvider::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)
end
end
end

context "string value" do
it do
expect(client).to respond_to(:fetch_string_details)
end

it do
expect(client.fetch_string_details(flag_key: flag_key, default_value: "some-string")).is_a?(OpenFeature::SDK::Provider::NoOpProvider::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)
end
end
end

context "object value" do
it do
expect(client).to respond_to(:fetch_object_details)
end

it do
expect(client.fetch_object_details(flag_key: flag_key,
default_value: { name: "some-name" })).is_a?(OpenFeature::SDK::Provider::NoOpProvider::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: { 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)
end
end
end
end
end
end
end

0 comments on commit 92f8d0d

Please sign in to comment.