statsig 1.34.1 → 2.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.
data/lib/evaluator.rb CHANGED
@@ -40,12 +40,13 @@ module Statsig
40
40
  end
41
41
 
42
42
  def lookup_gate_override(gate_name)
43
- if @gate_overrides.key?(gate_name)
43
+ gate_name_sym = gate_name.to_sym
44
+ if @gate_overrides.key?(gate_name_sym)
44
45
  return ConfigResult.new(
45
46
  name: gate_name,
46
- gate_value: @gate_overrides[gate_name],
47
+ gate_value: @gate_overrides[gate_name_sym],
47
48
  rule_id: Const::OVERRIDE,
48
- id_type: @spec_store.has_gate?(gate_name) ? @spec_store.get_gate(gate_name).id_type : Const::EMPTY_STR,
49
+ id_type: @spec_store.has_gate?(gate_name) ? @spec_store.get_gate(gate_name)[:idType] : Const::EMPTY_STR,
49
50
  evaluation_details: EvaluationDetails.local_override(
50
51
  @spec_store.last_config_sync_time,
51
52
  @spec_store.initial_config_sync_time
@@ -56,12 +57,13 @@ module Statsig
56
57
  end
57
58
 
58
59
  def lookup_config_override(config_name)
59
- if @config_overrides.key?(config_name)
60
+ config_name_sym = config_name.to_sym
61
+ if @config_overrides.key?(config_name_sym)
60
62
  return ConfigResult.new(
61
63
  name: config_name,
62
- json_value: @config_overrides[config_name],
64
+ json_value: @config_overrides[config_name_sym],
63
65
  rule_id: Const::OVERRIDE,
64
- id_type: @spec_store.has_config?(config_name) ? @spec_store.get_config(config_name).id_type : Const::EMPTY_STR,
66
+ id_type: @spec_store.has_config?(config_name) ? @spec_store.get_config(config_name)[:idType] : Const::EMPTY_STR,
65
67
  evaluation_details: EvaluationDetails.local_override(
66
68
  @spec_store.last_config_sync_time,
67
69
  @spec_store.initial_config_sync_time
@@ -96,7 +98,7 @@ module Statsig
96
98
  return
97
99
  end
98
100
 
99
- eval_spec(user, @spec_store.get_gate(gate_name), end_result, is_nested: is_nested)
101
+ eval_spec(gate_name, user, @spec_store.get_gate(gate_name), end_result, is_nested: is_nested)
100
102
  end
101
103
 
102
104
  def get_config(user, config_name, end_result, user_persisted_values: nil, ignore_local_overrides: false)
@@ -128,7 +130,7 @@ module Statsig
128
130
  config = @spec_store.get_config(config_name)
129
131
 
130
132
  # If persisted values is provided and the experiment is active, return sticky values if exists.
131
- if !user_persisted_values.nil? && config.is_active == true
133
+ if !user_persisted_values.nil? && config[:isActive] == true
132
134
  sticky_values = user_persisted_values[config_name]
133
135
  unless sticky_values.nil?
134
136
  end_result.gate_value = sticky_values[Statsig::Const::GATE_VALUE]
@@ -148,16 +150,16 @@ module Statsig
148
150
  end
149
151
 
150
152
  # If it doesn't exist, then save to persisted storage if the user was assigned to an experiment group.
151
- eval_spec(user, config, end_result)
153
+ eval_spec(config_name, user, config, end_result)
152
154
  if end_result.is_experiment_group
153
155
  @persistent_storage_utils.add_evaluation_to_user_persisted_values(user_persisted_values, config_name,
154
156
  end_result)
155
- @persistent_storage_utils.save_to_storage(user, config.id_type, user_persisted_values)
157
+ @persistent_storage_utils.save_to_storage(user, config[:idType], user_persisted_values)
156
158
  end
157
159
  # Otherwise, remove from persisted storage
158
160
  else
159
- @persistent_storage_utils.remove_experiment_from_storage(user, config.id_type, config_name)
160
- eval_spec(user, config, end_result)
161
+ @persistent_storage_utils.remove_experiment_from_storage(user, config[:idType], config_name)
162
+ eval_spec(config_name, user, config, end_result)
161
163
  end
162
164
  end
163
165
 
@@ -174,27 +176,44 @@ module Statsig
174
176
  return
175
177
  end
176
178
 
177
- eval_spec(user, @spec_store.get_layer(layer_name), end_result)
179
+ eval_spec(layer_name, user, @spec_store.get_layer(layer_name), end_result)
178
180
  end
179
181
 
180
182
  def list_gates
181
- @spec_store.gates.map { |name, _| name }
183
+ @spec_store.gates.keys.map(&:to_s)
182
184
  end
183
185
 
184
186
  def list_configs
185
- @spec_store.configs.map { |name, config| name if config.entity == :dynamic_config }.compact
187
+ keys = []
188
+ @spec_store.configs.each do |key, value|
189
+ if value[:entity] == Const::TYPE_DYNAMIC_CONFIG
190
+ keys << key.to_s
191
+ end
192
+ end
193
+ keys
186
194
  end
187
195
 
188
196
  def list_experiments
189
- @spec_store.configs.map { |name, config| name if config.entity == :experiment }.compact
197
+ keys = []
198
+ @spec_store.configs.each do |key, value|
199
+ if value[:entity] == Const::TYPE_EXPERIMENT
200
+ keys << key.to_s
201
+ end
202
+ end
203
+ keys
190
204
  end
191
205
 
192
206
  def list_autotunes
193
- @spec_store.configs.map { |name, config| name if config.entity == :autotune }.compact
194
- end
207
+ keys = []
208
+ @spec_store.configs.each do |key, value|
209
+ if value[:entity] == Const::TYPE_AUTOTUNE
210
+ keys << key.to_s
211
+ end
212
+ end
213
+ keys end
195
214
 
196
215
  def list_layers
197
- @spec_store.layers.map { |name, _| name }
216
+ @spec_store.layers.keys.map(&:to_s)
198
217
  end
199
218
 
200
219
  def get_client_initialize_response(user, hash_algo, client_sdk_key, include_local_overrides)
@@ -258,11 +277,11 @@ module Statsig
258
277
  end
259
278
 
260
279
  def override_gate(gate, value)
261
- @gate_overrides[gate] = value
280
+ @gate_overrides[gate.to_sym] = value
262
281
  end
263
282
 
264
283
  def remove_gate_override(gate)
265
- @gate_overrides.delete(gate)
284
+ @gate_overrides.delete(gate.to_sym)
266
285
  end
267
286
 
268
287
  def clear_gate_overrides
@@ -270,33 +289,28 @@ module Statsig
270
289
  end
271
290
 
272
291
  def override_config(config, value)
273
- @config_overrides[config] = value
292
+ @config_overrides[config.to_sym] = value
274
293
  end
275
294
 
276
295
  def remove_config_override(config)
277
- @config_overrides.delete(config)
296
+ @config_overrides.delete(config.to_sym)
278
297
  end
279
298
 
280
299
  def clear_config_overrides
281
300
  @config_overrides.clear
282
301
  end
283
302
 
284
- def eval_spec(user, config, end_result, is_nested: false)
285
- unless config.enabled
286
- finalize_eval_result(config, end_result, did_pass: false, rule: nil, is_nested: is_nested)
287
- return
288
- end
289
-
290
- config.rules.each do |rule|
303
+ def eval_spec(config_name, user, config, end_result, is_nested: false)
304
+ config[:rules].each do |rule|
291
305
  eval_rule(user, rule, end_result)
292
306
 
293
307
  if end_result.gate_value
294
- if eval_delegate(config.name, user, rule, end_result)
308
+ if eval_delegate(config_name, user, rule, end_result)
295
309
  finalize_secondary_exposures(end_result)
296
310
  return
297
311
  end
298
312
 
299
- pass = eval_pass_percent(user, rule, config.salt)
313
+ pass = eval_pass_percent(user, rule, config[:salt])
300
314
  finalize_eval_result(config, end_result, did_pass: pass, rule: rule, is_nested: is_nested)
301
315
  return
302
316
  end
@@ -308,20 +322,20 @@ module Statsig
308
322
  private
309
323
 
310
324
  def finalize_eval_result(config, end_result, did_pass:, rule:, is_nested: false)
311
- end_result.id_type = config.id_type
312
- end_result.target_app_ids = config.target_app_ids
313
- end_result.gate_value = did_pass
325
+ end_result.id_type = config[:idType]
326
+ end_result.target_app_ids = config[:targetAppIDs]
327
+ end_result.gate_value = did_pass ? rule[:returnValue] == true : config[:defaultValue] == true
314
328
 
315
329
  if rule.nil?
316
- end_result.json_value = config.default_value
330
+ end_result.json_value = config[:defaultValue]
317
331
  end_result.group_name = nil
318
332
  end_result.is_experiment_group = false
319
- end_result.rule_id = config.enabled ? Const::DEFAULT : Const::DISABLED
333
+ end_result.rule_id = config[:enabled] ? Const::DEFAULT : Const::DISABLED
320
334
  else
321
- end_result.json_value = did_pass ? rule.return_value : config.default_value
322
- end_result.group_name = rule.group_name
323
- end_result.is_experiment_group = rule.is_experiment_group == true
324
- end_result.rule_id = rule.id
335
+ end_result.json_value = did_pass ? rule[:returnValue] : config[:defaultValue]
336
+ end_result.group_name = rule[:groupName]
337
+ end_result.is_experiment_group = rule[:isExperimentGroup] == true
338
+ end_result.rule_id = rule[:id]
325
339
  end
326
340
 
327
341
  unless end_result.disable_evaluation_details
@@ -345,7 +359,7 @@ module Statsig
345
359
  def clean_exposures(exposures)
346
360
  seen = {}
347
361
  exposures.reject do |exposure|
348
- if exposure[:gate].to_s.start_with?('segment:')
362
+ if exposure[:gate].to_s.start_with?(Const::SEGMENT_PREFIX)
349
363
  should_reject = true
350
364
  else
351
365
  key = "#{exposure[:gate]}|#{exposure[:gateValue]}|#{exposure[:ruleID]}}"
@@ -360,19 +374,10 @@ module Statsig
360
374
  pass = true
361
375
  i = 0
362
376
 
363
- memo = user.get_memo
364
- until i >= rule.conditions.length
365
-
366
- condition = rule.conditions[i]
367
-
368
- if condition.type == :fail_gate || condition.type == :pass_gate
369
- result = eval_condition(user, condition, end_result)
370
- else
371
- result = Memo.for(memo, :eval_rule, condition.hash) do
372
- eval_condition(user, condition, end_result)
373
- end
374
- end
375
-
377
+ until i >= rule[:conditions].length
378
+ condition_hash = rule[:conditions][i]
379
+ condition = @spec_store.get_condition(condition_hash)
380
+ result = eval_condition(user, condition, end_result)
376
381
  pass = false if result != true
377
382
  i += 1
378
383
  end
@@ -381,82 +386,74 @@ module Statsig
381
386
  end
382
387
 
383
388
  def eval_delegate(name, user, rule, end_result)
384
- return false unless (delegate = rule.config_delegate)
385
- return false unless (config = @spec_store.get_config(delegate))
389
+ return false unless (delegate = rule[:configDelegate])
390
+ return false unless (delegate_config = @spec_store.get_config(delegate))
386
391
 
387
392
  end_result.undelegated_sec_exps = end_result.secondary_exposures.dup
388
393
 
389
- eval_spec(user, config, end_result, is_nested: true)
394
+ eval_spec(delegate, user, delegate_config, end_result, is_nested: true)
390
395
 
391
396
  end_result.name = name
392
397
  end_result.config_delegate = delegate
393
- end_result.explicit_parameters = config.explicit_parameters
398
+ end_result.explicit_parameters = delegate_config[:explicitParameters]
394
399
 
395
400
  true
396
401
  end
397
402
 
398
403
  def eval_condition(user, condition, end_result)
399
404
  value = nil
400
- field = condition.field
401
- target = condition.target_value
402
- type = condition.type
403
- operator = condition.operator
404
- additional_values = condition.additional_values
405
- id_type = condition.id_type
405
+ field = condition[:field]
406
+ target = condition[:targetValue]
407
+ type = condition[:type]
408
+ operator = condition[:operator]
409
+ additional_values = condition[:additionalValues]
410
+ id_type = condition[:idType]
406
411
 
407
412
  case type
408
- when :public
413
+ when Const::CND_PUBLIC
409
414
  return true
410
- when :fail_gate, :pass_gate
411
- check_gate(user, target, end_result, is_nested: true)
412
- gate_value = end_result.gate_value
413
-
414
- unless end_result.disable_exposures
415
- new_exposure = {
416
- gate: target,
417
- gateValue: gate_value ? Const::TRUE : Const::FALSE,
418
- ruleID: end_result.rule_id
419
- }
420
- end_result.secondary_exposures.append(new_exposure)
421
- end
422
- return type == :pass_gate ? gate_value : !gate_value
423
- when :ip_based
415
+ when Const::CND_PASS_GATE, Const::CND_FAIL_GATE
416
+ result = eval_nested_gate(target, user, end_result)
417
+ return type == Const::CND_PASS_GATE ? result : !result
418
+ when Const::CND_MULTI_PASS_GATE, Const::CND_MULTI_FAIL_GATE
419
+ return eval_nested_gates(target, type, user, end_result)
420
+ when Const::CND_IP_BASED
424
421
  value = get_value_from_user(user, field) || get_value_from_ip(user, field)
425
- when :ua_based
422
+ when Const::CND_UA_BASED
426
423
  value = get_value_from_user(user, field) || get_value_from_ua(user, field)
427
- when :user_field
424
+ when Const::CND_USER_FIELD
428
425
  value = get_value_from_user(user, field)
429
- when :environment_field
426
+ when Const::CND_ENVIRONMENT_FIELD
430
427
  value = get_value_from_environment(user, field)
431
- when :current_time
428
+ when Const::CND_CURRENT_TIME
432
429
  value = Time.now.to_i # epoch time in seconds
433
- when :user_bucket
430
+ when Const::CND_USER_BUCKET
434
431
  begin
435
432
  salt = additional_values[:salt]
436
433
  unit_id = user.get_unit_id(id_type) || Const::EMPTY_STR
437
434
  # there are only 1000 user buckets as opposed to 10k for gate pass %
438
- value = compute_user_hash("#{salt}.#{unit_id}") % 1000
435
+ value = (compute_user_hash("#{salt}.#{unit_id}") % 1000).to_s
439
436
  rescue StandardError
440
437
  return false
441
438
  end
442
- when :unit_id
439
+ when Const::CND_UNIT_ID
443
440
  value = user.get_unit_id(id_type)
444
441
  end
445
442
 
446
443
  case operator
447
444
  # numerical comparison
448
- when :gt
445
+ when Const::OP_GREATER_THAN
449
446
  return EvaluationHelpers.compare_numbers(value, target, ->(a, b) { a > b })
450
- when :gte
447
+ when Const::OP_GREATER_THAN_OR_EQUAL
451
448
  return EvaluationHelpers.compare_numbers(value, target, ->(a, b) { a >= b })
452
- when :lt
449
+ when Const::OP_LESS_THAN
453
450
  return EvaluationHelpers.compare_numbers(value, target, ->(a, b) { a < b })
454
- when :lte
451
+ when Const::OP_LESS_THAN_OR_EQUAL
455
452
  return EvaluationHelpers.compare_numbers(value, target, ->(a, b) { a <= b })
456
453
 
457
454
  # version comparison
458
455
  # need to check for nil or empty value because Version takes them as valid values
459
- when :version_gt
456
+ when Const::OP_VERSION_GREATER_THAN
460
457
  return false if value.to_s.empty?
461
458
 
462
459
  return begin
@@ -464,7 +461,7 @@ module Statsig
464
461
  rescue StandardError
465
462
  false
466
463
  end
467
- when :version_gte
464
+ when Const::OP_VERSION_GREATER_THAN_OR_EQUAL
468
465
  return false if value.to_s.empty?
469
466
 
470
467
  return begin
@@ -472,7 +469,7 @@ module Statsig
472
469
  rescue StandardError
473
470
  false
474
471
  end
475
- when :version_lt
472
+ when Const::OP_VERSION_LESS_THAN
476
473
  return false if value.to_s.empty?
477
474
 
478
475
  return begin
@@ -480,7 +477,7 @@ module Statsig
480
477
  rescue StandardError
481
478
  false
482
479
  end
483
- when :version_lte
480
+ when Const::OP_VERSION_LESS_THAN_OR_EQUAL
484
481
  return false if value.to_s.empty?
485
482
 
486
483
  return begin
@@ -488,7 +485,7 @@ module Statsig
488
485
  rescue StandardError
489
486
  false
490
487
  end
491
- when :version_eq
488
+ when Const::OP_VERSION_EQUAL
492
489
  return false if value.to_s.empty?
493
490
 
494
491
  return begin
@@ -496,7 +493,7 @@ module Statsig
496
493
  rescue StandardError
497
494
  false
498
495
  end
499
- when :version_neq
496
+ when Const::OP_VERSION_NOT_EQUAL
500
497
  return false if value.to_s.empty?
501
498
 
502
499
  return begin
@@ -506,45 +503,47 @@ module Statsig
506
503
  end
507
504
 
508
505
  # array operations
509
- when :any
506
+ when Const::OP_ANY
510
507
  return EvaluationHelpers::equal_string_in_array(target, value, true)
511
- when :none
508
+ when Const::OP_NONE
512
509
  return !EvaluationHelpers::equal_string_in_array(target, value, true)
513
- when :any_case_sensitive
510
+ when Const::OP_ANY_CASE_SENSITIVE
514
511
  return EvaluationHelpers::equal_string_in_array(target, value, false)
515
- when :none_case_sensitive
512
+ when Const::OP_NONE_CASE_SENSITIVE
516
513
  return !EvaluationHelpers::equal_string_in_array(target, value, false)
517
514
 
518
515
  # string
519
- when :str_starts_with_any
516
+ when Const::OP_STR_STARTS_WITH_ANY
520
517
  return EvaluationHelpers.match_string_in_array(target, value, true, ->(a, b) { a.start_with?(b) })
521
- when :str_ends_with_any
518
+ when Const::OP_STR_END_WITH_ANY
522
519
  return EvaluationHelpers.match_string_in_array(target, value, true, ->(a, b) { a.end_with?(b) })
523
- when :str_contains_any
520
+ when Const::OP_STR_CONTAINS_ANY
524
521
  return EvaluationHelpers.match_string_in_array(target, value, true, ->(a, b) { a.include?(b) })
525
- when :str_contains_none
522
+ when Const::OP_STR_CONTAINS_NONE
526
523
  return !EvaluationHelpers.match_string_in_array(target, value, true, ->(a, b) { a.include?(b) })
527
- when :str_matches
524
+ when Const::OP_STR_MATCHES
528
525
  return begin
529
526
  value&.is_a?(String) && !(value =~ Regexp.new(target)).nil?
530
527
  rescue StandardError
531
528
  false
532
529
  end
533
- when :eq
530
+ when Const::OP_EQUAL
534
531
  return value == target
535
- when :neq
532
+ when Const::OP_NOT_EQUAL
536
533
  return value != target
537
534
 
538
535
  # dates
539
- when :before
536
+ when Const::OP_BEFORE
540
537
  return EvaluationHelpers.compare_times(value, target, ->(a, b) { a < b })
541
- when :after
538
+ when Const::OP_AFTER
542
539
  return EvaluationHelpers.compare_times(value, target, ->(a, b) { a > b })
543
- when :on
540
+ when Const::OP_ON
544
541
  return EvaluationHelpers.compare_times(value, target, lambda { |a, b|
545
542
  a.year == b.year && a.month == b.month && a.day == b.day
546
543
  })
547
- when :in_segment_list, :not_in_segment_list
544
+
545
+ # segments
546
+ when Const::OP_IN_SEGMENT_LIST, Const::OP_NOT_IN_SEGMENT_LIST
548
547
  begin
549
548
  is_in_list = false
550
549
  id_list = @spec_store.get_id_list(target)
@@ -552,7 +551,7 @@ module Statsig
552
551
  hashed_id = Digest::SHA256.base64digest(value.to_s)[0, 8]
553
552
  is_in_list = id_list.ids.include?(hashed_id)
554
553
  end
555
- return is_in_list if operator == :in_segment_list
554
+ return is_in_list if operator == Const::OP_IN_SEGMENT_LIST
556
555
 
557
556
  return !is_in_list
558
557
  rescue StandardError
@@ -562,6 +561,37 @@ module Statsig
562
561
  return false
563
562
  end
564
563
 
564
+ def eval_nested_gate(gate_name, user, end_result)
565
+ check_gate(user, gate_name, end_result, is_nested: true)
566
+ gate_value = end_result.gate_value
567
+
568
+ unless end_result.disable_exposures
569
+ new_exposure = {
570
+ gate: gate_name,
571
+ gateValue: gate_value ? Const::TRUE : Const::FALSE,
572
+ ruleID: end_result.rule_id
573
+ }
574
+ end_result.secondary_exposures.append(new_exposure)
575
+ end
576
+
577
+ gate_value
578
+ end
579
+
580
+ def eval_nested_gates(gate_names, condition_type, user, end_result)
581
+ has_passing_gate = false
582
+ is_multi_pass_gate_type = condition_type == Const::CND_MULTI_PASS_GATE
583
+ gate_names.each { |gate_name|
584
+ result = eval_nested_gate(gate_name, user, end_result)
585
+
586
+ if is_multi_pass_gate_type == result
587
+ has_passing_gate = true
588
+ break
589
+ end
590
+ }
591
+
592
+ has_passing_gate
593
+ end
594
+
565
595
  def get_value_from_user(user, field)
566
596
  return nil unless field.is_a?(String)
567
597
 
@@ -640,10 +670,10 @@ module Statsig
640
670
  end
641
671
 
642
672
  def eval_pass_percent(user, rule, config_salt)
643
- unit_id = user.get_unit_id(rule.id_type) || Const::EMPTY_STR
644
- rule_salt = rule.salt || rule.id || Const::EMPTY_STR
673
+ unit_id = user.get_unit_id(rule[:idType]) || Const::EMPTY_STR
674
+ rule_salt = rule[:salt] || rule[:id] || Const::EMPTY_STR
645
675
  hash = compute_user_hash("#{config_salt}.#{rule_salt}.#{unit_id}")
646
- return (hash % 10_000) < (rule.pass_percentage * 100)
676
+ return (hash % 10_000) < (rule[:passPercentage] * 100)
647
677
  end
648
678
 
649
679
  def compute_user_hash(user_hash)
@@ -2,7 +2,7 @@
2
2
  module Statsig
3
3
  module Interfaces
4
4
  class IDataStore
5
- CONFIG_SPECS_KEY = "statsig.cache"
5
+ CONFIG_SPECS_V2_KEY = "statsig.dcs_v2"
6
6
  ID_LISTS_KEY = "statsig.id_lists"
7
7
 
8
8
  def init
data/lib/layer.rb CHANGED
@@ -28,13 +28,16 @@ class Layer
28
28
  # @param index The name of parameter being fetched
29
29
  # @param default_value The fallback value if the name cannot be found
30
30
  def get(index, default_value)
31
- return default_value if @value.nil? || !@value.key?(index)
31
+ return default_value if @value.nil?
32
+
33
+ index_sym = index.to_sym
34
+ return default_value unless @value.key?(index_sym)
32
35
 
33
36
  if @exposure_log_func.is_a? Proc
34
37
  @exposure_log_func.call(self, index)
35
38
  end
36
39
 
37
- @value[index]
40
+ @value[index_sym]
38
41
  end
39
42
 
40
43
  ##
@@ -44,13 +47,17 @@ class Layer
44
47
  # @param index The name of parameter being fetched
45
48
  # @param default_value The fallback value if the name cannot be found
46
49
  def get_typed(index, default_value)
47
- return default_value if @value.nil? || !@value.key?(index)
48
- return default_value if @value[index].class != default_value.class and default_value.class != TrueClass and default_value.class != FalseClass
50
+ return default_value if @value.nil?
51
+
52
+ index_sym = index.to_sym
53
+ return default_value unless @value.key?(index_sym)
54
+
55
+ return default_value if @value[index_sym].class != default_value.class and default_value.class != TrueClass and default_value.class != FalseClass
49
56
 
50
57
  if @exposure_log_func.is_a? Proc
51
58
  @exposure_log_func.call(self, index)
52
59
  end
53
60
 
54
- @value[index]
61
+ @value[index_sym]
55
62
  end
56
63
  end
data/lib/memo.rb CHANGED
@@ -4,13 +4,15 @@ module Statsig
4
4
  @global_memo = {}
5
5
 
6
6
  def self.for(hash, method, key)
7
- method_hash = hash[method]
8
- unless method_hash
9
- method_hash = hash[method] = {}
7
+ if key != nil
8
+ method_hash = hash[method]
9
+ unless method_hash
10
+ method_hash = hash[method] = {}
11
+ end
12
+
13
+ return method_hash[key] if method_hash.key?(key)
10
14
  end
11
-
12
- return method_hash[key] if method_hash.key?(key)
13
-
15
+
14
16
  method_hash[key] = yield
15
17
  end
16
18