devcycle-ruby-server-sdk 3.6.1 → 3.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b68b0885c55e6e3f724d839500536a7022f6ec40d691b291125aa9e3b2d09669
4
- data.tar.gz: c45af7b53ca8e12b7b6d4d8f109d2f3b8c4ff67471736e952c7d8eed0aa32a08
3
+ metadata.gz: 914b4bf9e916b2796dcd6ce88be3ecf784b074fd30784bf8f28e2156b0bd8c66
4
+ data.tar.gz: dc4a335584b6618b4664203460530bc4421232dfdcc6362399694d7278f34bee
5
5
  SHA512:
6
- metadata.gz: a906bf6a2b314c043b765dbffacd7bd08975b7c69e89ec28a72abbb312a091c4c2a9fa4615076ffe8c400ef4f8d539fe4977fbe31247866d728ca2f33dc88eff
7
- data.tar.gz: b7ba08571dba965a3af92c8188b52ce06547e298afff943387246e67565daf8070daf5fe67552c629086bfb563b4a6856d98b31b21b949792011aa580b3e1ed9
6
+ metadata.gz: 6ca7f4b518dfa6bbd6ffa817263df4f625e7583a2ea61ad6b6ead5cd10ce12f2f586e205e457c39dc9d86831aadfd8e1ce7f1346c02362b1427bce7768a9fac0
7
+ data.tar.gz: 2d35f58deaa7a1309d8cb1aa6c5e7002e96e957f920d64d49d14a11e03a108051c679fc521acb451c1cd90a5dc32e9caa1754c92f308efecf592cd87da8cb78b
@@ -14,6 +14,7 @@ module DevCycle
14
14
  @sdkKey = sdkKey
15
15
  @dvc_options = dvc_options
16
16
  @logger = dvc_options.logger
17
+ @eval_hooks_runner = EvalHooksRunner.new
17
18
 
18
19
  if @dvc_options.enable_cloud_bucketing
19
20
  @api_client = ApiClient.default
@@ -172,35 +173,68 @@ module DevCycle
172
173
 
173
174
  validate_model(user)
174
175
 
175
- if @dvc_options.enable_cloud_bucketing
176
- data, _status_code, _headers = variable_with_http_info(key, user, default, opts)
177
- return data
176
+ # Create hook context
177
+ hook_context = HookContext.new(key: key, user: user, default_value: default)
178
+
179
+ before_hook_error = nil
180
+ # Run before hooks
181
+ begin
182
+ hook_context = @eval_hooks_runner.run_before_hooks(hook_context)
183
+ rescue BeforeHookError => e
184
+ before_hook_error = e
185
+ @logger.warn("Error in before hooks: #{e.message}")
178
186
  end
179
187
 
180
- value = default
181
- type = determine_variable_type(default)
182
- defaulted = true
183
- if local_bucketing_initialized? && @local_bucketing.has_config
184
- type_code = variable_type_code_from_type(type)
185
- variable_pb = variable_for_user_pb(user, key, type_code)
186
- unless variable_pb.nil?
187
- value = get_variable_value(variable_pb)
188
- defaulted = false
188
+ variable_result = nil
189
+
190
+ begin
191
+ if @dvc_options.enable_cloud_bucketing
192
+ data, _status_code, _headers = variable_with_http_info(key, user, default, opts)
193
+ variable_result = data
194
+ else
195
+ value = default
196
+ type = determine_variable_type(default)
197
+ defaulted = true
198
+ if local_bucketing_initialized? && @local_bucketing.has_config
199
+ type_code = variable_type_code_from_type(type)
200
+ variable_pb = variable_for_user_pb(user, key, type_code)
201
+ unless variable_pb.nil?
202
+ value = get_variable_value(variable_pb)
203
+ defaulted = false
204
+ end
205
+ else
206
+ @logger.warn("Local bucketing not initialized, returning default value for variable #{key}")
207
+ variable_event = Event.new({ type: DevCycle::EventTypes[:agg_variable_defaulted], target: key })
208
+ bucketed_config = BucketedUserConfig.new({}, {}, {}, {}, {}, {}, [])
209
+ @event_queue.queue_aggregate_event(variable_event, bucketed_config)
210
+ end
211
+
212
+ variable_result = Variable.new({
213
+ key: key,
214
+ value: value,
215
+ type: type,
216
+ defaultValue: default,
217
+ isDefaulted: defaulted
218
+ })
189
219
  end
190
- else
191
- @logger.warn("Local bucketing not initialized, returning default value for variable #{key}")
192
- variable_event = Event.new({ type: DevCycle::EventTypes[:agg_variable_defaulted], target: key })
193
- bucketed_config = BucketedUserConfig.new({}, {}, {}, {}, {}, {}, [])
194
- @event_queue.queue_aggregate_event(variable_event, bucketed_config)
195
- end
196
-
197
- Variable.new({
198
- key: key,
199
- value: value,
200
- type: type,
201
- defaultValue: default,
202
- isDefaulted: defaulted
203
- })
220
+
221
+
222
+ # Run after hooks only if no before hook error occurred
223
+ if before_hook_error != nil
224
+ @logger.info("before_hook_error is not nil, skipping after hooks")
225
+ raise before_hook_error
226
+ else
227
+ @eval_hooks_runner.run_after_hooks(hook_context)
228
+ end
229
+ rescue => e
230
+ # Run error hooks
231
+ @eval_hooks_runner.run_error_hooks(hook_context, e)
232
+ ensure
233
+ # Run finally hooks in all cases
234
+ @eval_hooks_runner.run_finally_hooks(hook_context)
235
+ end
236
+
237
+ variable_result
204
238
  end
205
239
 
206
240
  def variable_for_user(user, key, variable_type_code)
@@ -526,6 +560,32 @@ module DevCycle
526
560
  raise ArgumentError.new("Invalid type code for variable: #{type_code}")
527
561
  end
528
562
  end
563
+
564
+ def get_variable_value(variable_pb)
565
+ case variable_pb.type
566
+ when :Boolean
567
+ variable_pb.boolValue
568
+ when :Number
569
+ variable_pb.doubleValue
570
+ when :String
571
+ variable_pb.stringValue
572
+ when :JSON
573
+ JSON.parse variable_pb.stringValue
574
+ end
575
+ end
576
+
577
+ # Adds an eval hook to the client
578
+ # @param [EvalHook] eval_hook The eval hook to add
579
+ # @return [void]
580
+ def add_eval_hook(eval_hook)
581
+ @eval_hooks_runner.add_hook(eval_hook)
582
+ end
583
+
584
+ # Clears all eval hooks from the client
585
+ # @return [void]
586
+ def clear_eval_hooks
587
+ @eval_hooks_runner.clear_hooks
588
+ end
529
589
  end
