optimizely-sdk 3.1.1 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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