togglecraft 1.0.0 → 1.0.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: 6e77c8218c124ce83fbdc25f2f2975f50ca69fde64b3109116e639580ac5be42
4
- data.tar.gz: 872451734b10596e4b11337275454a998605a7966ff7e9f5d575c4764ff2a89e
3
+ metadata.gz: 7b9dd914fcd5b95e495b7f63183f53fa849a7f1bd7291548c39cf46d98a3be1d
4
+ data.tar.gz: 9f6f56bea1e188e1c2688c16b726140ae9700811db4d3406332defe8501b6f51
5
5
  SHA512:
6
- metadata.gz: 3353a6384c9c2ab564c5244f0c015a723c403287213e6fa6542c0b8cbfbb49bf804d303f3c1d51510c1a07858884b2626a0128a05744d2d0a7e394ad6f1f43b0
7
- data.tar.gz: 6a31210c7fb15839f8b5bbb2b85b37c13129f00ea26a57796d885331a5cf981e6c4eefaf7ad69fdd882a05a29acb2b9175321784892a750d087ef9deba4c4e96
6
+ metadata.gz: a55edef61bcbf57d1bfb8db39ec69ee26ad20406d472b429c6a231fb8437b100b43ccbe49fdd2addd69439e4ad06d187cf5e7562e00ea1c21c1ab50235374c12
7
+ data.tar.gz: da54c02f224ea950aa4f0e06564378bf25d9176987b0b25509a73320a23cf26de28cc491cb36219d4def4c5f076174715e433944776e355aa8a5390c0723bbac
@@ -13,11 +13,11 @@ module ToggleCraft
13
13
  def initialize(adapter: :memory, ttl: 300)
14
14
  @default_ttl = ttl
15
15
  @adapter = case adapter
16
- when :memory
16
+ when :memory
17
17
  CacheAdapters::MemoryAdapter.new
18
- else
18
+ else
19
19
  adapter
20
- end
20
+ end
21
21
  @cleanup_thread = nil
22
22
  start_cleanup
23
23
  end
@@ -26,7 +26,8 @@ module ToggleCraft
26
26
  enable_rollout_stage_polling: true,
27
27
  rollout_stage_check_interval: 60,
28
28
  fetch_jitter: 1500, # milliseconds
29
- api_domain: 'https://togglecraft.io'
29
+ api_domain: 'https://togglecraft.io',
30
+ heartbeat_domain: 'https://togglecraft.io' # Default heartbeat domain
30
31
  }.merge(options).merge(sdk_key: sdk_key)
31
32
 
32
33
  # Initialize components
@@ -334,7 +335,6 @@ module ToggleCraft
334
335
  max_reconnect_interval: @config[:max_reconnect_interval],
335
336
  max_reconnect_attempts: @config[:max_reconnect_attempts],
336
337
  slow_reconnect_interval: @config[:slow_reconnect_interval],
337
- heartbeat_domain: @config[:heartbeat_domain],
338
338
  debug: @config[:debug],
339
339
  on_message: method(:handle_flags_update),
340
340
  on_connect: method(:handle_connect),
@@ -393,7 +393,8 @@ module ToggleCraft
393
393
  @fetch_in_progress.make_true
394
394
 
395
395
  begin
396
- versioned_url = "#{@config[:api_domain]}/api/v1/flags/#{@project_key.get}/#{@environment_key.get}/v/#{version}"
396
+ # API domain always hardcoded to production
397
+ versioned_url = "https://togglecraft.io/api/v1/flags/#{@project_key.get}/#{@environment_key.get}/v/#{version}"
397
398
  log "Fetching flags from #{versioned_url}"
398
399
 
399
400
  response = HTTP.timeout(10).get(versioned_url)
@@ -18,12 +18,13 @@ module ToggleCraft
18
18
  end
19
19
 
20
20
  # Evaluate a boolean flag
21
- # @param flag_key [String] The flag key
21
+ # @param flag_key [String, Symbol] The flag key
22
22
  # @param context [Hash] The evaluation context
23
23
  # @param default_value [Boolean] Default value if flag not found
24
24
  # @return [Boolean]
25
25
  def evaluate_boolean(flag_key, context = {}, default_value = false)
26
- flag = @flags[flag_key]
26
+ # Normalize key to symbol for lookup (flags are stored with symbol keys)
27
+ flag = @flags[flag_key.to_sym]
27
28
 
28
29
  # Flag doesn't exist
29
30
  return default_value unless flag
@@ -56,7 +57,8 @@ module ToggleCraft
56
57
  # @param default_variant [String, nil] Default variant if flag not found
57
58
  # @return [String, nil]
58
59
  def evaluate_multivariate(flag_key, context = {}, default_variant = nil)
59
- flag = @flags[flag_key]
60
+ # Normalize key to symbol for lookup (flags are stored with symbol keys)
61
+ flag = @flags[flag_key.to_sym]
60
62
 
61
63
  # Flag doesn't exist
62
64
  return default_variant unless flag
@@ -88,12 +90,13 @@ module ToggleCraft
88
90
  end
89
91
 
90
92
  # Evaluate a percentage rollout flag