530
590
 
531
591
  # @deprecated Use `DevCycle::Client` instead.
@@ -48,15 +48,42 @@ module DevCycle
48
48
  raise ArgumentError, "Invalid context type, expected OpenFeature::SDK::EvaluationContext but got #{context.class}"
49
49
  end
50
50
  args = {}
51
- if context.field('user_id')
52
- args.merge!(user_id: context.field('user_id'))
53
- elsif context.field('targeting_key')
54
- args.merge!(user_id: context.field('targeting_key'))
51
+ user_id = nil
52
+ user_id_field = nil
53
+
54
+ # Priority order: targeting_key -> user_id -> userId
55
+ if context.field('targeting_key')
56
+ user_id = context.field('targeting_key')
57
+ user_id_field = 'targeting_key'
58
+ elsif context.field('user_id')
59
+ user_id = context.field('user_id')
60
+ user_id_field = 'user_id'
61
+ elsif context.field('userId')
62
+ user_id = context.field('userId')
63
+ user_id_field = 'userId'
55
64
  end
65
+
66
+ # Validate user_id is present and is a string
67
+ if user_id.nil?
68
+ raise ArgumentError, "User ID is required. Must provide one of: targeting_key, user_id, or userId"
69
+ end
70
+
71
+ unless user_id.is_a?(String)
72
+ raise ArgumentError, "User ID field '#{user_id_field}' must be a string, got #{user_id.class}"
73
+ end
74
+
75
+ # Check after type validation to avoid NoMethodError on non-strings
76
+ if user_id.empty?
77
+ raise ArgumentError, "User ID is required. Must provide one of: targeting_key, user_id, or userId"
78
+ end
79
+
80
+ args.merge!(user_id: user_id)
81
+
56
82
  customData = {}
57
83
  privateCustomData = {}
58
84
  context.fields.each do |field, value|
59
- if field === 'user_id' || field === 'targeting_key'
85
+ # Skip all user ID fields from custom data
86
+ if field === 'targeting_key' || field === 'user_id' || field === 'userId'
60
87
  next
61
88
  end
62
89
  if !(field === 'privateCustomData' || field === 'customData') && value.is_a?(Hash)
@@ -0,0 +1,135 @@
1
+ require 'devcycle-ruby-server-sdk/models/eval_hook'
2
+ require 'devcycle-ruby-server-sdk/models/eval_hook_context'
3
+
4
+ module DevCycle
5
+ # Custom error raised when a before hook fails
6
+ class BeforeHookError < StandardError
7
+ attr_reader :original_error, :hook_context
8
+
9
+ def initialize(message = nil, original_error = nil, hook_context = nil)
10
+ super(message || "Before hook execution failed")
11
+ @original_error = original_error
12
+ @hook_context = hook_context
13
+ end
14
+
15
+ def to_s
16
+ msg = super
17
+ msg += "\nOriginal error: #{@original_error.message}" if @original_error
18
+ msg
19
+ end
20
+ end
21
+
22
+ # Custom error raised when an after hook fails
23
+ class AfterHookError < StandardError
24
+ attr_reader :original_error, :hook_context
25
+
26
+ def initialize(message = nil, original_error = nil, hook_context = nil)
27
+ super(message || "After hook execution failed")
28
+ @original_error = original_error
29
+ @hook_context = hook_context
30
+ end
31
+
32
+ def to_s
33
+ msg = super
34
+ msg += "\nOriginal error: #{@original_error.message}" if @original_error
35
+ msg
36
+ end
37
+ end
38
+
39
+ class EvalHooksRunner
40
+ # @return [Array<EvalHook>] Array of eval hooks to run
41
+ attr_reader :eval_hooks
42
+
43
+ # Initializes the EvalHooksRunner with an optional array of eval hooks
44
+ # @param [Array<EvalHook>, nil] eval_hooks Array of eval hooks to run
45
+ def initialize(eval_hooks = [])
46
+ @eval_hooks = eval_hooks || []
47
+ end
48
+
49
+ # Runs all before hooks with the given context
50
+ # @param [HookContext] context The context to pass to the hooks
51
+ # @return [HookContext] The potentially modified context
52
+ # @raise [BeforeHookError] when a before hook fails
53
+ def run_before_hooks(context)
54
+ current_context = context
55
+
56
+ @eval_hooks.each do |hook|
57
+ next unless hook.before
58
+
59
+ begin
60
+ result = hook.before.call(current_context)
61
+ # If the hook returns a new context, use it for subsequent hooks
62
+ current_context = result if result.is_a?(DevCycle::HookContext)
63
+ rescue => e
64
+ # Raise BeforeHookError to allow client to handle and skip after hooks
65
+ raise BeforeHookError.new(e.message, e, current_context)
66
+ end
67
+ end
68
+
69
+ current_context
70
+ end
71
+
72
+ # Runs all after hooks with the given context
73
+ # @param [HookContext] context The context to pass to the hooks
74
+ # @return [void]
75
+ # @raise [AfterHookError] when an after hook fails
76
+ def run_after_hooks(context)
77
+ @eval_hooks.each do |hook|
78
+ next unless hook.after
79
+
80
+ begin
81
+ hook.after.call(context)
82
+ rescue => e
83
+ # Log error but continue with next hook
84
+ raise AfterHookError.new(e.message, e, context)
85
+ end
86
+ end
87
+ end
88
+
89
+ # Runs all finally hooks with the given context
90
+ # @param [HookContext] context The context to pass to the hooks
91
+ # @return [void]
92
+ def run_finally_hooks(context)
93
+ @eval_hooks.each do |hook|
94
+ next unless hook.on_finally
95
+
96
+ begin
97
+ hook.on_finally.call(context)
98
+ rescue => e
99
+ # Log error but don't re-raise to prevent blocking evaluation
100
+ warn "Error in finally hook: #{e.message}"
101
+ end
102
+ end
103
+ end
104
+
105
+ # Runs all error hooks with the given context and error
106
+ # @param [HookContext] context The context to pass to the hooks
107
+ # @param [Exception] error The error that occurred
108
+ # @return [void]
109
+ def run_error_hooks(context, error)
110
+ @eval_hooks.each do |hook|
111
+ next unless hook.error
112
+
113
+ begin
114
+ hook.error.call(context, error)
115
+ rescue => e
116
+ # Log error but don't re-raise to prevent blocking evaluation
117
+ warn "Error in error hook: #{e.message}"
118
+ end
119
+ end
120
+ end
121
+
122
+ # Adds an eval hook to the runner
123
+ # @param [EvalHook] eval_hook The eval hook to add
124
+ # @return [void]
125
+ def add_hook(eval_hook)
126
+ @eval_hooks << eval_hook
127
+ end
128
+
129
+ # Clears all eval hooks from the runner
130
+ # @return [void]
131
+ def clear_hooks
132
+ @eval_hooks.clear
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,28 @@
1
+ module DevCycle
2
+ class EvalHook
3
+ # Callback to be executed before evaluation
4
+ attr_accessor :before
5
+
6
+ # Callback to be executed after evaluation
7
+ attr_accessor :after
8
+
9
+ # Callback to be executed finally (always runs)
10
+ attr_accessor :on_finally
11
+
12
+ # Callback to be executed on error
13
+ attr_accessor :error
14
+
15
+ # Initializes the object with optional callback functions
16
+ # @param [Hash] callbacks Callback functions in the form of hash
17
+ # @option callbacks [Proc, nil] :before Callback to execute before evaluation
18
+ # @option callbacks [Proc, nil] :after Callback to execute after evaluation
19
+ # @option callbacks [Proc, nil] :on_finally Callback to execute finally (always runs)
20
+ # @option callbacks [Proc, nil] :error Callback to execute on error
21
+ def initialize(callbacks = {})
22
+ @before = callbacks[:before]
23
+ @after = callbacks[:after]
24
+ @on_finally = callbacks[:on_finally]
25
+ @error = callbacks[:error]
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,22 @@
1
+ module DevCycle
2
+ class HookContext
3
+ # The key of the variable being evaluated
4
+ attr_accessor :key
5
+
6
+ # The user for whom the variable is being evaluated
7
+ attr_accessor :user
8
+
9
+ # The default value for the variable
10
+ attr_accessor :default_value
11
+
12
+ # Initializes the object
13
+ # @param [String] key The key of the variable being evaluated
14
+ # @param [DevCycle::User] user The user for whom the variable is being evaluated
15
+ # @param [Object] default_value The default value for the variable
16
+ def initialize(key:, user:, default_value:)
17
+ @key = key
18
+ @user = user
19
+ @default_value = default_value
20
+ end
21
+ end
22
+ end
@@ -1,3 +1,3 @@
1
1
  module DevCycle
