togglecraft 1.0.1 → 1.0.3
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 +4 -4
- data/lib/togglecraft/cache.rb +3 -3
- data/lib/togglecraft/client.rb +7 -3
- data/lib/togglecraft/evaluator.rb +2 -2
- data/lib/togglecraft/shared_sse_connection.rb +0 -1
- data/lib/togglecraft/sse_connection.rb +8 -7
- data/lib/togglecraft/version.rb +1 -1
- data/spec/togglecraft/cache/memory_adapter_spec.rb +2 -2
- data/spec/togglecraft/cache_spec.rb +2 -2
- data/spec/togglecraft/client_spec.rb +4 -4
- data/spec/togglecraft/evaluator_spec.rb +12 -12
- data/spec/togglecraft/sse_connection_spec.rb +1 -1
- data/spec/togglecraft/utils_spec.rb +69 -69
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 33360223d2f26e037faa039bc9c8033cda78f7597e90dac2b2998e9a921daab7
|
|
4
|
+
data.tar.gz: 8522409a6d3d4b8e6ed49a7db8da1ef8bba17f4c498523b9c9a2c0c48440ef6b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 92920a17fdadae7951231d95455e1a4925d3c1be1db3beef2286c389208e6ff989126665605e5b7fcf49c7b9056a1a7785aaf62a5be9addd605cae36842734ee
|
|
7
|
+
data.tar.gz: 44f8e14db2f45b284396b774291394df7a5d904bd596f6d94c3d5dab0c65d9b6700585416a9f8b1f3cddf18e4389f66fae360f509e7368acd1f2e8164695fc50
|
data/lib/togglecraft/cache.rb
CHANGED
|
@@ -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
|
-
|
|
16
|
+
when :memory
|
|
17
17
|
CacheAdapters::MemoryAdapter.new
|
|
18
|
-
|
|
18
|
+
else
|
|
19
19
|
adapter
|
|
20
|
-
|
|
20
|
+
end
|
|
21
21
|
@cleanup_thread = nil
|
|
22
22
|
start_cleanup
|
|
23
23
|
end
|
data/lib/togglecraft/client.rb
CHANGED
|
@@ -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
|
-
|
|
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)
|
|
@@ -428,6 +429,9 @@ module ToggleCraft
|
|
|
428
429
|
# Update cache
|
|
429
430
|
@cache&.set_all_flags(payload[:flags] || {})
|
|
430
431
|
|
|
432
|
+
# Clear evaluation cache for all updated flags
|
|
433
|
+
clear_evaluation_cache_for_flags(payload[:flags]&.keys || [])
|
|
434
|
+
|
|
431
435
|
# Initialize rollout stage tracking
|
|
432
436
|
initialize_rollout_stage_tracking
|
|
433
437
|
|
|
@@ -187,7 +187,7 @@ module ToggleCraft
|
|
|
187
187
|
|
|
188
188
|
result = if result.nil?
|
|
189
189
|
condition_result
|
|
190
|
-
|
|
190
|
+
else
|
|
191
191
|
# Apply combinator logic
|
|
192
192
|
case previous_combinator
|
|
193
193
|
when 'OR'
|
|
@@ -196,7 +196,7 @@ module ToggleCraft
|
|
|
196
196
|
# Default to AND
|
|
197
197
|
result && condition_result
|
|
198
198
|
end
|
|
199
|
-
|
|
199
|
+
end
|
|
200
200
|
|
|
201
201
|
previous_combinator = condition[:combinator] || 'AND'
|
|
202
202
|
|
|
@@ -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),
|
|
@@ -33,10 +33,10 @@ module ToggleCraft
|
|
|
33
33
|
@should_reconnect = true
|
|
34
34
|
|
|
35
35
|
# Callbacks
|
|
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 {}
|
|
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 { }
|
|
40
40
|
|
|
41
41
|
# Threads
|
|
42
42
|
@connection_thread = nil
|
|
@@ -204,8 +204,8 @@ module ToggleCraft
|
|
|
204
204
|
log "Max fast reconnection attempts reached, switching to slow mode (every #{interval}s)"
|
|
205
205
|
else
|
|
206
206
|
@reconnect_attempts += 1
|
|
207
|
-
interval = [@options[:reconnect_interval] * (2**(@reconnect_attempts - 1)),
|
|
208
|
-
@options[:max_reconnect_interval]].min
|
|
207
|
+
interval = [ @options[:reconnect_interval] * (2**(@reconnect_attempts - 1)),
|
|
208
|
+
@options[:max_reconnect_interval] ].min
|
|
209
209
|
log "Scheduling reconnection attempt #{@reconnect_attempts}/" \
|
|
210
210
|
"#{@options[:max_reconnect_attempts]} in #{interval}s"
|
|
211
211
|
end
|
|
@@ -263,7 +263,8 @@ module ToggleCraft
|
|
|
263
263
|
end
|
|
264
264
|
|
|
265
265
|
begin
|
|
266
|
-
|
|
266
|
+
# Heartbeat always goes to production API (hardcoded)
|
|
267
|
+
heartbeat_url = "https://togglecraft.io/api/v1/heartbeat"
|
|
267
268
|
log "Sending heartbeat to #{heartbeat_url}"
|
|
268
269
|
|
|
269
270
|
response = HTTP.timeout(10)
|
data/lib/togglecraft/version.rb
CHANGED
|
@@ -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
|
|
|
@@ -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
|