91
- # @param flag_key [String] The flag key
93
+ # @param flag_key [String, Symbol] The flag key
92
94
  # @param context [Hash] The evaluation context
93
95
  # @param default_value [Boolean] Default value if flag not found
94
96
  # @return [Boolean]
95
97
  def evaluate_percentage(flag_key, context = {}, default_value = false)
96
- flag = @flags[flag_key]
98
+ # Normalize key to symbol for lookup (flags are stored with symbol keys)
99
+ flag = @flags[flag_key.to_sym]
97
100
 
98
101
  # Flag doesn't exist
99
102
  return default_value unless flag
@@ -158,10 +161,11 @@ module ToggleCraft
158
161
  end
159
162
 
160
163
  # Get the current percentage value of a flag
161
- # @param flag_key [String] The flag key
164
+ # @param flag_key [String, Symbol] The flag key
162
165
  # @return [Integer, nil]
163
166
  def percentage(flag_key)
164
- flag = @flags[flag_key]
167
+ # Normalize key to symbol for lookup (flags are stored with symbol keys)
168
+ flag = @flags[flag_key.to_sym]
165
169
 
166
170
  return nil unless flag && flag[:type] == 'percentage'
167
171
 
@@ -183,7 +187,7 @@ module ToggleCraft
183
187
 
184
188
  result = if result.nil?
185
189
  condition_result
186
- else
190
+ else
187
191
  # Apply combinator logic
188
192
  case previous_combinator
189
193
  when 'OR'
@@ -192,7 +196,7 @@ module ToggleCraft
192
196
  # Default to AND
193
197
  result && condition_result
194
198
  end
195
- end
199
+ end
196
200
 
197
201
  previous_combinator = condition[:combinator] || 'AND'
198
202
 
@@ -252,7 +256,8 @@ module ToggleCraft
252
256
  # @param flag_key [String] The flag key
253
257
  # @return [Boolean]
254
258
  def has_flag?(flag_key)
255
- @flags.key?(flag_key)
259
+ # Normalize key to symbol for lookup (flags are stored with symbol keys)
260
+ @flags.key?(flag_key.to_sym)
256
261
  end
257
262
 
258
263
  # Get all flag keys
@@ -262,10 +267,11 @@ module ToggleCraft
262
267
  end
263
268
 
264
269
  # Get flag metadata
265
- # @param flag_key [String] The flag key
270
+ # @param flag_key [String, Symbol] The flag key
266
271
  # @return [Hash, nil]
267
272
  def flag_metadata(flag_key)
268
- flag = @flags[flag_key]
273
+ # Normalize key to symbol for lookup (flags are stored with symbol keys)
274
+ flag = @flags[flag_key.to_sym]
269
275
  return nil unless flag
270
276
 