2
- VERSION = '3.6.1'
2
+ VERSION = '3.6.2'
3
3
  end
@@ -25,6 +25,11 @@ require 'devcycle-ruby-server-sdk/models/user'
25
25
  require 'devcycle-ruby-server-sdk/models/user_data_and_events_body'
26
26
  require 'devcycle-ruby-server-sdk/models/variable'
27
27
 
28
+ # Eval Hooks
29
+ require 'devcycle-ruby-server-sdk/eval_hooks_runner'
30
+ require 'devcycle-ruby-server-sdk/models/eval_hook'
31
+ require 'devcycle-ruby-server-sdk/models/eval_hook_context'
32
+
28
33
  # APIs
29
34
  require 'devcycle-ruby-server-sdk/api/client'
30
35
  require 'devcycle-ruby-server-sdk/api/dev_cycle_provider'
@@ -4,18 +4,109 @@ require 'spec_helper'
4
4
  require 'open_feature/sdk'
5
5
 
6
6
  context 'user_from_openfeature_context' do
7
- context 'user_id' do
7
+ context 'user_id validation' do
8
+ it 'raises error when no user ID fields are provided' do
9
+ context = OpenFeature::SDK::EvaluationContext.new(email: 'test@example.com')
10
+ expect {
11
+ DevCycle::Provider.user_from_openfeature_context(context)
12
+ }.to raise_error(ArgumentError, "User ID is required. Must provide one of: targeting_key, user_id, or userId")
13
+ end
14
+
15
+ it 'raises error when targeting_key is not a string' do
16
+ context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 123)
17
+ expect {
18
+ DevCycle::Provider.user_from_openfeature_context(context)
19
+ }.to raise_error(ArgumentError, "User ID field 'targeting_key' must be a string, got Integer")
20
+ end
21
+
22
+ it 'raises error when user_id is not a string' do
23
+ context = OpenFeature::SDK::EvaluationContext.new(user_id: 123)
24
+ expect {
25
+ DevCycle::Provider.user_from_openfeature_context(context)
26
+ }.to raise_error(ArgumentError, "User ID field 'user_id' must be a string, got Integer")
27
+ end
28
+
29
+ it 'raises error when userId is not a string' do
30
+ context = OpenFeature::SDK::EvaluationContext.new(userId: 123)
31
+ expect {
32
+ DevCycle::Provider.user_from_openfeature_context(context)
33
+ }.to raise_error(ArgumentError, "User ID field 'userId' must be a string, got Integer")
34
+ end
35
+
36
+ it 'raises error when targeting_key is nil' do
37
+ context = OpenFeature::SDK::EvaluationContext.new(targeting_key: nil)
38
+ expect {
39
+ DevCycle::Provider.user_from_openfeature_context(context)
40
+ }.to raise_error(ArgumentError, "User ID is required. Must provide one of: targeting_key, user_id, or userId")
41
+ end
8
42
 
9
- it 'returns a user with the user_id from the context' do
10
- context = OpenFeature::SDK::EvaluationContext.new(user_id: 'user_id')
43
+ it 'raises error when targeting_key is empty string' do
44
+ context = OpenFeature::SDK::EvaluationContext.new(targeting_key: '')
45
+ expect {
46
+ DevCycle::Provider.user_from_openfeature_context(context)
47
+ }.to raise_error(ArgumentError, "User ID is required. Must provide one of: targeting_key, user_id, or userId")
48
+ end
49
+
50
+ it 'raises error when user_id is empty string' do
51
+ context = OpenFeature::SDK::EvaluationContext.new(user_id: '')
52
+ expect {
53
+ DevCycle::Provider.user_from_openfeature_context(context)
54
+ }.to raise_error(ArgumentError, "User ID is required. Must provide one of: targeting_key, user_id, or userId")
55
+ end
56
+
57
+ it 'raises error when userId is empty string' do
58
+ context = OpenFeature::SDK::EvaluationContext.new(userId: '')
59
+ expect {
60
+ DevCycle::Provider.user_from_openfeature_context(context)
61
+ }.to raise_error(ArgumentError, "User ID is required. Must provide one of: targeting_key, user_id, or userId")
62
+ end
63
+ end
64
+
65
+ context 'user_id fields priority' do
66
+ it 'returns a user with the user_id from the context when only user_id is provided' do
67
+ context = OpenFeature::SDK::EvaluationContext.new(user_id: 'user_id_value')
11
68
  user = DevCycle::Provider.user_from_openfeature_context(context)
12
- expect(user.user_id).to eq('user_id')
69
+ expect(user.user_id).to eq('user_id_value')
13
70
  end
