togglecraft 1.0.0
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 +7 -0
- data/CHANGELOG.md +108 -0
- data/LICENSE +18 -0
- data/README.md +347 -0
- data/lib/togglecraft/cache.rb +151 -0
- data/lib/togglecraft/cache_adapters/memory_adapter.rb +54 -0
- data/lib/togglecraft/client.rb +568 -0
- data/lib/togglecraft/connection_pool.rb +134 -0
- data/lib/togglecraft/evaluator.rb +309 -0
- data/lib/togglecraft/shared_sse_connection.rb +266 -0
- data/lib/togglecraft/sse_connection.rb +296 -0
- data/lib/togglecraft/utils.rb +179 -0
- data/lib/togglecraft/version.rb +5 -0
- data/lib/togglecraft.rb +19 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/togglecraft/cache/memory_adapter_spec.rb +128 -0
- data/spec/togglecraft/cache_spec.rb +274 -0
- data/spec/togglecraft/client_spec.rb +728 -0
- data/spec/togglecraft/connection_pool_spec.rb +178 -0
- data/spec/togglecraft/evaluator_spec.rb +443 -0
- data/spec/togglecraft/shared_sse_connection_spec.rb +585 -0
- data/spec/togglecraft/sse_connection_spec.rb +691 -0
- data/spec/togglecraft/utils_spec.rb +506 -0
- metadata +151 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe ToggleCraft::Utils do
|
|
4
|
+
describe '.hash_key' do
|
|
5
|
+
it 'returns a value between 0 and 100' do
|
|
6
|
+
result = described_class.hash_key('test-key')
|
|
7
|
+
expect(result).to be >= 0
|
|
8
|
+
expect(result).to be < 100
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'returns consistent results for the same key' do
|
|
12
|
+
key = 'user-123:new-feature'
|
|
13
|
+
result1 = described_class.hash_key(key)
|
|
14
|
+
result2 = described_class.hash_key(key)
|
|
15
|
+
expect(result1).to eq(result2)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'returns different results for different keys' do
|
|
19
|
+
result1 = described_class.hash_key('key1')
|
|
20
|
+
result2 = described_class.hash_key('key2')
|
|
21
|
+
expect(result1).not_to eq(result2)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'handles empty strings' do
|
|
25
|
+
result = described_class.hash_key('')
|
|
26
|
+
expect(result).to be >= 0
|
|
27
|
+
expect(result).to be < 100
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'handles special characters' do
|
|
31
|
+
result = described_class.hash_key('user@example.com:flag-123')
|
|
32
|
+
expect(result).to be >= 0
|
|
33
|
+
expect(result).to be < 100
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Test known values to ensure consistency with JS implementation
|
|
37
|
+
it 'matches JavaScript implementation for known values' do
|
|
38
|
+
# These values should match the JavaScript implementation
|
|
39
|
+
# Testing the hash algorithm produces consistent results
|
|
40
|
+
result = described_class.hash_key('test')
|
|
41
|
+
expect(result).to be_a(Integer)
|
|
42
|
+
expect(result).to be >= 0
|
|
43
|
+
expect(result).to be < 100
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe '.get_nested_property' do
|
|
48
|
+
let(:obj) do
|
|
49
|
+
{
|
|
50
|
+
user: {
|
|
51
|
+
id: '123',
|
|
52
|
+
email: 'test@example.com',
|
|
53
|
+
profile: {
|
|
54
|
+
name: 'Test User'
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
request: {
|
|
58
|
+
ip: '192.168.1.1'
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'retrieves top-level properties with symbol keys' do
|
|
64
|
+
result = described_class.get_nested_property(obj, 'user')
|
|
65
|
+
expect(result).to eq(obj[:user])
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'retrieves nested properties with dot notation' do
|
|
69
|
+
result = described_class.get_nested_property(obj, 'user.id')
|
|
70
|
+
expect(result).to eq('123')
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'retrieves deeply nested properties' do
|
|
74
|
+
result = described_class.get_nested_property(obj, 'user.profile.name')
|
|
75
|
+
expect(result).to eq('Test User')
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'works with string keys in the hash' do
|
|
79
|
+
string_obj = {
|
|
80
|
+
'user' => {
|
|
81
|
+
'id' => '456'
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
result = described_class.get_nested_property(string_obj, 'user.id')
|
|
85
|
+
expect(result).to eq('456')
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'returns nil for missing properties' do
|
|
89
|
+
result = described_class.get_nested_property(obj, 'user.missing')
|
|
90
|
+
expect(result).to be_nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'returns nil for missing nested properties' do
|
|
94
|
+
result = described_class.get_nested_property(obj, 'user.profile.missing.deeply')
|
|
95
|
+
expect(result).to be_nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'returns nil when obj is nil' do
|
|
99
|
+
result = described_class.get_nested_property(nil, 'user.id')
|
|
100
|
+
expect(result).to be_nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it 'returns nil when path is nil' do
|
|
104
|
+
result = described_class.get_nested_property(obj, nil)
|
|
105
|
+
expect(result).to be_nil
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'returns nil when path is empty' do
|
|
109
|
+
result = described_class.get_nested_property(obj, '')
|
|
110
|
+
expect(result).to be_nil
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'handles mixed symbol and string keys' do
|
|
114
|
+
mixed_obj = {
|
|
115
|
+
user: {
|
|
116
|
+
'email' => 'test@example.com'
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
result = described_class.get_nested_property(mixed_obj, 'user.email')
|
|
120
|
+
expect(result).to eq('test@example.com')
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
describe '.compare_semver' do
|
|
125
|
+
it 'returns 0 for equal versions' do
|
|
126
|
+
expect(described_class.compare_semver('1.0.0', '1.0.0')).to eq(0)
|
|
127
|
+
expect(described_class.compare_semver('2.5.3', '2.5.3')).to eq(0)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it 'returns 1 when first version is greater' do
|
|
131
|
+
expect(described_class.compare_semver('2.0.0', '1.0.0')).to eq(1)
|
|
132
|
+
expect(described_class.compare_semver('1.2.0', '1.1.0')).to eq(1)
|
|
133
|
+
expect(described_class.compare_semver('1.0.2', '1.0.1')).to eq(1)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it 'returns -1 when first version is less' do
|
|
137
|
+
expect(described_class.compare_semver('1.0.0', '2.0.0')).to eq(-1)
|
|
138
|
+
expect(described_class.compare_semver('1.1.0', '1.2.0')).to eq(-1)
|
|
139
|
+
expect(described_class.compare_semver('1.0.1', '1.0.2')).to eq(-1)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it 'handles versions with different part counts' do
|
|
143
|
+
expect(described_class.compare_semver('1.0', '1.0.0')).to eq(0)
|
|
144
|
+
expect(described_class.compare_semver('1', '1.0.0')).to eq(0)
|
|
145
|
+
expect(described_class.compare_semver('2.0', '1.5.9')).to eq(1)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it 'handles missing version parts as zero' do
|
|
149
|
+
expect(described_class.compare_semver('1.2', '1.2.0')).to eq(0)
|
|
150
|
+
expect(described_class.compare_semver('1.2', '1.2.1')).to eq(-1)
|
|
151
|
+
expect(described_class.compare_semver('1.3', '1.2.9')).to eq(1)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it 'compares major versions correctly' do
|
|
155
|
+
expect(described_class.compare_semver('10.0.0', '9.0.0')).to eq(1)
|
|
156
|
+
expect(described_class.compare_semver('2.0.0', '10.0.0')).to eq(-1)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it 'compares minor versions correctly' do
|
|
160
|
+
expect(described_class.compare_semver('1.10.0', '1.9.0')).to eq(1)
|
|
161
|
+
expect(described_class.compare_semver('1.2.0', '1.10.0')).to eq(-1)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it 'compares patch versions correctly' do
|
|
165
|
+
expect(described_class.compare_semver('1.0.10', '1.0.9')).to eq(1)
|
|
166
|
+
expect(described_class.compare_semver('1.0.2', '1.0.10')).to eq(-1)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
describe '.generate_jitter' do
|
|
171
|
+
it 'returns a value within the specified range' do
|
|
172
|
+
100.times do
|
|
173
|
+
result = described_class.generate_jitter(1500)
|
|
174
|
+
expect(result).to be >= 0
|
|
175
|
+
expect(result).to be <= 1500
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it 'returns different values on multiple calls' do
|
|
180
|
+
results = 100.times.map { described_class.generate_jitter(1500) }
|
|
181
|
+
unique_results = results.uniq
|
|
182
|
+
# Should have some variety (not all the same)
|
|
183
|
+
expect(unique_results.length).to be > 1
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it 'works with different max values' do
|
|
187
|
+
result = described_class.generate_jitter(5000)
|
|
188
|
+
expect(result).to be >= 0
|
|
189
|
+
expect(result).to be <= 5000
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it 'handles zero max jitter' do
|
|
193
|
+
result = described_class.generate_jitter(0)
|
|
194
|
+
expect(result).to eq(0)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
describe '.create_cache_key' do
|
|
199
|
+
it 'creates a key from flag and user id' do
|
|
200
|
+
context = { user: { id: '123' } }
|
|
201
|
+
result = described_class.create_cache_key('my-flag', context)
|
|
202
|
+
expect(result).to eq('my-flag:123')
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
it 'uses email if id is missing' do
|
|
206
|
+
context = { user: { email: 'test@example.com' } }
|
|
207
|
+
result = described_class.create_cache_key('my-flag', context)
|
|
208
|
+
expect(result).to eq('my-flag:test@example.com')
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it "uses 'anonymous' if both id and email are missing" do
|
|
212
|
+
context = { user: {} }
|
|
213
|
+
result = described_class.create_cache_key('my-flag', context)
|
|
214
|
+
expect(result).to eq('my-flag:anonymous')
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it "uses 'anonymous' if user is missing" do
|
|
218
|
+
context = {}
|
|
219
|
+
result = described_class.create_cache_key('my-flag', context)
|
|
220
|
+
expect(result).to eq('my-flag:anonymous')
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it 'prefers id over email' do
|
|
224
|
+
context = { user: { id: '123', email: 'test@example.com' } }
|
|
225
|
+
result = described_class.create_cache_key('my-flag', context)
|
|
226
|
+
expect(result).to eq('my-flag:123')
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
describe '.evaluate_operator' do
|
|
231
|
+
describe 'equals operator' do
|
|
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
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
it 'returns false when values are not equal' do
|
|
239
|
+
expect(described_class.evaluate_operator('test', 'equals', ['other'])).to be false
|
|
240
|
+
end
|
|
241
|
+
|
|
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
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
describe 'not_equals operator' do
|
|
249
|
+
it 'returns false when values are equal' do
|
|
250
|
+
expect(described_class.evaluate_operator('test', 'not_equals', ['test'])).to be false
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
it 'returns true when values are not equal' do
|
|
254
|
+
expect(described_class.evaluate_operator('test', 'not_equals', ['other'])).to be true
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
it 'returns true for nil values' do
|
|
258
|
+
expect(described_class.evaluate_operator(nil, 'not_equals', ['test'])).to be true
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
describe 'contains operator' do
|
|
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
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
it 'returns false when attribute does not contain value' do
|
|
269
|
+
expect(described_class.evaluate_operator('hello world', 'contains', ['xyz'])).to be false
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
describe 'not_contains operator' do
|
|
274
|
+
it 'returns false when attribute contains value' do
|
|
275
|
+
expect(described_class.evaluate_operator('hello world', 'not_contains', ['world'])).to be false
|
|
276
|
+
end
|
|
277
|
+
|
|
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
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
describe 'starts_with operator' do
|
|
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
|
|
287
|
+
end
|
|
288
|
+
|
|
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
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
describe 'ends_with operator' do
|
|
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
|
|
298
|
+
end
|
|
299
|
+
|
|
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
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
describe 'in operator' do
|
|
306
|
+
it 'returns true when attribute is in the list' do
|
|
307
|
+
expect(described_class.evaluate_operator('admin', 'in', %w[admin user guest])).to be true
|
|
308
|
+
expect(described_class.evaluate_operator('ADMIN', 'in', %w[admin user guest])).to be true
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
it 'returns false when attribute is not in the list' do
|
|
312
|
+
expect(described_class.evaluate_operator('superuser', 'in', %w[admin user guest])).to be false
|
|
313
|
+
end
|
|
314
|
+
|
|
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
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
describe 'not_in operator' do
|
|
322
|
+
it 'returns false when attribute is in the list' do
|
|
323
|
+
expect(described_class.evaluate_operator('admin', 'not_in', %w[admin user guest])).to be false
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
it 'returns true when attribute is not in the list' do
|
|
327
|
+
expect(described_class.evaluate_operator('superuser', 'not_in', %w[admin user guest])).to be true
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
it 'returns true for nil values' do
|
|
331
|
+
expect(described_class.evaluate_operator(nil, 'not_in', %w[admin user guest])).to be true
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
describe 'gt operator' do
|
|
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
|
|
339
|
+
end
|
|
340
|
+
|
|
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
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
it 'handles string numeric values' do
|
|
347
|
+
expect(described_class.evaluate_operator('10', 'gt', ['5'])).to be true
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
describe 'gte operator' do
|
|
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
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
it 'returns false when attribute is less than value' do
|
|
358
|
+
expect(described_class.evaluate_operator(5, 'gte', [10])).to be false
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
describe 'lt operator' do
|
|
363
|
+
it 'returns true when attribute is less than value' do
|
|
364
|
+
expect(described_class.evaluate_operator(5, 'lt', [10])).to be true
|
|
365
|
+
end
|
|
366
|
+
|
|
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
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
describe 'lte operator' do
|
|
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
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
it 'returns false when attribute is greater than value' do
|
|
380
|
+
expect(described_class.evaluate_operator(10, 'lte', [5])).to be false
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
describe 'between operator' do
|
|
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
|
|
389
|
+
end
|
|
390
|
+
|
|
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
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
it 'returns false when condition values are insufficient' do
|
|
397
|
+
expect(described_class.evaluate_operator(5, 'between', [1])).to be false
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
it 'handles decimal values' do
|
|
401
|
+
expect(described_class.evaluate_operator(5.5, 'between', [5, 6])).to be true
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
describe 'regex operator' do
|
|
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
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
it 'returns false when attribute does not match regex' do
|
|
412
|
+
expect(described_class.evaluate_operator('test', 'regex', ['^\\d+$'])).to be false
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
it 'returns false for invalid regex' do
|
|
416
|
+
expect(described_class.evaluate_operator('test', 'regex', ['[invalid'])).to be false
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
it 'handles complex patterns' do
|
|
420
|
+
expect(described_class.evaluate_operator('192.168.1.1', 'regex', ['^\\d+\\.\\d+\\.\\d+\\.\\d+$'])).to be true
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
describe 'semver operators' do
|
|
425
|
+
describe 'semver_eq' do
|
|
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
|
|
428
|
+
end
|
|
429
|
+
|
|
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
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
describe 'semver_gt' do
|
|
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
|
|
439
|
+
end
|
|
440
|
+
|
|
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
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
describe 'semver_gte' do
|
|
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
|
|
451
|
+
end
|
|
452
|
+
|
|
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
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
describe 'semver_lt' do
|
|
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
|
|
461
|
+
end
|
|
462
|
+
|
|
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
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
describe 'semver_lte' do
|
|
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
|
|
473
|
+
end
|
|
474
|
+
|
|
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
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
describe 'nil attribute handling' do
|
|
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
|
|
486
|
+
end
|
|
487
|
+
|
|
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
|
|
490
|
+
end
|
|
491
|
+
|
|
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
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
describe 'unknown operator' do
|
|
498
|
+
it 'returns false and warns for unknown operator' do
|
|
499
|
+
expect do
|
|
500
|
+
result = described_class.evaluate_operator('test', 'unknown_op', ['value'])
|
|
501
|
+
expect(result).to be false
|
|
502
|
+
end.to output(/Unknown operator: unknown_op/).to_stderr
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: togglecraft
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- ToggleCraft
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: concurrent-ruby
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.2'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.2'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: http
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '5.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '5.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rspec
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.12'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.12'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rubocop
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.59'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '1.59'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: simplecov
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0.22'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0.22'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: webmock
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '3.19'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '3.19'
|
|
96
|
+
description: Real-time feature flags with SSE streaming, local evaluation, and smart
|
|
97
|
+
caching
|
|
98
|
+
email:
|
|
99
|
+
- support@togglecraft.io
|
|
100
|
+
executables: []
|
|
101
|
+
extensions: []
|
|
102
|
+
extra_rdoc_files: []
|
|
103
|
+
files:
|
|
104
|
+
- CHANGELOG.md
|
|
105
|
+
- LICENSE
|
|
106
|
+
- README.md
|
|
107
|
+
- lib/togglecraft.rb
|
|
108
|
+
- lib/togglecraft/cache.rb
|
|
109
|
+
- lib/togglecraft/cache_adapters/memory_adapter.rb
|
|
110
|
+
- lib/togglecraft/client.rb
|
|
111
|
+
- lib/togglecraft/connection_pool.rb
|
|
112
|
+
- lib/togglecraft/evaluator.rb
|
|
113
|
+
- lib/togglecraft/shared_sse_connection.rb
|
|
114
|
+
- lib/togglecraft/sse_connection.rb
|
|
115
|
+
- lib/togglecraft/utils.rb
|
|
116
|
+
- lib/togglecraft/version.rb
|
|
117
|
+
- spec/spec_helper.rb
|
|
118
|
+
- spec/togglecraft/cache/memory_adapter_spec.rb
|
|
119
|
+
- spec/togglecraft/cache_spec.rb
|
|
120
|
+
- spec/togglecraft/client_spec.rb
|
|
121
|
+
- spec/togglecraft/connection_pool_spec.rb
|
|
122
|
+
- spec/togglecraft/evaluator_spec.rb
|
|
123
|
+
- spec/togglecraft/shared_sse_connection_spec.rb
|
|
124
|
+
- spec/togglecraft/sse_connection_spec.rb
|
|
125
|
+
- spec/togglecraft/utils_spec.rb
|
|
126
|
+
homepage: https://github.com/togglecraft/ruby-sdk
|
|
127
|
+
licenses:
|
|
128
|
+
- MIT
|
|
129
|
+
metadata:
|
|
130
|
+
homepage_uri: https://github.com/togglecraft/ruby-sdk
|
|
131
|
+
source_code_uri: https://github.com/togglecraft/ruby-sdk
|
|
132
|
+
changelog_uri: https://github.com/togglecraft/ruby-sdk/blob/main/CHANGELOG.md
|
|
133
|
+
rubygems_mfa_required: 'true'
|
|
134
|
+
rdoc_options: []
|
|
135
|
+
require_paths:
|
|
136
|
+
- lib
|
|
137
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
138
|
+
requirements:
|
|
139
|
+
- - ">="
|
|
140
|
+
- !ruby/object:Gem::Version
|
|
141
|
+
version: 3.0.0
|
|
142
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
143
|
+
requirements:
|
|
144
|
+
- - ">="
|
|
145
|
+
- !ruby/object:Gem::Version
|
|
146
|
+
version: '0'
|
|
147
|
+
requirements: []
|
|
148
|
+
rubygems_version: 3.6.7
|
|
149
|
+
specification_version: 4
|
|
150
|
+
summary: Ruby SDK for ToggleCraft feature flag management
|
|
151
|
+
test_files: []
|