diff --git a/lib/optimizely/config/datafile_project_config.rb b/lib/optimizely/config/datafile_project_config.rb index 25357133..9be9ba5d 100644 --- a/lib/optimizely/config/datafile_project_config.rb +++ b/lib/optimizely/config/datafile_project_config.rb @@ -27,7 +27,7 @@ class DatafileProjectConfig < ProjectConfig attr_reader :datafile, :account_id, :attributes, :audiences, :typed_audiences, :events, :experiments, :feature_flags, :groups, :project_id, :bot_filtering, :revision, :sdk_key, :environment_key, :rollouts, :version, :send_flag_decisions, - :attribute_key_map, :audience_id_map, :event_key_map, :experiment_feature_map, + :attribute_key_map, :attribute_id_to_key_map, :audience_id_map, :event_key_map, :experiment_feature_map, :experiment_id_map, :experiment_key_map, :feature_flag_key_map, :feature_variable_key_map, :group_id_map, :rollout_id_map, :rollout_experiment_id_map, :variation_id_map, :variation_id_to_variable_usage_map, :variation_key_map, :variation_id_map_by_experiment_id, @@ -82,6 +82,10 @@ def initialize(datafile, logger, error_handler) # Utility maps for quick lookup @attribute_key_map = generate_key_map(@attributes, 'key') + @attribute_id_to_key_map = {} + @attributes.each do |attribute| + @attribute_id_to_key_map[attribute['id']] = attribute['key'] + end @event_key_map = generate_key_map(@events, 'key') @group_id_map = generate_key_map(@groups, 'id') @group_id_map.each do |key, group| @@ -440,6 +444,40 @@ def get_attribute_id(attribute_key) nil end + def get_attribute_by_key(attribute_key) + # Get attribute for the provided attribute key. + # + # Args: + # Attribute key for which attribute is to be fetched. + # + # Returns: + # Attribute corresponding to the provided attribute key. + attribute = @attribute_key_map[attribute_key] + return attribute if @attribute_key_map.key?(attribute_key) + + invalid_attribute_error = InvalidAttributeError.new(attribute_key) + @logger.log Logger::ERROR, invalid_attribute_error.message + @error_handler.handle_error invalid_attribute_error + nil + end + + def get_attribute_key_by_id(attribute_id) + # Get attribute key for the provided attribute ID. + # + # Args: + # Attribute ID for which attribute is to be fetched. + # + # Returns: + # Attribute key corresponding to the provided attribute ID. + attribute = @attribute_id_to_key_map[attribute_id] + return attribute if @attribute_id_to_key_map.key?(attribute_id) + + invalid_attribute_error = InvalidAttributeError.new(attribute_id) + @logger.log Logger::ERROR, invalid_attribute_error.message + @error_handler.handle_error invalid_attribute_error + nil + end + def variation_id_exists?(experiment_id, variation_id) # Determines if a given experiment ID / variation ID pair exists in the datafile # diff --git a/lib/optimizely/helpers/constants.rb b/lib/optimizely/helpers/constants.rb index 02b815ae..eba265b3 100644 --- a/lib/optimizely/helpers/constants.rb +++ b/lib/optimizely/helpers/constants.rb @@ -201,6 +201,9 @@ module Constants }, 'forcedVariations' => { 'type' => 'object' + }, + 'cmab' => { + 'type' => 'object' } }, 'required' => %w[ @@ -303,6 +306,20 @@ module Constants }, 'required' => %w[key] } + }, + 'cmab' => { + 'type' => 'object', + 'items' => { + 'type' => 'object', + 'properties' => { + 'attributeIds' => { + 'type' => 'array' + }, + 'trafficAllocation' => { + 'type' => 'integer' + } + } + } } }, 'required' => %w[ diff --git a/lib/optimizely/project_config.rb b/lib/optimizely/project_config.rb index b0d43aa3..43e86441 100644 --- a/lib/optimizely/project_config.rb +++ b/lib/optimizely/project_config.rb @@ -86,6 +86,10 @@ def get_whitelisted_variations(experiment_id); end def get_attribute_id(attribute_key); end + def get_attribute_by_key(attribute_key); end + + def get_attribute_key_by_id(attribute_id); end + def variation_id_exists?(experiment_id, variation_id); end def get_feature_flag_from_key(feature_flag_key); end diff --git a/spec/config/datafile_project_config_spec.rb b/spec/config/datafile_project_config_spec.rb index e30d07e1..362141d6 100644 --- a/spec/config/datafile_project_config_spec.rb +++ b/spec/config/datafile_project_config_spec.rb @@ -1078,6 +1078,67 @@ end end + describe '#test_cmab_field_population' do + it 'Should return CMAB details' do + config_dict = Marshal.load(Marshal.dump(OptimizelySpec::VALID_CONFIG_BODY)) + config_dict['experiments'][0]['cmab'] = {'attributeIds' => %w[808797688 808797689], 'trafficAllocation' => 4000} + config_dict['experiments'][0]['trafficAllocation'] = [] + + config_json = JSON.dump(config_dict) + project_config = Optimizely::DatafileProjectConfig.new(config_json, logger, error_handler) + + experiment = project_config.get_experiment_from_key('test_experiment') + expect(experiment['cmab']).to eq({'attributeIds' => %w[808797688 808797689], 'trafficAllocation' => 4000}) + + experiment2 = project_config.get_experiment_from_key('test_experiment_with_audience') + expect(experiment2['cmab']).to eq(nil) + end + it 'should return nil if cmab field is missing' do + config_dict = Marshal.load(Marshal.dump(OptimizelySpec::VALID_CONFIG_BODY)) + config_dict['experiments'][0].delete('cmab') + config_json = JSON.dump(config_dict) + project_config = Optimizely::DatafileProjectConfig.new(config_json, logger, error_handler) + experiment = project_config.get_experiment_from_key('test_experiment') + expect(experiment['cmab']).to eq(nil) + end + + it 'should handle empty cmab object' do + config_dict = Marshal.load(Marshal.dump(OptimizelySpec::VALID_CONFIG_BODY)) + config_dict['experiments'][0]['cmab'] = {} + config_json = JSON.dump(config_dict) + project_config = Optimizely::DatafileProjectConfig.new(config_json, logger, error_handler) + experiment = project_config.get_experiment_from_key('test_experiment') + expect(experiment['cmab']).to eq({}) + end + + it 'should handle cmab with only attributeIds' do + config_dict = Marshal.load(Marshal.dump(OptimizelySpec::VALID_CONFIG_BODY)) + config_dict['experiments'][0]['cmab'] = {'attributeIds' => %w[808797688]} + config_json = JSON.dump(config_dict) + project_config = Optimizely::DatafileProjectConfig.new(config_json, logger, error_handler) + experiment = project_config.get_experiment_from_key('test_experiment') + expect(experiment['cmab']).to eq({'attributeIds' => %w[808797688]}) + end + + it 'should handle cmab with only trafficAllocation' do + config_dict = Marshal.load(Marshal.dump(OptimizelySpec::VALID_CONFIG_BODY)) + config_dict['experiments'][0]['cmab'] = {'trafficAllocation' => 1234} + config_json = JSON.dump(config_dict) + project_config = Optimizely::DatafileProjectConfig.new(config_json, logger, error_handler) + experiment = project_config.get_experiment_from_key('test_experiment') + expect(experiment['cmab']).to eq({'trafficAllocation' => 1234}) + end + + it 'should not affect other experiments when cmab is set' do + config_dict = Marshal.load(Marshal.dump(OptimizelySpec::VALID_CONFIG_BODY)) + config_dict['experiments'][0]['cmab'] = {'attributeIds' => %w[808797688 808797689], 'trafficAllocation' => 4000} + config_json = JSON.dump(config_dict) + project_config = Optimizely::DatafileProjectConfig.new(config_json, logger, error_handler) + experiment2 = project_config.get_experiment_from_key('test_experiment_with_audience') + expect(experiment2['cmab']).to eq(nil) + end + end + describe '#feature_experiment' do let(:config) { Optimizely::DatafileProjectConfig.new(config_body_JSON, logger, error_handler) }