14
71
 
15
- it 'returns a user with the targeting_key from the context' do
16
- context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key')
72
+ it 'returns a user with the targeting_key from the context when only targeting_key is provided' do
73
+ context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key_value')
17
74
  user = DevCycle::Provider.user_from_openfeature_context(context)
18
- expect(user.user_id).to eq('targeting_key')
75
+ expect(user.user_id).to eq('targeting_key_value')
76
+ end
77
+
78
+ it 'returns a user with the userId from the context when only userId is provided' do
79
+ context = OpenFeature::SDK::EvaluationContext.new(userId: 'userId_value')
80
+ user = DevCycle::Provider.user_from_openfeature_context(context)
81
+ expect(user.user_id).to eq('userId_value')
82
+ end
83
+
84
+ it 'prioritizes targeting_key over user_id' do
85
+ context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key_value', user_id: 'user_id_value')
86
+ user = DevCycle::Provider.user_from_openfeature_context(context)
87
+ expect(user.user_id).to eq('targeting_key_value')
88
+ expect(user.customData).to eq({})
89
+ end
90
+
91
+ it 'prioritizes targeting_key over userId' do
92
+ context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key_value', userId: 'userId_value')
93
+ user = DevCycle::Provider.user_from_openfeature_context(context)
94
+ expect(user.user_id).to eq('targeting_key_value')
95
+ expect(user.customData).to eq({})
96
+ end
97
+
98
+ it 'prioritizes user_id over userId' do
99
+ context = OpenFeature::SDK::EvaluationContext.new(user_id: 'user_id_value', userId: 'userId_value')
100
+ user = DevCycle::Provider.user_from_openfeature_context(context)
101
+ expect(user.user_id).to eq('user_id_value')
102
+ expect(user.customData).to eq({})
103
+ end
104
+
105
+ it 'prioritizes targeting_key over both user_id and userId' do
106
+ context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key_value', user_id: 'user_id_value', userId: 'userId_value')
107
+ user = DevCycle::Provider.user_from_openfeature_context(context)
108
+ expect(user.user_id).to eq('targeting_key_value')
109
+ expect(user.customData).to eq({})
19
110
  end
20
111
  end
21
112
  context 'email' do
@@ -31,6 +122,19 @@ context 'user_from_openfeature_context' do
31
122
  expect(user.user_id).to eq('targeting_key')
32
123
  expect(user.email).to eq('email')
33
124
  end
125
+ it 'returns a user with a valid userId and email' do
126
+ context = OpenFeature::SDK::EvaluationContext.new(userId: 'userId', email: 'email')
127
+ user = DevCycle::Provider.user_from_openfeature_context(context)
128
+ expect(user.user_id).to eq('userId')
129
+ expect(user.email).to eq('email')
130
+ end
131
+ it 'prioritizes targeting_key over user_id with email' do
132
+ context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key', user_id: 'user_id', email: 'email')
133
+ user = DevCycle::Provider.user_from_openfeature_context(context)
134
+ expect(user.user_id).to eq('targeting_key')
135
+ expect(user.email).to eq('email')
136
+ expect(user.customData).to eq({})
137
+ end
34
138
  end
35
139
 
36
140
  context 'customData' do
@@ -40,6 +144,18 @@ context 'user_from_openfeature_context' do
40
144
  expect(user.user_id).to eq('user_id')
41
145
  expect(user.customData).to eq({ 'key' => 'value' })
42
146
  end
147
+ it 'returns a user with userId and customData' do
148
+ context = OpenFeature::SDK::EvaluationContext.new(userId: 'userId', customData: { 'key' => 'value' })
149
+ user = DevCycle::Provider.user_from_openfeature_context(context)
150
+ expect(user.user_id).to eq('userId')
151
+ expect(user.customData).to eq({ 'key' => 'value' })
152
+ end
153
+ it 'excludes all user ID fields from customData' do
154
+ context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key', user_id: 'user_id', userId: 'userId', customData: { 'key' => 'value' })
155
+ user = DevCycle::Provider.user_from_openfeature_context(context)
156
+ expect(user.user_id).to eq('targeting_key')
157
+ expect(user.customData).to eq({ 'key' => 'value' })
158
+ end
43
159
  end
44
160
 
45
161
  context 'privateCustomData' do
@@ -49,6 +165,12 @@ context 'user_from_openfeature_context' do
49
165
  expect(user.user_id).to eq('user_id')
50
166
  expect(user.privateCustomData).to eq({ 'key' => 'value' })
51
167
  end
168
+ it 'returns a user with userId and privateCustomData' do
169
+ context = OpenFeature::SDK::EvaluationContext.new(userId: 'userId', privateCustomData: { 'key' => 'value' })
170
+ user = DevCycle::Provider.user_from_openfeature_context(context)
171
+ expect(user.user_id).to eq('userId')
172
+ expect(user.privateCustomData).to eq({ 'key' => 'value' })
173
+ end
52
174
  end
53
175
 
54
176
  context 'appVersion' do
@@ -65,6 +187,20 @@ context 'user_from_openfeature_context' do
65
187
  expect(user.user_id).to eq('user_id')
66
188
  expect(user.appBuild).to eq(1)
67
189
  end
190
+
191
+ it 'returns a user with userId and appVersion' do
192
+ context = OpenFeature::SDK::EvaluationContext.new(userId: 'userId', appVersion: '1.0.0')
193
+ user = DevCycle::Provider.user_from_openfeature_context(context)
194
+ expect(user.user_id).to eq('userId')
195
+ expect(user.appVersion).to eq('1.0.0')
196
+ end
197
+
198
+ it 'returns a user with userId and appBuild' do
199
+ context = OpenFeature::SDK::EvaluationContext.new(userId: 'userId', appBuild: 1)
200
+ user = DevCycle::Provider.user_from_openfeature_context(context)
201
+ expect(user.user_id).to eq('userId')
202
+ expect(user.appBuild).to eq(1)
203
+ end
68
204
  end
69
205
  context 'randomFields' do
70
206
  it 'returns a user with customData fields mapped to any non-standard fields' do
@@ -73,6 +209,20 @@ context 'user_from_openfeature_context' do
73
209
  expect(user.user_id).to eq('user_id')
74
210
  expect(user.customData).to eq({ 'randomField' => 'value' })
75
211
  end