271
277
  metadata = {
@@ -285,12 +291,13 @@ module ToggleCraft
285
291
  end
286
292
 
287
293
  # Evaluate any flag type
288
- # @param flag_key [String] The flag key
294
+ # @param flag_key [String, Symbol] The flag key
289
295
  # @param context [Hash] The evaluation context
290
296
  # @param default_value [Object] Default value
291
297
  # @return [Object]
292
298
  def evaluate(flag_key, context = {}, default_value = false)
293
- flag = @flags[flag_key]
299
+ # Normalize key to symbol for lookup (flags are stored with symbol keys)
300
+ flag = @flags[flag_key.to_sym]
294
301
  return default_value unless flag
295
302
 
296
303
  case flag[:type]
@@ -158,7 +158,6 @@ module ToggleCraft
158
158
  max_reconnect_interval: @config[:max_reconnect_interval],
159
159
  max_reconnect_attempts: @config[:max_reconnect_attempts],
160
160
  slow_reconnect_interval: @config[:slow_reconnect_interval],
161
- heartbeat_domain: @config[:heartbeat_domain],
162
161
  debug: @debug,
163
162
  on_message: method(:handle_message),
164
163
  on_connect: method(:handle_connect),
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'cgi'
3
4
  require 'http'
4
5
  require 'json'
5
6
 
@@ -32,10 +33,10 @@ module ToggleCraft
32
33
  @should_reconnect = true
33
34
 
34
35
  # Callbacks
35
- @on_message = options[:on_message] || proc {}
36
- @on_connect = options[:on_connect] || proc {}
37
- @on_disconnect = options[:on_disconnect] || proc {}
38
- @on_error = options[:on_error] || proc {}
36
+ @on_message = options[:on_message] || proc { }
37
+ @on_connect = options[:on_connect] || proc { }
38
+ @on_disconnect = options[:on_disconnect] || proc { }
39
+ @on_error = options[:on_error] || proc { }
39
40
 
40
41
  # Threads
41
42
  @connection_thread = nil
@@ -111,24 +112,30 @@ module ToggleCraft
111
112
  sse_url = "#{@url}/events?identifier=#{CGI.escape(channel_identifier)}"
112
113
  log "Connecting to #{sse_url}"
113
114
 
114
- HTTP.get(sse_url) do |response|
115
- raise "HTTP #{response.status}: #{response.status.reason}" unless response.status.success?
115
+ # Don't use block form - open connection and keep reference
116
+ response = HTTP.timeout(connect: 10, read: 300)
117
+ .headers('Accept' => 'text/event-stream')
118
+ .get(sse_url)
116
119
 
117
- @connected = true
118
- @reconnect_attempts = 0
119
- @on_connect.call
120
- start_heartbeat
120
+ raise "HTTP #{response.status}: #{response.status.reason}" unless response.status.success?
121
121
 
122
- # Read SSE stream
123
- buffer = ''
124
- response.body.each do |chunk|
125
- buffer += chunk
122
+ @connected = true
123
+ @reconnect_attempts = 0
124
+ @on_connect.call
125
+ start_heartbeat
126
+
127
+ # Read SSE stream using readpartial (body.each doesn't work properly)
128
+ buffer = ''
129
+ loop do
130
+ chunk = response.body.readpartial
131
+ break unless chunk # Connection closed
132
+
133
+ buffer += chunk
126
134
 
127
- # Process complete SSE messages (ending with \n\n)
128
- while buffer.include?("\n\n")
129
- message, buffer = buffer.split("\n\n", 2)
130
- process_sse_message(message)
131
- end
135
+ # Process complete SSE messages (ending with \n\n)
136
+ while buffer.include?("\n\n")
137
+ message, buffer = buffer.split("\n\n", 2)
138
+ process_sse_message(message)
132
139
  end
133
140
  end
134
141
  rescue StandardError => e
@@ -197,8 +204,8 @@ module ToggleCraft
197
204
  log "Max fast reconnection attempts reached, switching to slow mode (every #{interval}s)"
198
205
  else
199
206
  @reconnect_attempts += 1
200
- interval = [@options[:reconnect_interval] * (2**(@reconnect_attempts - 1)),
201
- @options[:max_reconnect_interval]].min
207
+ interval = [ @options[:reconnect_interval] * (2**(@reconnect_attempts - 1)),
208
+ @options[:max_reconnect_interval] ].min
202
209
  log "Scheduling reconnection attempt #{@reconnect_attempts}/" \
203
210
  "#{@options[:max_reconnect_attempts]} in #{interval}s"
204
211
  end
@@ -256,7 +263,8 @@ module ToggleCraft
256
263
  end
257
264
 
258
265
  begin
259
- heartbeat_url = "#{@options[:heartbeat_domain]}/api/v1/heartbeat"
266
+ # Heartbeat always goes to production API (hardcoded)
267
+ heartbeat_url = "https://togglecraft.io/api/v1/heartbeat"
260
268
  log "Sending heartbeat to #{heartbeat_url}"
261
269
 
262
270
  response = HTTP.timeout(10)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ToggleCraft
4
- VERSION = '1.0.0'
4
+ VERSION = '1.0.2'
5
5
  end
@@ -30,12 +30,12 @@ RSpec.describe ToggleCraft::CacheAdapters::MemoryAdapter do
30
30
  adapter.set('string', 'text')
31
31
  adapter.set('number', 123)
32
32
  adapter.set('hash', { foo: 'bar' })
33
- adapter.set('array', [1, 2, 3])
33
+ adapter.set('array', [ 1, 2, 3 ])
34
34
 
35
35
  expect(adapter.get('string')).to eq('text')
36
36
  expect(adapter.get('number')).to eq(123)
37
37
  expect(adapter.get('hash')).to eq({ foo: 'bar' })
38
- expect(adapter.get('array')).to eq([1, 2, 3])
38
+ expect(adapter.get('array')).to eq([ 1, 2, 3 ])
39
39
  end
40
40
  end
41
41
 
@@ -48,13 +48,13 @@ RSpec.describe ToggleCraft::Cache do
48
48
  cache.set('string', 'text')
49
49
  cache.set('number', 123)
50
50
  cache.set('hash', { foo: 'bar' })
51
- cache.set('array', [1, 2, 3])
51
+ cache.set('array', [ 1, 2, 3 ])
52
52
  cache.set('boolean', true)
53
53
 
54
54
  expect(cache.get('string')).to eq('text')
55
55
  expect(cache.get('number')).to eq(123)
56
56
  expect(cache.get('hash')).to eq({ foo: 'bar' })
57
- expect(cache.get('array')).to eq([1, 2, 3])
57
+ expect(cache.get('array')).to eq([ 1, 2, 3 ])
58
58
  expect(cache.get('boolean')).to be true
59
59
  end
60
60
  end
@@ -203,7 +203,7 @@ RSpec.describe ToggleCraft::Client do
203
203
  it 'accepts context for evaluation' do
204
204
  context = { user: { id: '123', role: 'admin' } }
205
205
  result = client.enabled?('boolean-flag', context)
206
- expect([true, false]).to include(result)
206
+ expect([ true, false ]).to include(result)
207
207
  end
208
208
 
209
209
  it 'caches evaluation results when cache is enabled' do
@@ -261,7 +261,7 @@ RSpec.describe ToggleCraft::Client do
261
261
  it 'evaluates percentage flags' do
262
262
  context = { user: { id: '123' } }
263
263
  result = client.in_percentage?('percentage-flag', context)
264
- expect([true, false]).to include(result)
264
+ expect([ true, false ]).to include(result)
265
265
  end
266
266
 
267
267
  it 'returns default value for missing flags' do
@@ -297,7 +297,7 @@ RSpec.describe ToggleCraft::Client do
297
297
  expect(client.evaluate('boolean-flag')).to be true
298
298
  expect(%w[control variant-a]).to include(client.evaluate('multivariate-flag', { user: { id: '123' } }))
299
299
  result = client.evaluate('percentage-flag', { user: { id: '123' } })
300
- expect([true, false]).to include(result)
300
+ expect([ true, false ]).to include(result)
301
301
  end
302
302
 
303
303
  it 'returns default for missing flags' do
@@ -396,7 +396,7 @@ RSpec.describe ToggleCraft::Client do
396
396
  client.send(:emit, :reconnecting)
397
397
  client.send(:emit, :rollout_stage_changed, {})
398
398
 
399
- expect(events_received).to eq([:ready, :flags_updated, :error, :disconnected, :reconnecting, :rollout_stage_changed])
399
+ expect(events_received).to eq([ :ready, :flags_updated, :error, :disconnected, :reconnecting, :rollout_stage_changed ])
400
400
  end
401
401
 
402
402
  it 'handles errors in event listeners gracefully' do
@@ -41,7 +41,7 @@ RSpec.describe ToggleCraft::Evaluator do
41
41
  rules: [
42
42
  {
43
43
  conditions: [
44
- { attribute: 'user.role', operator: 'equals', values: ['admin'] }
44
+ { attribute: 'user.role', operator: 'equals', values: [ 'admin' ] }
45
45
  ],
46
46
  value: true
47
47
  }
@@ -110,7 +110,7 @@ RSpec.describe ToggleCraft::Evaluator do
110
110
  rules: [
111
111
  {
112
112
  conditions: [
113
- { attribute: 'user.plan', operator: 'equals', values: ['premium'] }
113
+ { attribute: 'user.plan', operator: 'equals', values: [ 'premium' ] }
114
114
  ],
115
115
  variant: 'premium-variant'
116
116
  }
@@ -179,7 +179,7 @@ RSpec.describe ToggleCraft::Evaluator do
179
179
  rules: [
180
180
  {
181
181
  conditions: [
182
- { attribute: 'user.beta_tester', operator: 'equals', values: ['true'] }
182
+ { attribute: 'user.beta_tester', operator: 'equals', values: [ 'true' ] }
183
183
  ],
184
184
  enabled: true
185
185
  }
@@ -301,8 +301,8 @@ RSpec.describe ToggleCraft::Evaluator do
301
301
  it 'returns true when all AND conditions pass' do
302
302
  rule = {
303
303
  conditions: [
304
- { attribute: 'user.role', operator: 'equals', values: ['admin'], combinator: 'AND' },
305
- { attribute: 'user.active', operator: 'equals', values: ['true'], combinator: 'AND' }
304
+ { attribute: 'user.role', operator: 'equals', values: [ 'admin' ], combinator: 'AND' },
305
+ { attribute: 'user.active', operator: 'equals', values: [ 'true' ], combinator: 'AND' }
306
306
  ]
307
307
  }
308
308
  context = { user: { role: 'admin', active: 'true' } }
@@ -313,8 +313,8 @@ RSpec.describe ToggleCraft::Evaluator do
313
313
  it 'returns false when any AND condition fails' do
314
314
  rule = {
315
315
  conditions: [
316
- { attribute: 'user.role', operator: 'equals', values: ['admin'], combinator: 'AND' },
317
- { attribute: 'user.active', operator: 'equals', values: ['true'], combinator: 'AND' }
316
+ { attribute: 'user.role', operator: 'equals', values: [ 'admin' ], combinator: 'AND' },
317
+ { attribute: 'user.active', operator: 'equals', values: [ 'true' ], combinator: 'AND' }
318
318
  ]
319
319
  }
320
320
  context = { user: { role: 'admin', active: 'false' } }
@@ -325,8 +325,8 @@ RSpec.describe ToggleCraft::Evaluator do
325
325
  it 'returns true when any OR condition passes' do
326
326
  rule = {
327
327
  conditions: [
328
- { attribute: 'user.role', operator: 'equals', values: ['admin'], combinator: 'OR' },
329
- { attribute: 'user.role', operator: 'equals', values: ['superuser'], combinator: 'OR' }
328
+ { attribute: 'user.role', operator: 'equals', values: [ 'admin' ], combinator: 'OR' },
329
+ { attribute: 'user.role', operator: 'equals', values: [ 'superuser' ], combinator: 'OR' }
330
330
  ]
331
331
  }
332
332
  context = { user: { role: 'superuser' } }
@@ -337,9 +337,9 @@ RSpec.describe ToggleCraft::Evaluator do
337
337
  it 'handles mixed AND/OR combinators' do
338
338
  rule = {
339
339
  conditions: [
340
- { attribute: 'user.role', operator: 'equals', values: ['admin'], combinator: 'OR' },
341
- { attribute: 'user.department', operator: 'equals', values: ['engineering'], combinator: 'AND' },
342
- { attribute: 'user.level', operator: 'gt', values: ['5'] }
340
+ { attribute: 'user.role', operator: 'equals', values: [ 'admin' ], combinator: 'OR' },
341
+ { attribute: 'user.department', operator: 'equals', values: [ 'engineering' ], combinator: 'AND' },
342
+ { attribute: 'user.level', operator: 'gt', values: [ '5' ] }
343
343
  ]
344
344
  }
345
345
 
@@ -312,7 +312,7 @@ RSpec.describe ToggleCraft::SSEConnection do
312
312
  )
313
313
 
314
314
  conn.instance_variable_get(:@on_message).call({ test: 'data' })
315
- expect(messages).to eq([{ test: 'data' }])
315
+ expect(messages).to eq([ { test: 'data' } ])
316
316
 
317
317
  conn.disconnect
318
318
  end
@@ -230,75 +230,75 @@ RSpec.describe ToggleCraft::Utils do
230
230
  describe '.evaluate_operator' do
231
231
  describe 'equals operator' do
232
232
  it 'returns true when values are equal (case-insensitive)' do
233
- expect(described_class.evaluate_operator('test', 'equals', ['test'])).to be true
234
- expect(described_class.evaluate_operator('Test', 'equals', ['test'])).to be true
235
- expect(described_class.evaluate_operator('TEST', 'equals', ['test'])).to be true
233
+ expect(described_class.evaluate_operator('test', 'equals', [ 'test' ])).to be true
234
+ expect(described_class.evaluate_operator('Test', 'equals', [ 'test' ])).to be true
235
+ expect(described_class.evaluate_operator('TEST', 'equals', [ 'test' ])).to be true
236
236
  end
237
237
 
238
238
  it 'returns false when values are not equal' do
239
- expect(described_class.evaluate_operator('test', 'equals', ['other'])).to be false
239
+ expect(described_class.evaluate_operator('test', 'equals', [ 'other' ])).to be false
240
240
  end
241
241
 
242
242
  it 'handles numeric values' do
243
- expect(described_class.evaluate_operator(123, 'equals', [123])).to be true
244
- expect(described_class.evaluate_operator(123, 'equals', ['123'])).to be true
243
+ expect(described_class.evaluate_operator(123, 'equals', [ 123 ])).to be true
244
+ expect(described_class.evaluate_operator(123, 'equals', [ '123' ])).to be true
245
245
  end
246
246
  end
247
247
 
248
248
  describe 'not_equals operator' do
249
249
  it 'returns false when values are equal' do
250
- expect(described_class.evaluate_operator('test', 'not_equals', ['test'])).to be false
250
+ expect(described_class.evaluate_operator('test', 'not_equals', [ 'test' ])).to be false
251
251
  end
252
252
 
253
253
  it 'returns true when values are not equal' do
254
- expect(described_class.evaluate_operator('test', 'not_equals', ['other'])).to be true
254
+ expect(described_class.evaluate_operator('test', 'not_equals', [ 'other' ])).to be true
255
255
  end
256
256
 
257
257
  it 'returns true for nil values' do
258
- expect(described_class.evaluate_operator(nil, 'not_equals', ['test'])).to be true
258
+ expect(described_class.evaluate_operator(nil, 'not_equals', [ 'test' ])).to be true
259
259
  end
260
260
  end
261
261
 
262
262
  describe 'contains operator' do
263
263
  it 'returns true when attribute contains value' do
264
- expect(described_class.evaluate_operator('hello world', 'contains', ['world'])).to be true
265
- expect(described_class.evaluate_operator('HELLO WORLD', 'contains', ['world'])).to be true
264
+ expect(described_class.evaluate_operator('hello world', 'contains', [ 'world' ])).to be true
265
+ expect(described_class.evaluate_operator('HELLO WORLD', 'contains', [ 'world' ])).to be true
266
266
  end
267
267
 
268
268
  it 'returns false when attribute does not contain value' do
269
- expect(described_class.evaluate_operator('hello world', 'contains', ['xyz'])).to be false
269
+ expect(described_class.evaluate_operator('hello world', 'contains', [ 'xyz' ])).to be false
270
270
  end
271
271
  end
272
272
 
273
273
  describe 'not_contains operator' do
274
274
  it 'returns false when attribute contains value' do
275
- expect(described_class.evaluate_operator('hello world', 'not_contains', ['world'])).to be false
275
+ expect(described_class.evaluate_operator('hello world', 'not_contains', [ 'world' ])).to be false
276
276
  end
277
277
 
278
278
  it 'returns true when attribute does not contain value' do
279
- expect(described_class.evaluate_operator('hello world', 'not_contains', ['xyz'])).to be true
279
+ expect(described_class.evaluate_operator('hello world', 'not_contains', [ 'xyz' ])).to be true
280
280
  end
281
281
  end
282
282
 
283
283
  describe 'starts_with operator' do
284
284
  it 'returns true when attribute starts with value' do
285
- expect(described_class.evaluate_operator('hello world', 'starts_with', ['hello'])).to be true
286
- expect(described_class.evaluate_operator('HELLO WORLD', 'starts_with', ['hello'])).to be true
285
+ expect(described_class.evaluate_operator('hello world', 'starts_with', [ 'hello' ])).to be true
286
+ expect(described_class.evaluate_operator('HELLO WORLD', 'starts_with', [ 'hello' ])).to be true
287
287
  end
288
288
 
289
289
  it 'returns false when attribute does not start with value' do
290
- expect(described_class.evaluate_operator('hello world', 'starts_with', ['world'])).to be false
290
+ expect(described_class.evaluate_operator('hello world', 'starts_with', [ 'world' ])).to be false
291
291
  end
292
292
  end
293
293
 
294
294
  describe 'ends_with operator' do
295
295
  it 'returns true when attribute ends with value' do
296
- expect(described_class.evaluate_operator('hello world', 'ends_with', ['world'])).to be true
297
- expect(described_class.evaluate_operator('HELLO WORLD', 'ends_with', ['world'])).to be true
296
+ expect(described_class.evaluate_operator('hello world', 'ends_with', [ 'world' ])).to be true
297
+ expect(described_class.evaluate_operator('HELLO WORLD', 'ends_with', [ 'world' ])).to be true
298
298
  end
299
299
 
300
300
  it 'returns false when attribute does not end with value' do
301
- expect(described_class.evaluate_operator('hello world', 'ends_with', ['hello'])).to be false
301
+ expect(described_class.evaluate_operator('hello world', 'ends_with', [ 'hello' ])).to be false
302
302
  end
303
303
  end
304
304
 
@@ -313,8 +313,8 @@ RSpec.describe ToggleCraft::Utils do
313
313
  end
314
314
 
315
315
  it 'handles numeric values' do
316
- expect(described_class.evaluate_operator(1, 'in', [1, 2, 3])).to be true
317
- expect(described_class.evaluate_operator(4, 'in', [1, 2, 3])).to be false
316
+ expect(described_class.evaluate_operator(1, 'in', [ 1, 2, 3 ])).to be true
317
+ expect(described_class.evaluate_operator(4, 'in', [ 1, 2, 3 ])).to be false
318
318
  end
319
319
  end
320
320
 
@@ -334,170 +334,170 @@ RSpec.describe ToggleCraft::Utils do
334
334
 
335
335
  describe 'gt operator' do
336
336
  it 'returns true when attribute is greater than value' do
337
- expect(described_class.evaluate_operator(10, 'gt', [5])).to be true
338
- expect(described_class.evaluate_operator(5.5, 'gt', [5])).to be true
337
+ expect(described_class.evaluate_operator(10, 'gt', [ 5 ])).to be true
338
+ expect(described_class.evaluate_operator(5.5, 'gt', [ 5 ])).to be true
339
339
  end
340
340
 
341
341
  it 'returns false when attribute is less than or equal to value' do
342
- expect(described_class.evaluate_operator(5, 'gt', [10])).to be false
343
- expect(described_class.evaluate_operator(5, 'gt', [5])).to be false
342
+ expect(described_class.evaluate_operator(5, 'gt', [ 10 ])).to be false
343
+ expect(described_class.evaluate_operator(5, 'gt', [ 5 ])).to be false
344
344
  end
345
345
 
346
346
  it 'handles string numeric values' do
347
- expect(described_class.evaluate_operator('10', 'gt', ['5'])).to be true
347
+ expect(described_class.evaluate_operator('10', 'gt', [ '5' ])).to be true
348
348
  end
349
349
  end
350
350
 
351
351
  describe 'gte operator' do
352
352
  it 'returns true when attribute is greater than or equal to value' do
353
- expect(described_class.evaluate_operator(10, 'gte', [5])).to be true
354
- expect(described_class.evaluate_operator(5, 'gte', [5])).to be true
353
+ expect(described_class.evaluate_operator(10, 'gte', [ 5 ])).to be true
354
+ expect(described_class.evaluate_operator(5, 'gte', [ 5 ])).to be true
355
355
  end
356
356
 
357
357
  it 'returns false when attribute is less than value' do
358
- expect(described_class.evaluate_operator(5, 'gte', [10])).to be false
358
+ expect(described_class.evaluate_operator(5, 'gte', [ 10 ])).to be false
359
359
  end
360
360
  end
361
361
 
362
362
  describe 'lt operator' do
363
363
  it 'returns true when attribute is less than value' do
364
- expect(described_class.evaluate_operator(5, 'lt', [10])).to be true
364
+ expect(described_class.evaluate_operator(5, 'lt', [ 10 ])).to be true
365
365
  end
366
366
 
367
367
  it 'returns false when attribute is greater than or equal to value' do
368
- expect(described_class.evaluate_operator(10, 'lt', [5])).to be false
369
- expect(described_class.evaluate_operator(5, 'lt', [5])).to be false
368
+ expect(described_class.evaluate_operator(10, 'lt', [ 5 ])).to be false
369
+ expect(described_class.evaluate_operator(5, 'lt', [ 5 ])).to be false
370
370
  end
371
371
  end
372
372
 
373
373
  describe 'lte operator' do
374
374
  it 'returns true when attribute is less than or equal to value' do
375
- expect(described_class.evaluate_operator(5, 'lte', [10])).to be true
376
- expect(described_class.evaluate_operator(5, 'lte', [5])).to be true
375
+ expect(described_class.evaluate_operator(5, 'lte', [ 10 ])).to be true
376
+ expect(described_class.evaluate_operator(5, 'lte', [ 5 ])).to be true
377
377
  end
378
378
 
379
379
  it 'returns false when attribute is greater than value' do
380
- expect(described_class.evaluate_operator(10, 'lte', [5])).to be false
380
+ expect(described_class.evaluate_operator(10, 'lte', [ 5 ])).to be false
381
381
  end
382
382
  end
383
383
 
384
384
  describe 'between operator' do
385
385
  it 'returns true when attribute is between min and max (inclusive)' do
386
- expect(described_class.evaluate_operator(5, 'between', [1, 10])).to be true
387
- expect(described_class.evaluate_operator(1, 'between', [1, 10])).to be true
388
- expect(described_class.evaluate_operator(10, 'between', [1, 10])).to be true
386
+ expect(described_class.evaluate_operator(5, 'between', [ 1, 10 ])).to be true
387
+ expect(described_class.evaluate_operator(1, 'between', [ 1, 10 ])).to be true
388
+ expect(described_class.evaluate_operator(10, 'between', [ 1, 10 ])).to be true
389
389
  end
390
390
 
391
391
  it 'returns false when attribute is outside range' do
392
- expect(described_class.evaluate_operator(0, 'between', [1, 10])).to be false
393
- expect(described_class.evaluate_operator(11, 'between', [1, 10])).to be false
392
+ expect(described_class.evaluate_operator(0, 'between', [ 1, 10 ])).to be false
393
+ expect(described_class.evaluate_operator(11, 'between', [ 1, 10 ])).to be false
394
394
  end
395
395
 
396
396
  it 'returns false when condition values are insufficient' do
397
- expect(described_class.evaluate_operator(5, 'between', [1])).to be false
397
+ expect(described_class.evaluate_operator(5, 'between', [ 1 ])).to be false
398
398
  end
399
399
 
400
400
  it 'handles decimal values' do
401
- expect(described_class.evaluate_operator(5.5, 'between', [5, 6])).to be true
401
+ expect(described_class.evaluate_operator(5.5, 'between', [ 5, 6 ])).to be true
402
402
  end
403
403
  end
404
404
 
405
405
  describe 'regex operator' do
406
406
  it 'returns true when attribute matches regex' do
407
- expect(described_class.evaluate_operator('test123', 'regex', ['^test\\d+$'])).to be true
408
- expect(described_class.evaluate_operator('user@example.com', 'regex', ['^[^@]+@[^@]+$'])).to be true
407
+ expect(described_class.evaluate_operator('test123', 'regex', [ '^test\\d+$' ])).to be true
408
+ expect(described_class.evaluate_operator('user@example.com', 'regex', [ '^[^@]+@[^@]+$' ])).to be true
409
409
  end
410
410
 
411
411
  it 'returns false when attribute does not match regex' do
412
- expect(described_class.evaluate_operator('test', 'regex', ['^\\d+$'])).to be false
412
+ expect(described_class.evaluate_operator('test', 'regex', [ '^\\d+$' ])).to be false
413
413
  end
414
414
 
415
415
  it 'returns false for invalid regex' do
416
- expect(described_class.evaluate_operator('test', 'regex', ['[invalid'])).to be false
416
+ expect(described_class.evaluate_operator('test', 'regex', [ '[invalid' ])).to be false
417
417
  end
418
418
 
419
419
  it 'handles complex patterns' do
420
- expect(described_class.evaluate_operator('192.168.1.1', 'regex', ['^\\d+\\.\\d+\\.\\d+\\.\\d+$'])).to be true
420
+ expect(described_class.evaluate_operator('192.168.1.1', 'regex', [ '^\\d+\\.\\d+\\.\\d+\\.\\d+$' ])).to be true
421
421
  end
422
422
  end
423
423
 
424
424
  describe 'semver operators' do
425
425
  describe 'semver_eq' do
426
426
  it 'returns true for equal versions' do
427
- expect(described_class.evaluate_operator('1.2.3', 'semver_eq', ['1.2.3'])).to be true
427
+ expect(described_class.evaluate_operator('1.2.3', 'semver_eq', [ '1.2.3' ])).to be true
428
428
  end
429
429
 
430
430
  it 'returns false for different versions' do
431
- expect(described_class.evaluate_operator('1.2.3', 'semver_eq', ['1.2.4'])).to be false
431
+ expect(described_class.evaluate_operator('1.2.3', 'semver_eq', [ '1.2.4' ])).to be false
432
432
  end
433
433
  end
434
434
 
435
435
  describe 'semver_gt' do
436
436
  it 'returns true when attribute version is greater' do
437
- expect(described_class.evaluate_operator('2.0.0', 'semver_gt', ['1.0.0'])).to be true
438
- expect(described_class.evaluate_operator('1.1.0', 'semver_gt', ['1.0.0'])).to be true
437
+ expect(described_class.evaluate_operator('2.0.0', 'semver_gt', [ '1.0.0' ])).to be true
438
+ expect(described_class.evaluate_operator('1.1.0', 'semver_gt', [ '1.0.0' ])).to be true
439
439
  end
440
440
 
441
441
  it 'returns false when attribute version is less or equal' do
442
- expect(described_class.evaluate_operator('1.0.0', 'semver_gt', ['2.0.0'])).to be false
443
- expect(described_class.evaluate_operator('1.0.0', 'semver_gt', ['1.0.0'])).to be false
442
+ expect(described_class.evaluate_operator('1.0.0', 'semver_gt', [ '2.0.0' ])).to be false
443
+ expect(described_class.evaluate_operator('1.0.0', 'semver_gt', [ '1.0.0' ])).to be false
444
444
  end
445
445
  end
446
446
 
447
447
  describe 'semver_gte' do
448
448
  it 'returns true when attribute version is greater or equal' do
449
- expect(described_class.evaluate_operator('2.0.0', 'semver_gte', ['1.0.0'])).to be true
450
- expect(described_class.evaluate_operator('1.0.0', 'semver_gte', ['1.0.0'])).to be true
449
+ expect(described_class.evaluate_operator('2.0.0', 'semver_gte', [ '1.0.0' ])).to be true
450
+ expect(described_class.evaluate_operator('1.0.0', 'semver_gte', [ '1.0.0' ])).to be true
451
451
  end
452
452
 
453
453
  it 'returns false when attribute version is less' do
454
- expect(described_class.evaluate_operator('1.0.0', 'semver_gte', ['2.0.0'])).to be false
454
+ expect(described_class.evaluate_operator('1.0.0', 'semver_gte', [ '2.0.0' ])).to be false
455
455
  end
456
456
  end
457
457
 
458
458
  describe 'semver_lt' do
459
459
  it 'returns true when attribute version is less' do
460
- expect(described_class.evaluate_operator('1.0.0', 'semver_lt', ['2.0.0'])).to be true
460
+ expect(described_class.evaluate_operator('1.0.0', 'semver_lt', [ '2.0.0' ])).to be true
461
461
  end
462
462
 
463
463
  it 'returns false when attribute version is greater or equal' do
464
- expect(described_class.evaluate_operator('2.0.0', 'semver_lt', ['1.0.0'])).to be false
465
- expect(described_class.evaluate_operator('1.0.0', 'semver_lt', ['1.0.0'])).to be false
464
+ expect(described_class.evaluate_operator('2.0.0', 'semver_lt', [ '1.0.0' ])).to be false
465
+ expect(described_class.evaluate_operator('1.0.0', 'semver_lt', [ '1.0.0' ])).to be false
466
466
  end
467
467
  end
468
468
 
469
469
  describe 'semver_lte' do
470
470
  it 'returns true when attribute version is less or equal' do
471
- expect(described_class.evaluate_operator('1.0.0', 'semver_lte', ['2.0.0'])).to be true
472
- expect(described_class.evaluate_operator('1.0.0', 'semver_lte', ['1.0.0'])).to be true
471
+ expect(described_class.evaluate_operator('1.0.0', 'semver_lte', [ '2.0.0' ])).to be true
472
+ expect(described_class.evaluate_operator('1.0.0', 'semver_lte', [ '1.0.0' ])).to be true
473
473
  end
474
474
 
475
475
  it 'returns false when attribute version is greater' do
476
- expect(described_class.evaluate_operator('2.0.0', 'semver_lte', ['1.0.0'])).to be false
476
+ expect(described_class.evaluate_operator('2.0.0', 'semver_lte', [ '1.0.0' ])).to be false
477
477
  end
478
478
  end
479
479
  end
480
480
 
481
481
  describe 'nil attribute handling' do
482
482
  it 'returns false for most operators when attribute is nil' do
483
- expect(described_class.evaluate_operator(nil, 'equals', ['test'])).to be false
484
- expect(described_class.evaluate_operator(nil, 'contains', ['test'])).to be false
485
- expect(described_class.evaluate_operator(nil, 'in', ['test'])).to be false
483
+ expect(described_class.evaluate_operator(nil, 'equals', [ 'test' ])).to be false
484
+ expect(described_class.evaluate_operator(nil, 'contains', [ 'test' ])).to be false
485
+ expect(described_class.evaluate_operator(nil, 'in', [ 'test' ])).to be false
486
486
  end
487
487
 
488
488
  it 'returns true for not_equals when attribute is nil' do
489
- expect(described_class.evaluate_operator(nil, 'not_equals', ['test'])).to be true
489
+ expect(described_class.evaluate_operator(nil, 'not_equals', [ 'test' ])).to be true
490
490
  end
491
491
 
492
492
  it 'returns true for not_in when attribute is nil' do
493
- expect(described_class.evaluate_operator(nil, 'not_in', ['test'])).to be true
493
+ expect(described_class.evaluate_operator(nil, 'not_in', [ 'test' ])).to be true
494
494
  end
495
495
  end
496
496
 
497
497
  describe 'unknown operator' do
498
498
  it 'returns false and warns for unknown operator' do
499
499
  expect do
500
- result = described_class.evaluate_operator('test', 'unknown_op', ['value'])
500
+ result = described_class.evaluate_operator('test', 'unknown_op', [ 'value' ])
501
501
  expect(result).to be false
502
502
  end.to output(/Unknown operator: unknown_op/).to_stderr
503
503
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: togglecraft
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - ToggleCraft