optimizely-sdk 3.1.1 → 3.2.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.
@@ -24,7 +24,7 @@ module Optimizely
24
24
  module Audience
25
25
  module_function
26
26
 
27
- def user_in_experiment?(config, experiment, attributes)
27
+ def user_in_experiment?(config, experiment, attributes, logger)
28
28
  # Determine for given experiment if user satisfies the audiences for the experiment.
29
29
  #
30
30
  # config - Representation of the Optimizely project config.
@@ -36,7 +36,7 @@ module Optimizely
36
36
 
37
37
  audience_conditions = experiment['audienceConditions'] || experiment['audienceIds']
38
38
 
39
- config.logger.log(
39
+ logger.log(
40
40
  Logger::DEBUG,
41
41
  format(
42
42
  Helpers::Constants::AUDIENCE_EVALUATION_LOGS['EVALUATING_AUDIENCES_COMBINED'],
@@ -47,7 +47,7 @@ module Optimizely
47
47
 
48
48
  # Return true if there are no audiences
49
49
  if audience_conditions.empty?
50
- config.logger.log(
50
+ logger.log(
51
51
  Logger::INFO,
52
52
  format(
53
53
  Helpers::Constants::AUDIENCE_EVALUATION_LOGS['AUDIENCE_EVALUATION_RESULT_COMBINED'],
@@ -60,7 +60,7 @@ module Optimizely
60
60
 
61
61
  attributes ||= {}
62
62
 
63
- custom_attr_condition_evaluator = CustomAttributeConditionEvaluator.new(attributes, config.logger)
63
+ custom_attr_condition_evaluator = CustomAttributeConditionEvaluator.new(attributes, logger)
64
64
 
65
65
  evaluate_custom_attr = lambda do |condition|
66
66
  return custom_attr_condition_evaluator.evaluate(condition)
@@ -71,7 +71,7 @@ module Optimizely
71
71
  return nil unless audience
72
72
 
73
73
  audience_conditions = audience['conditions']
74
- config.logger.log(
74
+ logger.log(
75
75
  Logger::DEBUG,
76
76
  format(
77
77
  Helpers::Constants::AUDIENCE_EVALUATION_LOGS['EVALUATING_AUDIENCE'],
@@ -83,7 +83,7 @@ module Optimizely
83
83
  audience_conditions = JSON.parse(audience_conditions) if audience_conditions.is_a?(String)
84
84
  result = ConditionTreeEvaluator.evaluate(audience_conditions, evaluate_custom_attr)
85
85
  result_str = result.nil? ? 'UNKNOWN' : result.to_s.upcase
86
- config.logger.log(
86
+ logger.log(
87
87
  Logger::INFO,
88
88
  format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['AUDIENCE_EVALUATION_RESULT'], audience_id, result_str)
89
89
  )
@@ -94,7 +94,7 @@ module Optimizely
94
94
 
95
95
  eval_result ||= false
96
96
 
97
- config.logger.log(
97
+ logger.log(
98
98
  Logger::INFO,
99
99
  format(
100
100
  Helpers::Constants::AUDIENCE_EVALUATION_LOGS['AUDIENCE_EVALUATION_RESULT_COMBINED'],
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2016-2017, Optimizely and contributors
4
+ # Copyright 2016-2017, 2019 Optimizely and contributors
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
7
7
  # you may not use this file except in compliance with the License.
@@ -28,18 +28,17 @@ module Optimizely
28
28
  MAX_TRAFFIC_VALUE = 10_000
29
29
  UNSIGNED_MAX_32_BIT_VALUE = 0xFFFFFFFF
30
30
 
31
- def initialize(config)
32
- # Bucketer init method to set bucketing seed and project config data.
33
- #
34
- # config - ProjectConfig data to be used in making bucketing decisions.
35
-
31
+ def initialize(logger)
32
+ # Bucketer init method to set bucketing seed and logger.
33
+ # logger - Optional component which provides a log method to log messages.
34
+ @logger = logger
36
35
  @bucket_seed = HASH_SEED
37
- @config = config
38
36
  end
39
37
 
40
- def bucket(experiment, bucketing_id, user_id)
38
+ def bucket(project_config, experiment, bucketing_id, user_id)
41
39
  # Determines ID of variation to be shown for a given experiment key and user ID.
42
40
  #
41
+ # project_config - Instance of ProjectConfig
43
42
  # experiment - Experiment for which visitor is to be bucketed.
44
43
  # bucketing_id - String A customer-assigned value used to generate the bucketing key
45
44
  # user_id - String ID for user.
@@ -52,19 +51,19 @@ module Optimizely
52
51
  experiment_key = experiment['key']
53
52
  group_id = experiment['groupId']
54
53
  if group_id
55
- group = @config.group_key_map.fetch(group_id)
54
+ group = project_config.group_id_map.fetch(group_id)
56
55
  if Helpers::Group.random_policy?(group)
57
56
  traffic_allocations = group.fetch('trafficAllocation')
58
57
  bucketed_experiment_id = find_bucket(bucketing_id, user_id, group_id, traffic_allocations)
59
58
  # return if the user is not bucketed into any experiment
60
59
  unless bucketed_experiment_id
61
- @config.logger.log(Logger::INFO, "User '#{user_id}' is in no experiment.")
60
+ @logger.log(Logger::INFO, "User '#{user_id}' is in no experiment.")
62
61
  return nil
63
62
  end
64
63
 
65
64
  # return if the user is bucketed into a different experiment than the one specified
66
65
  if bucketed_experiment_id != experiment_id
67
- @config.logger.log(
66
+ @logger.log(
68
67
  Logger::INFO,
69
68
  "User '#{user_id}' is not in experiment '#{experiment_key}' of group #{group_id}."
70
69
  )
@@ -72,7 +71,7 @@ module Optimizely
72
71
  end
73
72
 
74
73
  # continue bucketing if the user is bucketed into the experiment specified
75
- @config.logger.log(
74
+ @logger.log(
76
75
  Logger::INFO,
77
76
  "User '#{user_id}' is in experiment '#{experiment_key}' of group #{group_id}."
78
77
  )
@@ -82,9 +81,9 @@ module Optimizely
82
81
  traffic_allocations = experiment['trafficAllocation']
83
82
  variation_id = find_bucket(bucketing_id, user_id, experiment_id, traffic_allocations)
84
83
  if variation_id && variation_id != ''
85
- variation = @config.get_variation_from_id(experiment_key, variation_id)
84
+ variation = project_config.get_variation_from_id(experiment_key, variation_id)
86
85
  variation_key = variation ? variation['key'] : nil
87
- @config.logger.log(
86
+ @logger.log(
88
87
  Logger::INFO,
89
88
  "User '#{user_id}' is in variation '#{variation_key}' of experiment '#{experiment_key}'."
90
89
  )
@@ -93,13 +92,13 @@ module Optimizely
93
92
 
94
93
  # Handle the case when the traffic range is empty due to sticky bucketing
95
94
  if variation_id == ''
96
- @config.logger.log(
95
+ @logger.log(
97
96
  Logger::DEBUG,
98
97
  'Bucketed into an empty traffic range. Returning nil.'
99
98
  )
100
99
  end
101
100
 
102
- @config.logger.log(Logger::INFO, "User '#{user_id}' is in no variation.")
101
+ @logger.log(Logger::INFO, "User '#{user_id}' is in no variation.")
103
102
  nil
104
103
  end
105
104
 
@@ -114,7 +113,7 @@ module Optimizely
114
113
  # Returns entity ID corresponding to the provided bucket value or nil if no match is found.
115
114
  bucketing_key = format(BUCKETING_ID_TEMPLATE, bucketing_id: bucketing_id, entity_id: parent_id)
116
115
  bucket_value = generate_bucket_value(bucketing_key)
117
- @config.logger.log(Logger::DEBUG, "Assigned bucket #{bucket_value} to user '#{user_id}' "\
116
+ @logger.log(Logger::DEBUG, "Assigned bucket #{bucket_value} to user '#{user_id}' "\
118
117
  "with bucketing ID: '#{bucketing_id}'.")
119
118
 
120
119
  traffic_allocations.each do |traffic_allocation|
@@ -0,0 +1,411 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2019, Optimizely and contributors
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+ require 'json'
18
+ require 'optimizely/project_config'
19
+ require 'optimizely/helpers/constants'
20
+ require 'optimizely/helpers/validator'
21
+ module Optimizely
22
+ class DatafileProjectConfig < ProjectConfig
23
+ # Representation of the Optimizely project config.
24
+ RUNNING_EXPERIMENT_STATUS = ['Running'].freeze
25
+ RESERVED_ATTRIBUTE_PREFIX = '$opt_'
26
+
27
+ attr_reader :account_id
28
+ attr_reader :attributes
29
+ attr_reader :audiences
30
+ attr_reader :typed_audiences
31
+ attr_reader :events
32
+ attr_reader :experiments
33
+ attr_reader :feature_flags
34
+ attr_reader :groups
35
+ attr_reader :project_id
36
+ # Boolean - denotes if Optimizely should remove the last block of visitors' IP address before storing event data
37
+ attr_reader :anonymize_ip
38
+ attr_reader :bot_filtering
39
+ attr_reader :revision
40
+ attr_reader :rollouts
41
+ attr_reader :version
42
+
43
+ attr_reader :attribute_key_map
44
+ attr_reader :audience_id_map
45
+ attr_reader :event_key_map
46
+ attr_reader :experiment_feature_map
47
+ attr_reader :experiment_id_map
48
+ attr_reader :experiment_key_map
49
+ attr_reader :feature_flag_key_map
50
+ attr_reader :feature_variable_key_map
51
+ attr_reader :group_id_map
52
+ attr_reader :rollout_id_map
53
+ attr_reader :rollout_experiment_key_map
54
+ attr_reader :variation_id_map
55
+ attr_reader :variation_id_to_variable_usage_map
56
+ attr_reader :variation_key_map
57
+
58
+ def initialize(datafile, logger, error_handler)
59
+ # ProjectConfig init method to fetch and set project config data
60
+ #
61
+ # datafile - JSON string representing the project
62
+
63
+ config = JSON.parse(datafile)
64
+
65
+ @error_handler = error_handler
66
+ @logger = logger
67
+ @version = config['version']
68
+
69
+ raise InvalidDatafileVersionError, @version unless Helpers::Constants::SUPPORTED_VERSIONS.value?(@version)
70
+
71
+ @account_id = config['accountId']
72
+ @attributes = config.fetch('attributes', [])
73
+ @audiences = config.fetch('audiences', [])
74
+ @typed_audiences = config.fetch('typedAudiences', [])
75
+ @events = config.fetch('events', [])
76
+ @experiments = config['experiments']
77
+ @feature_flags = config.fetch('featureFlags', [])
78
+ @groups = config.fetch('groups', [])
79
+ @project_id = config['projectId']
80
+ @anonymize_ip = config.key?('anonymizeIP') ? config['anonymizeIP'] : false
81
+ @bot_filtering = config['botFiltering']
82
+ @revision = config['revision']
83
+ @rollouts = config.fetch('rollouts', [])
84
+
85
+ # Utility maps for quick lookup
86
+ @attribute_key_map = generate_key_map(@attributes, 'key')
87
+ @event_key_map = generate_key_map(@events, 'key')
88
+ @group_id_map = generate_key_map(@groups, 'id')
89
+ @group_id_map.each do |key, group|
90
+ exps = group.fetch('experiments')
91
+ exps.each do |exp|
92
+ @experiments.push(exp.merge('groupId' => key))
93
+ end
94
+ end
95
+ @experiment_key_map = generate_key_map(@experiments, 'key')
96
+ @experiment_id_map = generate_key_map(@experiments, 'id')
97
+ @audience_id_map = generate_key_map(@audiences, 'id')
98
+ @audience_id_map = @audience_id_map.merge(generate_key_map(@typed_audiences, 'id')) unless @typed_audiences.empty?
99
+ @variation_id_map = {}
100
+ @variation_key_map = {}
101
+ @variation_id_to_variable_usage_map = {}
102
+ @variation_id_to_experiment_map = {}
103
+ @experiment_key_map.each_value do |exp|
104
+ # Excludes experiments from rollouts
105
+ variations = exp.fetch('variations')
106
+ variations.each do |variation|
107
+ variation_id = variation['id']
108
+ @variation_id_to_experiment_map[variation_id] = exp
109
+ end
110
+ end
111
+ @rollout_id_map = generate_key_map(@rollouts, 'id')
112
+ # split out the experiment key map for rollouts
113
+ @rollout_experiment_key_map = {}
114
+ @rollout_id_map.each_value do |rollout|
115
+ exps = rollout.fetch('experiments')
116
+ @rollout_experiment_key_map = @rollout_experiment_key_map.merge(generate_key_map(exps, 'key'))
117
+ end
118
+ @all_experiments = @experiment_key_map.merge(@rollout_experiment_key_map)
119
+ @all_experiments.each do |key, exp|
120
+ variations = exp.fetch('variations')
121
+ variations.each do |variation|
122
+ variation_id = variation['id']
123
+ variation['featureEnabled'] = variation['featureEnabled'] == true
124
+ variation_variables = variation['variables']
125
+ next if variation_variables.nil?
126
+
127
+ @variation_id_to_variable_usage_map[variation_id] = generate_key_map(variation_variables, 'id')
128
+ end
129
+ @variation_id_map[key] = generate_key_map(variations, 'id')
130
+ @variation_key_map[key] = generate_key_map(variations, 'key')
131
+ end
132
+ @feature_flag_key_map = generate_key_map(@feature_flags, 'key')
133
+ @experiment_feature_map = {}
134
+ @feature_variable_key_map = {}
135
+ @feature_flag_key_map.each do |key, feature_flag|
136
+ @feature_variable_key_map[key] = generate_key_map(feature_flag['variables'], 'key')
137
+ feature_flag['experimentIds'].each do |experiment_id|
138
+ @experiment_feature_map[experiment_id] = [feature_flag['id']]
139
+ end
140
+ end
141
+ end
142
+
143
+ def self.create(datafile, logger, error_handler, skip_json_validation)
144
+ # Looks up and sets datafile and config based on response body.
145
+ #
146
+ # datafile - JSON string representing the Optimizely project.
147
+ # logger - Provides a logger instance.
148
+ # error_handler - Provides a handle_error method to handle exceptions.
149
+ # skip_json_validation - Optional boolean param which allows skipping JSON schema
150
+ # validation upon object invocation. By default JSON schema validation will be performed.
151
+ # Returns instance of DatafileProjectConfig, nil otherwise.
152
+ if !skip_json_validation && !Helpers::Validator.datafile_valid?(datafile)
153
+ default_logger = SimpleLogger.new
154
+ default_logger.log(Logger::ERROR, InvalidInputError.new('datafile').message)
155
+ return nil
156
+ end
157
+
158
+ begin
159
+ config = new(datafile, logger, error_handler)
160
+ rescue StandardError => e
161
+ default_logger = SimpleLogger.new
162
+ error_msg = e.class == InvalidDatafileVersionError ? e.message : InvalidInputError.new('datafile').message
163
+ error_to_handle = e.class == InvalidDatafileVersionError ? InvalidDatafileVersionError : InvalidInputError
164
+ default_logger.log(Logger::ERROR, error_msg)
165
+ error_handler.handle_error error_to_handle
166
+ return nil
167
+ end
168
+
169
+ config
170
+ end
171
+
172
+ def experiment_running?(experiment)
173
+ # Determine if experiment corresponding to given key is running
174
+ #
175
+ # experiment - Experiment
176
+ #
177
+ # Returns true if experiment is running
178
+ RUNNING_EXPERIMENT_STATUS.include?(experiment['status'])
179
+ end
180
+
181
+ def get_experiment_from_key(experiment_key)
182
+ # Retrieves experiment ID for a given key
183
+ #
184
+ # experiment_key - String key representing the experiment
185
+ #
186
+ # Returns Experiment or nil if not found
187
+
188
+ experiment = @experiment_key_map[experiment_key]
189
+ return experiment if experiment
190
+
191
+ @logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
192
+ @error_handler.handle_error InvalidExperimentError
193
+ nil
194
+ end
195
+
196
+ def get_experiment_key(experiment_id)
197
+ # Retrieves experiment key for a given ID.
198
+ #
199
+ # experiment_id - String ID representing the experiment.
200
+ #
201
+ # Returns String key.
202
+
203
+ experiment = @experiment_id_map[experiment_id]
204
+ return experiment['key'] unless experiment.nil?
205
+
206
+ @logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
207
+ @error_handler.handle_error InvalidExperimentError
208
+ nil
209
+ end
210
+
211
+ def get_event_from_key(event_key)
212
+ # Get event for the provided event key.
213
+ #
214
+ # event_key - Event key for which event is to be determined.
215
+ #
216
+ # Returns Event corresponding to the provided event key.
217
+
218
+ event = @event_key_map[event_key]
219
+ return event if event
220
+
221
+ @logger.log Logger::ERROR, "Event '#{event_key}' is not in datafile."
222
+ @error_handler.handle_error InvalidEventError
223
+ nil
224
+ end
225
+
226
+ def get_audience_from_id(audience_id)
227
+ # Get audience for the provided audience ID
228
+ #
229
+ # audience_id - ID of the audience
230
+ #
231
+ # Returns the audience
232
+
233
+ audience = @audience_id_map[audience_id]
234
+ return audience if audience
235
+
236
+ @logger.log Logger::ERROR, "Audience '#{audience_id}' is not in datafile."
237
+ @error_handler.handle_error InvalidAudienceError
238
+ nil
239
+ end
240
+
241
+ def get_variation_from_id(experiment_key, variation_id)
242
+ # Get variation given experiment key and variation ID
243
+ #
244
+ # experiment_key - Key representing parent experiment of variation
245
+ # variation_id - ID of the variation
246
+ #
247
+ # Returns the variation or nil if not found
248
+
249
+ variation_id_map = @variation_id_map[experiment_key]
250
+ if variation_id_map
251
+ variation = variation_id_map[variation_id]
252
+ return variation if variation
253
+
254
+ @logger.log Logger::ERROR, "Variation id '#{variation_id}' is not in datafile."
255
+ @error_handler.handle_error InvalidVariationError
256
+ return nil
257
+ end
258
+
259
+ @logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
260
+ @error_handler.handle_error InvalidExperimentError
261
+ nil
262
+ end
263
+
264
+ def get_variation_id_from_key(experiment_key, variation_key)
265
+ # Get variation ID given experiment key and variation key
266
+ #
267
+ # experiment_key - Key representing parent experiment of variation
268
+ # variation_key - Key of the variation
269
+ #
270
+ # Returns ID of the variation
271
+
272
+ variation_key_map = @variation_key_map[experiment_key]
273
+ if variation_key_map
274
+ variation = variation_key_map[variation_key]
275
+ return variation['id'] if variation
276
+
277
+ @logger.log Logger::ERROR, "Variation key '#{variation_key}' is not in datafile."
278
+ @error_handler.handle_error InvalidVariationError
279
+ return nil
280
+ end
281
+
282
+ @logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
283
+ @error_handler.handle_error InvalidExperimentError
284
+ nil
285
+ end
286
+
287
+ def get_whitelisted_variations(experiment_key)
288
+ # Retrieves whitelisted variations for a given experiment Key
289
+ #
290
+ # experiment_key - String Key representing the experiment
291
+ #
292
+ # Returns whitelisted variations for the experiment or nil
293
+
294
+ experiment = @experiment_key_map[experiment_key]
295
+ return experiment['forcedVariations'] if experiment
296
+
297
+ @logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
298
+ @error_handler.handle_error InvalidExperimentError
299
+ end
300
+
301
+ def get_attribute_id(attribute_key)
302
+ # Get attribute ID for the provided attribute key.
303
+ #
304
+ # Args:
305
+ # Attribute key for which attribute is to be fetched.
306
+ #
307
+ # Returns:
308
+ # Attribute ID corresponding to the provided attribute key.
309
+ attribute = @attribute_key_map[attribute_key]
310
+ has_reserved_prefix = attribute_key.to_s.start_with?(RESERVED_ATTRIBUTE_PREFIX)
311
+ unless attribute.nil?
312
+ if has_reserved_prefix
313
+ @logger.log(Logger::WARN, "Attribute '#{attribute_key}' unexpectedly has reserved prefix '#{RESERVED_ATTRIBUTE_PREFIX}'; "\
314
+ 'using attribute ID instead of reserved attribute name.')
315
+ end
316
+ return attribute['id']
317
+ end
318
+ return attribute_key if has_reserved_prefix
319
+
320
+ @logger.log Logger::ERROR, "Attribute key '#{attribute_key}' is not in datafile."
321
+ @error_handler.handle_error InvalidAttributeError
322
+ nil
323
+ end
324
+
325
+ def variation_id_exists?(experiment_id, variation_id)
326
+ # Determines if a given experiment ID / variation ID pair exists in the datafile
327
+ #
328
+ # experiment_id - String experiment ID
329
+ # variation_id - String variation ID
330
+ #
331
+ # Returns true if variation is in datafile
332
+
333
+ experiment_key = get_experiment_key(experiment_id)
334
+ variation_id_map = @variation_id_map[experiment_key]
335
+ if variation_id_map
336
+ variation = variation_id_map[variation_id]
337
+ return true if variation
338
+
339
+ @logger.log Logger::ERROR, "Variation ID '#{variation_id}' is not in datafile."
340
+ @error_handler.handle_error InvalidVariationError
341
+ end
342
+
343
+ false
344
+ end
345
+
346
+ def get_feature_flag_from_key(feature_flag_key)
347
+ # Retrieves the feature flag with the given key
348
+ #
349
+ # feature_flag_key - String feature key
350
+ #
351
+ # Returns feature flag if found, otherwise nil
352
+ feature_flag = @feature_flag_key_map[feature_flag_key]
353
+ return feature_flag if feature_flag
354
+
355
+ @logger.log Logger::ERROR, "Feature flag key '#{feature_flag_key}' is not in datafile."
356
+ nil
357
+ end
358
+
359
+ def get_feature_variable(feature_flag, variable_key)
360
+ # Retrieves the variable with the given key for the given feature
361
+ #
362
+ # feature_flag - The feature flag for which we are retrieving the variable
363
+ # variable_key - String variable key
364
+ #
365
+ # Returns variable if found, otherwise nil
366
+ feature_flag_key = feature_flag['key']
367
+ variable = @feature_variable_key_map[feature_flag_key][variable_key]
368
+ return variable if variable
369
+
370
+ @logger.log Logger::ERROR, "No feature variable was found for key '#{variable_key}' in feature flag "\
371
+ "'#{feature_flag_key}'."
372
+ nil
373
+ end
374
+
375
+ def get_rollout_from_id(rollout_id)
376
+ # Retrieves the rollout with the given ID
377
+ #
378
+ # rollout_id - String rollout ID
379
+ #
380
+ # Returns the rollout if found, otherwise nil
381
+ rollout = @rollout_id_map[rollout_id]
382
+ return rollout if rollout
383
+
384
+ @logger.log Logger::ERROR, "Rollout with ID '#{rollout_id}' is not in the datafile."
385
+ nil
386
+ end
387
+
388
+ def feature_experiment?(experiment_id)
389
+ # Determines if given experiment is a feature test.
390
+ #
391
+ # experiment_id - String experiment ID
392
+ #
393
+ # Returns true if experiment belongs to any feature,
394
+ # false otherwise.
395
+ @experiment_feature_map.key?(experiment_id)
396
+ end
397
+
398
+ private
399
+
400
+ def generate_key_map(array, key)
401
+ # Helper method to generate map from key to hash in array of hashes
402
+ #
403
+ # array - Array consisting of hash
404
+ # key - Key in each hash which will be key in the map
405
+ #
406
+ # Returns map mapping key to hash
407
+
408
+ Hash[array.map { |obj| [obj[key], obj] }]
409
+ end
410
+ end
411
+ end