ff-ruby-server-sdk 0.0.2 → 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 +4 -4
- data/.run/build.sh.run.xml +17 -0
- data/.run/install.sh.run.xml +17 -0
- data/.run/openapi.sh.run.xml +17 -0
- data/.run/publish.sh.run.xml +17 -0
- data/.run/sdk_test.rb.run.xml +3 -3
- data/.run/unpublish.sh.run.xml +17 -0
- data/CHANGELOG.md +1 -1
- data/Gemfile +20 -3
- data/README.md +155 -7
- data/api.yaml +736 -0
- data/example/example.rb +99 -3
- data/lib/ff/ruby/server/sdk/api/auth_service.rb +91 -0
- data/lib/ff/ruby/server/sdk/api/cf_client.rb +93 -0
- data/lib/ff/ruby/server/sdk/api/client_callback.rb +45 -0
- data/lib/ff/ruby/server/sdk/api/config.rb +140 -0
- data/lib/ff/ruby/server/sdk/api/config_builder.rb +116 -0
- data/lib/ff/ruby/server/sdk/api/default_cache.rb +112 -0
- data/lib/ff/ruby/server/sdk/api/evaluation.rb +29 -0
- data/lib/ff/ruby/server/sdk/api/evaluator.rb +526 -0
- data/lib/ff/ruby/server/sdk/api/file_map_store.rb +60 -0
- data/lib/ff/ruby/server/sdk/api/flag_evaluate_callback.rb +13 -0
- data/lib/ff/ruby/server/sdk/api/inner_client.rb +311 -0
- data/lib/ff/ruby/server/sdk/api/inner_client_flag_evaluate_callback.rb +30 -0
- data/lib/ff/ruby/server/sdk/api/inner_client_metrics_callback.rb +33 -0
- data/lib/ff/ruby/server/sdk/api/inner_client_repository_callback.rb +44 -0
- data/lib/ff/ruby/server/sdk/api/inner_client_updater.rb +63 -0
- data/lib/ff/ruby/server/sdk/api/metrics_callback.rb +19 -0
- data/lib/ff/ruby/server/sdk/api/metrics_event.rb +16 -0
- data/lib/ff/ruby/server/sdk/api/metrics_processor.rb +297 -0
- data/lib/ff/ruby/server/sdk/api/operators.rb +20 -0
- data/lib/ff/ruby/server/sdk/api/polling_processor.rb +164 -0
- data/lib/ff/ruby/server/sdk/api/repository_callback.rb +28 -0
- data/lib/ff/ruby/server/sdk/api/storage_repository.rb +263 -0
- data/lib/ff/ruby/server/sdk/api/summary_metrics.rb +16 -0
- data/lib/ff/ruby/server/sdk/api/update_processor.rb +149 -0
- data/lib/ff/ruby/server/sdk/common/cache.rb +27 -0
- data/lib/ff/ruby/server/sdk/common/closeable.rb +7 -0
- data/lib/ff/ruby/server/sdk/common/destroyable.rb +12 -0
- data/lib/ff/ruby/server/sdk/common/repository.rb +45 -0
- data/lib/ff/ruby/server/sdk/common/storage.rb +29 -0
- data/lib/ff/ruby/server/sdk/connector/connector.rb +44 -0
- data/lib/ff/ruby/server/sdk/connector/events.rb +118 -0
- data/lib/ff/ruby/server/sdk/connector/harness_connector.rb +236 -0
- data/lib/ff/ruby/server/sdk/connector/service.rb +19 -0
- data/lib/ff/ruby/server/sdk/connector/updater.rb +32 -0
- data/lib/ff/ruby/server/sdk/dto/message.rb +13 -0
- data/lib/ff/ruby/server/sdk/dto/target.rb +24 -0
- data/lib/ff/ruby/server/sdk/version.rb +2 -1
- data/lib/ff/ruby/server/sdk.rb +39 -3
- data/openapitools.json +7 -0
- data/scripts/openapi.sh +35 -0
- data/scripts/sdk_specs.sh +1 -1
- metadata +46 -3
- data/lib/ff/ruby/server/sdk/cf_client.rb +0 -6
@@ -0,0 +1,29 @@
|
|
1
|
+
|
2
|
+
class Evaluation
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
super
|
6
|
+
|
7
|
+
@tbi = "To be implemented"
|
8
|
+
end
|
9
|
+
|
10
|
+
def bool_variation(identifier, target, default_value, callback)
|
11
|
+
|
12
|
+
raise @tbi
|
13
|
+
end
|
14
|
+
|
15
|
+
def string_variation(identifier, target, default_value, callback)
|
16
|
+
|
17
|
+
raise @tbi
|
18
|
+
end
|
19
|
+
|
20
|
+
def number_variation(identifier, target, default_value, callback)
|
21
|
+
|
22
|
+
raise @tbi
|
23
|
+
end
|
24
|
+
|
25
|
+
def json_variation(identifier, target, default_value, callback)
|
26
|
+
|
27
|
+
raise @tbi
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,526 @@
|
|
1
|
+
require "json"
|
2
|
+
require "murmurhash3"
|
3
|
+
|
4
|
+
require_relative "evaluation"
|
5
|
+
require_relative "../common/repository"
|
6
|
+
|
7
|
+
class Evaluator < Evaluation
|
8
|
+
|
9
|
+
def initialize(repository, logger = nil)
|
10
|
+
|
11
|
+
unless repository.kind_of?(Repository)
|
12
|
+
|
13
|
+
raise "The 'repository' parameter must be of '" + Repository.to_s + "' data type"
|
14
|
+
end
|
15
|
+
|
16
|
+
if logger != nil
|
17
|
+
|
18
|
+
@logger = logger
|
19
|
+
else
|
20
|
+
|
21
|
+
@logger = Logger.new(STDOUT)
|
22
|
+
end
|
23
|
+
|
24
|
+
@repository = repository
|
25
|
+
end
|
26
|
+
|
27
|
+
def bool_variation(identifier, target, default_value, callback)
|
28
|
+
|
29
|
+
variation = evaluate(identifier, target, "boolean", callback)
|
30
|
+
|
31
|
+
if variation != nil
|
32
|
+
|
33
|
+
return variation.value == "true"
|
34
|
+
end
|
35
|
+
|
36
|
+
default_value
|
37
|
+
end
|
38
|
+
|
39
|
+
def string_variation(identifier, target, default_value, callback)
|
40
|
+
|
41
|
+
variation = evaluate(identifier, target, "string", callback)
|
42
|
+
|
43
|
+
if variation != nil
|
44
|
+
|
45
|
+
return variation.value
|
46
|
+
end
|
47
|
+
|
48
|
+
default_value
|
49
|
+
end
|
50
|
+
|
51
|
+
def number_variation(identifier, target, default_value, callback)
|
52
|
+
|
53
|
+
variation = evaluate(identifier, target, "int", callback)
|
54
|
+
|
55
|
+
if variation != nil
|
56
|
+
|
57
|
+
return variation.value.to_i
|
58
|
+
end
|
59
|
+
|
60
|
+
default_value
|
61
|
+
end
|
62
|
+
|
63
|
+
def json_variation(identifier, target, default_value, callback)
|
64
|
+
|
65
|
+
variation = evaluate(identifier, target, "json", callback)
|
66
|
+
|
67
|
+
if variation != nil
|
68
|
+
|
69
|
+
return JSON.parse(variation.value)
|
70
|
+
end
|
71
|
+
|
72
|
+
default_value
|
73
|
+
end
|
74
|
+
|
75
|
+
def evaluate(identifier, target, expected, callback)
|
76
|
+
|
77
|
+
if callback != nil
|
78
|
+
|
79
|
+
unless callback.kind_of?(FlagEvaluateCallback)
|
80
|
+
|
81
|
+
raise "The 'callback' parameter must be of '" + FlagEvaluateCallback.to_s + "' data type"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
flag = @repository.get_flag(identifier)
|
86
|
+
|
87
|
+
if flag != nil && flag.kind == expected
|
88
|
+
|
89
|
+
unless flag.prerequisites.empty?
|
90
|
+
|
91
|
+
pre_req = check_pre_requisite(flag, target)
|
92
|
+
|
93
|
+
unless pre_req
|
94
|
+
|
95
|
+
return find_variation(flag.variations, flag.off_variation)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
variation = evaluate_flag(flag, target)
|
100
|
+
|
101
|
+
if variation != nil
|
102
|
+
|
103
|
+
if callback != nil
|
104
|
+
|
105
|
+
callback.process_evaluation(flag, target, variation)
|
106
|
+
end
|
107
|
+
|
108
|
+
return variation
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
nil
|
113
|
+
end
|
114
|
+
|
115
|
+
protected
|
116
|
+
|
117
|
+
def get_attr_value(target, attribute)
|
118
|
+
|
119
|
+
if attribute != nil && !attribute.empty?
|
120
|
+
|
121
|
+
if target.respond_to?(attribute, :include_private)
|
122
|
+
|
123
|
+
@logger.debug "The attribute " + attribute.to_s + " exists (1)"
|
124
|
+
|
125
|
+
return target.send(attribute)
|
126
|
+
else
|
127
|
+
|
128
|
+
result = target.attributes.key?(attribute)
|
129
|
+
|
130
|
+
if result == nil
|
131
|
+
|
132
|
+
@logger.debug "The attribute " + attribute.to_s + " does not exist"
|
133
|
+
|
134
|
+
else
|
135
|
+
|
136
|
+
@logger.debug "The attribute " + attribute.to_s + " exists (2)"
|
137
|
+
end
|
138
|
+
|
139
|
+
return result
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
@logger.debug "The passed attribute is empty"
|
144
|
+
|
145
|
+
nil
|
146
|
+
end
|
147
|
+
|
148
|
+
def find_variation(variations, identifier)
|
149
|
+
|
150
|
+
if identifier != nil && !identifier.empty?
|
151
|
+
|
152
|
+
variations.each do |v|
|
153
|
+
|
154
|
+
if v.identifier == identifier
|
155
|
+
|
156
|
+
return v
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
nil
|
162
|
+
end
|
163
|
+
|
164
|
+
def get_normalized_number(property, bucket_by)
|
165
|
+
|
166
|
+
joined = property.to_s + ":" + bucket_by.to_s
|
167
|
+
hash = MurmurHash3::V32.str_hash(joined, joined.length)
|
168
|
+
(hash % 100) + 1
|
169
|
+
end
|
170
|
+
|
171
|
+
def is_enabled(target, bucket_by, percentage)
|
172
|
+
|
173
|
+
property = get_attr_value(target, bucket_by)
|
174
|
+
|
175
|
+
if property != nil
|
176
|
+
|
177
|
+
bucket_id = get_normalized_number(property, bucket_by)
|
178
|
+
|
179
|
+
return percentage > 0 && bucket_id <= percentage
|
180
|
+
end
|
181
|
+
|
182
|
+
false
|
183
|
+
end
|
184
|
+
|
185
|
+
def evaluate_distribution(distribution, target)
|
186
|
+
|
187
|
+
if distribution != nil
|
188
|
+
|
189
|
+
variation = nil
|
190
|
+
|
191
|
+
distribution.variations.each do |weighted_variation|
|
192
|
+
|
193
|
+
variation = weighted_variation.variation
|
194
|
+
|
195
|
+
if is_enabled(target, distribution.bucket_by, weighted_variation.weight)
|
196
|
+
|
197
|
+
return variation
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
return variation
|
202
|
+
end
|
203
|
+
|
204
|
+
nil
|
205
|
+
end
|
206
|
+
|
207
|
+
def evaluate_clauses(clauses, target)
|
208
|
+
|
209
|
+
clauses.each do |clause|
|
210
|
+
|
211
|
+
unless evaluate_clause(clause, target)
|
212
|
+
|
213
|
+
return false
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
true
|
218
|
+
end
|
219
|
+
|
220
|
+
def evaluate_clause(clause, target)
|
221
|
+
|
222
|
+
if clause == nil
|
223
|
+
|
224
|
+
return false
|
225
|
+
end
|
226
|
+
|
227
|
+
operator = clause.op
|
228
|
+
|
229
|
+
if operator == nil || operator.empty?
|
230
|
+
|
231
|
+
return false
|
232
|
+
end
|
233
|
+
|
234
|
+
if operator == "segmentMatch"
|
235
|
+
|
236
|
+
return is_target_included_or_excluded_in_segment(clause.values, target)
|
237
|
+
end
|
238
|
+
|
239
|
+
if clause.values.empty?
|
240
|
+
|
241
|
+
return false
|
242
|
+
end
|
243
|
+
|
244
|
+
value = clause.values[0]
|
245
|
+
attr_value = get_attr_value(target, clause.attribute)
|
246
|
+
|
247
|
+
if attr_value == nil
|
248
|
+
|
249
|
+
return false
|
250
|
+
end
|
251
|
+
|
252
|
+
object = attr_value.to_s
|
253
|
+
|
254
|
+
if operator == "starts_with"
|
255
|
+
|
256
|
+
return object.start_with?(value)
|
257
|
+
end
|
258
|
+
|
259
|
+
if operator == "ends_with"
|
260
|
+
|
261
|
+
return object.end_with?(value)
|
262
|
+
end
|
263
|
+
|
264
|
+
if operator == "match"
|
265
|
+
|
266
|
+
match = object.match?(value)
|
267
|
+
return match != nil && !match.empty?
|
268
|
+
end
|
269
|
+
|
270
|
+
if operator == "contains"
|
271
|
+
|
272
|
+
return object.include?(value)
|
273
|
+
end
|
274
|
+
|
275
|
+
if operator == "equal"
|
276
|
+
|
277
|
+
return object.casecmp?(value)
|
278
|
+
end
|
279
|
+
|
280
|
+
if operator == "equal_sensitive"
|
281
|
+
|
282
|
+
return object == value
|
283
|
+
end
|
284
|
+
|
285
|
+
if operator == "in"
|
286
|
+
|
287
|
+
return object.include?(value)
|
288
|
+
end
|
289
|
+
|
290
|
+
if operator == "segmentMatch"
|
291
|
+
|
292
|
+
return is_target_included_or_excluded_in_segment(clause.values, target)
|
293
|
+
end
|
294
|
+
|
295
|
+
false
|
296
|
+
end
|
297
|
+
|
298
|
+
def is_target_included_or_excluded_in_segment(segment_list, target)
|
299
|
+
|
300
|
+
segment_list.each do |segment_identifier|
|
301
|
+
|
302
|
+
segment = @repository.get_segment(segment_identifier)
|
303
|
+
|
304
|
+
if segment != nil
|
305
|
+
|
306
|
+
if is_target_in_list(target, segment.excluded)
|
307
|
+
|
308
|
+
@logger.debug "Target " + target.name.to_s + " excluded from segment " + segment.name.to_s + " via exclude list"
|
309
|
+
|
310
|
+
return false
|
311
|
+
end
|
312
|
+
|
313
|
+
if is_target_in_list(target, segment.included)
|
314
|
+
|
315
|
+
@logger.debug "Target " + target.name.to_s + " included in segment " + segment.name.to_s + " via include list"
|
316
|
+
|
317
|
+
return true
|
318
|
+
end
|
319
|
+
|
320
|
+
rules = segment.rules
|
321
|
+
|
322
|
+
if rules != nil && !rules.empty? && evaluate_clauses(rules, target)
|
323
|
+
|
324
|
+
@logger.debug "Target " + target.name.to_s + " included in segment " + segment.name.to_s + " via rules"
|
325
|
+
|
326
|
+
return true
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
false
|
332
|
+
end
|
333
|
+
|
334
|
+
def evaluate_rules(serving_rules, target)
|
335
|
+
|
336
|
+
if target == nil || serving_rules == nil
|
337
|
+
|
338
|
+
return nil
|
339
|
+
end
|
340
|
+
|
341
|
+
sorted = serving_rules.sort do |a, b|
|
342
|
+
|
343
|
+
b.priority <=> a.priority
|
344
|
+
end
|
345
|
+
|
346
|
+
sorted.each do |rule|
|
347
|
+
|
348
|
+
next unless evaluate_rule(rule, target)
|
349
|
+
|
350
|
+
if rule.serve.distribution != nil
|
351
|
+
|
352
|
+
return evaluate_distribution(rule.serve.distribution, target)
|
353
|
+
end
|
354
|
+
|
355
|
+
if rule.serve.variation != nil
|
356
|
+
|
357
|
+
return rule.serve.variation
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
nil
|
362
|
+
end
|
363
|
+
|
364
|
+
def evaluate_rule(serving_rule, target)
|
365
|
+
|
366
|
+
evaluate_clauses(serving_rule.clauses, target)
|
367
|
+
end
|
368
|
+
|
369
|
+
def evaluate_variation_map(variation_maps, target)
|
370
|
+
|
371
|
+
if target == nil
|
372
|
+
|
373
|
+
return nil
|
374
|
+
end
|
375
|
+
|
376
|
+
variation_maps.each do |variation_map|
|
377
|
+
|
378
|
+
targets = variation_map.targets
|
379
|
+
|
380
|
+
if targets != nil
|
381
|
+
|
382
|
+
found = nil
|
383
|
+
|
384
|
+
targets.each do |t|
|
385
|
+
|
386
|
+
if t.identifier != nil && t.identifier == target.identifier
|
387
|
+
|
388
|
+
found = t
|
389
|
+
break
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
if found != nil
|
394
|
+
|
395
|
+
return variation_map.variation
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
segment_identifiers = variation_map.target_segments
|
400
|
+
|
401
|
+
if segment_identifiers != nil && is_target_included_or_excluded_in_segment(segment_identifiers, target)
|
402
|
+
|
403
|
+
return variation_map.variation
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
nil
|
408
|
+
end
|
409
|
+
|
410
|
+
def evaluate_flag(feature_config, target)
|
411
|
+
|
412
|
+
variation = feature_config.off_variation
|
413
|
+
|
414
|
+
if feature_config.state == OpenapiClient::FeatureState::ON
|
415
|
+
|
416
|
+
variation = nil
|
417
|
+
|
418
|
+
if feature_config.variation_to_target_map != nil
|
419
|
+
|
420
|
+
variation = evaluate_variation_map(feature_config.variation_to_target_map, target)
|
421
|
+
end
|
422
|
+
|
423
|
+
if variation == nil
|
424
|
+
|
425
|
+
variation = evaluate_rules(feature_config.rules, target)
|
426
|
+
end
|
427
|
+
|
428
|
+
if variation == nil
|
429
|
+
|
430
|
+
variation = evaluate_distribution(feature_config.default_serve.distribution, target)
|
431
|
+
end
|
432
|
+
|
433
|
+
if variation == nil
|
434
|
+
|
435
|
+
variation = feature_config.default_serve.variation
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
if variation != nil
|
440
|
+
|
441
|
+
return find_variation(feature_config.variations, variation)
|
442
|
+
end
|
443
|
+
|
444
|
+
nil
|
445
|
+
end
|
446
|
+
|
447
|
+
def check_pre_requisite(parent_feature_config, target)
|
448
|
+
|
449
|
+
prerequisites = parent_feature_config.prerequisites
|
450
|
+
|
451
|
+
if prerequisites != nil && !prerequisites.empty?
|
452
|
+
|
453
|
+
@logger.debug "Checking pre requisites " + prerequisites.to_s + " of parent feature " + parent_feature_config.feature.to_s
|
454
|
+
|
455
|
+
prerequisites.each do |pqs|
|
456
|
+
|
457
|
+
pre_req_feature = pqs.feature
|
458
|
+
|
459
|
+
pre_req_feature_config = @repository.get_flag(pre_req_feature)
|
460
|
+
|
461
|
+
if pre_req_feature_config == nil
|
462
|
+
|
463
|
+
@logger.debug "Could not retrieve the pre requisite details of feature flag: " + pre_req_feature.to_s
|
464
|
+
|
465
|
+
return true
|
466
|
+
end
|
467
|
+
|
468
|
+
pre_req_evaluated_variation = evaluate_flag(pre_req_feature_config, target)
|
469
|
+
|
470
|
+
if pre_req_evaluated_variation == nil
|
471
|
+
|
472
|
+
@logger.debug "Could not evaluate the prerequisite details of feature flag: " + pre_req_feature.to_s
|
473
|
+
|
474
|
+
return true
|
475
|
+
end
|
476
|
+
|
477
|
+
@logger.debug "Pre requisite flag " + pre_req_feature_config.feature + " has variation " +
|
478
|
+
pre_req_evaluated_variation.to_s + " for target " + target.to_s
|
479
|
+
|
480
|
+
valid_pre_req_variations = pqs.variations
|
481
|
+
|
482
|
+
@logger.debug "Pre requisite flag " + pre_req_feature_config.to_s + " should have the variations " +
|
483
|
+
valid_pre_req_variations.to_s
|
484
|
+
|
485
|
+
none_match = true
|
486
|
+
|
487
|
+
valid_pre_req_variations.each do |element|
|
488
|
+
|
489
|
+
if element.include?(pre_req_evaluated_variation.value)
|
490
|
+
|
491
|
+
none_match = false
|
492
|
+
break
|
493
|
+
end
|
494
|
+
end
|
495
|
+
|
496
|
+
if none_match
|
497
|
+
|
498
|
+
return false
|
499
|
+
else
|
500
|
+
|
501
|
+
return check_pre_requisite(pre_req_feature_config, target)
|
502
|
+
end
|
503
|
+
end
|
504
|
+
end
|
505
|
+
|
506
|
+
true
|
507
|
+
end
|
508
|
+
|
509
|
+
private
|
510
|
+
|
511
|
+
def is_target_in_list(target, list_of_targets)
|
512
|
+
|
513
|
+
if list_of_targets != nil
|
514
|
+
|
515
|
+
list_of_targets.each do |included_target|
|
516
|
+
|
517
|
+
if included_target.identifier.include?(target.identifier)
|
518
|
+
|
519
|
+
return true
|
520
|
+
end
|
521
|
+
end
|
522
|
+
end
|
523
|
+
|
524
|
+
false
|
525
|
+
end
|
526
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require "moneta"
|
2
|
+
require_relative "../common/storage"
|
3
|
+
|
4
|
+
class FileMapStore < Storage
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
|
8
|
+
@keys = Set[]
|
9
|
+
@store = Moneta.new(:File, dir: "moneta")
|
10
|
+
end
|
11
|
+
|
12
|
+
def set(key, value)
|
13
|
+
|
14
|
+
check_init
|
15
|
+
|
16
|
+
@store[key] = value
|
17
|
+
keys.add(key)
|
18
|
+
end
|
19
|
+
|
20
|
+
def get(key)
|
21
|
+
|
22
|
+
check_init
|
23
|
+
|
24
|
+
@store[key]
|
25
|
+
end
|
26
|
+
|
27
|
+
def delete(key)
|
28
|
+
|
29
|
+
check_init
|
30
|
+
|
31
|
+
@store.delete(key)
|
32
|
+
keys.delete(key)
|
33
|
+
end
|
34
|
+
|
35
|
+
def keys
|
36
|
+
|
37
|
+
check_init
|
38
|
+
|
39
|
+
@keys
|
40
|
+
end
|
41
|
+
|
42
|
+
def close
|
43
|
+
|
44
|
+
if @store != nil
|
45
|
+
|
46
|
+
@store.close
|
47
|
+
@keys = Set[]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def check_init
|
54
|
+
|
55
|
+
if @store == nil
|
56
|
+
|
57
|
+
raise "Not initialized"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|