Skip to content

Commit 92f8d0d

Browse files
authored
feat: Add client object (#34)
Signed-off-by: Jose Colella <[email protected]>
1 parent d6b0922 commit 92f8d0d

File tree

5 files changed

+223
-4
lines changed

5 files changed

+223
-4
lines changed

.rubocop.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,7 @@ Metrics/BlockLength:
2222

2323
Gemspec/RequireMFA:
2424
Enabled: false
25+
26+
Style/DocumentDynamicEvalDefinition:
27+
# TODO re-enable after figuring out what it actually wants
28+
Enabled: false

lib/openfeature/sdk/api.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def configure(&block)
4646
def build_client(name: nil, version: nil)
4747
client_options = Metadata.new(name: name, version: version).freeze
4848
provider = Provider::NoOpProvider.new if provider.nil?
49-
Client.new(provider, client_options, context)
49+
Client.new(provider: provider, client_options: client_options, context: context)
5050
end
5151
end
5252
end

lib/openfeature/sdk/client.rb

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,32 @@ module SDK
55
# TODO: Write documentation
66
#
77
class Client
8+
RESULT_TYPE = %i[boolean string number object].freeze
9+
SUFFIXES = %i[value details].freeze
10+
811
attr_reader :metadata
912

1013
attr_accessor :hooks
1114

12-
def initialize(provider, client_options, context)
15+
def initialize(provider:, client_options: nil, context: nil)
1316
@provider = provider
14-
@client_options = client_options
17+
@metadata = client_options
1518
@context = context
19+
@hooks = []
20+
end
21+
22+
RESULT_TYPE.each do |result_type|
23+
SUFFIXES.each do |suffix|
24+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
25+
# def fetch_boolean_details(flag_key:, default_value:, evaluation_context: nil)
26+
# result = @provider.fetch_boolean_value(flag_key: flag_key, default_value: default_value, evaluation_context: evaluation_context)
27+
# end
28+
def fetch_#{result_type}_#{suffix}(flag_key:, default_value:, evaluation_context: nil)
29+
result = @provider.fetch_#{result_type}_value(flag_key: flag_key, default_value: default_value, evaluation_context: evaluation_context)
30+
#{"result.value" if suffix == :value}
31+
end
32+
RUBY
33+
end
1634
end
1735
end
1836
end

lib/openfeature/sdk/provider/no_op_provider.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ class NoOpProvider
3030

3131
attr_reader :metadata
3232

33+
ResolutionDetails = Struct.new(:value, :reason, :variant, :error_code, :error_message)
34+
3335
def initialize
3436
@metadata = Metadata.new(name: NAME).freeze
3537
end
@@ -53,7 +55,7 @@ def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
5355
private
5456

5557
def no_op(default_value)
56-
Struct.new("ResolutionDetails", :value, :reason)
58+
ResolutionDetails.new(value: default_value, reason: REASON_NO_OP)
5759
end
5860
end
5961
end
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "../../spec_helper"
4+
5+
require "openfeature/sdk/client"
6+
require "openfeature/sdk/metadata"
7+
require "openfeature/sdk/provider/no_op_provider"
8+
9+
# https://docs.openfeature.dev/docs/specification/sections/flag-evaluation#12-client-usage
10+
11+
RSpec.describe OpenFeature::SDK::Client do
12+
subject(:client) { described_class.new(provider: provider, client_options: client_metadata) }
13+
let(:provider) { OpenFeature::SDK::Provider::NoOpProvider.new }
14+
let(:client_metadata) { OpenFeature::SDK::Metadata.new(name: name) }
15+
let(:name) { "my-openfeature-client" }
16+
17+
context "Requirement 1.2.1" do
18+
before do
19+
client.hooks << client_hook
20+
end
21+
22+
let(:client_hook) { "some_hook" }
23+
24+
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
25+
expect(client).to respond_to(:hooks)
26+
expect(client.hooks).to have_attributes(size: 1).and eq([client_hook])
27+
end
28+
end
29+
30+
context "Requirement 1.2.2" do
31+
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
32+
expect(client).to respond_to(:metadata)
33+
expect(client.metadata).to respond_to(:name)
34+
expect(client.metadata.name).to eq(name)
35+
end
36+
end
37+
38+
context "Flag evaluation" do
39+
context "Requirement 1.3.1" do
40+
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
41+
let(:flag_key) { "my-awesome-feature-flag-key" }
42+
43+
context "boolean value" do
44+
it do
45+
expect(client).to respond_to(:fetch_boolean_value)
46+
end
47+
48+
it do
49+
expect(client.fetch_boolean_value(flag_key: flag_key, default_value: false)).is_a?(FalseClass)
50+
end
51+
52+
it do
53+
expect(client.fetch_boolean_value(flag_key: flag_key, default_value: true)).is_a?(TrueClass)
54+
end
55+
end
56+
57+
context "string value" do
58+
it do
59+
expect(client).to respond_to(:fetch_string_value)
60+
end
61+
62+
it do
63+
expect(client.fetch_string_value(flag_key: flag_key, default_value: "default_value")).is_a?(String)
64+
end
65+
end
66+
67+
context "number value" do
68+
it do
69+
expect(client).to respond_to(:fetch_number_value)
70+
end
71+
72+
context "Condition 1.3.2 - The implementation language differentiates between floating-point numbers and integers." do
73+
it do
74+
expect(client.fetch_number_value(flag_key: flag_key, default_value: 4)).is_a?(Integer)
75+
end
76+
77+
it do
78+
expect(client.fetch_number_value(flag_key: flag_key, default_value: 95.5)).is_a?(Float)
79+
end
80+
end
81+
end
82+
83+
context "object value" do
84+
it do
85+
expect(client).to respond_to(:fetch_object_value)
86+
end
87+
88+
it do
89+
expect(client.fetch_object_value(flag_key: flag_key,
90+
default_value: { data: "some-data" })).is_a?(Hash)
91+
end
92+
end
93+
end
94+
end
95+
96+
context "Requirement 1.4.1" do
97+
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
98+
let(:flag_key) { "my-awesome-feature-flag-key" }
99+
100+
context "boolean value" do
101+
it do
102+
expect(client).to respond_to(:fetch_boolean_details)
103+
end
104+
105+
it do
106+
expect(client.fetch_boolean_details(flag_key: flag_key, default_value: false)).is_a?(OpenFeature::SDK::Provider::NoOpProvider::ResolutionDetails)
107+
end
108+
109+
context "Requirement 1.4.2" do
110+
it "The evaluation details structure's value field MUST contain the evaluated flag value" do
111+
expect(client.fetch_boolean_details(flag_key: flag_key, default_value: true).value).is_a?(TrueClass)
112+
expect(client.fetch_boolean_details(flag_key: flag_key, default_value: false).value).is_a?(FalseClass)
113+
end
114+
end
115+
116+
context "Requirement 1.4.4" do
117+
it "The evaluation details structure's flag key field MUST contain the flag key argument passed to the detailed flag evaluation method." do
118+
expect(client).to respond_to(:fetch_boolean_details)
119+
end
120+
end
121+
end
122+
123+
context "number value" do
124+
it do
125+
expect(client).to respond_to(:fetch_number_details)
126+
end
127+
128+
it do
129+
expect(client.fetch_number_details(flag_key: flag_key, default_value: 1.2)).is_a?(OpenFeature::SDK::Provider::NoOpProvider::ResolutionDetails)
130+
expect(client.fetch_number_details(flag_key: flag_key, default_value: 1)).is_a?(OpenFeature::SDK::Provider::NoOpProvider::ResolutionDetails)
131+
end
132+
133+
context "Requirement 1.4.2" do
134+
it "The evaluation details structure's value field MUST contain the evaluated flag value" do
135+
expect(client.fetch_number_details(flag_key: flag_key, default_value: 1.0).value).is_a?(Float)
136+
expect(client.fetch_number_details(flag_key: flag_key, default_value: 1).value).is_a?(Integer)
137+
end
138+
end
139+
140+
context "Requirement 1.4.4" do
141+
it "The evaluation details structure's flag key field MUST contain the flag key argument passed to the detailed flag evaluation method." do
142+
expect(client).to respond_to(:fetch_number_details)
143+
end
144+
end
145+
end
146+
147+
context "string value" do
148+
it do
149+
expect(client).to respond_to(:fetch_string_details)
150+
end
151+
152+
it do
153+
expect(client.fetch_string_details(flag_key: flag_key, default_value: "some-string")).is_a?(OpenFeature::SDK::Provider::NoOpProvider::ResolutionDetails)
154+
end
155+
156+
context "Requirement 1.4.2" do
157+
it "The evaluation details structure's value field MUST contain the evaluated flag value" do
158+
expect(client.fetch_string_details(flag_key: flag_key, default_value: "some-string").value).is_a?(String)
159+
end
160+
end
161+
162+
context "Requirement 1.4.4" do
163+
it "The evaluation details structure's flag key field MUST contain the flag key argument passed to the detailed flag evaluation method." do
164+
expect(client).to respond_to(:fetch_string_details)
165+
end
166+
end
167+
end
168+
169+
context "object value" do
170+
it do
171+
expect(client).to respond_to(:fetch_object_details)
172+
end
173+
174+
it do
175+
expect(client.fetch_object_details(flag_key: flag_key,
176+
default_value: { name: "some-name" })).is_a?(OpenFeature::SDK::Provider::NoOpProvider::ResolutionDetails)
177+
end
178+
179+
context "Requirement 1.4.2" do
180+
it "The evaluation details structure's value field MUST contain the evaluated flag value" do
181+
expect(client.fetch_object_details(flag_key: flag_key,
182+
default_value: { name: "some-name" }).value).is_a?(String)
183+
end
184+
end
185+
186+
context "Requirement 1.4.4" do
187+
it "The evaluation details structure's flag key field MUST contain the flag key argument passed to the detailed flag evaluation method." do
188+
expect(client).to respond_to(:fetch_object_details)
189+
end
190+
end
191+
end
192+
end
193+
end
194+
end
195+
end

0 commit comments

Comments
 (0)