212
+
213
+ it 'returns a user with userId and customData fields mapped to any non-standard fields' do
214
+ context = OpenFeature::SDK::EvaluationContext.new(userId: 'userId', randomField: 'value')
215
+ user = DevCycle::Provider.user_from_openfeature_context(context)
216
+ expect(user.user_id).to eq('userId')
217
+ expect(user.customData).to eq({ 'randomField' => 'value' })
218
+ end
219
+
220
+ it 'excludes all user ID fields from custom data with random fields' do
221
+ context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key', user_id: 'user_id', userId: 'userId', randomField: 'value')
222
+ user = DevCycle::Provider.user_from_openfeature_context(context)
223
+ expect(user.user_id).to eq('targeting_key')
224
+ expect(user.customData).to eq({ 'randomField' => 'value' })
225
+ end
76
226
  end
77
227
 
78
228
  context 'provider' do
@@ -0,0 +1,410 @@
1
+ require 'spec_helper'
2
+ require 'devcycle-ruby-server-sdk/eval_hooks_runner'
3
+
4
+ describe DevCycle::EvalHooksRunner do
5
+ let(:test_context) { DevCycle::HookContext.new(key: 'test-key', user: 'test-user', default_value: 'test-default') }
6
+
7
+ describe 'initialization' do
8
+ it 'initializes with empty hooks array' do
9
+ runner = DevCycle::EvalHooksRunner.new
10
+ expect(runner.eval_hooks).to be_empty
11
+ end
12
+
13
+ it 'initializes with provided hooks' do
14
+ hook = DevCycle::EvalHook.new
15
+ runner = DevCycle::EvalHooksRunner.new([hook])
16
+ expect(runner.eval_hooks).to include(hook)
17
+ end
18
+ end
19
+
20
+ describe '#add_hook' do
21
+ it 'adds a hook to the runner' do
22
+ runner = DevCycle::EvalHooksRunner.new
23
+ hook = DevCycle::EvalHook.new
24
+
25
+ runner.add_hook(hook)
26
+ expect(runner.eval_hooks).to include(hook)
27
+ end
28
+
29
+ it 'can add multiple hooks' do
30
+ runner = DevCycle::EvalHooksRunner.new
31
+ hook1 = DevCycle::EvalHook.new
32
+ hook2 = DevCycle::EvalHook.new
33
+
34
+ runner.add_hook(hook1)
35
+ runner.add_hook(hook2)
36
+
37
+ expect(runner.eval_hooks).to include(hook1, hook2)
38
+ expect(runner.eval_hooks.length).to eq(2)
39
+ end
40
+ end
41
+
42
+ describe '#clear_hooks' do
43
+ it 'removes all hooks from the runner' do
44
+ runner = DevCycle::EvalHooksRunner.new
45
+ hook = DevCycle::EvalHook.new
46
+
47
+ runner.add_hook(hook)
48
+ expect(runner.eval_hooks).not_to be_empty
49
+
50
+ runner.clear_hooks
51
+ expect(runner.eval_hooks).to be_empty
52
+ end
53
+ end
54
+
55
+ describe '#run_before_hooks' do
56
+ it 'runs before hooks in order' do
57
+ execution_order = []
58
+ runner = DevCycle::EvalHooksRunner.new
59
+
60
+ hook1 = DevCycle::EvalHook.new(
61
+ before: ->(context) {
62
+ execution_order << 'hook1'
63
+ context
64
+ }
65
+ )
66
+
67
+ hook2 = DevCycle::EvalHook.new(
68
+ before: ->(context) {
69
+ execution_order << 'hook2'
70
+ context
71
+ }
72
+ )
73
+
74
+ runner.add_hook(hook1)
75
+ runner.add_hook(hook2)
76
+
77
+ result = runner.run_before_hooks(test_context)
78
+
79
+ expect(execution_order).to eq(['hook1', 'hook2'])
80
+ expect(result).to eq(test_context)
81
+ end
82
+
83
+ it 'returns modified context from before hook' do
84
+ runner = DevCycle::EvalHooksRunner.new
85
+ modified_context = DevCycle::HookContext.new(key: 'modified', user: 'modified-user', default_value: 'modified-default')
86
+
87
+ hook = DevCycle::EvalHook.new(
88
+ before: ->(context) {
89
+ modified_context
90
+ }
91
+ )
92
+
93
+ runner.add_hook(hook)
94
+ result = runner.run_before_hooks(test_context)
95
+
96
+ expect(result).to eq(modified_context)
97
+ end
98
+
99
+ it 'handles hooks without before callbacks' do
100
+ runner = DevCycle::EvalHooksRunner.new
101
+ hook = DevCycle::EvalHook.new # No before callback
102
+
103
+ runner.add_hook(hook)
104
+ result = runner.run_before_hooks(test_context)
105
+
106
+ expect(result).to eq(test_context)
107
+ end
108
+
109
+ it 'raises BeforeHookError when a before hook raises an error' do
110
+ runner = DevCycle::EvalHooksRunner.new
111
+ hook1_called = false
112
+
113
+ hook1 = DevCycle::EvalHook.new(
114
+ before: ->(context) {
115
+ hook1_called = true
116
+ raise StandardError, 'Hook 1 error'
117
+ }
118
+ )
119
+
120
+ hook2 = DevCycle::EvalHook.new(
121
+ before: ->(context) {
122
+ # This should not be called because hook1 raises an error
123
+ context
124
+ }
125
+ )
126
+
127
+ runner.add_hook(hook1)
128
+ runner.add_hook(hook2)
129
+
130
+ expect { runner.run_before_hooks(test_context) }.to raise_error(DevCycle::BeforeHookError, /Hook 1 error/)
131
+ expect(hook1_called).to be true
132
+ end
133
+ end
134
+
135
+ describe '#run_after_hooks' do
136
+ it 'runs after hooks in order' do
137
+ execution_order = []
138
+ runner = DevCycle::EvalHooksRunner.new
139
+
140
+ hook1 = DevCycle::EvalHook.new(
141
+ after: ->(context) {
142
+ execution_order << 'hook1'
143
+ }
144
+ )
145
+
146
+ hook2 = DevCycle::EvalHook.new(
147
+ after: ->(context) {
148
+ execution_order << 'hook2'
149
+ }
150
+ )
151
+
152
+ runner.add_hook(hook1)
153
+ runner.add_hook(hook2)
154
+
155
+ runner.run_after_hooks(test_context)
156
+
157
+ expect(execution_order).to eq(['hook1', 'hook2'])
158
+ end
159
+
160
+ it 'handles hooks without after callbacks' do
161
+ runner = DevCycle::EvalHooksRunner.new
162
+ hook = DevCycle::EvalHook.new # No after callback
163
+
164
+ expect { runner.run_after_hooks(test_context) }.not_to raise_error
165
+ end
166
+
167
+ it 'raises AfterHookError when an after hook raises an error' do
168
+ runner = DevCycle::EvalHooksRunner.new
169
+ hook1_called = false
170
+
171
+ hook1 = DevCycle::EvalHook.new(
172
+ after: ->(context) {
173
+ hook1_called = true
174
+ raise StandardError, 'Hook 1 error'
175
+ }
176
+ )
177
+
178
+ hook2 = DevCycle::EvalHook.new(
179
+ after: ->(context) {
180
+ # This should not be called because hook1 raises an error
181
+ }
182
+ )
183
+
184
+ runner.add_hook(hook1)
185
+ runner.add_hook(hook2)
186
+
187
+ expect { runner.run_after_hooks(test_context) }.to raise_error(DevCycle::AfterHookError, /Hook 1 error/)
188
+ expect(hook1_called).to be true
189
+ end
190
+ end
191
+
192
+ describe '#run_error_hooks' do
193
+ it 'runs error hooks with context and error' do
194
+ runner = DevCycle::EvalHooksRunner.new
195
+ error_hook_called = false
196
+ received_error = nil
197
+
198
+ hook = DevCycle::EvalHook.new(
199
+ error: ->(context, error) {
200
+ error_hook_called = true
201
+ received_error = error
202
+ }
203
+ )
204
+
205
+ runner.add_hook(hook)
206
+ test_error = StandardError.new('Test error')
207
+
208
+ runner.run_error_hooks(test_context, test_error)
209
+
210
+ expect(error_hook_called).to be true
211
+ expect(received_error).to eq(test_error)
212
+ end
213
+
214
+ it 'runs multiple error hooks in order' do
215
+ execution_order = []
216
+ runner = DevCycle::EvalHooksRunner.new
217
+
218
+ hook1 = DevCycle::EvalHook.new(
219
+ error: ->(context, error) {
220
+ execution_order << 'hook1'
221
+ }
222
+ )
223
+
224
+ hook2 = DevCycle::EvalHook.new(
225
+ error: ->(context, error) {
226
+ execution_order << 'hook2'
227
+ }
228
+ )
229
+
230
+ runner.add_hook(hook1)
231
+ runner.add_hook(hook2)
232
+
233
+ test_error = StandardError.new('Test error')
234
+ runner.run_error_hooks(test_context, test_error)
235
+
236
+ expect(execution_order).to eq(['hook1', 'hook2'])
237
+ end
238
+
239
+ it 'handles hooks without error callbacks' do
240
+ runner = DevCycle::EvalHooksRunner.new
241
+ hook = DevCycle::EvalHook.new # No error callback
242
+
243
+ test_error = StandardError.new('Test error')
244
+ expect { runner.run_error_hooks(test_context, test_error) }.not_to raise_error
245
+ end
246
+
247
+ it 'continues execution when an error hook raises an error' do
248
+ runner = DevCycle::EvalHooksRunner.new
249
+ hook1_called = false
250
+ hook2_called = false
251
+
252
+ hook1 = DevCycle::EvalHook.new(
253
+ error: ->(context, error) {
254
+ hook1_called = true
255
+ raise StandardError, 'Error hook error'
256
+ }
257
+ )
258
+
259
+ hook2 = DevCycle::EvalHook.new(
260
+ error: ->(context, error) {
261
+ hook2_called = true
262
+ }
263
+ )
264
+
265
+ runner.add_hook(hook1)
266
+ runner.add_hook(hook2)
267
+
268
+ test_error = StandardError.new('Test error')
269
+ runner.run_error_hooks(test_context, test_error)
270
+
271
+ expect(hook1_called).to be true
272
+ expect(hook2_called).to be true
273
+ end
274
+ end
275
+
276
+ describe '#run_finally_hooks' do
277
+ it 'runs finally hooks in order' do
278
+ execution_order = []
279
+ runner = DevCycle::EvalHooksRunner.new
280
+
281
+ hook1 = DevCycle::EvalHook.new(
282
+ on_finally: ->(context) {
283
+ execution_order << 'hook1'
284
+ }
285
+ )
286
+
287
+ hook2 = DevCycle::EvalHook.new(
288
+ on_finally: ->(context) {
289
+ execution_order << 'hook2'
290
+ }
291
+ )
292
+
293
+ runner.add_hook(hook1)
294
+ runner.add_hook(hook2)
295
+
296
+ runner.run_finally_hooks(test_context)
297
+
298
+ expect(execution_order).to eq(['hook1', 'hook2'])
299
+ end
300
+
301
+ it 'handles hooks without finally callbacks' do
302
+ runner = DevCycle::EvalHooksRunner.new
303
+ hook = DevCycle::EvalHook.new # No finally callback
304
+
305
+ expect { runner.run_finally_hooks(test_context) }.not_to raise_error
306
+ end
307
+
308
+ it 'continues execution when a finally hook raises an error' do
309
+ runner = DevCycle::EvalHooksRunner.new
310
+ hook1_called = false
311
+ hook2_called = false
312
+
313
+ hook1 = DevCycle::EvalHook.new(
314
+ on_finally: ->(context) {
315
+ hook1_called = true
316
+ raise StandardError, 'Finally hook error'
317
+ }
318
+ )
319
+
320
+ hook2 = DevCycle::EvalHook.new(
321
+ on_finally: ->(context) {
322
+ hook2_called = true
323
+ }
324
+ )
325
+
326
+ runner.add_hook(hook1)
327
+ runner.add_hook(hook2)
328
+
329
+ runner.run_finally_hooks(test_context)
330
+
331
+ expect(hook1_called).to be true
332
+ expect(hook2_called).to be true
333
+ end
334
+ end
335
+ end
336
+
337
+ describe DevCycle::EvalHook do
338
+ describe 'initialization' do
339
+ it 'initializes with no callbacks' do
340
+ hook = DevCycle::EvalHook.new
341
+ expect(hook.before).to be_nil
342
+ expect(hook.after).to be_nil
343
+ expect(hook.on_finally).to be_nil
344
+ expect(hook.error).to be_nil
345
+ end
346
+
347
+ it 'initializes with provided callbacks' do
348
+ before_callback = ->(context) { context }
349
+ after_callback = ->(context) { }
350
+ error_callback = ->(context, error) { }
351
+ finally_callback = ->(context) { }
352
+
353
+ hook = DevCycle::EvalHook.new(
354
+ before: before_callback,
355
+ after: after_callback,
356
+ error: error_callback,
357
+ on_finally: finally_callback
358
+ )
359
+
360
+ expect(hook.before).to eq(before_callback)
361
+ expect(hook.after).to eq(after_callback)
362
+ expect(hook.error).to eq(error_callback)
363
+ expect(hook.on_finally).to eq(finally_callback)
364
+ end
365
+
366
+ it 'initializes with partial callbacks' do
367
+ before_callback = ->(context) { context }
368
+
369
+ hook = DevCycle::EvalHook.new(before: before_callback)
370
+
371
+ expect(hook.before).to eq(before_callback)
372
+ expect(hook.after).to be_nil
373
+ expect(hook.on_finally).to be_nil
374
+ expect(hook.error).to be_nil
375
+ end
376
+ end
377
+ end
378
+
379
+ describe DevCycle::HookContext do
380
+ describe 'initialization' do
381
+ it 'initializes with required parameters' do
382
+ user = { user_id: 'test-user' }
383
+ context = DevCycle::HookContext.new(
384
+ key: 'test-key',
385
+ user: user,
386
+ default_value: 'test-default'
387
+ )
388
+
389
+ expect(context.key).to eq('test-key')
390
+ expect(context.user).to eq(user)
391
+ expect(context.default_value).to eq('test-default')
392
+ end
393
+
394
+ it 'allows modification of attributes' do
395
+ context = DevCycle::HookContext.new(
396
+ key: 'original-key',
397
+ user: 'original-user',
398
+ default_value: 'original-default'
399
+ )
400
+
401
+ context.key = 'modified-key'
402
+ context.user = 'modified-user'
403
+ context.default_value = 'modified-default'
404
+
405
+ expect(context.key).to eq('modified-key')
406
+ expect(context.user).to eq('modified-user')
407
+ expect(context.default_value).to eq('modified-default')
408
+ end
409
+ end
410
+ end
@@ -0,0 +1,245 @@
1
+ require 'spec_helper'
2
+
3
+ describe DevCycle::Client do
4
+ let(:test_user) { DevCycle::User.new(user_id: 'test-user', email: 'test@example.com') }
5
+ let(:test_key) { 'test-variable' }
6
+ let(:test_default) { 'default-value' }
7
+ let(:options) { DevCycle::Options.new }
8
+
9
+ # Use unique SDK keys for each test to avoid WASM initialization conflicts
10
+ let(:valid_sdk_key) { "server-test-key-#{SecureRandom.hex(4)}" }
11
+ let(:client) { DevCycle::Client.new(valid_sdk_key, options) }
12
+
13
+ after(:each) do
14
+ client.close if client.respond_to?(:close)
15
+ end
16
+
17
+ describe 'eval hooks functionality' do
18
+ context 'hook management' do
19
+ it 'initializes with an empty eval hooks runner' do
20
+ expect(client.instance_variable_get(:@eval_hooks_runner)).to be_a(DevCycle::EvalHooksRunner)
21
+ expect(client.instance_variable_get(:@eval_hooks_runner).eval_hooks).to be_empty
22
+ end
23
+
24
+ it 'can add eval hooks' do
25
+ hook = DevCycle::EvalHook.new
26
+ client.add_eval_hook(hook)
27
+ expect(client.instance_variable_get(:@eval_hooks_runner).eval_hooks).to include(hook)
28
+ end
29
+
30
+ it 'can clear eval hooks' do
31
+ hook = DevCycle::EvalHook.new
32
+ client.add_eval_hook(hook)
33
+ expect(client.instance_variable_get(:@eval_hooks_runner).eval_hooks).not_to be_empty
34
+
35
+ client.clear_eval_hooks
36
+ expect(client.instance_variable_get(:@eval_hooks_runner).eval_hooks).to be_empty
37
+ end
38
+ end
39
+
40
+ context 'variable evaluation with hooks' do
41
+ it 'runs before hooks before variable evaluation' do
42
+ before_hook_called = false
43
+ hook = DevCycle::EvalHook.new(
44
+ before: ->(context) {
45
+ before_hook_called = true
46
+ expect(context.key).to eq(test_key)
47
+ expect(context.user).to eq(test_user)
48
+ expect(context.default_value).to eq(test_default)
49
+ context
50
+ }
51
+ )
52
+ client.add_eval_hook(hook)
53
+
54
+ result = client.variable(test_user, test_key, test_default)
55
+ expect(before_hook_called).to be true
56
+ expect(result.isDefaulted).to be true
57
+ expect(result.value).to eq(test_default)
58
+ end
59
+
60
+ it 'runs after hooks after successful variable evaluation' do
61
+ after_hook_called = false
62
+ hook = DevCycle::EvalHook.new(
63
+ after: ->(context) {
64
+ after_hook_called = true
65
+ expect(context.key).to eq(test_key)
66
+ expect(context.user).to eq(test_user)
67
+ expect(context.default_value).to eq(test_default)
68
+ }
69
+ )
70
+ client.add_eval_hook(hook)
71
+
72
+ result = client.variable(test_user, test_key, test_default)
73
+ expect(after_hook_called).to be true
74
+ expect(result.isDefaulted).to be true
75
+ expect(result.value).to eq(test_default)
76
+ end
77
+
78
+ it 'runs error hooks when variable evaluation fails' do
79
+ error_hook_called = false
80
+ hook = DevCycle::EvalHook.new(
81
+ error: ->(context, error) {
82
+ error_hook_called = true
83
+ expect(context.key).to eq(test_key)
84
+ expect(context.user).to eq(test_user)
85
+ expect(context.default_value).to eq(test_default)
86
+ }
87
+ )
88
+ client.add_eval_hook(hook)
89
+
90
+ # Force an error by making determine_variable_type raise an error
91
+ allow(client).to receive(:determine_variable_type).and_raise(StandardError, 'Variable type error')
92
+
93
+ client.variable(test_user, test_key, test_default)
94
+ expect(error_hook_called).to be true
95
+ end
96
+
97
+ it 'runs finally hooks regardless of success or failure' do
98
+ finally_hook_called = false
99
+ hook = DevCycle::EvalHook.new(
100
+ on_finally: ->(context) {
101
+ finally_hook_called = true
102
+ expect(context.key).to eq(test_key)
103
+ expect(context.user).to eq(test_user)
104
+ expect(context.default_value).to eq(test_default)
105
+ }
106
+ )
107
+ client.add_eval_hook(hook)
108
+
109
+ result = client.variable(test_user, test_key, test_default)
110
+ expect(finally_hook_called).to be true
111
+ expect(result.isDefaulted).to be true
112
+ expect(result.value).to eq(test_default)
113
+ end
114
+
115
+ it 'skips after hooks when before hook raises an error' do
116
+ before_hook_called = false
117
+ after_hook_called = false
118
+ error_hook_called = false
119
+ finally_hook_called = false
120
+
121
+ hook = DevCycle::EvalHook.new(
122
+ before: ->(context) {
123
+ before_hook_called = true
124
+ raise StandardError, 'Before hook error'
125
+ },
126
+ after: ->(context) {
127
+ after_hook_called = true
128
+ },
129
+ error: ->(context, error) {
130
+ error_hook_called = true
131
+ expect(error).to be_a(StandardError)
132
+ expect(error.message).to include('Before hook error')
133
+ },
134
+ on_finally: ->(context) {
135
+ finally_hook_called = true
136
+ }
137
+ )
138
+ client.add_eval_hook(hook)
139
+
140
+ client.variable(test_user, test_key, test_default)
141
+ expect(before_hook_called).to be true
142
+ expect(after_hook_called).to be false
143
+ expect(error_hook_called).to be true
144
+ expect(finally_hook_called).to be true
145
+ end
146
+
147
+ it 'runs multiple hooks in order' do
148
+ execution_order = []
149
+
150
+ hook1 = DevCycle::EvalHook.new(
151
+ before: ->(context) {
152
+ execution_order << 'hook1_before'
153
+ context
154
+ },
155
+ after: ->(context) {
156
+ execution_order << 'hook1_after'
157
+ },
158
+ on_finally: ->(context) {
159
+ execution_order << 'hook1_finally'
160
+ }
161
+ )
162
+
163
+ hook2 = DevCycle::EvalHook.new(
164
+ before: ->(context) {
165
+ execution_order << 'hook2_before'
166
+ context
167
+ },
168
+ after: ->(context) {
169
+ execution_order << 'hook2_after'
170
+ },
171
+ on_finally: ->(context) {
172
+ execution_order << 'hook2_finally'
173
+ }
174
+ )
175
+
176
+ client.add_eval_hook(hook1)
177
+ client.add_eval_hook(hook2)
178
+
179
+ result = client.variable(test_user, test_key, test_default)
180
+
181
+ expect(execution_order).to eq([
182
+ 'hook1_before', 'hook2_before',
183
+ 'hook1_after', 'hook2_after',
184
+ 'hook1_finally', 'hook2_finally'
185
+ ])
186
+ expect(result.isDefaulted).to be true
187
+ expect(result.value).to eq(test_default)
188
+ end
189
+
190
+ it 'allows before hooks to modify context' do
191
+ modified_context = nil
192
+ hook = DevCycle::EvalHook.new(
193
+ before: ->(context) {
194
+ # Modify the context
195
+ context.key = 'modified-key'
196
+ context.user = DevCycle::User.new(user_id: 'modified-user', email: 'modified@example.com')
197
+ context
198
+ },
199
+ after: ->(context) {
200
+ modified_context = context
201
+ }
202
+ )
203
+ client.add_eval_hook(hook)
204
+
205
+ result = client.variable(test_user, test_key, test_default)
206
+
207
+ expect(modified_context.key).to eq('modified-key')
208
+ expect(modified_context.user).to eq(DevCycle::User.new(user_id: 'modified-user', email: 'modified@example.com'))
209
+ expect(result.isDefaulted).to be true
210
+ expect(result.value).to eq(test_default)
211
+ end
212
+
213
+ it 'works with different variable types' do
214
+ # Test with boolean default
215
+ boolean_hook_called = false
216
+ boolean_hook = DevCycle::EvalHook.new(
217
+ after: ->(context) {
218
+ boolean_hook_called = true
219
+ }
220
+ )
221
+ client.add_eval_hook(boolean_hook)
222
+
223
+ boolean_result = client.variable(test_user, 'boolean-test', true)
224
+ expect(boolean_hook_called).to be true
225
+ expect(boolean_result.isDefaulted).to be true
226
+ expect(boolean_result.value).to eq(true)
227
+
228
+ # Test with number default
229
+ number_hook_called = false
230
+ number_hook = DevCycle::EvalHook.new(
231
+ after: ->(context) {
232
+ number_hook_called = true
233
+ }
234
+ )
235
+ client.add_eval_hook(number_hook)
236
+
237
+ number_result = client.variable(test_user, 'number-test', 42)
238
+ expect(number_hook_called).to be true
239
+ expect(number_result.isDefaulted).to be true
240
+ expect(number_result.value).to eq(42)
241
+ end
242
+
243
+ end
244
+ end
245
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: devcycle-ruby-server-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.6.1
4
+ version: 3.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - DevCycleHQ
@@ -165,6 +165,7 @@ files:
165
165
  - lib/devcycle-ruby-server-sdk/api_client.rb
166
166
  - lib/devcycle-ruby-server-sdk/api_error.rb
167
167
  - lib/devcycle-ruby-server-sdk/configuration.rb
168
+ - lib/devcycle-ruby-server-sdk/eval_hooks_runner.rb
168
169
  - lib/devcycle-ruby-server-sdk/localbucketing/bucketed_user_config.rb
169
170
  - lib/devcycle-ruby-server-sdk/localbucketing/bucketing-lib.release.wasm
170
171
  - lib/devcycle-ruby-server-sdk/localbucketing/config_manager.rb
@@ -179,6 +180,8 @@ files:
179
180
  - lib/devcycle-ruby-server-sdk/localbucketing/proto/variableForUserParams_pb.rb
180
181
  - lib/devcycle-ruby-server-sdk/localbucketing/update_wasm.sh
181
182
  - lib/devcycle-ruby-server-sdk/models/error_response.rb
183
+ - lib/devcycle-ruby-server-sdk/models/eval_hook.rb
184
+ - lib/devcycle-ruby-server-sdk/models/eval_hook_context.rb
182
185
  - lib/devcycle-ruby-server-sdk/models/event.rb
183
186
  - lib/devcycle-ruby-server-sdk/models/feature.rb
184
187
  - lib/devcycle-ruby-server-sdk/models/inline_response201.rb
@@ -190,6 +193,8 @@ files:
190
193
  - spec/api_client_spec.rb
191
194
  - spec/configuration_spec.rb
192
195
  - spec/devcycle_provider_spec.rb
196
+ - spec/eval_hooks_runner_spec.rb
197
+ - spec/eval_hooks_spec.rb
193
198
  - spec/spec_helper.rb
194
199
  homepage: https://devcycle.com
195
200
  licenses:
@@ -209,7 +214,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
209
214
  - !ruby/object:Gem::Version
210
215
  version: '0'
211
216
  requirements: []
212
- rubygems_version: 3.6.7
217
+ rubygems_version: 3.6.9
213
218
  specification_version: 4
214
219
  summary: DevCycle Bucketing API Ruby Gem
215
220
  test_files:
@@ -217,4 +222,6 @@ test_files:
217
222
  - spec/api_client_spec.rb
218
223
  - spec/configuration_spec.rb
219
224
  - spec/devcycle_provider_spec.rb
225
+ - spec/eval_hooks_runner_spec.rb
226
+ - spec/eval_hooks_spec.rb
220
227
  - spec/spec_helper.rb